From 9703a6b4d73026c8a719dc6aa94c63a502dd4996 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Fri, 23 Jan 2026 17:05:56 +0800 Subject: [PATCH 01/25] init --- resources/web/login/orca_login.html | 927 +++++++ src/libslic3r/AppConfig.cpp | 7 + src/libslic3r/AppConfig.hpp | 1 + src/libslic3r/Preset.cpp | 9 +- src/libslic3r/Preset.hpp | 3 +- src/libslic3r/PrintConfig.cpp | 7 + src/libslic3r/Time.cpp | 72 + src/libslic3r/Time.hpp | 9 + src/slic3r/CMakeLists.txt | 18 + src/slic3r/GUI/BindDialog.cpp | 2 +- src/slic3r/GUI/DeviceManager.hpp | 13 +- src/slic3r/GUI/GUI_App.cpp | 158 +- src/slic3r/GUI/GUI_App.hpp | 5 + src/slic3r/GUI/HttpServer.cpp | 57 +- src/slic3r/GUI/HttpServer.hpp | 13 + src/slic3r/GUI/Jobs/BindJob.cpp | 12 +- src/slic3r/GUI/Jobs/PrintJob.cpp | 20 +- src/slic3r/GUI/Jobs/SendJob.cpp | 16 +- src/slic3r/GUI/MediaPlayCtrl.cpp | 2 + src/slic3r/GUI/Monitor.cpp | 5 +- src/slic3r/GUI/NetworkPluginDialog.cpp | 2 +- src/slic3r/GUI/NetworkPluginDialog.hpp | 2 +- src/slic3r/GUI/PhysicalPrinterDialog.cpp | 91 +- src/slic3r/GUI/PhysicalPrinterDialog.hpp | 1 + src/slic3r/GUI/Plater.cpp | 63 +- src/slic3r/GUI/Preferences.cpp | 12 +- src/slic3r/GUI/Preferences.hpp | 2 +- src/slic3r/GUI/SendMultiMachinePage.cpp | 8 +- src/slic3r/GUI/SendMultiMachinePage.hpp | 2 +- src/slic3r/GUI/Tab.cpp | 27 + src/slic3r/GUI/Tab.hpp | 1 + src/slic3r/GUI/TaskManager.cpp | 12 +- src/slic3r/GUI/TaskManager.hpp | 10 +- src/slic3r/GUI/WebUserLoginDialog.cpp | 97 +- src/slic3r/GUI/WebUserLoginDialog.hpp | 1 + src/slic3r/Utils/BBLCloudServiceAgent.cpp | 864 +++++++ src/slic3r/Utils/BBLCloudServiceAgent.hpp | 136 + src/slic3r/Utils/BBLNetworkPlugin.cpp | 823 ++++++ src/slic3r/Utils/BBLNetworkPlugin.hpp | 515 ++++ src/slic3r/Utils/BBLPrinterAgent.cpp | 379 +++ src/slic3r/Utils/BBLPrinterAgent.hpp | 86 + src/slic3r/Utils/Http.cpp | 78 + src/slic3r/Utils/Http.hpp | 3 + src/slic3r/Utils/ICloudServiceAgent.hpp | 477 ++++ src/slic3r/Utils/IPrinterAgent.hpp | 260 ++ src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 2031 +++++++++++++++ src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 182 ++ src/slic3r/Utils/NetworkAgent.cpp | 1886 +++----------- src/slic3r/Utils/NetworkAgent.hpp | 259 +- src/slic3r/Utils/NetworkAgentFactory.cpp | 176 ++ src/slic3r/Utils/NetworkAgentFactory.hpp | 250 ++ src/slic3r/Utils/OrcaCloudServiceAgent.cpp | 2676 ++++++++++++++++++++ src/slic3r/Utils/OrcaCloudServiceAgent.hpp | 359 +++ src/slic3r/Utils/OrcaPrinterAgent.cpp | 245 ++ src/slic3r/Utils/OrcaPrinterAgent.hpp | 100 + src/slic3r/Utils/PresetUpdater.cpp | 2 +- src/slic3r/Utils/QidiPrinterAgent.cpp | 1566 ++++++++++++ src/slic3r/Utils/QidiPrinterAgent.hpp | 137 + src/slic3r/Utils/bambu_networking.hpp | 2 +- tests/libslic3r/test_bambu_networking.cpp | 2 +- 60 files changed, 13396 insertions(+), 1785 deletions(-) create mode 100644 resources/web/login/orca_login.html create mode 100644 src/slic3r/Utils/BBLCloudServiceAgent.cpp create mode 100644 src/slic3r/Utils/BBLCloudServiceAgent.hpp create mode 100644 src/slic3r/Utils/BBLNetworkPlugin.cpp create mode 100644 src/slic3r/Utils/BBLNetworkPlugin.hpp create mode 100644 src/slic3r/Utils/BBLPrinterAgent.cpp create mode 100644 src/slic3r/Utils/BBLPrinterAgent.hpp create mode 100644 src/slic3r/Utils/ICloudServiceAgent.hpp create mode 100644 src/slic3r/Utils/IPrinterAgent.hpp create mode 100644 src/slic3r/Utils/MoonrakerPrinterAgent.cpp create mode 100644 src/slic3r/Utils/MoonrakerPrinterAgent.hpp create mode 100644 src/slic3r/Utils/NetworkAgentFactory.cpp create mode 100644 src/slic3r/Utils/NetworkAgentFactory.hpp create mode 100644 src/slic3r/Utils/OrcaCloudServiceAgent.cpp create mode 100644 src/slic3r/Utils/OrcaCloudServiceAgent.hpp create mode 100644 src/slic3r/Utils/OrcaPrinterAgent.cpp create mode 100644 src/slic3r/Utils/OrcaPrinterAgent.hpp create mode 100644 src/slic3r/Utils/QidiPrinterAgent.cpp create mode 100644 src/slic3r/Utils/QidiPrinterAgent.hpp diff --git a/resources/web/login/orca_login.html b/resources/web/login/orca_login.html new file mode 100644 index 0000000000..69eec2aedb --- /dev/null +++ b/resources/web/login/orca_login.html @@ -0,0 +1,927 @@ + + + + + + OrcaCloud Login + + + +
+ +
+
+ +

OrcaCloud

+

Sign in to sync your settings

+
+ +
+ + +
+ +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + +
+ + Forgot password? + +
or continue with
+ +
+ + + + + +
+
+ + +
+ + + + + Back to sign in + + +
+

Reset Password

+
+ +

+ Enter your email address and we'll send you a link to reset your password. +

+ +
+
+ + + +
+ + +
+
+ + +
+ + +
+
+ Signing in... +
+ + +
+
+ + + + diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index c9ce68e3e0..e963948d98 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -300,6 +300,13 @@ void AppConfig::set_defaults() if (get("allow_abnormal_storage").empty()) { set_bool("allow_abnormal_storage", false); } +#ifdef __linux__ + if (get(SETTING_USE_ENCRYPTED_TOKEN_FILE).empty()) + set_bool(SETTING_USE_ENCRYPTED_TOKEN_FILE, true); +#else + if (get(SETTING_USE_ENCRYPTED_TOKEN_FILE).empty()) + set_bool(SETTING_USE_ENCRYPTED_TOKEN_FILE, false); +#endif if(get("check_stable_update_only").empty()) { set_bool("check_stable_update_only", false); diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index b521410ce9..e501860092 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -28,6 +28,7 @@ using namespace nlohmann; #define SETTING_NETWORK_PLUGIN_SKIPPED_VERSIONS "network_plugin_skipped_versions" #define SETTING_NETWORK_PLUGIN_UPDATE_DISABLED "network_plugin_update_prompts_disabled" #define SETTING_NETWORK_PLUGIN_REMIND_LATER "network_plugin_remind_later" +#define SETTING_USE_ENCRYPTED_TOKEN_FILE "use_encrypted_token_file" #define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.01" #define SUPPORT_DARK_MODE diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index a1d5f576cb..1dc13c3640 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1017,7 +1017,7 @@ static std::vector s_Preset_printer_options { "scan_first_layer", "enable_power_loss_recovery", "wrapping_detection_layers", "wrapping_exclude_area", "machine_load_filament_time", "machine_unload_filament_time", "machine_tool_change_time", "time_cost", "machine_pause_gcode", "template_custom_gcode", "nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", "travel_slope", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","printer_structure", "best_object_pos", "head_wrap_detect_zone", - "host_type", "print_host", "printhost_apikey", "bbl_use_printhost", + "host_type", "print_host", "printhost_apikey", "bbl_use_printhost", "printer_agent", "print_host_webui", "printhost_cafile","printhost_port","printhost_authorization_type", "printhost_user", "printhost_password", "printhost_ssl_ignore_revoke", "thumbnails", "thumbnails_format", @@ -1502,7 +1502,7 @@ int PresetCollection::get_differed_values_to_update(Preset& preset, std::map s_PhysicalPrinter_opts { "printer_technology", "bbl_use_printhost", "host_type", + "printer_agent", "print_host", "print_host_webui", "printhost_apikey", diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index 6afda07426..65da5d5e26 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -52,7 +52,8 @@ #define BBL_JSON_KEY_BASE_ID "base_id" #define BBL_JSON_KEY_USER_ID "user_id" #define BBL_JSON_KEY_FILAMENT_ID "filament_id" -#define BBL_JSON_KEY_UPDATE_TIME "updated_time" +#define ORCA_JSON_KEY_UPDATE_TIME "updated_time" +#define ORCA_JSON_KEY_CREATED_TIME "created_time" #define BBL_JSON_KEY_INHERITS "inherits" #define BBL_JSON_KEY_INSTANTIATION "instantiation" #define BBL_JSON_KEY_NOZZLE_DIAMETER "nozzle_diameter" diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 1d3c2cb963..b264814de5 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -740,6 +740,13 @@ void PrintConfigDef::init_common_params() def->cli = ConfigOptionDef::nocli; def->set_default_value(new ConfigOptionBool(false)); + def = this->add("printer_agent", coString); + def->label = L("Printer Agent"); + def->tooltip = L("Select the network agent implementation for printer communication."); + def->mode = comAdvanced; + def->cli = ConfigOptionDef::nocli; + def->set_default_value(new ConfigOptionString("")); + def = this->add("print_host", coString); def->label = L("Hostname, IP or URL"); def->tooltip = L("Orca Slicer can upload G-code files to a printer host. This field should contain " diff --git a/src/libslic3r/Time.cpp b/src/libslic3r/Time.cpp index 8faa14ade3..de94b20853 100644 --- a/src/libslic3r/Time.cpp +++ b/src/libslic3r/Time.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #ifdef _MSC_VER #include @@ -230,5 +231,76 @@ time_t str2time(const std::string &str, TimeZone zone, TimeFormat fmt) return str2time(ss, zone, fmtstr.c_str()); } +// ///////////////////////////////////////////////////////////////////////////// +// Millisecond timestamps for cloud sync protocol + +std::string millis_to_iso8601(long long unix_millis) +{ + time_t seconds = static_cast(unix_millis / 1000); + int millis = static_cast(unix_millis % 1000); + + std::tm tms = {}; + _gmtime_r(&seconds, &tms); + + char buf[32]; + std::snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", + tms.tm_year + 1900, + tms.tm_mon + 1, + tms.tm_mday, + tms.tm_hour, + tms.tm_min, + tms.tm_sec, + millis); + + return std::string(buf); +} + +long long iso8601_to_millis(const std::string& iso_time) +{ + if (iso_time.empty()) return -1; + + int y, M, d, h, m, s, ms = 0; + + // Try parsing with milliseconds: "2025-11-28T14:30:00.123Z" + int parsed = sscanf(iso_time.c_str(), "%d-%d-%dT%d:%d:%d.%dZ", + &y, &M, &d, &h, &m, &s, &ms); + + if (parsed < 6) { + // Try without milliseconds: "2025-11-28T14:30:00Z" + parsed = sscanf(iso_time.c_str(), "%d-%d-%dT%d:%d:%dZ", + &y, &M, &d, &h, &m, &s); + ms = 0; + } + + if (parsed < 6) return -1; + + // Normalize milliseconds (handle .1, .12, .123, .1234, etc.) + if (parsed >= 7) { + // Count digits in the fractional part to normalize + const char* dot = strchr(iso_time.c_str(), '.'); + if (dot) { + int digits = 0; + for (const char* p = dot + 1; *p && *p != 'Z' && std::isdigit(*p); ++p) + digits++; + // Normalize to 3 digits (milliseconds) + while (digits < 3) { ms *= 10; digits++; } + while (digits > 3) { ms /= 10; digits--; } + } + } + + std::tm tms = {}; + tms.tm_year = y - 1900; + tms.tm_mon = M - 1; + tms.tm_mday = d; + tms.tm_hour = h; + tms.tm_min = m; + tms.tm_sec = s; + + time_t seconds = _timegm(&tms); + if (seconds == time_t(-1)) return -1; + + return static_cast(seconds) * 1000 + ms; +} + }; // namespace Utils }; // namespace Slic3r diff --git a/src/libslic3r/Time.hpp b/src/libslic3r/Time.hpp index 1814d7af89..bed35665d1 100644 --- a/src/libslic3r/Time.hpp +++ b/src/libslic3r/Time.hpp @@ -63,6 +63,15 @@ inline time_t parse_iso_utc_timestamp(const std::string &str) return str2time(str, TimeZone::utc, TimeFormat::iso8601Z); } +// ///////////////////////////////////////////////////////////////////////////// +// Millisecond timestamps for cloud sync protocol +// Format: "2025-11-28T14:30:00.123Z" (ISO 8601 with milliseconds) + +// Lossless conversion: Unix milliseconds <-> ISO 8601 +// Format: "YYYY-MM-DDTHH:MM:SS.sssZ" (always 3 decimal places for milliseconds) +std::string millis_to_iso8601(long long unix_millis); +long long iso8601_to_millis(const std::string& iso_time); // Returns -1 on parse error + // ///////////////////////////////////////////////////////////////////////////// } // namespace Utils diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 051b5e5519..d016900572 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -598,6 +598,24 @@ set(SLIC3R_GUI_SOURCES Utils/MKS.hpp Utils/NetworkAgent.cpp Utils/NetworkAgent.hpp + Utils/NetworkAgentFactory.hpp + Utils/NetworkAgentFactory.cpp + Utils/ICloudServiceAgent.hpp + Utils/IPrinterAgent.hpp + Utils/OrcaCloudServiceAgent.cpp + Utils/OrcaCloudServiceAgent.hpp + Utils/OrcaPrinterAgent.cpp + Utils/OrcaPrinterAgent.hpp + Utils/QidiPrinterAgent.cpp + Utils/QidiPrinterAgent.hpp + Utils/MoonrakerPrinterAgent.cpp + Utils/MoonrakerPrinterAgent.hpp + Utils/BBLCloudServiceAgent.cpp + Utils/BBLCloudServiceAgent.hpp + Utils/BBLPrinterAgent.cpp + Utils/BBLPrinterAgent.hpp + Utils/BBLNetworkPlugin.cpp + Utils/BBLNetworkPlugin.hpp Utils/Obico.cpp Utils/Obico.hpp Utils/OctoPrint.cpp diff --git a/src/slic3r/GUI/BindDialog.cpp b/src/slic3r/GUI/BindDialog.cpp index 3847f8f2f1..29d84f67b0 100644 --- a/src/slic3r/GUI/BindDialog.cpp +++ b/src/slic3r/GUI/BindDialog.cpp @@ -869,7 +869,7 @@ void BindMachineDialog::on_show(wxShowEvent &event) m_printer_name->SetLabelText(from_u8(m_machine_info->get_dev_name())); if (wxGetApp().is_user_login()) { - wxString username_text = from_u8(wxGetApp().getAgent()->get_user_nickanme()); + wxString username_text = from_u8(wxGetApp().getAgent()->get_user_nickname()); m_user_name->SetLabelText(username_text); std::string avatar_url = wxGetApp().getAgent()->get_user_avatar(); diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index f4362cc3f7..4a871e6bdb 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -255,6 +255,8 @@ public: long tray_read_done_bits = 0; long tray_reading_bits = 0; bool ams_air_print_status { false }; + /** Whether this printer supports virtual trays (external/manual filament loading). + * When true, vt_slot data is used by build_filament_ams_list() to include external filaments. */ bool ams_support_virtual_tray { true }; time_t ams_user_setting_start = 0; time_t ams_switch_filament_start = 0; @@ -856,7 +858,16 @@ public: bool is_enable_np{ false }; bool is_enable_ams_np{ false }; - /*vi slot data*/ + /** + * Virtual Tray (vt_slot) - External/manual filament loading slots. + * + * Data Flow: Populated from printer JSON via parse_vt_tray() during MachineObject::parse_json(). + * Used by: Sidebar::build_filament_ams_list() when ams_support_virtual_tray is true. + * + * Virtual trays represent filament that is manually loaded into the extruder + * rather than fed through an AMS unit. This supports printers without AMS + * or scenarios where users want to bypass the AMS. + */ std::vector vt_slot; DevAmsTray parse_vt_tray(json vtray); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 841db411c7..73e09c5556 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -119,6 +119,10 @@ #include "ModelMall.hpp" #include "HintNotification.hpp" +#include "slic3r/Utils/NetworkAgentFactory.hpp" +#include "slic3r/Utils/BBLNetworkPlugin.hpp" +#include "slic3r/Utils/bambu_networking.hpp" + //#ifdef WIN32 //#include "BaseException.h" //#endif @@ -1176,9 +1180,9 @@ std::string GUI_App::get_plugin_url(std::string name, std::string country_code) curr_version = BAMBU_NETWORK_AGENT_VERSION_LEGACY; } else if (name == "plugins" && app_config) { std::string user_version = app_config->get_network_plugin_version(); - curr_version = user_version.empty() ? BBL::get_latest_network_version() : user_version; + curr_version = user_version.empty() ? get_latest_network_version() : user_version; } else { - curr_version = BBL::get_latest_network_version(); + curr_version = get_latest_network_version(); } std::string using_version = curr_version.substr(0, 9) + "00"; @@ -1519,7 +1523,7 @@ int GUI_App::install_plugin(std::string name, std::string package_name, InstallP if (name == "plugins") { std::string config_version = app_config->get_network_plugin_version(); if (config_version.empty()) { - config_version = BBL::get_latest_network_version(); + config_version = get_latest_network_version(); BOOST_LOG_TRIVIAL(info) << "[install_plugin] config_version was empty, using latest: " << config_version; app_config->set_network_plugin_version(config_version); GUI::wxGetApp().CallAfter([this] { @@ -1814,7 +1818,7 @@ bool GUI_App::hot_reload_network_plugin() std::string GUI_App::get_latest_network_version() const { - return BBL::get_latest_network_version(); + return Slic3r::get_latest_network_version(); } bool GUI_App::has_network_update_available() const @@ -1919,9 +1923,9 @@ bool GUI_App::check_networking_version() studio_ver = BAMBU_NETWORK_AGENT_VERSION_LEGACY; } else if (app_config) { std::string user_version = app_config->get_network_plugin_version(); - studio_ver = user_version.empty() ? BBL::get_latest_network_version() : user_version; + studio_ver = user_version.empty() ? get_latest_network_version() : user_version; } else { - studio_ver = BBL::get_latest_network_version(); + studio_ver = get_latest_network_version(); } BOOST_LOG_TRIVIAL(info) << "check_networking_version: network_ver=" << network_ver << ", expected=" << studio_ver; @@ -2233,6 +2237,8 @@ GUI_App::~GUI_App() } StaticBambuLib::release(); + BBLNetworkPlugin::shutdown(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": exit"); } @@ -3323,7 +3329,7 @@ __retry: std::string loaded_version = Slic3r::NetworkAgent::get_version(); if (app_config && !loaded_version.empty() && loaded_version != "00.00.00.00") { std::string config_version = app_config->get_network_plugin_version(); - std::string config_base = BBL::extract_base_version(config_version); + std::string config_base = extract_base_version(config_version); if (config_base != loaded_version) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": syncing config version from " << config_version << " to loaded " << loaded_version; app_config->set(SETTING_NETWORK_PLUGIN_VERSION, loaded_version); @@ -3370,7 +3376,12 @@ __retry: //std::string data_dir = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); std::string data_directory = data_dir(); - m_agent = new Slic3r::NetworkAgent(data_directory); + // Register all printer agents before creating the network agent + Slic3r::NetworkAgentFactory::register_all_agents(); + + // m_agent = new Slic3r::NetworkAgent(data_directory); + std::unique_ptr agent_ptr = Slic3r::create_agent_from_config(data_directory, app_config); + m_agent = agent_ptr.release(); if (!m_device_manager) m_device_manager = new Slic3r::DeviceManager(m_agent); @@ -3460,6 +3471,82 @@ unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) )); } +void GUI_App::switch_printer_agent(const std::string& agent_id) +{ + if (!m_agent) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": no agent exists"; + return; + } + + // Use registry to validate and create agent + // If empty, use default + std::string effective_agent_id = agent_id.empty() ? NetworkAgentFactory::get_default_printer_agent_id() : agent_id; + + // Check if agent is registered + if (!NetworkAgentFactory::is_printer_agent_registered(effective_agent_id)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": unregistered agent ID '" << effective_agent_id + << "', keeping current agent"; + // Keep current agent, don't switch + return; + } + + std::string current_agent_id; + if (m_agent && m_agent->get_printer_agent()) + current_agent_id = m_agent->get_printer_agent()->get_agent_info().id; + + if (!current_agent_id.empty() && current_agent_id == effective_agent_id) { + return; + } + + std::string log_dir = data_dir(); + std::shared_ptr cloud_agent = m_agent->get_cloud_agent(); + if (!cloud_agent) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": no cloud agent available"; + return; + } + + // Create new printer agent via registry + std::shared_ptr new_printer_agent = + NetworkAgentFactory::create_printer_agent_by_id(effective_agent_id, cloud_agent, log_dir); + + if (!new_printer_agent) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id + << "', keeping current agent"; + return; + } + + // Swap the agent + m_agent->set_printer_agent(new_printer_agent); + + // Update dependent managers + if (m_device_manager) { + m_device_manager->set_agent(m_agent); + + // If there's a selected machine that was deferred due to no printer agent, + // trigger a connection now that the agent is ready + MachineObject* selected = m_device_manager->get_selected_machine(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": checking for deferred connection - selected=" + << (selected ? selected->get_dev_id() : "null"); + if (selected) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": selected machine - is_lan_mode=" << selected->is_lan_mode_printer() + << " is_connected=" << selected->is_connected(); + } + if (selected && selected->is_lan_mode_printer() && !selected->is_connected()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": connecting deferred LAN machine dev_id=" << selected->get_dev_id(); +#if !BBL_RELEASE_TO_PUBLIC + selected->connect(app_config->get("enable_ssl_for_mqtt") == "true" ? true : false); +#else + selected->connect(selected->local_use_ssl_for_mqtt); +#endif + selected->set_lan_mode_connection_state(true); + } + } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": m_device_manager is null, cannot check for deferred connection"; + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id; +} + bool GUI_App::dark_mode() { #ifdef SUPPORT_DARK_MODE @@ -4283,10 +4370,14 @@ void GUI_App::get_login_info() GUI::wxGetApp().run_script(strJS); } else { - m_agent->user_logout(); - std::string logout_cmd = m_agent->build_logout_cmd(); - wxString strJS = wxString::Format("window.postMessage(%s)", logout_cmd); - GUI::wxGetApp().run_script(strJS); + // OrcaNetwork performs async refresh on startup; avoid clearing + // persisted tokens when the UI polls before refresh completes. + if (m_agent->get_version() != "orca_network") { + m_agent->user_logout(); + std::string logout_cmd = m_agent->build_logout_cmd(); + wxString strJS = wxString::Format("window.postMessage(%s)", logout_cmd); + GUI::wxGetApp().run_script(strJS); + } } mainframe->m_webview->SetLoginPanelVisibility(true); } @@ -5635,7 +5726,7 @@ void GUI_App::sync_preset(Preset* preset) if (!new_setting_id.empty()) { setting_id = new_setting_id; result = 0; - auto update_time_str = values_map[BBL_JSON_KEY_UPDATE_TIME]; + auto update_time_str = values_map[ORCA_JSON_KEY_UPDATE_TIME]; if (!update_time_str.empty()) update_time = std::atoll(update_time_str.c_str()); } @@ -5664,7 +5755,7 @@ void GUI_App::sync_preset(Preset* preset) if (!new_setting_id.empty()) { setting_id = new_setting_id; result = 0; - auto update_time_str = values_map[BBL_JSON_KEY_UPDATE_TIME]; + auto update_time_str = values_map[ORCA_JSON_KEY_UPDATE_TIME]; if (!update_time_str.empty()) update_time = std::atoll(update_time_str.c_str()); } else { @@ -5690,16 +5781,16 @@ void GUI_App::sync_preset(Preset* preset) result = 0; } else { - result = m_agent->put_setting(setting_id, preset->name, &values_map, &http_code); - if (http_code >= 400) { - result = 0; - updated_info = "hold"; - BOOST_LOG_TRIVIAL(error) << "[sync_preset] put setting_id = " << setting_id << " failed, http_code = " << http_code; - } else { - auto update_time_str = values_map[BBL_JSON_KEY_UPDATE_TIME]; + result = m_agent->put_setting(setting_id, preset->name, &values_map, &http_code); + if (http_code >= 400) { + result = 0; + updated_info = "hold"; + BOOST_LOG_TRIVIAL(error) << "[sync_preset] put setting_id = " << setting_id << " failed, http_code = " << http_code; + } else { + auto update_time_str = values_map[ORCA_JSON_KEY_UPDATE_TIME]; if (!update_time_str.empty()) update_time = std::atoll(update_time_str.c_str()); - } + } } } @@ -5752,6 +5843,8 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg) return; if (!m_agent || !m_agent->is_user_login()) return; + if(!m_agent->get_cloud_agent()) + return; // has already start sync if (m_user_sync_token) return; @@ -5801,7 +5894,7 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg) auto type = info[BBL_JSON_KEY_TYPE]; auto name = info[BBL_JSON_KEY_NAME]; auto setting_id = info[BBL_JSON_KEY_SETTING_ID]; - auto update_time_str = info[BBL_JSON_KEY_UPDATE_TIME]; + auto update_time_str = info[ORCA_JSON_KEY_UPDATE_TIME]; long long update_time = 0; if (!update_time_str.empty()) update_time = std::atoll(update_time_str.c_str()); @@ -5911,6 +6004,25 @@ void GUI_App::start_http_server() if (!m_http_server.is_started()) m_http_server.start(); } + +void GUI_App::start_http_server(int port) +{ + if (port <= 0) { + start_http_server(); + return; + } + + if (m_http_server.is_started()) { + if (m_http_server.get_port() == static_cast(port)) { + return; + } + m_http_server.stop(); + } + + m_http_server.set_port(static_cast(port)); + m_http_server.start(); +} + void GUI_App::stop_http_server() { m_http_server.stop(); diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index b84dc97c54..e494d0c2bb 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -340,6 +340,10 @@ public: Slic3r::TaskManager* getTaskManager() { return m_task_manager; } HMSQuery* get_hms_query() { return hms_query; } NetworkAgent* getAgent() { return m_agent; } + + // Dynamic printer agent switching + void switch_printer_agent(const std::string& agent_id); + FilamentColorCodeQuery* get_filament_color_code_query(); bool is_editor() const { return m_app_mode == EAppMode::Editor; } bool is_gcode_viewer() const { return m_app_mode == EAppMode::GCodeViewer; } @@ -501,6 +505,7 @@ public: void start_sync_user_preset(bool with_progress_dlg = false); void stop_sync_user_preset(); void start_http_server(); + void start_http_server(int port); void stop_http_server(); void switch_staff_pick(bool on); diff --git a/src/slic3r/GUI/HttpServer.cpp b/src/slic3r/GUI/HttpServer.cpp index 83757ed4f9..a806bd6e24 100644 --- a/src/slic3r/GUI/HttpServer.cpp +++ b/src/slic3r/GUI/HttpServer.cpp @@ -182,6 +182,42 @@ std::shared_ptr HttpServer::bbl_auth_handle_request(const { BOOST_LOG_TRIVIAL(info) << "thirdparty_login: get_response"; + const std::string auth_code = url_get_param(url, "code"); + if (!auth_code.empty()) { + std::string state = url_get_param(url, "state"); + NetworkAgent* agent = wxGetApp().getAgent(); + if (!agent) { + return std::make_shared(); + } + + json payload; + payload["command"] = "user_login"; + payload["data"]["code"] = auth_code; + payload["data"]["state"] = state; + + agent->change_user(payload.dump()); + const bool login_ok = agent->is_user_login(); + if (login_ok) { + wxGetApp().request_user_login(1); + GUI::wxGetApp().CallAfter([] { wxGetApp().ShowUserLogin(false); }); + } + + const std::string title = login_ok ? "Authentication complete" : "Authentication failed"; + const std::string message = login_ok + ? "You can return to OrcaSlicer. This window will close automatically." + : "Something went wrong. Please return to OrcaSlicer and try again."; + const std::string html = + "" + "
" + "

" + title + "

" + "

" + message + "

" + "" + "
"; + return std::make_shared(html); + } + if (boost::contains(url, "access_token")) { std::string redirect_url = url_get_param(url, "redirect_url"); std::string access_token = url_get_param(url, "access_token"); @@ -249,7 +285,17 @@ void HttpServer::ResponseNotFound::write_response(std::stringstream& ssOut) void HttpServer::ResponseRedirect::write_response(std::stringstream& ssOut) { - const std::string sHTML = "

redirect to url

"; + const std::string sHTML = + "" + "" + "
" + "

Authentication complete

" + "

You can return to OrcaSlicer. If your browser does not redirect automatically, use the button below.

" + "Continue" + "" + "
"; ssOut << "HTTP/1.1 302 Found" << std::endl; ssOut << "Location: " << location_str << std::endl; ssOut << "content-type: text/html" << std::endl; @@ -258,5 +304,14 @@ void HttpServer::ResponseRedirect::write_response(std::stringstream& ssOut) ssOut << sHTML; } +void HttpServer::ResponseHtml::write_response(std::stringstream& ssOut) +{ + ssOut << "HTTP/1.1 200 OK" << std::endl; + ssOut << "content-type: text/html" << std::endl; + ssOut << "content-length: " << html.length() << std::endl; + ssOut << std::endl; + ssOut << html; +} + } // GUI } //Slic3r diff --git a/src/slic3r/GUI/HttpServer.hpp b/src/slic3r/GUI/HttpServer.hpp index 5c1547f013..91e3cd9c15 100644 --- a/src/slic3r/GUI/HttpServer.hpp +++ b/src/slic3r/GUI/HttpServer.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #define LOCALHOST_PORT 13618 #define LOCALHOST_URL "http://localhost:" @@ -98,6 +99,16 @@ public: void write_response(std::stringstream& ssOut) override; }; + class ResponseHtml : public Response + { + const std::string html; + + public: + explicit ResponseHtml(std::string html) : html(std::move(html)) {} + ~ResponseHtml() override = default; + void write_response(std::stringstream& ssOut) override; + }; + HttpServer(boost::asio::ip::port_type port = LOCALHOST_PORT); boost::thread m_http_server_thread; @@ -106,6 +117,8 @@ public: bool is_started() { return start_http_server; } void start(); void stop(); + void set_port(boost::asio::ip::port_type new_port) { port = new_port; } + boost::asio::ip::port_type get_port() const { return port; } void set_request_handler(const std::function(const std::string&)>& m_request_handler); static std::shared_ptr bbl_auth_handle_request(const std::string& url); diff --git a/src/slic3r/GUI/Jobs/BindJob.cpp b/src/slic3r/GUI/Jobs/BindJob.cpp index 49e6455cd9..f40736525d 100644 --- a/src/slic3r/GUI/Jobs/BindJob.cpp +++ b/src/slic3r/GUI/Jobs/BindJob.cpp @@ -66,22 +66,22 @@ void BindJob::process(Ctl &ctl) result_code = code; result_info = info; - if (stage == BBL::BindJobStage::LoginStageConnect) { + if (stage == BindJobStage::LoginStageConnect) { curr_percent = 15; msg = _u8L("Logging in"); - } else if (stage == BBL::BindJobStage::LoginStageLogin) { + } else if (stage == BindJobStage::LoginStageLogin) { curr_percent = 30; msg = _u8L("Logging in"); - } else if (stage == BBL::BindJobStage::LoginStageWaitForLogin) { + } else if (stage == BindJobStage::LoginStageWaitForLogin) { curr_percent = 45; msg = _u8L("Logging in"); - } else if (stage == BBL::BindJobStage::LoginStageGetIdentify) { + } else if (stage == BindJobStage::LoginStageGetIdentify) { curr_percent = 60; msg = _u8L("Logging in"); - } else if (stage == BBL::BindJobStage::LoginStageWaitAuth) { + } else if (stage == BindJobStage::LoginStageWaitAuth) { curr_percent = 80; msg = _u8L("Logging in"); - } else if (stage == BBL::BindJobStage::LoginStageFinished) { + } else if (stage == BindJobStage::LoginStageFinished) { curr_percent = 100; msg = _u8L("Logging in"); } else { diff --git a/src/slic3r/GUI/Jobs/PrintJob.cpp b/src/slic3r/GUI/Jobs/PrintJob.cpp index f07e89aad7..a09bca61fe 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.cpp +++ b/src/slic3r/GUI/Jobs/PrintJob.cpp @@ -197,7 +197,7 @@ void PrintJob::process(Ctl &ctl) this->task_bed_type = bed_type_to_gcode_string(plate_data.is_valid ? plate_data.bed_type : curr_plate->get_bed_type(true)); } - BBL::PrintParams params; + PrintParams params; // local print access params.dev_ip = m_dev_ip; @@ -382,14 +382,14 @@ void PrintJob::process(Ctl &ctl) StagePercentPoint ](int stage, int code, std::string info) { - if (stage == BBL::SendingPrintJobStage::PrintingStageCreate && !is_try_lan_mode_failed) { + if (stage == SendingPrintJobStage::PrintingStageCreate && !is_try_lan_mode_failed) { if (this->connection_type == "lan") { msg = _u8L("Sending print job over LAN"); } else { msg = _u8L("Sending print job through cloud service"); } } - else if (stage == BBL::SendingPrintJobStage::PrintingStageUpload && !is_try_lan_mode_failed) { + else if (stage == SendingPrintJobStage::PrintingStageUpload && !is_try_lan_mode_failed) { if (code >= 0 && code <= 100 && !info.empty()) { if (this->connection_type == "lan") { msg = _u8L("Sending print job over LAN"); @@ -399,24 +399,24 @@ void PrintJob::process(Ctl &ctl) msg += format("(%s)", info); } } - else if (stage == BBL::SendingPrintJobStage::PrintingStageWaiting) { + else if (stage == SendingPrintJobStage::PrintingStageWaiting) { if (this->connection_type == "lan") { msg = _u8L("Sending print job over LAN"); } else { msg = _u8L("Sending print job through cloud service"); } } - else if (stage == BBL::SendingPrintJobStage::PrintingStageRecord && !is_try_lan_mode) { + else if (stage == SendingPrintJobStage::PrintingStageRecord && !is_try_lan_mode) { msg = _u8L("Sending print configuration"); } - else if (stage == BBL::SendingPrintJobStage::PrintingStageSending && !is_try_lan_mode) { + else if (stage == SendingPrintJobStage::PrintingStageSending && !is_try_lan_mode) { if (this->connection_type == "lan") { msg = _u8L("Sending print job over LAN"); } else { msg = _u8L("Sending print job through cloud service"); } } - else if (stage == BBL::SendingPrintJobStage::PrintingStageFinished) { + else if (stage == SendingPrintJobStage::PrintingStageFinished) { msg = format(_u8L("Successfully sent. Will automatically jump to the device page in %ss"), info); if (m_print_job_completed_id == wxGetApp().plater()->get_send_calibration_finished_event()) { msg = format(_u8L("Successfully sent. Will automatically jump to the next page in %ss"), info); @@ -433,15 +433,15 @@ void PrintJob::process(Ctl &ctl) // update current percnet if (stage >= 0 && stage <= (int) PrintingStageFinished) { curr_percent = StagePercentPoint[stage]; - if ((stage == BBL::SendingPrintJobStage::PrintingStageUpload - || stage == BBL::SendingPrintJobStage::PrintingStageRecord) + if ((stage == SendingPrintJobStage::PrintingStageUpload + || stage == SendingPrintJobStage::PrintingStageRecord) && (code > 0 && code <= 100)) { curr_percent = (StagePercentPoint[stage + 1] - StagePercentPoint[stage]) * code / 100 + StagePercentPoint[stage]; } } //get errors - if (code > 100 || code < 0 || stage == BBL::SendingPrintJobStage::PrintingStageERROR) { + if (code > 100 || code < 0 || stage == SendingPrintJobStage::PrintingStageERROR) { if (code == BAMBU_NETWORK_ERR_PRINT_WR_FILE_OVER_SIZE || code == BAMBU_NETWORK_ERR_PRINT_SP_FILE_OVER_SIZE) { m_plater->update_print_error_info(code, desc_file_too_large, info); }else if (code == BAMBU_NETWORK_ERR_PRINT_WR_FILE_NOT_EXIST || code == BAMBU_NETWORK_ERR_PRINT_SP_FILE_NOT_EXIST){ diff --git a/src/slic3r/GUI/Jobs/SendJob.cpp b/src/slic3r/GUI/Jobs/SendJob.cpp index 4d14ab2dcc..578d0d804b 100644 --- a/src/slic3r/GUI/Jobs/SendJob.cpp +++ b/src/slic3r/GUI/Jobs/SendJob.cpp @@ -100,10 +100,10 @@ inline std::string get_transform_string(int bytes) void SendJob::process(Ctl &ctl) { - BBL::PrintParams params; + PrintParams params; std::string msg; int curr_percent = 10; - NetworkAgent* m_agent = wxGetApp().getAgent(); + NetworkAgent* agent = wxGetApp().getAgent(); AppConfig* config = wxGetApp().app_config; int result = -1; std::string http_body; @@ -234,14 +234,14 @@ void SendJob::process(Ctl &ctl) // update current percnet if (stage >= 0 && stage <= (int) PrintingStageFinished) { curr_percent = StagePercentPoint[stage]; - if ((stage == BBL::SendingPrintJobStage::PrintingStageUpload) && + if ((stage == SendingPrintJobStage::PrintingStageUpload) && (code > 0 && code <= 100)) { curr_percent = (StagePercentPoint[stage + 1] - StagePercentPoint[stage]) * code / 100 + StagePercentPoint[stage]; } } //get errors - if (code > 100 || code < 0 || stage == BBL::SendingPrintJobStage::PrintingStageERROR) { + if (code > 100 || code < 0 || stage == SendingPrintJobStage::PrintingStageERROR) { if (code == BAMBU_NETWORK_ERR_PRINT_WR_FILE_OVER_SIZE || code == BAMBU_NETWORK_ERR_PRINT_SP_FILE_OVER_SIZE) { m_plater->update_print_error_info(code, desc_file_too_large, info); } @@ -281,7 +281,7 @@ void SendJob::process(Ctl &ctl) // try to send local with record BOOST_LOG_TRIVIAL(info) << "send_job: try to send gcode to printer"; ctl.update_status(curr_percent, _u8L("Sending G-code file over LAN")); - result = m_agent->start_send_gcode_to_sdcard(params, update_fn, cancel_fn, nullptr); + result = agent->start_send_gcode_to_sdcard(params, update_fn, cancel_fn, nullptr); if (result == BAMBU_NETWORK_ERR_FTP_UPLOAD_FAILED) { params.comments = "upload_failed"; } else { @@ -304,8 +304,8 @@ void SendJob::process(Ctl &ctl) case DevStorage::SdcardState::HAS_SDCARD_ABNORMAL: if(this->has_sdcard) { // means the sdcard is abnormal but can be used option is enabled - ctl.update_status(curr_percent, _u8L("Sending G-code file over LAN, but the Storage in the printer is abnormal and print-issues may be caused by this.")); - result = m_agent->start_send_gcode_to_sdcard(params, update_fn, cancel_fn, nullptr); + ctl.update_status(curr_percent, _u8L("Sending G-code file over LAN, but the Storage in the printer is abnormal and print-issues may be caused by this.")); + result = agent->start_send_gcode_to_sdcard(params, update_fn, cancel_fn, nullptr); break; } ctl.update_status(curr_percent, _u8L("The Storage in the printer is abnormal. Please replace it with a normal Storage before sending to printer.")); @@ -315,7 +315,7 @@ void SendJob::process(Ctl &ctl) return; case DevStorage::SdcardState::HAS_SDCARD_NORMAL: ctl.update_status(curr_percent, _u8L("Sending G-code file over LAN")); - result = m_agent->start_send_gcode_to_sdcard(params, update_fn, cancel_fn, nullptr); + result = agent->start_send_gcode_to_sdcard(params, update_fn, cancel_fn, nullptr); break; default: ctl.update_status(curr_percent, _u8L("Encountered an unknown error with the Storage status. Please try again.")); diff --git a/src/slic3r/GUI/MediaPlayCtrl.cpp b/src/slic3r/GUI/MediaPlayCtrl.cpp index c412b5585a..98b9fd797b 100644 --- a/src/slic3r/GUI/MediaPlayCtrl.cpp +++ b/src/slic3r/GUI/MediaPlayCtrl.cpp @@ -7,6 +7,8 @@ #include "I18N.hpp" #include "MsgDialog.hpp" #include "DownloadProgressDialog.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" + #include #include diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp index 02b2306ff3..8ffa275168 100644 --- a/src/slic3r/GUI/Monitor.cpp +++ b/src/slic3r/GUI/Monitor.cpp @@ -3,6 +3,7 @@ #include "libslic3r/Model.hpp" #include "libslic3r/AppConfig.hpp" #include "slic3r/Utils/bambu_networking.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" #include #include @@ -532,8 +533,8 @@ void MonitorPanel::update_network_version_footer() return; std::string configured_version = wxGetApp().app_config->get_network_plugin_version(); - std::string suffix = BBL::extract_suffix(configured_version); - std::string configured_base = BBL::extract_base_version(configured_version); + std::string suffix = extract_suffix(configured_version); + std::string configured_base = extract_base_version(configured_version); wxString footer_text; if (!suffix.empty() && configured_base == binary_version) { diff --git a/src/slic3r/GUI/NetworkPluginDialog.cpp b/src/slic3r/GUI/NetworkPluginDialog.cpp index 5c1e158748..e37d837b4f 100644 --- a/src/slic3r/GUI/NetworkPluginDialog.cpp +++ b/src/slic3r/GUI/NetworkPluginDialog.cpp @@ -211,7 +211,7 @@ void NetworkPluginDownloadDialog::setup_version_selector() wxDefaultPosition, wxSize(FromDIP(380), FromDIP(28)), 0, nullptr, wxCB_READONLY); m_version_combo->SetFont(::Label::Body_13); - m_available_versions = BBL::get_all_available_versions(); + m_available_versions = get_all_available_versions(); for (size_t i = 0; i < m_available_versions.size(); ++i) { const auto& ver = m_available_versions[i]; wxString label; diff --git a/src/slic3r/GUI/NetworkPluginDialog.hpp b/src/slic3r/GUI/NetworkPluginDialog.hpp index 88d4a3813c..d7b392533b 100644 --- a/src/slic3r/GUI/NetworkPluginDialog.hpp +++ b/src/slic3r/GUI/NetworkPluginDialog.hpp @@ -53,7 +53,7 @@ private: wxCollapsiblePane* m_details_pane{nullptr}; std::string m_error_message; std::string m_error_details; - std::vector m_available_versions; + std::vector m_available_versions; }; class NetworkPluginRestartDialog : public DPIDialog diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index e53c7afacd..d2fa243919 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -24,6 +24,7 @@ #include "GUI.hpp" #include "GUI_App.hpp" #include "MainFrame.hpp" +#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "format.hpp" #include "Tab.hpp" #include "wxExtensions.hpp" @@ -124,8 +125,22 @@ PhysicalPrinterDialog::~PhysicalPrinterDialog() void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgroup) { m_optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value) { - if (opt_key == "host_type" || opt_key == "printhost_authorization_type") + // Special handling for printer_agent: convert fake enum index to string agent ID + if (opt_key == "printer_agent") { + try { + int selected_idx = boost::any_cast(value); + auto agents = NetworkAgentFactory::get_registered_printer_agents(); + if (selected_idx >= 0 && selected_idx < static_cast(agents.size())) { + m_config->set_key_value("printer_agent", + new ConfigOptionString(agents[selected_idx].id)); + } + } catch (const boost::bad_any_cast&) { + // If value is not an int, ignore + } this->update(); + } else if (opt_key == "host_type" || opt_key == "printhost_authorization_type") { + this->update(); + } if (opt_key == "print_host") this->update_printhost_buttons(); if (opt_key == "printhost_port") @@ -136,6 +151,55 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr m_optgroup->append_single_option_line("host_type"); + // Build printer agent dropdown from registry (only if network agent is available) + if (wxGetApp().getAgent() != nullptr) { + auto agents = NetworkAgentFactory::get_registered_printer_agents(); + + if (!agents.empty()) { + // Create a fake enum option to force a Choice widget instead of TextCtrl + // (printer_agent is coString in config, but we need a dropdown) + ConfigOptionDef def; + def.type = coEnum; + def.width = Field::def_width(); + def.label = L("Printer Agent"); + def.tooltip = L("Select the network agent implementation for printer communication. " + "Available agents are registered at startup."); + def.mode = comAdvanced; + + // Populate enum values and labels from registered agents + for (const auto& agent : agents) { + def.enum_values.push_back(agent.id); + def.enum_labels.push_back(agent.display_name); + } + + // Set initial selection based on current config value or default + const std::string current_agent = m_config->opt_string("printer_agent"); + std::string selected_agent = current_agent; + + if (selected_agent.empty()) { + selected_agent = NetworkAgentFactory::get_default_printer_agent_id(); + } + + // Verify selected agent is valid + auto it = std::find_if(agents.begin(), agents.end(), [&selected_agent](const auto& a) { return a.id == selected_agent; }); + if (it == agents.end()) { + selected_agent = NetworkAgentFactory::get_default_printer_agent_id(); + } + + // Set default value for the enum (using the index) + auto def_it = std::find_if(agents.begin(), agents.end(), [&selected_agent](const auto& a) { return a.id == selected_agent; }); + if (def_it != agents.end()) { + size_t default_idx = std::distance(agents.begin(), def_it); + def.set_default_value(new ConfigOptionInt(static_cast(default_idx))); + } + + // Create and append the option line + auto agent_option = Option(def, "printer_agent"); + Line agent_line = m_optgroup->create_single_option_line(agent_option); + m_optgroup->append_line(agent_line); + } + } + auto create_sizer_with_btn = [](wxWindow* parent, Button** btn, const std::string& icon_name, const wxString& label) { *btn = new Button(parent, label); (*btn)->SetStyle(ButtonStyle::Regular, ButtonType::Parameter); @@ -688,6 +752,31 @@ void PhysicalPrinterDialog::update_host_type(bool printer_change) } } +void PhysicalPrinterDialog::update_printer_agent_type() +{ + if (m_config == nullptr) + return; + + Field* agent_field = m_optgroup->get_field("printer_agent"); + if (!agent_field) + return; + + Choice* agent_choice = dynamic_cast(agent_field); + if (!agent_choice) + return; + + // Sync selection with current config value + const std::string current_agent = m_config->opt_string("printer_agent"); + + auto agents = NetworkAgentFactory::get_registered_printer_agents(); + for (size_t i = 0; i < agents.size(); ++i) { + if (agents[i].id == current_agent) { + agent_choice->set_value(i); + return; + } + } +} + void PhysicalPrinterDialog::update_printers() { wxBusyCursor wait; diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.hpp b/src/slic3r/GUI/PhysicalPrinterDialog.hpp index 0ba2cad54f..694e7aaf90 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.hpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.hpp @@ -60,6 +60,7 @@ public: void update(bool printer_change = false); void update_host_type(bool printer_change); + void update_printer_agent_type(); void update_preset_input(); void update_printhost_buttons(); void update_printers(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 7fdc0b0659..880bdde503 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2371,7 +2371,14 @@ void Sidebar::update_all_preset_comboboxes() } else { //p->btn_connect_printer->Show(); p->m_printer_connect->Show(); - p->m_bpButton_ams_filament->Hide(); + + // ORCA: show/hide sync-ams button based on filament sync mode + auto agent = wxGetApp().getAgent(); + if (agent && agent->get_filament_sync_mode() != FilamentSyncMode::none) + p->m_bpButton_ams_filament->Show(); + else + p->m_bpButton_ams_filament->Hide(); + auto print_btn_type = MainFrame::PrintSelectType::eExportGcode; wxString url = cfg.opt_string("print_host_webui").empty() ? cfg.opt_string("print_host") : cfg.opt_string("print_host_webui"); wxString apikey; @@ -3161,11 +3168,49 @@ void Sidebar::on_bed_type_change(BedType bed_type) p->combo_printer_bed->SetSelection(0); } +/** + * Build a map of filament configurations from the connected printer's AMS (Automatic Material System). + * + * Data Flow Architecture: + * ======================= + * This function reads pre-populated state from MachineObject - it does NOT directly call + * NetworkAgent APIs. The data pipeline is: + * + * Printer Device (MQTT/LAN messages) + * ↓ + * NetworkAgent (receives JSON, triggers OnMessageFn callbacks) + * ↓ + * MachineObject::parse_json() (updates device state) + * ├── vt_slot (std::vector) - virtual tray data for external filament + * └── DevFilaSystem → DevAms → DevAmsTray - AMS unit hierarchy + * ↓ + * build_filament_ams_list() [THIS FUNCTION] - aggregates into DynamicPrintConfig maps + * + * Data Sources: + * - obj->vt_slot: Virtual trays for external/manual filament loading (when ams_support_virtual_tray is true) + * - obj->GetFilaSystem()->GetAmsList(): Map of AMS units, each containing multiple DevAmsTray slots + * + * Return Value: + * - Map key encoding: + * - Virtual trays: 0x10000 + vt_tray.id (first/main extruder), or just vt_tray.id (secondary) + * - AMS trays: 0x10000 + (ams_id * 4 + slot_id) (main extruder), or (ams_id * 4 + slot_id) (secondary) + * - The 0x10000 flag indicates the main/right extruder + * - Map value: DynamicPrintConfig with filament properties (id, type, color, etc.) + * + * @param obj The MachineObject representing the connected printer (nullable) + * @return Map of tray indices to filament configurations + */ std::map Sidebar::build_filament_ams_list(MachineObject* obj) { std::map filament_ams_list; if (!obj) return filament_ams_list; + // For pull-mode agents (e.g., HTTP REST API), refresh DevFilaSystem first + auto* agent = wxGetApp().getDeviceManager()->get_agent(); + if (agent && agent->get_filament_sync_mode() == FilamentSyncMode::pull) { + agent->fetch_filament_info(obj->get_dev_id()); + } + auto build_tray_config = [](DevAmsTray const &tray, std::string const &name, std::string ams_id, std::string slot_id) { BOOST_LOG_TRIVIAL(info) << boost::format("build_filament_ams_list: name %1% setting_id %2% type %3% color %4%") % name % tray.setting_id % tray.m_fila_type % tray.color; @@ -3285,7 +3330,14 @@ void Sidebar::get_small_btn_sync_pos_size(wxPoint &pt, wxSize &size) { void Sidebar::load_ams_list(MachineObject* obj) { - std::map filament_ams_list = build_filament_ams_list(obj); + std::map filament_ams_list; + + // build_filament_ams_list handles both subscription-based and non-subscription-based agents: + // - For non-subscription agents, it calls fetch_filament_info() first to populate DevFilaSystem + // - Then it always reads from DevFilaSystem to build the filament list + if (obj) { + filament_ams_list = build_filament_ams_list(obj); + } bool device_change = false; const std::string& device = obj ? obj->get_dev_id() : ""; @@ -3315,8 +3367,9 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) wxBusyCursor cursor; // Force load ams list auto obj = wxGetApp().getDeviceManager()->get_selected_machine(); - if (obj) - GUI::wxGetApp().sidebar().load_ams_list(obj); + if (!obj) + return; + GUI::wxGetApp().sidebar().load_ams_list(obj); auto & list = wxGetApp().preset_bundle->filament_ams_list; if (list.empty()) { @@ -15134,7 +15187,7 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy // set designInfo before export and reset after export if (wxGetApp().is_user_login()) { p->model.design_info = std::make_shared(); - //p->model.design_info->Designer = wxGetApp().getAgent()->get_user_nickanme(); + //p->model.design_info->Designer = wxGetApp().getAgent()->get_user_nickname(); p->model.design_info->Designer = ""; p->model.design_info->DesignerUserId = wxGetApp().getAgent()->get_user_id(); BOOST_LOG_TRIVIAL(trace) << "design_info prepare, designer = "<< ""; diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 0dedacb9d0..c2aedae4dc 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -15,6 +15,7 @@ #include "Widgets/StaticLine.hpp" #include "Widgets/RadioGroup.hpp" #include "slic3r/Utils/bambu_networking.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" #include "DownloadProgressDialog.hpp" #ifdef __WINDOWS__ @@ -1411,6 +1412,11 @@ void PreferencesDialog::create_items() auto item_system_sync = create_item_checkbox(_L("Update built-in Presets automatically."), "", "sync_system_preset"); g_sizer->Add(item_system_sync); + auto item_token_storage = create_item_checkbox(_L("Use encrypted file for token storage"), + _L("Store authentication tokens in an encrypted file instead of the system keychain. (Requires restart)"), + SETTING_USE_ENCRYPTED_TOKEN_FILE); + g_sizer->Add(item_token_storage); + //// ONLINE > Network plugin g_sizer->Add(create_item_title(_L("Network plugin")), 1, wxEXPAND); @@ -1433,11 +1439,11 @@ void PreferencesDialog::create_items() std::string current_version = app_config->get_network_plugin_version(); if (current_version.empty()) { - current_version = BBL::get_latest_network_version(); + current_version = get_latest_network_version(); } int current_selection = 0; - m_available_versions = BBL::get_all_available_versions(); + m_available_versions = get_all_available_versions(); for (size_t i = 0; i < m_available_versions.size(); i++) { const auto& ver = m_available_versions[i]; @@ -1468,7 +1474,7 @@ void PreferencesDialog::create_items() std::string new_version = selected_ver.version; std::string old_version = app_config->get_network_plugin_version(); if (old_version.empty()) { - old_version = BBL::get_latest_network_version(); + old_version = get_latest_network_version(); } app_config->set(SETTING_NETWORK_PLUGIN_VERSION, new_version); diff --git a/src/slic3r/GUI/Preferences.hpp b/src/slic3r/GUI/Preferences.hpp index 892c1ec35c..7470d53224 100644 --- a/src/slic3r/GUI/Preferences.hpp +++ b/src/slic3r/GUI/Preferences.hpp @@ -70,7 +70,7 @@ public: ::TextInput *m_backup_interval_textinput = {nullptr}; ::ComboBox * m_network_version_combo = {nullptr}; wxBoxSizer * m_network_version_sizer = {nullptr}; - std::vector m_available_versions; + std::vector m_available_versions; wxString m_developer_mode_def; wxString m_internal_developer_mode_def; diff --git a/src/slic3r/GUI/SendMultiMachinePage.cpp b/src/slic3r/GUI/SendMultiMachinePage.cpp index 3578e4047c..569f2d561d 100644 --- a/src/slic3r/GUI/SendMultiMachinePage.cpp +++ b/src/slic3r/GUI/SendMultiMachinePage.cpp @@ -439,9 +439,9 @@ void SendMultiMachinePage::refresh_user_device() Fit(); } -BBL::PrintParams SendMultiMachinePage::request_params(MachineObject* obj) +PrintParams SendMultiMachinePage::request_params(MachineObject* obj) { - BBL::PrintParams params; + PrintParams params; //get all setting bool bed_leveling = app_config->get("print", "bed_leveling") == "1" ? true : false; @@ -734,7 +734,7 @@ void SendMultiMachinePage::on_send(wxCommandEvent& event) } - std::vector print_params; + std::vector print_params; for (auto it = m_device_items.begin(); it != m_device_items.end(); ++it) { auto obj = it->second->get_obj(); @@ -742,7 +742,7 @@ void SendMultiMachinePage::on_send(wxCommandEvent& event) if (obj && obj->is_online() && !obj->can_abort() && !obj->is_in_upgrading() && it->second->get_state_selected() == 1 && it->second->state_printable <= 2) { if (!it->second->is_blocking_printing(obj)) { - BBL::PrintParams params = request_params(obj); + PrintParams params = request_params(obj); print_params.push_back(params); } } diff --git a/src/slic3r/GUI/SendMultiMachinePage.hpp b/src/slic3r/GUI/SendMultiMachinePage.hpp index e07b37f22c..a531e01262 100644 --- a/src/slic3r/GUI/SendMultiMachinePage.hpp +++ b/src/slic3r/GUI/SendMultiMachinePage.hpp @@ -170,7 +170,7 @@ public: void on_send(wxCommandEvent& event); bool Show(bool show); - BBL::PrintParams request_params(MachineObject* obj); + PrintParams request_params(MachineObject* obj); bool get_ams_mapping_result(std::string &mapping_array_str, std::string &mapping_array_str2, std::string &ams_mapping_info); wxBoxSizer* create_item_title(wxString title, wxWindow* parent, wxString tooltip); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index aa45495fb7..0487c2840e 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2099,6 +2099,11 @@ void Tab::on_presets_changed() // Instead of PostEvent (EVT_TAB_PRESETS_CHANGED) just call update_presets wxGetApp().plater()->sidebar().update_presets(m_type); + // Check if printer agent needs switching + if (m_type == Preset::TYPE_PRINTER) { + update_printer_agent_if_needed(); + } + bool is_bbl_vendor_preset = m_preset_bundle->is_bbl_vendor(); if (is_bbl_vendor_preset) { wxGetApp().plater()->get_partplate_list().set_render_option(true, true); @@ -2129,6 +2134,28 @@ void Tab::on_presets_changed() wxGetApp().plater()->update_project_dirty_from_presets(); } +void Tab::update_printer_agent_if_needed() +{ + std::string agent_id = "orca"; + if (m_preset_bundle) { + if (wxGetApp().preset_bundle->is_bbl_vendor()) { + agent_id = "bbl"; + } else if (wxGetApp().preset_bundle->is_qidi_vendor()) { + agent_id = "qidi"; + } + } + + const DynamicPrintConfig& config = m_preset_bundle->printers.get_edited_preset().config; + if (config.has("printer_agent")) { + std::string value = config.option("printer_agent")->value; + if (!value.empty()) { + agent_id = value; + } + } + // Switch agent in GUI_App + wxGetApp().switch_printer_agent(agent_id); +} + void Tab::build_preset_description_line(ConfigOptionsGroup* optgroup) { auto description_line = [this](wxWindow* parent) { diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 96b35b26e1..9421d88ce8 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -437,6 +437,7 @@ protected: // return true if cancelled bool tree_sel_change_delayed(wxCommandEvent& event); void on_presets_changed(); + void update_printer_agent_if_needed(); void build_preset_description_line(ConfigOptionsGroup* optgroup); void update_preset_description_line(); void update_frequently_changed_parameters(); diff --git a/src/slic3r/GUI/TaskManager.cpp b/src/slic3r/GUI/TaskManager.cpp index 0c4169ad9a..d294d140c5 100644 --- a/src/slic3r/GUI/TaskManager.cpp +++ b/src/slic3r/GUI/TaskManager.cpp @@ -62,7 +62,7 @@ TaskState parse_task_status(int status) int TaskStateInfo::g_task_info_id = 0; -TaskStateInfo::TaskStateInfo(BBL::PrintParams param) +TaskStateInfo::TaskStateInfo(PrintParams param) : m_state(TaskState::TS_PENDING) , m_params(param) , m_sending_percent(0) @@ -103,8 +103,8 @@ TaskStateInfo::TaskStateInfo(BBL::PrintParams param) int curr_percent = 0; if (stage >= 0 && stage <= (int)PrintingStageFinished) { curr_percent = StagePercentPoint[stage]; - if ((stage == BBL::SendingPrintJobStage::PrintingStageUpload - || stage == BBL::SendingPrintJobStage::PrintingStageRecord) + if ((stage == SendingPrintJobStage::PrintingStageUpload + || stage == SendingPrintJobStage::PrintingStageRecord) && (code > 0 && code <= 100)) { curr_percent = (StagePercentPoint[stage + 1] - StagePercentPoint[stage]) * code / 100 + StagePercentPoint[stage]; BOOST_LOG_TRIVIAL(trace) << "task_manager: percent = " << curr_percent; @@ -156,7 +156,7 @@ TaskManager::TaskManager(NetworkAgent* agent) } -int TaskManager::start_print(const std::vector& params, TaskSettings* settings) +int TaskManager::start_print(const std::vector& params, TaskSettings* settings) { BOOST_LOG_TRIVIAL(info) << "task_manager: start_print size = " << params.size(); TaskManager::MaxSendingAtSameTime = settings->max_sending_at_same_time; @@ -173,7 +173,7 @@ int TaskManager::start_print(const std::vector& params, TaskSe return 0; } -static int start_print_test(BBL::PrintParams& params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +static int start_print_test(PrintParams& params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { int tick = 2; for (int i = 0; i < 100 * tick; i++) { @@ -321,7 +321,7 @@ std::map TaskManager::get_task_list(int curr_page, i { std::map out; if (m_agent) { - BBL::TaskQueryParams task_query_params; + TaskQueryParams task_query_params; task_query_params.limit = page_count; task_query_params.offset = curr_page * page_count; std::string task_info; diff --git a/src/slic3r/GUI/TaskManager.hpp b/src/slic3r/GUI/TaskManager.hpp index aacdc1095e..5c42d352d6 100644 --- a/src/slic3r/GUI/TaskManager.hpp +++ b/src/slic3r/GUI/TaskManager.hpp @@ -33,7 +33,7 @@ public: static int g_task_info_id; typedef std::function StateChangedFn; - TaskStateInfo(const BBL::PrintParams param); + TaskStateInfo(const PrintParams param); TaskStateInfo() { task_info_id = ++TaskStateInfo::g_task_info_id; @@ -47,9 +47,9 @@ public: m_state_changed_fn(m_state, m_sending_percent); } } - BBL::PrintParams get_params() { return m_params; } + PrintParams get_params() { return m_params; } - BBL::PrintParams& params() { return m_params; } + PrintParams& params() { return m_params; } std::string get_job_id(){return profile_id;} @@ -109,7 +109,7 @@ private: TaskState m_state; std::string m_task_name; std::string m_device_name; - BBL::PrintParams m_params; + PrintParams m_params; int m_sending_percent; std::string m_job_id; StateChangedFn m_state_changed_fn; @@ -147,7 +147,7 @@ public: static int SendingInterval; TaskManager(NetworkAgent* agent); - int start_print(const std::vector& params, TaskSettings* settings = nullptr); + int start_print(const std::vector& params, TaskSettings* settings = nullptr); static void set_max_send_at_same_time(int count); diff --git a/src/slic3r/GUI/WebUserLoginDialog.cpp b/src/slic3r/GUI/WebUserLoginDialog.cpp index 2522efbf4f..58f7b575ef 100644 --- a/src/slic3r/GUI/WebUserLoginDialog.cpp +++ b/src/slic3r/GUI/WebUserLoginDialog.cpp @@ -75,15 +75,11 @@ ZUserLogin::ZUserLogin() : wxDialog((wxWindow *) (wxGetApp().mainframe), wxID_AN CentreOnParent(); } else { - std::string host_url = agent->get_bambulab_host(); - TargetUrl = host_url + "/sign-in"; - m_networkOk = false; - + // Get the login URL from the cloud service agent wxString strlang = wxGetApp().current_language_code_safe(); - if (strlang != "") { - strlang.Replace("_", "-"); - TargetUrl = host_url + "/" + strlang + "/sign-in"; - } + strlang.Replace("_", "-"); + TargetUrl = wxString::FromUTF8(agent->get_cloud_login_url(strlang.ToStdString())); + m_networkOk = TargetUrl.StartsWith("file://"); BOOST_LOG_TRIVIAL(info) << "login url = " << TargetUrl.ToStdString(); @@ -225,9 +221,9 @@ void ZUserLogin::OnDocumentLoaded(wxWebViewEvent &evt) // Only notify if the document is the main frame, not a subframe wxString tmpUrl = evt.GetURL(); NetworkAgent* agent = wxGetApp().getAgent(); - std::string strHost = agent->get_bambulab_host(); + std::string strHost = agent->get_cloud_service_host(); - if ( tmpUrl.Contains(strHost) ) { + if (tmpUrl.StartsWith("file://") || tmpUrl.Contains(strHost)) { m_networkOk = true; // wxLogMessage("%s", "Document loaded; url='" + evt.GetURL() + "'"); } @@ -268,28 +264,97 @@ void ZUserLogin::OnFullScreenChanged(wxWebViewEvent &evt) void ZUserLogin::OnScriptMessage(wxWebViewEvent &evt) { wxString str_input = evt.GetString(); + BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] OnScriptMessage received: " << str_input.ToStdString(); + try { json j = json::parse(into_u8(str_input)); + BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] Parsed JSON successfully"; + wxString strCmd = j["command"]; + BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] Command: " << strCmd.ToStdString(); + + NetworkAgent* agent = wxGetApp().getAgent(); + if (agent && strCmd == "get_login_cmd" && agent->get_cloud_agent()) { + // Return login config (backend_url, apikey, pkce) + // WebView handles provider selection internally + std::string login_cmd = agent->build_login_cmd(); + m_loopback_port = 0; + try { + json cfg = json::parse(login_cmd); + if (cfg.contains("pkce")) { + const auto& pkce = cfg["pkce"]; + if (pkce.contains("loopback_port")) { + if (pkce["loopback_port"].is_number_integer()) { + m_loopback_port = pkce["loopback_port"].get(); + } else if (pkce["loopback_port"].is_string()) { + m_loopback_port = std::stoi(pkce["loopback_port"].get()); + } + } + + if (m_loopback_port <= 0 && pkce.contains("redirect_uri") && pkce["redirect_uri"].is_string()) { + const std::string redirect_uri = pkce["redirect_uri"].get(); + const char* prefixes[] = {"localhost:", "127.0.0.1:"}; + for (const char* prefix : prefixes) { + auto start = redirect_uri.find(prefix); + if (start == std::string::npos) + continue; + start += strlen(prefix); + auto end = redirect_uri.find('/', start); + std::string port_str = redirect_uri.substr(start, end - start); + try { + m_loopback_port = std::stoi(port_str); + } catch (...) { + m_loopback_port = 0; + } + break; + } + } + } + } catch (...) { + m_loopback_port = 0; + } + wxString str_js = wxString::FromUTF8("window.postMessage(") + wxString::FromUTF8(login_cmd.c_str()) + + wxString::FromUTF8(", '*')"); + this->RunScript(str_js); + return; + } if (strCmd == "autotest_token") { m_AutotestToken = j["data"]["token"]; + BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] Stored autotest_token"; } if (strCmd == "user_login") { + BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] Processing user_login command"; + BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] User data: " << j["data"].dump(); + j["data"]["autotest_token"] = m_AutotestToken; - wxGetApp().handle_script_message(j.dump()); - Close(); + std::string message_json = j.dump(); + + BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] Calling handle_script_message with: " << message_json; + + // End modal dialog first to unblock event loop before processing callbacks + BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] Ending modal dialog"; + EndModal(wxID_OK); + + // Handle message after modal dialog ends to avoid deadlock + // Use wxTheApp->CallAfter to ensure it runs after modal loop exits + wxTheApp->CallAfter([message_json]() { + BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] Processing login message after modal ended"; + wxGetApp().handle_script_message(message_json); + }); } else if (strCmd == "get_localhost_url") { - BOOST_LOG_TRIVIAL(info) << "thirdparty_login: get_localhost_url"; - wxGetApp().start_http_server(); + BOOST_LOG_TRIVIAL(debug) << "thirdparty_login: get_localhost_url"; + int loopback_port = m_loopback_port > 0 ? m_loopback_port : LOCALHOST_PORT; + wxGetApp().start_http_server(loopback_port); std::string sequence_id = j["sequence_id"].get(); CallAfter([this, sequence_id] { json ack_j; ack_j["command"] = "get_localhost_url"; - ack_j["response"]["base_url"] = std::string(LOCALHOST_URL) + std::to_string(LOCALHOST_PORT); + int loopback_port = m_loopback_port > 0 ? m_loopback_port : LOCALHOST_PORT; + ack_j["response"]["base_url"] = std::string(LOCALHOST_URL) + std::to_string(loopback_port); ack_j["response"]["result"] = "success"; ack_j["sequence_id"] = sequence_id; wxString str_js = wxString::Format("window.postMessage(%s)", ack_j.dump()); @@ -300,6 +365,8 @@ void ZUserLogin::OnScriptMessage(wxWebViewEvent &evt) BOOST_LOG_TRIVIAL(info) << "thirdparty_login: thirdparty_login"; if (j["data"].contains("url")) { std::string jump_url = j["data"]["url"].get(); + int loopback_port = m_loopback_port > 0 ? m_loopback_port : LOCALHOST_PORT; + wxGetApp().start_http_server(loopback_port); CallAfter([this, jump_url] { wxString url = wxString::FromUTF8(jump_url); wxLaunchDefaultBrowser(url); diff --git a/src/slic3r/GUI/WebUserLoginDialog.hpp b/src/slic3r/GUI/WebUserLoginDialog.hpp index 3d0e2629db..0493aa8722 100644 --- a/src/slic3r/GUI/WebUserLoginDialog.hpp +++ b/src/slic3r/GUI/WebUserLoginDialog.hpp @@ -71,6 +71,7 @@ private: wxWebView *m_browser; std::string m_AutotestToken; + int m_loopback_port { 0 }; #if wxUSE_WEBVIEW_IE wxMenuItem *m_script_object_el; diff --git a/src/slic3r/Utils/BBLCloudServiceAgent.cpp b/src/slic3r/Utils/BBLCloudServiceAgent.cpp new file mode 100644 index 0000000000..ee7fcf4355 --- /dev/null +++ b/src/slic3r/Utils/BBLCloudServiceAgent.cpp @@ -0,0 +1,864 @@ +#include "BBLCloudServiceAgent.hpp" +#include "BBLNetworkPlugin.hpp" + +#include + +namespace Slic3r { + +BBLCloudServiceAgent::BBLCloudServiceAgent() +{ + BOOST_LOG_TRIVIAL(info) << "BBLCloudServiceAgent: Constructor - using BBLNetworkPlugin singleton"; +} + +BBLCloudServiceAgent::~BBLCloudServiceAgent() = default; + +// ============================================================================ +// Lifecycle (merged from BBLAuthAgent) +// ============================================================================ + +int BBLCloudServiceAgent::init_log() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_init_log(); + if (func && agent) { + return func(agent); + } + return -1; +} + +int BBLCloudServiceAgent::set_config_dir(std::string config_dir) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_config_dir(); + if (func && agent) { + return func(agent, config_dir); + } + return -1; +} + +int BBLCloudServiceAgent::set_cert_file(std::string folder, std::string filename) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_cert_file(); + if (func && agent) { + return func(agent, folder, filename); + } + return -1; +} + +int BBLCloudServiceAgent::set_country_code(std::string country_code) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_country_code(); + if (func && agent) { + return func(agent, country_code); + } + return -1; +} + +int BBLCloudServiceAgent::start() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_start(); + if (func && agent) { + return func(agent); + } + return -1; +} + +// ============================================================================ +// User Session Management (merged from BBLAuthAgent) +// ============================================================================ + +int BBLCloudServiceAgent::change_user(std::string user_info) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_change_user(); + if (func && agent) { + return func(agent, user_info); + } + return -1; +} + +bool BBLCloudServiceAgent::is_user_login() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_is_user_login(); + if (func && agent) { + return func(agent); + } + return false; +} + +int BBLCloudServiceAgent::user_logout(bool request) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_user_logout(); + if (func && agent) { + return func(agent, request); + } + return -1; +} + +std::string BBLCloudServiceAgent::get_user_id() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_user_id(); + if (func && agent) { + return func(agent); + } + return ""; +} + +std::string BBLCloudServiceAgent::get_user_name() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_user_name(); + if (func && agent) { + return func(agent); + } + return ""; +} + +std::string BBLCloudServiceAgent::get_user_avatar() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_user_avatar(); + if (func && agent) { + return func(agent); + } + return ""; +} + +std::string BBLCloudServiceAgent::get_user_nickname() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_user_nickanme(); + if (func && agent) { + return func(agent); + } + return ""; +} + +// ============================================================================ +// Login UI Support (merged from BBLAuthAgent) +// ============================================================================ + +std::string BBLCloudServiceAgent::build_login_cmd() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_build_login_cmd(); + if (func && agent) { + return func(agent); + } + return ""; +} + +std::string BBLCloudServiceAgent::build_logout_cmd() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_build_logout_cmd(); + if (func && agent) { + return func(agent); + } + return ""; +} + +std::string BBLCloudServiceAgent::build_login_info() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_build_login_info(); + if (func && agent) { + return func(agent); + } + return ""; +} + +// ============================================================================ +// Token Access (merged from BBLAuthAgent) +// ============================================================================ + +std::string BBLCloudServiceAgent::get_access_token() const +{ + // BBL DLL manages tokens internally, not exposed via function pointer + // Return empty string - BBL agents inject tokens automatically + return ""; +} + +std::string BBLCloudServiceAgent::get_refresh_token() const +{ + // BBL DLL manages tokens internally, not exposed via function pointer + return ""; +} + +bool BBLCloudServiceAgent::ensure_token_fresh(const std::string& reason) +{ + // BBL DLL handles token refresh internally + // Always return true assuming the DLL manages this + (void)reason; + return true; +} + +// ============================================================================ +// Auth Callbacks (merged from BBLAuthAgent) +// ============================================================================ + +int BBLCloudServiceAgent::set_on_user_login_fn(OnUserLoginFn fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_on_user_login_fn(); + if (func && agent) { + return func(agent, fn); + } + return -1; +} + +// ============================================================================ +// Server Connectivity +// ============================================================================ + +std::string BBLCloudServiceAgent::get_cloud_service_host() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_bambulab_host(); + if (func && agent) { + return func(agent); + } + return ""; +} + +std::string BBLCloudServiceAgent::get_cloud_login_url(const std::string& language) +{ + std::string host_url = get_cloud_service_host(); + if (host_url.empty()) { + return ""; + } + + if (language.empty()) { + return host_url + "/sign-in"; + } + return host_url + "/" + language + "/sign-in"; +} + +int BBLCloudServiceAgent::connect_server() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_connect_server(); + if (func && agent) { + return func(agent); + } + return -1; +} + +bool BBLCloudServiceAgent::is_server_connected() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_is_server_connected(); + if (func && agent) { + return func(agent); + } + return false; +} + +int BBLCloudServiceAgent::refresh_connection() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_refresh_connection(); + if (func && agent) { + return func(agent); + } + return -1; +} + +int BBLCloudServiceAgent::start_subscribe(std::string module) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_start_subscribe(); + if (func && agent) { + return func(agent, module); + } + return -1; +} + +int BBLCloudServiceAgent::stop_subscribe(std::string module) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_stop_subscribe(); + if (func && agent) { + return func(agent, module); + } + return -1; +} + +int BBLCloudServiceAgent::add_subscribe(std::vector dev_list) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_add_subscribe(); + if (func && agent) { + return func(agent, dev_list); + } + return -1; +} + +int BBLCloudServiceAgent::del_subscribe(std::vector dev_list) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_del_subscribe(); + if (func && agent) { + return func(agent, dev_list); + } + return -1; +} + +void BBLCloudServiceAgent::enable_multi_machine(bool enable) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_enable_multi_machine(); + if (func && agent) { + func(agent, enable); + } +} + +// ============================================================================ +// Settings Synchronization +// ============================================================================ + +int BBLCloudServiceAgent::get_user_presets(std::map>* user_presets) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_user_presets(); + if (func && agent) { + return func(agent, user_presets); + } + return -1; +} + +std::string BBLCloudServiceAgent::request_setting_id(std::string name, std::map* values_map, unsigned int* http_code) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_request_setting_id(); + if (func && agent) { + return func(agent, name, values_map, http_code); + } + return ""; +} + +int BBLCloudServiceAgent::put_setting(std::string setting_id, std::string name, std::map* values_map, unsigned int* http_code) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_put_setting(); + if (func && agent) { + return func(agent, setting_id, name, values_map, http_code); + } + return -1; +} + +int BBLCloudServiceAgent::get_setting_list(std::string bundle_version, ProgressFn pro_fn, WasCancelledFn cancel_fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_setting_list(); + if (func && agent) { + return func(agent, bundle_version, pro_fn, cancel_fn); + } + return -1; +} + +int BBLCloudServiceAgent::get_setting_list2(std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn, WasCancelledFn cancel_fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_setting_list2(); + if (func && agent) { + return func(agent, bundle_version, chk_fn, pro_fn, cancel_fn); + } + return -1; +} + +int BBLCloudServiceAgent::delete_setting(std::string setting_id) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_delete_setting(); + if (func && agent) { + return func(agent, setting_id); + } + return -1; +} + +// ============================================================================ +// Cloud User Services +// ============================================================================ + +int BBLCloudServiceAgent::get_my_message(int type, int after, int limit, unsigned int* http_code, std::string* http_body) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_my_message(); + if (func && agent) { + return func(agent, type, after, limit, http_code, http_body); + } + return -1; +} + +int BBLCloudServiceAgent::check_user_task_report(int* task_id, bool* printable) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_check_user_task_report(); + if (func && agent) { + return func(agent, task_id, printable); + } + return -1; +} + +int BBLCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::string* http_body) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_user_print_info(); + if (func && agent) { + return func(agent, http_code, http_body); + } + return -1; +} + +int BBLCloudServiceAgent::get_user_tasks(TaskQueryParams params, std::string* http_body) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_user_tasks(); + if (func && agent) { + return func(agent, params, http_body); + } + return -1; +} + +int BBLCloudServiceAgent::get_printer_firmware(std::string dev_id, unsigned* http_code, std::string* http_body) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_printer_firmware(); + if (func && agent) { + return func(agent, dev_id, http_code, http_body); + } + return -1; +} + +int BBLCloudServiceAgent::get_task_plate_index(std::string task_id, int* plate_index) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_task_plate_index(); + if (func && agent) { + return func(agent, task_id, plate_index); + } + return -1; +} + +int BBLCloudServiceAgent::get_user_info(int* identifier) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_user_info(); + if (func && agent) { + return func(agent, identifier); + } + return -1; +} + +int BBLCloudServiceAgent::get_subtask_info(std::string subtask_id, std::string* task_json, unsigned int* http_code, std::string* http_body) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_subtask_info(); + if (func && agent) { + return func(agent, subtask_id, task_json, http_code, http_body); + } + return -1; +} + +int BBLCloudServiceAgent::get_slice_info(std::string project_id, std::string profile_id, int plate_index, std::string* slice_json) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_slice_info(); + if (func && agent) { + return func(agent, project_id, profile_id, plate_index, slice_json); + } + return -1; +} + +int BBLCloudServiceAgent::query_bind_status(std::vector query_list, unsigned int* http_code, std::string* http_body) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_query_bind_status(); + if (func && agent) { + return func(agent, query_list, http_code, http_body); + } + return -1; +} + +int BBLCloudServiceAgent::modify_printer_name(std::string dev_id, std::string dev_name) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_modify_printer_name(); + if (func && agent) { + return func(agent, dev_id, dev_name); + } + return -1; +} + +// ============================================================================ +// Model Mall & Publishing +// ============================================================================ + +int BBLCloudServiceAgent::get_camera_url(std::string dev_id, std::function callback) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_camera_url(); + if (func && agent) { + return func(agent, dev_id, callback); + } + return -1; +} + +int BBLCloudServiceAgent::get_design_staffpick(int offset, int limit, std::function callback) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_design_staffpick(); + if (func && agent) { + return func(agent, offset, limit, callback); + } + return -1; +} + +int BBLCloudServiceAgent::start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_start_publish(); + if (func && agent) { + return func(agent, params, update_fn, cancel_fn, out); + } + return -1; +} + +int BBLCloudServiceAgent::get_model_publish_url(std::string* url) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_model_publish_url(); + if (func && agent) { + return func(agent, url); + } + return -1; +} + +int BBLCloudServiceAgent::get_subtask(BBLModelTask* task, OnGetSubTaskFn getsub_fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_subtask(); + if (func && agent) { + return func(agent, task, getsub_fn); + } + return -1; +} + +int BBLCloudServiceAgent::get_model_mall_home_url(std::string* url) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_model_mall_home_url(); + if (func && agent) { + return func(agent, url); + } + return -1; +} + +int BBLCloudServiceAgent::get_model_mall_detail_url(std::string* url, std::string id) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_model_mall_detail_url(); + if (func && agent) { + return func(agent, url, id); + } + return -1; +} + +int BBLCloudServiceAgent::get_my_profile(std::string token, unsigned int* http_code, std::string* http_body) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_my_profile(); + if (func && agent) { + return func(agent, token, http_code, http_body); + } + return -1; +} + +// ============================================================================ +// Analytics & Tracking +// ============================================================================ + +int BBLCloudServiceAgent::track_enable(bool enable) +{ + m_enable_track = enable; + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_track_enable(); + if (func && agent) { + return func(agent, enable); + } + return -1; +} + +int BBLCloudServiceAgent::track_remove_files() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_track_remove_files(); + if (func && agent) { + return func(agent); + } + return -1; +} + +int BBLCloudServiceAgent::track_event(std::string evt_key, std::string content) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_track_event(); + if (func && agent) { + return func(agent, evt_key, content); + } + return -1; +} + +int BBLCloudServiceAgent::track_header(std::string header) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_track_header(); + if (func && agent) { + return func(agent, header); + } + return -1; +} + +int BBLCloudServiceAgent::track_update_property(std::string name, std::string value, std::string type) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_track_update_property(); + if (func && agent) { + return func(agent, name, value, type); + } + return -1; +} + +int BBLCloudServiceAgent::track_get_property(std::string name, std::string& value, std::string type) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_track_get_property(); + if (func && agent) { + return func(agent, name, value, type); + } + return -1; +} + +bool BBLCloudServiceAgent::get_track_enable() +{ + return m_enable_track; +} + +// ============================================================================ +// Ratings & Reviews +// ============================================================================ + +int BBLCloudServiceAgent::put_model_mall_rating(int design_id, int score, std::string content, std::vector images, unsigned int& http_code, std::string& http_error) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_put_model_mall_rating(); + if (func && agent) { + return func(agent, design_id, score, content, images, http_code, http_error); + } + return -1; +} + +int BBLCloudServiceAgent::get_oss_config(std::string& config, std::string country_code, unsigned int& http_code, std::string& http_error) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_oss_config(); + if (func && agent) { + return func(agent, config, country_code, http_code, http_error); + } + return -1; +} + +int BBLCloudServiceAgent::put_rating_picture_oss(std::string& config, std::string& pic_oss_path, std::string model_id, int profile_id, unsigned int& http_code, std::string& http_error) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_put_rating_picture_oss(); + if (func && agent) { + return func(agent, config, pic_oss_path, model_id, profile_id, http_code, http_error); + } + return -1; +} + +int BBLCloudServiceAgent::get_model_mall_rating_result(int job_id, std::string& rating_result, unsigned int& http_code, std::string& http_error) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_model_mall_rating_result(); + if (func && agent) { + return func(agent, job_id, rating_result, http_code, http_error); + } + return -1; +} + +// ============================================================================ +// Extra Features +// ============================================================================ + +int BBLCloudServiceAgent::set_extra_http_header(std::map extra_headers) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_extra_http_header(); + if (func && agent) { + return func(agent, extra_headers); + } + return -1; +} + +std::string BBLCloudServiceAgent::get_studio_info_url() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_studio_info_url(); + if (func && agent) { + return func(agent); + } + return ""; +} + +int BBLCloudServiceAgent::get_mw_user_preference(std::function callback) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_mw_user_preference(); + if (func && agent) { + return func(agent, callback); + } + return -1; +} + +int BBLCloudServiceAgent::get_mw_user_4ulist(int seed, int limit, std::function callback) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_mw_user_4ulist(); + if (func && agent) { + return func(agent, seed, limit, callback); + } + return -1; +} + +std::string BBLCloudServiceAgent::get_version() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto func = plugin.get_get_version(); + if (func) { + return func(); + } + return ""; +} + +// ============================================================================ +// Cloud Callbacks +// ============================================================================ + +int BBLCloudServiceAgent::set_on_server_connected_fn(OnServerConnectedFn fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_on_server_connected_fn(); + if (func && agent) { + return func(agent, fn); + } + return -1; +} + +int BBLCloudServiceAgent::set_on_http_error_fn(OnHttpErrorFn fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_on_http_error_fn(); + if (func && agent) { + return func(agent, fn); + } + return -1; +} + +int BBLCloudServiceAgent::set_get_country_code_fn(GetCountryCodeFn fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_get_country_code_fn(); + if (func && agent) { + return func(agent, fn); + } + return -1; +} + +int BBLCloudServiceAgent::set_queue_on_main_fn(QueueOnMainFn fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_queue_on_main_fn(); + if (func && agent) { + return func(agent, fn); + } + return -1; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/BBLCloudServiceAgent.hpp b/src/slic3r/Utils/BBLCloudServiceAgent.hpp new file mode 100644 index 0000000000..0c960cf5d7 --- /dev/null +++ b/src/slic3r/Utils/BBLCloudServiceAgent.hpp @@ -0,0 +1,136 @@ +#ifndef __BBL_CLOUD_SERVICE_AGENT_HPP__ +#define __BBL_CLOUD_SERVICE_AGENT_HPP__ + +#include "ICloudServiceAgent.hpp" +#include +#include + +namespace Slic3r { + +/** + * BBLCloudServiceAgent - BBL DLL wrapper implementation of ICloudServiceAgent. + * + * Delegates all cloud service and authentication operations to the proprietary + * BBL network DLL through function pointers obtained from BBLNetworkPlugin singleton. + * This class combines the functionality of the former BBLAuthAgent and BBLCloudServiceAgent. + */ +class BBLCloudServiceAgent : public ICloudServiceAgent { +public: + BBLCloudServiceAgent(); + ~BBLCloudServiceAgent() override; + + // ======================================================================== + // ICloudServiceAgent Interface Implementation - Auth Methods + // ======================================================================== + + // Lifecycle + int init_log() override; + int set_config_dir(std::string config_dir) override; + int set_cert_file(std::string folder, std::string filename) override; + int set_country_code(std::string country_code) override; + int start() override; + + // User Session Management + int change_user(std::string user_info) override; + bool is_user_login() override; + int user_logout(bool request = false) override; + std::string get_user_id() override; + std::string get_user_name() override; + std::string get_user_avatar() override; + std::string get_user_nickname() override; + + // Login UI Support + std::string build_login_cmd() override; + std::string build_logout_cmd() override; + std::string build_login_info() override; + + // Token Access (BBL manages tokens internally) + std::string get_access_token() const override; + std::string get_refresh_token() const override; + bool ensure_token_fresh(const std::string& reason) override; + + // Auth Callbacks + int set_on_user_login_fn(OnUserLoginFn fn) override; + + // ======================================================================== + // ICloudServiceAgent Interface Implementation - Cloud Methods + // ======================================================================== + + // Server Connectivity + std::string get_cloud_service_host() override; + std::string get_cloud_login_url(const std::string& language = "") override; + int connect_server() override; + bool is_server_connected() override; + int refresh_connection() override; + int start_subscribe(std::string module) override; + int stop_subscribe(std::string module) override; + int add_subscribe(std::vector dev_list) override; + int del_subscribe(std::vector dev_list) override; + void enable_multi_machine(bool enable) override; + + // Settings Synchronization + int get_user_presets(std::map>* user_presets) override; + std::string request_setting_id(std::string name, std::map* values_map, unsigned int* http_code) override; + int put_setting(std::string setting_id, std::string name, std::map* values_map, unsigned int* http_code) override; + int get_setting_list(std::string bundle_version, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr) override; + int get_setting_list2(std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr) override; + int delete_setting(std::string setting_id) override; + + // Cloud User Services + int get_my_message(int type, int after, int limit, unsigned int* http_code, std::string* http_body) override; + int check_user_task_report(int* task_id, bool* printable) override; + int get_user_print_info(unsigned int* http_code, std::string* http_body) override; + int get_user_tasks(TaskQueryParams params, std::string* http_body) override; + int get_printer_firmware(std::string dev_id, unsigned* http_code, std::string* http_body) override; + int get_task_plate_index(std::string task_id, int* plate_index) override; + int get_user_info(int* identifier) override; + int get_subtask_info(std::string subtask_id, std::string* task_json, unsigned int* http_code, std::string* http_body) override; + int get_slice_info(std::string project_id, std::string profile_id, int plate_index, std::string* slice_json) override; + int query_bind_status(std::vector query_list, unsigned int* http_code, std::string* http_body) override; + int modify_printer_name(std::string dev_id, std::string dev_name) override; + + // Model Mall & Publishing + int get_camera_url(std::string dev_id, std::function callback) override; + int get_design_staffpick(int offset, int limit, std::function callback) override; + int start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out) override; + int get_model_publish_url(std::string* url) override; + int get_subtask(BBLModelTask* task, OnGetSubTaskFn getsub_fn) override; + int get_model_mall_home_url(std::string* url) override; + int get_model_mall_detail_url(std::string* url, std::string id) override; + int get_my_profile(std::string token, unsigned int* http_code, std::string* http_body) override; + + // Analytics & Tracking + int track_enable(bool enable) override; + int track_remove_files() override; + int track_event(std::string evt_key, std::string content) override; + int track_header(std::string header) override; + int track_update_property(std::string name, std::string value, std::string type = "string") override; + int track_get_property(std::string name, std::string& value, std::string type = "string") override; + bool get_track_enable() override; + + // Ratings & Reviews + int put_model_mall_rating(int design_id, int score, std::string content, std::vector images, unsigned int& http_code, std::string& http_error) override; + int get_oss_config(std::string& config, std::string country_code, unsigned int& http_code, std::string& http_error) override; + int put_rating_picture_oss(std::string& config, std::string& pic_oss_path, std::string model_id, int profile_id, unsigned int& http_code, std::string& http_error) override; + int get_model_mall_rating_result(int job_id, std::string& rating_result, unsigned int& http_code, std::string& http_error) override; + + // Extra Features + int set_extra_http_header(std::map extra_headers) override; + std::string get_studio_info_url() override; + int get_mw_user_preference(std::function callback) override; + int get_mw_user_4ulist(int seed, int limit, std::function callback) override; + std::string get_version() override; + + // Cloud Callbacks + int set_on_server_connected_fn(OnServerConnectedFn fn) override; + int set_on_http_error_fn(OnHttpErrorFn fn) override; + int set_get_country_code_fn(GetCountryCodeFn fn) override; + int set_queue_on_main_fn(QueueOnMainFn fn) override; + +private: + bool m_enable_track{false}; +}; + +} // namespace Slic3r + +#endif // __BBL_CLOUD_SERVICE_AGENT_HPP__ diff --git a/src/slic3r/Utils/BBLNetworkPlugin.cpp b/src/slic3r/Utils/BBLNetworkPlugin.cpp new file mode 100644 index 0000000000..e584bed17e --- /dev/null +++ b/src/slic3r/Utils/BBLNetworkPlugin.cpp @@ -0,0 +1,823 @@ +#include "BBLNetworkPlugin.hpp" + +#include +#include +#include +#include +#include +#include "libslic3r/Utils.hpp" +#include "slic3r/Utils/FileTransferUtils.hpp" + +#if !defined(_MSC_VER) && !defined(_WIN32) +#include +#endif + +namespace Slic3r { + +#define BAMBU_SOURCE_LIBRARY "BambuSource" + +// ============================================================================ +// Singleton Implementation +// ============================================================================ + +// Static pointer initialization (null by default, created on first access) +BBLNetworkPlugin* BBLNetworkPlugin::s_instance = nullptr; + +BBLNetworkPlugin& BBLNetworkPlugin::instance() +{ + static std::once_flag flag; + std::call_once(flag, [] { + s_instance = new BBLNetworkPlugin(); + }); + return *s_instance; +} + +void BBLNetworkPlugin::shutdown() +{ + // Note: Do not call instance() after shutdown() - the singleton is destroyed. + if (s_instance) { + delete s_instance; + s_instance = nullptr; + } +} + +BBLNetworkPlugin::BBLNetworkPlugin() = default; + +BBLNetworkPlugin::~BBLNetworkPlugin() +{ + destroy_agent(); + unload(); +} + +// ============================================================================ +// Module Lifecycle +// ============================================================================ + +int BBLNetworkPlugin::initialize(bool using_backup, const std::string& version) +{ + clear_load_error(); + + std::string library; + std::string data_dir_str = data_dir(); + boost::filesystem::path data_dir_path(data_dir_str); + auto plugin_folder = data_dir_path / "plugins"; + + if (using_backup) { + plugin_folder = plugin_folder / "backup"; + } + + if (version.empty()) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": version is required but not provided"; + set_load_error( + "Network library version not specified", + "A version must be specified to load the network library", + "" + ); + return -1; + } + + // Auto-migration: If loading legacy version and versioned library doesn't exist, + // but unversioned legacy library does exist, rename it to versioned format + if (version == BAMBU_NETWORK_AGENT_VERSION_LEGACY) { + boost::filesystem::path versioned_path; + boost::filesystem::path legacy_path; +#if defined(_MSC_VER) || defined(_WIN32) + versioned_path = plugin_folder / (std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".dll"); + legacy_path = plugin_folder / (std::string(BAMBU_NETWORK_LIBRARY) + ".dll"); +#elif defined(__WXMAC__) + versioned_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".dylib"); + legacy_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".dylib"); +#else + versioned_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".so"); + legacy_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".so"); +#endif + if (!boost::filesystem::exists(versioned_path) && boost::filesystem::exists(legacy_path)) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": auto-migrating unversioned legacy library to versioned format"; + + try { + boost::filesystem::rename(legacy_path, versioned_path); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": successfully renamed " << legacy_path.string() << " to " + << versioned_path.string(); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to rename legacy library: " << e.what(); + } + } + } + + // Load versioned library +#if defined(_MSC_VER) || defined(_WIN32) + library = plugin_folder.string() + "\\" + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".dll"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": loading versioned library at " << library; +#else + #if defined(__WXMAC__) + std::string lib_ext = ".dylib"; + #else + std::string lib_ext = ".so"; + #endif + library = plugin_folder.string() + "/" + std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + lib_ext; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": loading versioned library at " << library; +#endif + +#if defined(_MSC_VER) || defined(_WIN32) + wchar_t lib_wstr[256]; + memset(lib_wstr, 0, sizeof(lib_wstr)); + ::MultiByteToWideChar(CP_UTF8, NULL, library.c_str(), strlen(library.c_str())+1, lib_wstr, sizeof(lib_wstr) / sizeof(lib_wstr[0])); + m_networking_module = LoadLibrary(lib_wstr); + if (!m_networking_module) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": versioned library not found, trying current directory"; + std::string library_path = get_libpath_in_current_directory(std::string(BAMBU_NETWORK_LIBRARY)); + if (library_path.empty()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", can not get path in current directory for %1%") % BAMBU_NETWORK_LIBRARY; + set_load_error( + "Network library not found", + "Could not locate versioned library: " + library, + library + ); + return -1; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", current path %1%")%library_path; + memset(lib_wstr, 0, sizeof(lib_wstr)); + ::MultiByteToWideChar(CP_UTF8, NULL, library_path.c_str(), strlen(library_path.c_str())+1, lib_wstr, sizeof(lib_wstr) / sizeof(lib_wstr[0])); + m_networking_module = LoadLibrary(lib_wstr); + } +#else + m_networking_module = dlopen(library.c_str(), RTLD_LAZY); + if (!m_networking_module) { + char* dll_error = dlerror(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": dlopen failed: " << (dll_error ? dll_error : "unknown error"); + set_load_error( + "Failed to load network library", + dll_error ? std::string(dll_error) : "Unknown dlopen error", + library + ); + } + printf("after dlopen, network_module is %p\n", m_networking_module); +#endif + + if (!m_networking_module) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", can not Load Library for %1%")%library; + if (!m_load_error.has_error) { + set_load_error( + "Network library failed to load", + "LoadLibrary/dlopen returned null", + library + ); + } + return -1; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", successfully loaded library %1%, module %2%")%library %m_networking_module; + + // Load file transfer interface + InitFTModule(m_networking_module); + + // Load all function pointers + load_all_function_pointers(); + + if (m_get_version) { + std::string ver = m_get_version(); + printf("network plugin version: %s\n", ver.c_str()); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": network plugin version = " << ver; + } + + return 0; +} + +int BBLNetworkPlugin::unload() +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", network module %1%")%m_networking_module; + + UnloadFTModule(); + +#if defined(_MSC_VER) || defined(_WIN32) + if (m_networking_module) { + FreeLibrary(m_networking_module); + m_networking_module = NULL; + } + if (m_source_module) { + FreeLibrary(m_source_module); + m_source_module = NULL; + } +#else + if (m_networking_module) { + dlclose(m_networking_module); + m_networking_module = NULL; + } + if (m_source_module) { + dlclose(m_source_module); + m_source_module = NULL; + } +#endif + + clear_all_function_pointers(); + + return 0; +} + +bool BBLNetworkPlugin::is_loaded() const +{ + return m_networking_module != nullptr; +} + +std::string BBLNetworkPlugin::get_version() const +{ + bool consistent = true; + // Check the debug consistent first + if (m_check_debug_consistent) { +#if defined(NDEBUG) + consistent = m_check_debug_consistent(false); +#else + consistent = m_check_debug_consistent(true); +#endif + } + if (!consistent) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", inconsistent library, return 00.00.00.00!"); + return "00.00.00.00"; + } + if (m_get_version) { + return m_get_version(); + } + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", get_version not supported, return 00.00.00.00!"); + return "00.00.00.00"; +} + +// ============================================================================ +// Agent Lifecycle +// ============================================================================ + +void* BBLNetworkPlugin::create_agent(const std::string& log_dir) +{ + if (m_agent) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": agent already exists"; + return m_agent; + } + + if (m_create_agent) { + m_agent = m_create_agent(log_dir); + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", agent=%1%, create_agent=%2%, log_dir=%3%") + % m_agent % (m_create_agent ? "yes" : "no") % log_dir; + + return m_agent; +} + +int BBLNetworkPlugin::destroy_agent() +{ + int ret = 0; + if (m_agent && m_destroy_agent) { + ret = m_destroy_agent(m_agent); + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", agent=%1%, destroy_agent=%2%, ret=%3%") + % m_agent % (m_destroy_agent ? "yes" : "no") % ret; + m_agent = nullptr; + return ret; +} + +// ============================================================================ +// DLL Module Accessors +// ============================================================================ + +#if defined(_MSC_VER) || defined(_WIN32) +HMODULE BBLNetworkPlugin::get_source_module() +#else +void* BBLNetworkPlugin::get_source_module() +#endif +{ + if ((m_source_module) || (!m_networking_module)) + return m_source_module; + + std::string library; + std::string data_dir_str = data_dir(); + boost::filesystem::path data_dir_path(data_dir_str); + auto plugin_folder = data_dir_path / "plugins"; + +#if defined(_MSC_VER) || defined(_WIN32) + wchar_t lib_wstr[128]; + + library = plugin_folder.string() + "/" + std::string(BAMBU_SOURCE_LIBRARY) + ".dll"; + memset(lib_wstr, 0, sizeof(lib_wstr)); + ::MultiByteToWideChar(CP_UTF8, NULL, library.c_str(), strlen(library.c_str())+1, lib_wstr, sizeof(lib_wstr) / sizeof(lib_wstr[0])); + m_source_module = LoadLibrary(lib_wstr); + if (!m_source_module) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", try load BambuSource directly from current directory"); + std::string library_path = get_libpath_in_current_directory(std::string(BAMBU_SOURCE_LIBRARY)); + if (library_path.empty()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", can not get path in current directory for %1%") % BAMBU_SOURCE_LIBRARY; + return m_source_module; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", current path %1%")%library_path; + memset(lib_wstr, 0, sizeof(lib_wstr)); + ::MultiByteToWideChar(CP_UTF8, NULL, library_path.c_str(), strlen(library_path.c_str()) + 1, lib_wstr, sizeof(lib_wstr) / sizeof(lib_wstr[0])); + m_source_module = LoadLibrary(lib_wstr); + } +#else +#if defined(__WXMAC__) + library = plugin_folder.string() + "/" + std::string("lib") + std::string(BAMBU_SOURCE_LIBRARY) + ".dylib"; +#else + library = plugin_folder.string() + "/" + std::string("lib") + std::string(BAMBU_SOURCE_LIBRARY) + ".so"; +#endif + m_source_module = dlopen(library.c_str(), RTLD_LAZY); +#endif + + return m_source_module; +} + +void* BBLNetworkPlugin::get_function(const char* name) +{ + void* function = nullptr; + + if (!m_networking_module) + return function; + +#if defined(_MSC_VER) || defined(_WIN32) + function = GetProcAddress(m_networking_module, name); +#else + function = dlsym(m_networking_module, name); +#endif + + if (!function) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", can not find function %1%")%name; + } + return function; +} + +// ============================================================================ +// Utility Methods +// ============================================================================ + +std::string BBLNetworkPlugin::get_libpath_in_current_directory(const std::string& library_name) +{ + std::string lib_path; +#if defined(_MSC_VER) || defined(_WIN32) + wchar_t file_name[512]; + DWORD ret = GetModuleFileNameW(NULL, file_name, 512); + if (!ret) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", GetModuleFileNameW return error, can not Load Library for %1%") % library_name; + return lib_path; + } + int size_needed = ::WideCharToMultiByte(0, 0, file_name, wcslen(file_name), nullptr, 0, nullptr, nullptr); + std::string file_name_string(size_needed, 0); + ::WideCharToMultiByte(0, 0, file_name, wcslen(file_name), file_name_string.data(), size_needed, nullptr, nullptr); + + std::size_t found = file_name_string.find("orca-slicer.exe"); + if (found == (file_name_string.size() - 16)) { + lib_path = library_name + ".dll"; + lib_path = file_name_string.replace(found, 16, lib_path); + } +#else + (void)library_name; +#endif + return lib_path; +} + +std::string BBLNetworkPlugin::get_versioned_library_path(const std::string& version) +{ + std::string data_dir_str = data_dir(); + boost::filesystem::path data_dir_path(data_dir_str); + auto plugin_folder = data_dir_path / "plugins"; + +#if defined(_MSC_VER) || defined(_WIN32) + return (plugin_folder / (std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".dll")).string(); +#elif defined(__WXMAC__) + return (plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".dylib")).string(); +#else + return (plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".so")).string(); +#endif +} + +bool BBLNetworkPlugin::versioned_library_exists(const std::string& version) +{ + if (version.empty()) return false; + std::string path = get_versioned_library_path(version); + + if (boost::filesystem::exists(path)) return true; + + if (version == BAMBU_NETWORK_AGENT_VERSION_LEGACY) { + return legacy_library_exists(); + } + + return false; +} + +bool BBLNetworkPlugin::legacy_library_exists() +{ + std::string data_dir_str = data_dir(); + boost::filesystem::path data_dir_path(data_dir_str); + auto plugin_folder = data_dir_path / "plugins"; + +#if defined(_MSC_VER) || defined(_WIN32) + auto legacy_path = plugin_folder / (std::string(BAMBU_NETWORK_LIBRARY) + ".dll"); +#elif defined(__WXMAC__) + auto legacy_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".dylib"); +#else + auto legacy_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".so"); +#endif + return boost::filesystem::exists(legacy_path); +} + +void BBLNetworkPlugin::remove_legacy_library() +{ + std::string data_dir_str = data_dir(); + boost::filesystem::path data_dir_path(data_dir_str); + auto plugin_folder = data_dir_path / "plugins"; + +#if defined(_MSC_VER) || defined(_WIN32) + auto legacy_path = plugin_folder / (std::string(BAMBU_NETWORK_LIBRARY) + ".dll"); +#elif defined(__WXMAC__) + auto legacy_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".dylib"); +#else + auto legacy_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".so"); +#endif + + if (boost::filesystem::exists(legacy_path)) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": removing legacy library at " << legacy_path.string(); + boost::system::error_code ec; + boost::filesystem::remove(legacy_path, ec); + if (ec) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to remove legacy library: " << ec.message(); + } + } +} + +std::vector BBLNetworkPlugin::scan_plugin_versions() +{ + std::vector discovered_versions; + std::string data_dir_str = data_dir(); + boost::filesystem::path plugin_folder = boost::filesystem::path(data_dir_str) / "plugins"; + + if (!boost::filesystem::is_directory(plugin_folder)) { + return discovered_versions; + } + +#if defined(_MSC_VER) || defined(_WIN32) + std::string prefix = std::string(BAMBU_NETWORK_LIBRARY) + "_"; + std::string extension = ".dll"; +#elif defined(__WXMAC__) + std::string prefix = std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_"; + std::string extension = ".dylib"; +#else + std::string prefix = std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_"; + std::string extension = ".so"; +#endif + + boost::system::error_code ec; + for (auto& entry : boost::filesystem::directory_iterator(plugin_folder, ec)) { + if (ec) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": error iterating directory: " << ec.message(); + break; + } + if (!boost::filesystem::is_regular_file(entry.status())) + continue; + + std::string filename = entry.path().filename().string(); + + if (filename.rfind(prefix, 0) != 0) + continue; + if (filename.size() <= extension.size() || + filename.compare(filename.size() - extension.size(), extension.size(), extension) != 0) + continue; + + std::string version = filename.substr(prefix.size(), + filename.size() - prefix.size() - extension.size()); + discovered_versions.push_back(version); + } + + return discovered_versions; +} + +// ============================================================================ +// Error Handling +// ============================================================================ + +void BBLNetworkPlugin::clear_load_error() +{ + m_load_error = NetworkLibraryLoadError{}; +} + +void BBLNetworkPlugin::set_load_error(const std::string& message, + const std::string& technical_details, + const std::string& attempted_path) +{ + m_load_error.has_error = true; + m_load_error.message = message; + m_load_error.technical_details = technical_details; + m_load_error.attempted_path = attempted_path; +} + +// ============================================================================ +// Legacy Helper +// ============================================================================ + +PrintParams_Legacy BBLNetworkPlugin::as_legacy(PrintParams& param) +{ + PrintParams_Legacy l; + + l.dev_id = std::move(param.dev_id); + l.task_name = std::move(param.task_name); + l.project_name = std::move(param.project_name); + l.preset_name = std::move(param.preset_name); + l.filename = std::move(param.filename); + l.config_filename = std::move(param.config_filename); + l.plate_index = param.plate_index; + l.ftp_folder = std::move(param.ftp_folder); + l.ftp_file = std::move(param.ftp_file); + l.ftp_file_md5 = std::move(param.ftp_file_md5); + l.ams_mapping = std::move(param.ams_mapping); + l.ams_mapping_info = std::move(param.ams_mapping_info); + l.connection_type = std::move(param.connection_type); + l.comments = std::move(param.comments); + l.origin_profile_id = param.origin_profile_id; + l.stl_design_id = param.stl_design_id; + l.origin_model_id = std::move(param.origin_model_id); + l.print_type = std::move(param.print_type); + l.dst_file = std::move(param.dst_file); + l.dev_name = std::move(param.dev_name); + l.dev_ip = std::move(param.dev_ip); + l.use_ssl_for_ftp = param.use_ssl_for_ftp; + l.use_ssl_for_mqtt = param.use_ssl_for_mqtt; + l.username = std::move(param.username); + l.password = std::move(param.password); + l.task_bed_leveling = param.task_bed_leveling; + l.task_flow_cali = param.task_flow_cali; + l.task_vibration_cali = param.task_vibration_cali; + l.task_layer_inspect = param.task_layer_inspect; + l.task_record_timelapse = param.task_record_timelapse; + l.task_use_ams = param.task_use_ams; + l.task_bed_type = std::move(param.task_bed_type); + l.extra_options = std::move(param.extra_options); + + return l; +} + +// ============================================================================ +// Function Pointer Loading +// ============================================================================ + +void BBLNetworkPlugin::load_all_function_pointers() +{ + m_check_debug_consistent = reinterpret_cast(get_function("bambu_network_check_debug_consistent")); + m_get_version = reinterpret_cast(get_function("bambu_network_get_version")); + m_create_agent = reinterpret_cast(get_function("bambu_network_create_agent")); + m_destroy_agent = reinterpret_cast(get_function("bambu_network_destroy_agent")); + m_init_log = reinterpret_cast(get_function("bambu_network_init_log")); + m_set_config_dir = reinterpret_cast(get_function("bambu_network_set_config_dir")); + m_set_cert_file = reinterpret_cast(get_function("bambu_network_set_cert_file")); + m_set_country_code = reinterpret_cast(get_function("bambu_network_set_country_code")); + m_start = reinterpret_cast(get_function("bambu_network_start")); + m_set_on_ssdp_msg_fn = reinterpret_cast(get_function("bambu_network_set_on_ssdp_msg_fn")); + m_set_on_user_login_fn = reinterpret_cast(get_function("bambu_network_set_on_user_login_fn")); + m_set_on_printer_connected_fn = reinterpret_cast(get_function("bambu_network_set_on_printer_connected_fn")); + m_set_on_server_connected_fn = reinterpret_cast(get_function("bambu_network_set_on_server_connected_fn")); + m_set_on_http_error_fn = reinterpret_cast(get_function("bambu_network_set_on_http_error_fn")); + m_set_get_country_code_fn = reinterpret_cast(get_function("bambu_network_set_get_country_code_fn")); + m_set_on_subscribe_failure_fn = reinterpret_cast(get_function("bambu_network_set_on_subscribe_failure_fn")); + m_set_on_message_fn = reinterpret_cast(get_function("bambu_network_set_on_message_fn")); + m_set_on_user_message_fn = reinterpret_cast(get_function("bambu_network_set_on_user_message_fn")); + m_set_on_local_connect_fn = reinterpret_cast(get_function("bambu_network_set_on_local_connect_fn")); + m_set_on_local_message_fn = reinterpret_cast(get_function("bambu_network_set_on_local_message_fn")); + m_set_queue_on_main_fn = reinterpret_cast(get_function("bambu_network_set_queue_on_main_fn")); + m_connect_server = reinterpret_cast(get_function("bambu_network_connect_server")); + m_is_server_connected = reinterpret_cast(get_function("bambu_network_is_server_connected")); + m_refresh_connection = reinterpret_cast(get_function("bambu_network_refresh_connection")); + m_start_subscribe = reinterpret_cast(get_function("bambu_network_start_subscribe")); + m_stop_subscribe = reinterpret_cast(get_function("bambu_network_stop_subscribe")); + m_add_subscribe = reinterpret_cast(get_function("bambu_network_add_subscribe")); + m_del_subscribe = reinterpret_cast(get_function("bambu_network_del_subscribe")); + m_enable_multi_machine = reinterpret_cast(get_function("bambu_network_enable_multi_machine")); + m_send_message = reinterpret_cast(get_function("bambu_network_send_message")); + m_connect_printer = reinterpret_cast(get_function("bambu_network_connect_printer")); + m_disconnect_printer = reinterpret_cast(get_function("bambu_network_disconnect_printer")); + m_send_message_to_printer = reinterpret_cast(get_function("bambu_network_send_message_to_printer")); + m_check_cert = reinterpret_cast(get_function("bambu_network_update_cert")); + m_install_device_cert = reinterpret_cast(get_function("bambu_network_install_device_cert")); + m_start_discovery = reinterpret_cast(get_function("bambu_network_start_discovery")); + m_change_user = reinterpret_cast(get_function("bambu_network_change_user")); + m_is_user_login = reinterpret_cast(get_function("bambu_network_is_user_login")); + m_user_logout = reinterpret_cast(get_function("bambu_network_user_logout")); + m_get_user_id = reinterpret_cast(get_function("bambu_network_get_user_id")); + m_get_user_name = reinterpret_cast(get_function("bambu_network_get_user_name")); + m_get_user_avatar = reinterpret_cast(get_function("bambu_network_get_user_avatar")); + m_get_user_nickanme = reinterpret_cast(get_function("bambu_network_get_user_nickanme")); + m_build_login_cmd = reinterpret_cast(get_function("bambu_network_build_login_cmd")); + m_build_logout_cmd = reinterpret_cast(get_function("bambu_network_build_logout_cmd")); + m_build_login_info = reinterpret_cast(get_function("bambu_network_build_login_info")); + m_ping_bind = reinterpret_cast(get_function("bambu_network_ping_bind")); + m_bind_detect = reinterpret_cast(get_function("bambu_network_bind_detect")); + m_set_server_callback = reinterpret_cast(get_function("bambu_network_set_server_callback")); + m_bind = reinterpret_cast(get_function("bambu_network_bind")); + m_unbind = reinterpret_cast(get_function("bambu_network_unbind")); + m_get_bambulab_host = reinterpret_cast(get_function("bambu_network_get_bambulab_host")); + m_get_user_selected_machine = reinterpret_cast(get_function("bambu_network_get_user_selected_machine")); + m_set_user_selected_machine = reinterpret_cast(get_function("bambu_network_set_user_selected_machine")); + m_start_print = reinterpret_cast(get_function("bambu_network_start_print")); + m_start_local_print_with_record = reinterpret_cast(get_function("bambu_network_start_local_print_with_record")); + m_start_send_gcode_to_sdcard = reinterpret_cast(get_function("bambu_network_start_send_gcode_to_sdcard")); + m_start_local_print = reinterpret_cast(get_function("bambu_network_start_local_print")); + m_start_sdcard_print = reinterpret_cast(get_function("bambu_network_start_sdcard_print")); + m_get_user_presets = reinterpret_cast(get_function("bambu_network_get_user_presets")); + m_request_setting_id = reinterpret_cast(get_function("bambu_network_request_setting_id")); + m_put_setting = reinterpret_cast(get_function("bambu_network_put_setting")); + m_get_setting_list = reinterpret_cast(get_function("bambu_network_get_setting_list")); + m_get_setting_list2 = reinterpret_cast(get_function("bambu_network_get_setting_list2")); + m_delete_setting = reinterpret_cast(get_function("bambu_network_delete_setting")); + m_get_studio_info_url = reinterpret_cast(get_function("bambu_network_get_studio_info_url")); + m_set_extra_http_header = reinterpret_cast(get_function("bambu_network_set_extra_http_header")); + m_get_my_message = reinterpret_cast(get_function("bambu_network_get_my_message")); + m_check_user_task_report = reinterpret_cast(get_function("bambu_network_check_user_task_report")); + m_get_user_print_info = reinterpret_cast(get_function("bambu_network_get_user_print_info")); + m_get_user_tasks = reinterpret_cast(get_function("bambu_network_get_user_tasks")); + m_get_printer_firmware = reinterpret_cast(get_function("bambu_network_get_printer_firmware")); + m_get_task_plate_index = reinterpret_cast(get_function("bambu_network_get_task_plate_index")); + m_get_user_info = reinterpret_cast(get_function("bambu_network_get_user_info")); + m_request_bind_ticket = reinterpret_cast(get_function("bambu_network_request_bind_ticket")); + m_get_subtask_info = reinterpret_cast(get_function("bambu_network_get_subtask_info")); + m_get_slice_info = reinterpret_cast(get_function("bambu_network_get_slice_info")); + m_query_bind_status = reinterpret_cast(get_function("bambu_network_query_bind_status")); + m_modify_printer_name = reinterpret_cast(get_function("bambu_network_modify_printer_name")); + m_get_camera_url = reinterpret_cast(get_function("bambu_network_get_camera_url")); + m_get_design_staffpick = reinterpret_cast(get_function("bambu_network_get_design_staffpick")); + m_start_publish = reinterpret_cast(get_function("bambu_network_start_publish")); + m_get_model_publish_url = reinterpret_cast(get_function("bambu_network_get_model_publish_url")); + m_get_subtask = reinterpret_cast(get_function("bambu_network_get_subtask")); + m_get_model_mall_home_url = reinterpret_cast(get_function("bambu_network_get_model_mall_home_url")); + m_get_model_mall_detail_url = reinterpret_cast(get_function("bambu_network_get_model_mall_detail_url")); + m_get_my_profile = reinterpret_cast(get_function("bambu_network_get_my_profile")); + m_track_enable = reinterpret_cast(get_function("bambu_network_track_enable")); + m_track_remove_files = reinterpret_cast(get_function("bambu_network_track_remove_files")); + m_track_event = reinterpret_cast(get_function("bambu_network_track_event")); + m_track_header = reinterpret_cast(get_function("bambu_network_track_header")); + m_track_update_property = reinterpret_cast(get_function("bambu_network_track_update_property")); + m_track_get_property = reinterpret_cast(get_function("bambu_network_track_get_property")); + m_put_model_mall_rating = reinterpret_cast(get_function("bambu_network_put_model_mall_rating")); + m_get_oss_config = reinterpret_cast(get_function("bambu_network_get_oss_config")); + m_put_rating_picture_oss = reinterpret_cast(get_function("bambu_network_put_rating_picture_oss")); + m_get_model_mall_rating_result = reinterpret_cast(get_function("bambu_network_get_model_mall_rating")); + m_get_mw_user_preference = reinterpret_cast(get_function("bambu_network_get_mw_user_preference")); + m_get_mw_user_4ulist = reinterpret_cast(get_function("bambu_network_get_mw_user_4ulist")); +} + +void BBLNetworkPlugin::clear_all_function_pointers() +{ + m_check_debug_consistent = nullptr; + m_get_version = nullptr; + m_create_agent = nullptr; + m_destroy_agent = nullptr; + m_init_log = nullptr; + m_set_config_dir = nullptr; + m_set_cert_file = nullptr; + m_set_country_code = nullptr; + m_start = nullptr; + m_set_on_ssdp_msg_fn = nullptr; + m_set_on_user_login_fn = nullptr; + m_set_on_printer_connected_fn = nullptr; + m_set_on_server_connected_fn = nullptr; + m_set_on_http_error_fn = nullptr; + m_set_get_country_code_fn = nullptr; + m_set_on_subscribe_failure_fn = nullptr; + m_set_on_message_fn = nullptr; + m_set_on_user_message_fn = nullptr; + m_set_on_local_connect_fn = nullptr; + m_set_on_local_message_fn = nullptr; + m_set_queue_on_main_fn = nullptr; + m_connect_server = nullptr; + m_is_server_connected = nullptr; + m_refresh_connection = nullptr; + m_start_subscribe = nullptr; + m_stop_subscribe = nullptr; + m_add_subscribe = nullptr; + m_del_subscribe = nullptr; + m_enable_multi_machine = nullptr; + m_send_message = nullptr; + m_connect_printer = nullptr; + m_disconnect_printer = nullptr; + m_send_message_to_printer = nullptr; + m_check_cert = nullptr; + m_install_device_cert = nullptr; + m_start_discovery = nullptr; + m_change_user = nullptr; + m_is_user_login = nullptr; + m_user_logout = nullptr; + m_get_user_id = nullptr; + m_get_user_name = nullptr; + m_get_user_avatar = nullptr; + m_get_user_nickanme = nullptr; + m_build_login_cmd = nullptr; + m_build_logout_cmd = nullptr; + m_build_login_info = nullptr; + m_ping_bind = nullptr; + m_bind_detect = nullptr; + m_set_server_callback = nullptr; + m_bind = nullptr; + m_unbind = nullptr; + m_get_bambulab_host = nullptr; + m_get_user_selected_machine = nullptr; + m_set_user_selected_machine = nullptr; + m_start_print = nullptr; + m_start_local_print_with_record = nullptr; + m_start_send_gcode_to_sdcard = nullptr; + m_start_local_print = nullptr; + m_start_sdcard_print = nullptr; + m_get_user_presets = nullptr; + m_request_setting_id = nullptr; + m_put_setting = nullptr; + m_get_setting_list = nullptr; + m_get_setting_list2 = nullptr; + m_delete_setting = nullptr; + m_get_studio_info_url = nullptr; + m_set_extra_http_header = nullptr; + m_get_my_message = nullptr; + m_check_user_task_report = nullptr; + m_get_user_print_info = nullptr; + m_get_user_tasks = nullptr; + m_get_printer_firmware = nullptr; + m_get_task_plate_index = nullptr; + m_get_user_info = nullptr; + m_request_bind_ticket = nullptr; + m_get_subtask_info = nullptr; + m_get_slice_info = nullptr; + m_query_bind_status = nullptr; + m_modify_printer_name = nullptr; + m_get_camera_url = nullptr; + m_get_design_staffpick = nullptr; + m_start_publish = nullptr; + m_get_model_publish_url = nullptr; + m_get_subtask = nullptr; + m_get_model_mall_home_url = nullptr; + m_get_model_mall_detail_url = nullptr; + m_get_my_profile = nullptr; + m_track_enable = nullptr; + m_track_remove_files = nullptr; + m_track_event = nullptr; + m_track_header = nullptr; + m_track_update_property = nullptr; + m_track_get_property = nullptr; + m_put_model_mall_rating = nullptr; + m_get_oss_config = nullptr; + m_put_rating_picture_oss = nullptr; + m_get_model_mall_rating_result = nullptr; + m_get_mw_user_preference = nullptr; + m_get_mw_user_4ulist = nullptr; +} + +std::vector get_all_available_versions() +{ + std::vector result; + std::set known_base_versions; + std::set all_known_versions; + + for (size_t i = 0; i < AVAILABLE_NETWORK_VERSIONS_COUNT; ++i) { + result.push_back(NetworkLibraryVersionInfo::from_static(AVAILABLE_NETWORK_VERSIONS[i])); + known_base_versions.insert(AVAILABLE_NETWORK_VERSIONS[i].version); + all_known_versions.insert(AVAILABLE_NETWORK_VERSIONS[i].version); + } + + std::vector discovered = BBLNetworkPlugin::scan_plugin_versions(); + + std::vector> suffixed_versions; + + for (const auto& version : discovered) { + if (all_known_versions.count(version) > 0) + continue; + + std::string base = extract_base_version(version); + std::string suffix = extract_suffix(version); + + if (suffix.empty()) + continue; + + if (known_base_versions.count(base) == 0) + continue; + + suffixed_versions.emplace_back(base, version); + all_known_versions.insert(version); + } + + std::sort(suffixed_versions.begin(), suffixed_versions.end(), + [](const auto& a, const auto& b) { + if (a.first != b.first) return a.first > b.first; + return a.second < b.second; + }); + + for (const auto& [base, full] : suffixed_versions) { + size_t insert_pos = 0; + for (size_t i = 0; i < result.size(); ++i) { + if (result[i].base_version == base) { + insert_pos = i + 1; + while (insert_pos < result.size() && + result[insert_pos].base_version == base) { + ++insert_pos; + } + break; + } + } + + std::string sfx = extract_suffix(full); + result.insert(result.begin() + insert_pos, + NetworkLibraryVersionInfo::from_discovered(full, base, sfx)); + } + + return result; +} + + +} // namespace Slic3r diff --git a/src/slic3r/Utils/BBLNetworkPlugin.hpp b/src/slic3r/Utils/BBLNetworkPlugin.hpp new file mode 100644 index 0000000000..523d3962d6 --- /dev/null +++ b/src/slic3r/Utils/BBLNetworkPlugin.hpp @@ -0,0 +1,515 @@ +#ifndef __BBL_NETWORK_PLUGIN_HPP__ +#define __BBL_NETWORK_PLUGIN_HPP__ + +#include "bambu_networking.hpp" +#include "libslic3r/ProjectTask.hpp" +#include +#include +#include +#include +#include +#include + +#if defined(_MSC_VER) || defined(_WIN32) +#include +#endif + +namespace Slic3r { + +// ============================================================================ +// Function Pointer Types (copied from NetworkAgent.hpp) +// ============================================================================ + +typedef bool (*func_check_debug_consistent)(bool is_debug); +typedef std::string (*func_get_version)(void); +typedef void* (*func_create_agent)(std::string log_dir); +typedef int (*func_destroy_agent)(void *agent); +typedef int (*func_init_log)(void *agent); +typedef int (*func_set_config_dir)(void *agent, std::string config_dir); +typedef int (*func_set_cert_file)(void *agent, std::string folder, std::string filename); +typedef int (*func_set_country_code)(void *agent, std::string country_code); +typedef int (*func_start)(void *agent); +typedef int (*func_set_on_ssdp_msg_fn)(void *agent, OnMsgArrivedFn fn); +typedef int (*func_set_on_user_login_fn)(void *agent, OnUserLoginFn fn); +typedef int (*func_set_on_printer_connected_fn)(void *agent, OnPrinterConnectedFn fn); +typedef int (*func_set_on_server_connected_fn)(void *agent, OnServerConnectedFn fn); +typedef int (*func_set_on_http_error_fn)(void *agent, OnHttpErrorFn fn); +typedef int (*func_set_get_country_code_fn)(void *agent, GetCountryCodeFn fn); +typedef int (*func_set_on_subscribe_failure_fn)(void *agent, GetSubscribeFailureFn fn); +typedef int (*func_set_on_message_fn)(void *agent, OnMessageFn fn); +typedef int (*func_set_on_user_message_fn)(void *agent, OnMessageFn fn); +typedef int (*func_set_on_local_connect_fn)(void *agent, OnLocalConnectedFn fn); +typedef int (*func_set_on_local_message_fn)(void *agent, OnMessageFn fn); +typedef int (*func_set_queue_on_main_fn)(void *agent, QueueOnMainFn fn); +typedef int (*func_connect_server)(void *agent); +typedef bool (*func_is_server_connected)(void *agent); +typedef int (*func_refresh_connection)(void *agent); +typedef int (*func_start_subscribe)(void *agent, std::string module); +typedef int (*func_stop_subscribe)(void *agent, std::string module); +typedef int (*func_add_subscribe)(void *agent, std::vector dev_list); +typedef int (*func_del_subscribe)(void *agent, std::vector dev_list); +typedef void (*func_enable_multi_machine)(void *agent, bool enable); +typedef int (*func_send_message)(void *agent, std::string dev_id, std::string json_str, int qos, int flag); +typedef int (*func_connect_printer)(void *agent, std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); +typedef int (*func_disconnect_printer)(void *agent); +typedef int (*func_send_message_to_printer)(void *agent, std::string dev_id, std::string json_str, int qos, int flag); +typedef int (*func_check_cert)(void* agent); +typedef void (*func_install_device_cert)(void* agent, std::string dev_id, bool lan_only); +typedef bool (*func_start_discovery)(void *agent, bool start, bool sending); +typedef int (*func_change_user)(void *agent, std::string user_info); +typedef bool (*func_is_user_login)(void *agent); +typedef int (*func_user_logout)(void *agent, bool request); +typedef std::string (*func_get_user_id)(void *agent); +typedef std::string (*func_get_user_name)(void *agent); +typedef std::string (*func_get_user_avatar)(void *agent); +typedef std::string (*func_get_user_nickanme)(void *agent); +typedef std::string (*func_build_login_cmd)(void *agent); +typedef std::string (*func_build_logout_cmd)(void *agent); +typedef std::string (*func_build_login_info)(void *agent); +typedef int (*func_ping_bind)(void *agent, std::string ping_code); +typedef int (*func_bind_detect)(void *agent, std::string dev_ip, std::string sec_link, detectResult& detect); +typedef int (*func_set_server_callback)(void *agent, OnServerErrFn fn); +typedef int (*func_bind)(void *agent, std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn); +typedef int (*func_unbind)(void *agent, std::string dev_id); +typedef std::string (*func_get_bambulab_host)(void *agent); +typedef std::string (*func_get_user_selected_machine)(void *agent); +typedef int (*func_set_user_selected_machine)(void *agent, std::string dev_id); +typedef int (*func_start_print)(void *agent, PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); +typedef int (*func_start_local_print_with_record)(void *agent, PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); +typedef int (*func_start_send_gcode_to_sdcard)(void *agent, PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); +typedef int (*func_start_local_print)(void *agent, PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); +typedef int (*func_start_sdcard_print)(void *agent, PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); +typedef int (*func_get_user_presets)(void *agent, std::map>* user_presets); +typedef std::string (*func_request_setting_id)(void *agent, std::string name, std::map* values_map, unsigned int* http_code); +typedef int (*func_put_setting)(void *agent, std::string setting_id, std::string name, std::map* values_map, unsigned int* http_code); +typedef int (*func_get_setting_list)(void *agent, std::string bundle_version, ProgressFn pro_fn, WasCancelledFn cancel_fn); +typedef int (*func_get_setting_list2)(void *agent, std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn, WasCancelledFn cancel_fn); +typedef int (*func_delete_setting)(void *agent, std::string setting_id); +typedef std::string (*func_get_studio_info_url)(void *agent); +typedef int (*func_set_extra_http_header)(void *agent, std::map extra_headers); +typedef int (*func_get_my_message)(void *agent, int type, int after, int limit, unsigned int* http_code, std::string* http_body); +typedef int (*func_check_user_task_report)(void *agent, int* task_id, bool* printable); +typedef int (*func_get_user_print_info)(void *agent, unsigned int* http_code, std::string* http_body); +typedef int (*func_get_user_tasks)(void *agent, TaskQueryParams params, std::string* http_body); +typedef int (*func_get_printer_firmware)(void *agent, std::string dev_id, unsigned* http_code, std::string* http_body); +typedef int (*func_get_task_plate_index)(void *agent, std::string task_id, int* plate_index); +typedef int (*func_get_user_info)(void *agent, int* identifier); +typedef int (*func_request_bind_ticket)(void *agent, std::string* ticket); +typedef int (*func_get_subtask_info)(void *agent, std::string subtask_id, std::string* task_json, unsigned int* http_code, std::string *http_body); +typedef int (*func_get_slice_info)(void *agent, std::string project_id, std::string profile_id, int plate_index, std::string* slice_json); +typedef int (*func_query_bind_status)(void *agent, std::vector query_list, unsigned int* http_code, std::string* http_body); +typedef int (*func_modify_printer_name)(void *agent, std::string dev_id, std::string dev_name); +typedef int (*func_get_camera_url)(void *agent, std::string dev_id, std::function callback); +typedef int (*func_get_design_staffpick)(void *agent, int offset, int limit, std::function callback); +typedef int (*func_start_pubilsh)(void *agent, PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out); +typedef int (*func_get_model_publish_url)(void *agent, std::string* url); +typedef int (*func_get_subtask)(void *agent, BBLModelTask* task, OnGetSubTaskFn getsub_fn); +typedef int (*func_get_model_mall_home_url)(void *agent, std::string* url); +typedef int (*func_get_model_mall_detail_url)(void *agent, std::string* url, std::string id); +typedef int (*func_get_my_profile)(void *agent, std::string token, unsigned int *http_code, std::string *http_body); +typedef int (*func_track_enable)(void *agent, bool enable); +typedef int (*func_track_remove_files)(void *agent); +typedef int (*func_track_event)(void *agent, std::string evt_key, std::string content); +typedef int (*func_track_header)(void *agent, std::string header); +typedef int (*func_track_update_property)(void *agent, std::string name, std::string value, std::string type); +typedef int (*func_track_get_property)(void *agent, std::string name, std::string& value, std::string type); +typedef int (*func_put_model_mall_rating_url)(void *agent, int rating_id, int score, std::string content, std::vector images, unsigned int &http_code, std::string &http_error); +typedef int (*func_get_oss_config)(void *agent, std::string &config, std::string country_code, unsigned int &http_code, std::string &http_error); +typedef int (*func_put_rating_picture_oss)(void *agent, std::string &config, std::string &pic_oss_path, std::string model_id, int profile_id, unsigned int &http_code, std::string &http_error); +typedef int (*func_get_model_mall_rating_result)(void *agent, int job_id, std::string &rating_result, unsigned int &http_code, std::string &http_error); +typedef int (*func_get_mw_user_preference)(void *agent, std::function callback); +typedef int (*func_get_mw_user_4ulist)(void *agent, int seed, int limit, std::function callback); + +// Legacy function pointer types (for older DLL versions) +typedef int (*func_start_print_legacy)(void *agent, PrintParams_Legacy params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); +typedef int (*func_start_local_print_with_record_legacy)(void *agent, PrintParams_Legacy params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); +typedef int (*func_start_send_gcode_to_sdcard_legacy)(void *agent, PrintParams_Legacy params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); +typedef int (*func_start_local_print_legacy)(void *agent, PrintParams_Legacy params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); +typedef int (*func_start_sdcard_print_legacy)(void* agent, PrintParams_Legacy params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); +typedef int (*func_send_message_legacy)(void* agent, std::string dev_id, std::string json_str, int qos); +typedef int (*func_send_message_to_printer_legacy)(void* agent, std::string dev_id, std::string json_str, int qos); + +/** + * BBLNetworkPlugin - Singleton managing the Bambu Lab network DLL. + * + * Responsibilities: + * - Owns the DLL module handle (netwoking_module) + * - Owns the DLL source module handle (source_module) + * - Manages the shared void* agent handle + * - Provides all function pointers to BBL agents + * + * Usage: + * auto& plugin = BBLNetworkPlugin::instance(); + * if (plugin.initialize(version)) { + * plugin.create_agent(log_dir); + * // Now BBLCloudServiceAgent/BBLPrinterAgent can use plugin + * } + */ +class BBLNetworkPlugin { +public: + // Singleton access + static BBLNetworkPlugin& instance(); + + // Delete copy/move + BBLNetworkPlugin(const BBLNetworkPlugin&) = delete; + BBLNetworkPlugin& operator=(const BBLNetworkPlugin&) = delete; + BBLNetworkPlugin(BBLNetworkPlugin&&) = delete; + BBLNetworkPlugin& operator=(BBLNetworkPlugin&&) = delete; + + // ======================================================================== + // Module Lifecycle + // ======================================================================== + + /** + * Load the network DLL from the plugins folder. + * @param using_backup If true, look in plugins/backup folder + * @param version Required version string (e.g., "01.09.05.01") + * @return 0 on success, -1 on failure + */ + int initialize(bool using_backup = false, const std::string& version = ""); + + /** + * Unload the network DLL and clear all function pointers. + * @return 0 on success + */ + int unload(); + + /** + * Destroy the singleton instance. + * Safe to call multiple times - does nothing if already destroyed. + * Must be called during application shutdown before main() returns. + */ + static void shutdown(); + + /** + * Check if DLL is currently loaded. + */ + bool is_loaded() const; + + /** + * Get the plugin version string. + */ + std::string get_version() const; + + // ======================================================================== + // Agent Lifecycle + // ======================================================================== + + /** + * Create the shared agent handle. + * Only one agent can exist at a time. + * @param log_dir Directory for log files + * @return The created agent handle, or nullptr on failure + */ + void* create_agent(const std::string& log_dir); + + /** + * Destroy the shared agent handle. + * @return 0 on success + */ + int destroy_agent(); + + /** + * Get the current agent handle. + * Returns nullptr if no agent created. + */ + void* get_agent() const { return m_agent; } + + /** + * Check if an agent has been created. + */ + bool has_agent() const { return m_agent != nullptr; } + + // ======================================================================== + // DLL Module Accessors + // ======================================================================== + +#if defined(_MSC_VER) || defined(_WIN32) + HMODULE get_networking_module() const { return m_networking_module; } + HMODULE get_source_module(); +#else + void* get_networking_module() const { return m_networking_module; } + void* get_source_module(); +#endif + + void* get_function(const char* name); + + // Aliases for backward compatibility with NetworkAgent API + void* get_network_function(const char* name) { return get_function(name); } +#if defined(_MSC_VER) || defined(_WIN32) + HMODULE get_bambu_source_entry() { return get_source_module(); } +#else + void* get_bambu_source_entry() { return get_source_module(); } +#endif + + // ======================================================================== + // Utility Methods + // ======================================================================== + + static std::string get_libpath_in_current_directory(const std::string& library_name); + static std::string get_versioned_library_path(const std::string& version); + static bool versioned_library_exists(const std::string& version); + static bool legacy_library_exists(); + static void remove_legacy_library(); + static std::vector scan_plugin_versions(); + + // ======================================================================== + // Error Handling + // ======================================================================== + + NetworkLibraryLoadError get_load_error() const { return m_load_error; } + void clear_load_error(); + void set_load_error(const std::string& message, + const std::string& technical_details, + const std::string& attempted_path); + + // ======================================================================== + // Legacy Network Flag + // ======================================================================== + + bool use_legacy_network() const { return m_use_legacy_network; } + void set_use_legacy_network(bool legacy) { m_use_legacy_network = legacy; } + + // ======================================================================== + // Function Pointer Accessors + // ======================================================================== + + func_check_debug_consistent get_check_debug_consistent() const { return m_check_debug_consistent; } + func_get_version get_get_version() const { return m_get_version; } + func_create_agent get_create_agent() const { return m_create_agent; } + func_destroy_agent get_destroy_agent() const { return m_destroy_agent; } + func_init_log get_init_log() const { return m_init_log; } + func_set_config_dir get_set_config_dir() const { return m_set_config_dir; } + func_set_cert_file get_set_cert_file() const { return m_set_cert_file; } + func_set_country_code get_set_country_code() const { return m_set_country_code; } + func_start get_start() const { return m_start; } + func_set_on_ssdp_msg_fn get_set_on_ssdp_msg_fn() const { return m_set_on_ssdp_msg_fn; } + func_set_on_user_login_fn get_set_on_user_login_fn() const { return m_set_on_user_login_fn; } + func_set_on_printer_connected_fn get_set_on_printer_connected_fn() const { return m_set_on_printer_connected_fn; } + func_set_on_server_connected_fn get_set_on_server_connected_fn() const { return m_set_on_server_connected_fn; } + func_set_on_http_error_fn get_set_on_http_error_fn() const { return m_set_on_http_error_fn; } + func_set_get_country_code_fn get_set_get_country_code_fn() const { return m_set_get_country_code_fn; } + func_set_on_subscribe_failure_fn get_set_on_subscribe_failure_fn() const { return m_set_on_subscribe_failure_fn; } + func_set_on_message_fn get_set_on_message_fn() const { return m_set_on_message_fn; } + func_set_on_user_message_fn get_set_on_user_message_fn() const { return m_set_on_user_message_fn; } + func_set_on_local_connect_fn get_set_on_local_connect_fn() const { return m_set_on_local_connect_fn; } + func_set_on_local_message_fn get_set_on_local_message_fn() const { return m_set_on_local_message_fn; } + func_set_queue_on_main_fn get_set_queue_on_main_fn() const { return m_set_queue_on_main_fn; } + func_connect_server get_connect_server() const { return m_connect_server; } + func_is_server_connected get_is_server_connected() const { return m_is_server_connected; } + func_refresh_connection get_refresh_connection() const { return m_refresh_connection; } + func_start_subscribe get_start_subscribe() const { return m_start_subscribe; } + func_stop_subscribe get_stop_subscribe() const { return m_stop_subscribe; } + func_add_subscribe get_add_subscribe() const { return m_add_subscribe; } + func_del_subscribe get_del_subscribe() const { return m_del_subscribe; } + func_enable_multi_machine get_enable_multi_machine() const { return m_enable_multi_machine; } + func_send_message get_send_message() const { return m_send_message; } + func_connect_printer get_connect_printer() const { return m_connect_printer; } + func_disconnect_printer get_disconnect_printer() const { return m_disconnect_printer; } + func_send_message_to_printer get_send_message_to_printer() const { return m_send_message_to_printer; } + func_check_cert get_check_cert() const { return m_check_cert; } + func_install_device_cert get_install_device_cert() const { return m_install_device_cert; } + func_start_discovery get_start_discovery() const { return m_start_discovery; } + func_change_user get_change_user() const { return m_change_user; } + func_is_user_login get_is_user_login() const { return m_is_user_login; } + func_user_logout get_user_logout() const { return m_user_logout; } + func_get_user_id get_get_user_id() const { return m_get_user_id; } + func_get_user_name get_get_user_name() const { return m_get_user_name; } + func_get_user_avatar get_get_user_avatar() const { return m_get_user_avatar; } + func_get_user_nickanme get_get_user_nickanme() const { return m_get_user_nickanme; } + func_build_login_cmd get_build_login_cmd() const { return m_build_login_cmd; } + func_build_logout_cmd get_build_logout_cmd() const { return m_build_logout_cmd; } + func_build_login_info get_build_login_info() const { return m_build_login_info; } + func_ping_bind get_ping_bind() const { return m_ping_bind; } + func_bind_detect get_bind_detect() const { return m_bind_detect; } + func_set_server_callback get_set_server_callback() const { return m_set_server_callback; } + func_bind get_bind() const { return m_bind; } + func_unbind get_unbind() const { return m_unbind; } + func_get_bambulab_host get_get_bambulab_host() const { return m_get_bambulab_host; } + func_get_user_selected_machine get_get_user_selected_machine() const { return m_get_user_selected_machine; } + func_set_user_selected_machine get_set_user_selected_machine() const { return m_set_user_selected_machine; } + func_start_print get_start_print() const { return m_start_print; } + func_start_local_print_with_record get_start_local_print_with_record() const { return m_start_local_print_with_record; } + func_start_send_gcode_to_sdcard get_start_send_gcode_to_sdcard() const { return m_start_send_gcode_to_sdcard; } + func_start_local_print get_start_local_print() const { return m_start_local_print; } + func_start_sdcard_print get_start_sdcard_print() const { return m_start_sdcard_print; } + func_get_user_presets get_get_user_presets() const { return m_get_user_presets; } + func_request_setting_id get_request_setting_id() const { return m_request_setting_id; } + func_put_setting get_put_setting() const { return m_put_setting; } + func_get_setting_list get_get_setting_list() const { return m_get_setting_list; } + func_get_setting_list2 get_get_setting_list2() const { return m_get_setting_list2; } + func_delete_setting get_delete_setting() const { return m_delete_setting; } + func_get_studio_info_url get_get_studio_info_url() const { return m_get_studio_info_url; } + func_set_extra_http_header get_set_extra_http_header() const { return m_set_extra_http_header; } + func_get_my_message get_get_my_message() const { return m_get_my_message; } + func_check_user_task_report get_check_user_task_report() const { return m_check_user_task_report; } + func_get_user_print_info get_get_user_print_info() const { return m_get_user_print_info; } + func_get_user_tasks get_get_user_tasks() const { return m_get_user_tasks; } + func_get_printer_firmware get_get_printer_firmware() const { return m_get_printer_firmware; } + func_get_task_plate_index get_get_task_plate_index() const { return m_get_task_plate_index; } + func_get_user_info get_get_user_info() const { return m_get_user_info; } + func_request_bind_ticket get_request_bind_ticket() const { return m_request_bind_ticket; } + func_get_subtask_info get_get_subtask_info() const { return m_get_subtask_info; } + func_get_slice_info get_get_slice_info() const { return m_get_slice_info; } + func_query_bind_status get_query_bind_status() const { return m_query_bind_status; } + func_modify_printer_name get_modify_printer_name() const { return m_modify_printer_name; } + func_get_camera_url get_get_camera_url() const { return m_get_camera_url; } + func_get_design_staffpick get_get_design_staffpick() const { return m_get_design_staffpick; } + func_start_pubilsh get_start_publish() const { return m_start_publish; } + func_get_model_publish_url get_get_model_publish_url() const { return m_get_model_publish_url; } + func_get_subtask get_get_subtask() const { return m_get_subtask; } + func_get_model_mall_home_url get_get_model_mall_home_url() const { return m_get_model_mall_home_url; } + func_get_model_mall_detail_url get_get_model_mall_detail_url() const { return m_get_model_mall_detail_url; } + func_get_my_profile get_get_my_profile() const { return m_get_my_profile; } + func_track_enable get_track_enable() const { return m_track_enable; } + func_track_remove_files get_track_remove_files() const { return m_track_remove_files; } + func_track_event get_track_event() const { return m_track_event; } + func_track_header get_track_header() const { return m_track_header; } + func_track_update_property get_track_update_property() const { return m_track_update_property; } + func_track_get_property get_track_get_property() const { return m_track_get_property; } + func_put_model_mall_rating_url get_put_model_mall_rating() const { return m_put_model_mall_rating; } + func_get_oss_config get_get_oss_config() const { return m_get_oss_config; } + func_put_rating_picture_oss get_put_rating_picture_oss() const { return m_put_rating_picture_oss; } + func_get_model_mall_rating_result get_get_model_mall_rating_result() const { return m_get_model_mall_rating_result; } + func_get_mw_user_preference get_get_mw_user_preference() const { return m_get_mw_user_preference; } + func_get_mw_user_4ulist get_get_mw_user_4ulist() const { return m_get_mw_user_4ulist; } + + // ======================================================================== + // Legacy Helper + // ======================================================================== + + static PrintParams_Legacy as_legacy(PrintParams& param); + +private: + // Singleton instance pointer (heap-allocated for explicit lifetime control) + static BBLNetworkPlugin* s_instance; + + BBLNetworkPlugin(); + ~BBLNetworkPlugin(); + + void load_all_function_pointers(); + void clear_all_function_pointers(); + + // Module handles +#if defined(_MSC_VER) || defined(_WIN32) + HMODULE m_networking_module{nullptr}; + HMODULE m_source_module{nullptr}; +#else + void* m_networking_module{nullptr}; + void* m_source_module{nullptr}; +#endif + + // Shared agent handle + void* m_agent{nullptr}; + + // Load error state + NetworkLibraryLoadError m_load_error; + + // Legacy network compatibility flag + bool m_use_legacy_network{true}; + + // Function pointers + func_check_debug_consistent m_check_debug_consistent{nullptr}; + func_get_version m_get_version{nullptr}; + func_create_agent m_create_agent{nullptr}; + func_destroy_agent m_destroy_agent{nullptr}; + func_init_log m_init_log{nullptr}; + func_set_config_dir m_set_config_dir{nullptr}; + func_set_cert_file m_set_cert_file{nullptr}; + func_set_country_code m_set_country_code{nullptr}; + func_start m_start{nullptr}; + func_set_on_ssdp_msg_fn m_set_on_ssdp_msg_fn{nullptr}; + func_set_on_user_login_fn m_set_on_user_login_fn{nullptr}; + func_set_on_printer_connected_fn m_set_on_printer_connected_fn{nullptr}; + func_set_on_server_connected_fn m_set_on_server_connected_fn{nullptr}; + func_set_on_http_error_fn m_set_on_http_error_fn{nullptr}; + func_set_get_country_code_fn m_set_get_country_code_fn{nullptr}; + func_set_on_subscribe_failure_fn m_set_on_subscribe_failure_fn{nullptr}; + func_set_on_message_fn m_set_on_message_fn{nullptr}; + func_set_on_user_message_fn m_set_on_user_message_fn{nullptr}; + func_set_on_local_connect_fn m_set_on_local_connect_fn{nullptr}; + func_set_on_local_message_fn m_set_on_local_message_fn{nullptr}; + func_set_queue_on_main_fn m_set_queue_on_main_fn{nullptr}; + func_connect_server m_connect_server{nullptr}; + func_is_server_connected m_is_server_connected{nullptr}; + func_refresh_connection m_refresh_connection{nullptr}; + func_start_subscribe m_start_subscribe{nullptr}; + func_stop_subscribe m_stop_subscribe{nullptr}; + func_add_subscribe m_add_subscribe{nullptr}; + func_del_subscribe m_del_subscribe{nullptr}; + func_enable_multi_machine m_enable_multi_machine{nullptr}; + func_send_message m_send_message{nullptr}; + func_connect_printer m_connect_printer{nullptr}; + func_disconnect_printer m_disconnect_printer{nullptr}; + func_send_message_to_printer m_send_message_to_printer{nullptr}; + func_check_cert m_check_cert{nullptr}; + func_install_device_cert m_install_device_cert{nullptr}; + func_start_discovery m_start_discovery{nullptr}; + func_change_user m_change_user{nullptr}; + func_is_user_login m_is_user_login{nullptr}; + func_user_logout m_user_logout{nullptr}; + func_get_user_id m_get_user_id{nullptr}; + func_get_user_name m_get_user_name{nullptr}; + func_get_user_avatar m_get_user_avatar{nullptr}; + func_get_user_nickanme m_get_user_nickanme{nullptr}; + func_build_login_cmd m_build_login_cmd{nullptr}; + func_build_logout_cmd m_build_logout_cmd{nullptr}; + func_build_login_info m_build_login_info{nullptr}; + func_ping_bind m_ping_bind{nullptr}; + func_bind_detect m_bind_detect{nullptr}; + func_set_server_callback m_set_server_callback{nullptr}; + func_bind m_bind{nullptr}; + func_unbind m_unbind{nullptr}; + func_get_bambulab_host m_get_bambulab_host{nullptr}; + func_get_user_selected_machine m_get_user_selected_machine{nullptr}; + func_set_user_selected_machine m_set_user_selected_machine{nullptr}; + func_start_print m_start_print{nullptr}; + func_start_local_print_with_record m_start_local_print_with_record{nullptr}; + func_start_send_gcode_to_sdcard m_start_send_gcode_to_sdcard{nullptr}; + func_start_local_print m_start_local_print{nullptr}; + func_start_sdcard_print m_start_sdcard_print{nullptr}; + func_get_user_presets m_get_user_presets{nullptr}; + func_request_setting_id m_request_setting_id{nullptr}; + func_put_setting m_put_setting{nullptr}; + func_get_setting_list m_get_setting_list{nullptr}; + func_get_setting_list2 m_get_setting_list2{nullptr}; + func_delete_setting m_delete_setting{nullptr}; + func_get_studio_info_url m_get_studio_info_url{nullptr}; + func_set_extra_http_header m_set_extra_http_header{nullptr}; + func_get_my_message m_get_my_message{nullptr}; + func_check_user_task_report m_check_user_task_report{nullptr}; + func_get_user_print_info m_get_user_print_info{nullptr}; + func_get_user_tasks m_get_user_tasks{nullptr}; + func_get_printer_firmware m_get_printer_firmware{nullptr}; + func_get_task_plate_index m_get_task_plate_index{nullptr}; + func_get_user_info m_get_user_info{nullptr}; + func_request_bind_ticket m_request_bind_ticket{nullptr}; + func_get_subtask_info m_get_subtask_info{nullptr}; + func_get_slice_info m_get_slice_info{nullptr}; + func_query_bind_status m_query_bind_status{nullptr}; + func_modify_printer_name m_modify_printer_name{nullptr}; + func_get_camera_url m_get_camera_url{nullptr}; + func_get_design_staffpick m_get_design_staffpick{nullptr}; + func_start_pubilsh m_start_publish{nullptr}; + func_get_model_publish_url m_get_model_publish_url{nullptr}; + func_get_subtask m_get_subtask{nullptr}; + func_get_model_mall_home_url m_get_model_mall_home_url{nullptr}; + func_get_model_mall_detail_url m_get_model_mall_detail_url{nullptr}; + func_get_my_profile m_get_my_profile{nullptr}; + func_track_enable m_track_enable{nullptr}; + func_track_remove_files m_track_remove_files{nullptr}; + func_track_event m_track_event{nullptr}; + func_track_header m_track_header{nullptr}; + func_track_update_property m_track_update_property{nullptr}; + func_track_get_property m_track_get_property{nullptr}; + func_put_model_mall_rating_url m_put_model_mall_rating{nullptr}; + func_get_oss_config m_get_oss_config{nullptr}; + func_put_rating_picture_oss m_put_rating_picture_oss{nullptr}; + func_get_model_mall_rating_result m_get_model_mall_rating_result{nullptr}; + func_get_mw_user_preference m_get_mw_user_preference{nullptr}; + func_get_mw_user_4ulist m_get_mw_user_4ulist{nullptr}; +}; + +} // namespace Slic3r + +#endif // __BBL_NETWORK_PLUGIN_HPP__ diff --git a/src/slic3r/Utils/BBLPrinterAgent.cpp b/src/slic3r/Utils/BBLPrinterAgent.cpp new file mode 100644 index 0000000000..a1be053368 --- /dev/null +++ b/src/slic3r/Utils/BBLPrinterAgent.cpp @@ -0,0 +1,379 @@ +#include "BBLPrinterAgent.hpp" +#include "BBLNetworkPlugin.hpp" + +#include + +namespace Slic3r { + +BBLPrinterAgent::BBLPrinterAgent() +{ + BOOST_LOG_TRIVIAL(info) << "BBLPrinterAgent: Constructor - using BBLNetworkPlugin singleton"; +} + +BBLPrinterAgent::~BBLPrinterAgent() = default; + +void BBLPrinterAgent::set_cloud_agent(std::shared_ptr cloud) +{ + m_cloud_agent = cloud; + // BBL DLL manages tokens internally, so this is just for interface compliance +} + +// ============================================================================ +// Communication +// ============================================================================ + +int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_send_message(); + if (func && agent) { + return func(agent, dev_id, json_str, qos, flag); + } + return -1; +} + +int BBLPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_connect_printer(); + if (func && agent) { + return func(agent, dev_id, dev_ip, username, password, use_ssl); + } + return -1; +} + +int BBLPrinterAgent::disconnect_printer() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_disconnect_printer(); + if (func && agent) { + return func(agent); + } + return -1; +} + +int BBLPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_send_message_to_printer(); + if (func && agent) { + return func(agent, dev_id, json_str, qos, flag); + } + return -1; +} + +// ============================================================================ +// Certificates +// ============================================================================ + +int BBLPrinterAgent::check_cert() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_check_cert(); + if (func && agent) { + return func(agent); + } + return -1; +} + +void BBLPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_install_device_cert(); + if (func && agent) { + func(agent, dev_id, lan_only); + } +} + +// ============================================================================ +// Discovery +// ============================================================================ + +bool BBLPrinterAgent::start_discovery(bool start, bool sending) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_start_discovery(); + if (func && agent) { + return func(agent, start, sending); + } + return false; +} + +// ============================================================================ +// Binding +// ============================================================================ + +int BBLPrinterAgent::ping_bind(std::string ping_code) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_ping_bind(); + if (func && agent) { + return func(agent, ping_code); + } + return -1; +} + +int BBLPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_bind_detect(); + if (func && agent) { + return func(agent, dev_ip, sec_link, detect); + } + return -1; +} + +int BBLPrinterAgent::bind(std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_bind(); + if (func && agent) { + return func(agent, dev_ip, dev_id, sec_link, timezone, improved, update_fn); + } + return -1; +} + +int BBLPrinterAgent::unbind(std::string dev_id) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_unbind(); + if (func && agent) { + return func(agent, dev_id); + } + return -1; +} + +int BBLPrinterAgent::request_bind_ticket(std::string* ticket) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_request_bind_ticket(); + if (func && agent) { + return func(agent, ticket); + } + return -1; +} + +int BBLPrinterAgent::set_server_callback(OnServerErrFn fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_server_callback(); + if (func && agent) { + return func(agent, fn); + } + return -1; +} + +// ============================================================================ +// Machine Selection +// ============================================================================ + +std::string BBLPrinterAgent::get_user_selected_machine() +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_user_selected_machine(); + if (func && agent) { + return func(agent); + } + return ""; +} + +int BBLPrinterAgent::set_user_selected_machine(std::string dev_id) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_user_selected_machine(); + if (func && agent) { + return func(agent, dev_id); + } + return -1; +} + +// ============================================================================ +// Agent Information +// ============================================================================ +AgentInfo BBLPrinterAgent::get_agent_info_static() +{ + return AgentInfo{ + .id = "bbl", + .name = "Bambu Lab Printer Agent", + .version = "", + .description = "Bambu Lab printer agent" + }; +} + +// ============================================================================ +// Print Job Operations +// ============================================================================ + +int BBLPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_start_print(); + if (func && agent) { + return func(agent, params, update_fn, cancel_fn, wait_fn); + } + return -1; +} + +int BBLPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_start_local_print_with_record(); + if (func && agent) { + return func(agent, params, update_fn, cancel_fn, wait_fn); + } + return -1; +} + +int BBLPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_start_send_gcode_to_sdcard(); + if (func && agent) { + return func(agent, params, update_fn, cancel_fn, wait_fn); + } + return -1; +} + +int BBLPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_start_local_print(); + if (func && agent) { + return func(agent, params, update_fn, cancel_fn); + } + return -1; +} + +int BBLPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_start_sdcard_print(); + if (func && agent) { + return func(agent, params, update_fn, cancel_fn); + } + return -1; +} + +// ============================================================================ +// Callbacks +// ============================================================================ + +int BBLPrinterAgent::set_on_ssdp_msg_fn(OnMsgArrivedFn fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_on_ssdp_msg_fn(); + if (func && agent) { + return func(agent, fn); + } + return -1; +} + +int BBLPrinterAgent::set_on_printer_connected_fn(OnPrinterConnectedFn fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_on_printer_connected_fn(); + if (func && agent) { + return func(agent, fn); + } + return -1; +} + +int BBLPrinterAgent::set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_on_subscribe_failure_fn(); + if (func && agent) { + return func(agent, fn); + } + return -1; +} + +int BBLPrinterAgent::set_on_message_fn(OnMessageFn fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_on_message_fn(); + if (func && agent) { + return func(agent, fn); + } + return -1; +} + +int BBLPrinterAgent::set_on_user_message_fn(OnMessageFn fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_on_user_message_fn(); + if (func && agent) { + return func(agent, fn); + } + return -1; +} + +int BBLPrinterAgent::set_on_local_connect_fn(OnLocalConnectedFn fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_on_local_connect_fn(); + if (func && agent) { + return func(agent, fn); + } + return -1; +} + +int BBLPrinterAgent::set_on_local_message_fn(OnMessageFn fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_on_local_message_fn(); + if (func && agent) { + return func(agent, fn); + } + return -1; +} + +int BBLPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_queue_on_main_fn(); + if (func && agent) { + return func(agent, fn); + } + return -1; +} + +// ============================================================================ +// Filament Operations +// ============================================================================ + +FilamentSyncMode BBLPrinterAgent::get_filament_sync_mode() const +{ + // BBL uses MQTT subscription for real-time filament updates + return FilamentSyncMode::subscription; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/BBLPrinterAgent.hpp b/src/slic3r/Utils/BBLPrinterAgent.hpp new file mode 100644 index 0000000000..4300709f84 --- /dev/null +++ b/src/slic3r/Utils/BBLPrinterAgent.hpp @@ -0,0 +1,86 @@ +#ifndef __BBL_PRINTER_AGENT_HPP__ +#define __BBL_PRINTER_AGENT_HPP__ + +#include "IPrinterAgent.hpp" +#include "ICloudServiceAgent.hpp" +#include +#include + +namespace Slic3r { + +/** + * BBLPrinterAgent - BBL DLL wrapper implementation of IPrinterAgent. + * + * Delegates all printer operations to the proprietary BBL network DLL + * through function pointers obtained from BBLNetworkPlugin singleton. + */ +class BBLPrinterAgent : public IPrinterAgent { +public: + BBLPrinterAgent(); + ~BBLPrinterAgent() override; + + // Cloud Agent Dependency (not used by BBL - tokens managed internally) + void set_cloud_agent(std::shared_ptr cloud) override; + + // ======================================================================== + // IPrinterAgent Interface Implementation + // ======================================================================== + + // Communication + int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; + int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; + int disconnect_printer() override; + int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; + + // Certificates + int check_cert() override; + void install_device_cert(std::string dev_id, bool lan_only) override; + + // Discovery + bool start_discovery(bool start, bool sending) override; + + // Binding + int ping_bind(std::string ping_code) override; + int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override; + int bind(std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override; + int unbind(std::string dev_id) override; + int request_bind_ticket(std::string* ticket) override; + int set_server_callback(OnServerErrFn fn) override; + + // Machine Selection + std::string get_user_selected_machine() override; + int set_user_selected_machine(std::string dev_id) override; + + /** + * Get agent information. + * + * @return AgentInfo struct containing agent identification and descriptive information + */ + static AgentInfo get_agent_info_static(); + AgentInfo get_agent_info() override { return get_agent_info_static(); } + + // Print Job Operations + int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; + int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; + int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; + int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override; + int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override; + + // Callbacks + int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override; + int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override; + int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override; + int set_on_message_fn(OnMessageFn fn) override; + int set_on_user_message_fn(OnMessageFn fn) override; + int set_on_local_connect_fn(OnLocalConnectedFn fn) override; + int set_on_local_message_fn(OnMessageFn fn) override; + int set_queue_on_main_fn(QueueOnMainFn fn) override; + FilamentSyncMode get_filament_sync_mode() const override; + +private: + std::shared_ptr m_cloud_agent; +}; + +} // namespace Slic3r + +#endif // __BBL_PRINTER_AGENT_HPP__ diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index e10115b1f7..6c4d90035e 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -110,6 +110,9 @@ struct Http::priv ::curl_httppost *form_end; ::curl_mime* mime; ::curl_slist *headerlist; + // For debug printing + std::string url; + std::string method; // Used for reading the body std::string buffer; // Used for storing file streams added as multipart form parts @@ -170,6 +173,8 @@ Http::priv::priv(const std::string &url) , form_end(nullptr) , mime(nullptr) , headerlist(nullptr) + , url(url) + , method("GET") , error_buffer(CURL_ERROR_SIZE + 1, '\0') , limit(0) , cancel(false) @@ -757,6 +762,74 @@ void Http::cancel() if (p) { p->cancel = true; } } +void Http::print() const +{ + if (!p) { + BOOST_LOG_TRIVIAL(info) << "Http::print() - no request data"; + return; + } + + std::ostringstream cmd; + cmd << "curl"; + + // Method + if (p->method != "GET") { + cmd << " -X " << p->method; + } + + // URL + cmd << " '" << p->url << "'"; + + // Headers (iterate through curl_slist) + ::curl_slist *header = p->headerlist; + while (header) { + // Skip empty "Expect:" header we add by default + if (header->data && std::string(header->data) != "Expect:") { + cmd << " \\\n -H '" << header->data << "'"; + } + header = header->next; + } + + // Form fields (multipart) - iterate through curl_httppost + ::curl_httppost *formpost = p->form; + while (formpost) { + if (formpost->showfilename) { + // File upload (showfilename is set when CURLFORM_FILENAME is used) + cmd << " \\\n -F '" << formpost->name << "=@" << formpost->showfilename << "'"; + } else if (formpost->contents) { + // Regular form field with contents + cmd << " \\\n -F '" << formpost->name << "=" << formpost->contents << "'"; + } else { + // Stream or other type without direct contents + cmd << " \\\n -F '" << formpost->name << "='"; + } + formpost = formpost->next; + } + + // Post body + if (!p->postfields.empty()) { + // Escape single quotes in the body for shell safety + std::string escaped_body = p->postfields; + size_t pos = 0; + while ((pos = escaped_body.find('\'', pos)) != std::string::npos) { + escaped_body.replace(pos, 1, "'\\''"); + pos += 4; + } + // Truncate if too long for display + if (escaped_body.length() > 1000) { + escaped_body = escaped_body.substr(0, 1000) + "..."; + } + cmd << " \\\n -d '" << escaped_body << "'"; + } + + // Put file + if (p->putFile) { + cmd << " \\\n --upload-file "; + } + + BOOST_LOG_TRIVIAL(info) << "Http request:\n" << cmd.str(); +} + Http Http::get(std::string url) { return Http{std::move(url)}; @@ -765,6 +838,7 @@ Http Http::get(std::string url) Http Http::post(std::string url) { Http http{std::move(url)}; + http.p->method = "POST"; curl_easy_setopt(http.p->curl, CURLOPT_POST, 1L); return http; } @@ -772,6 +846,7 @@ Http Http::post(std::string url) Http Http::put(std::string url) { Http http{std::move(url)}; + http.p->method = "PUT"; curl_easy_setopt(http.p->curl, CURLOPT_UPLOAD, 1L); return http; } @@ -779,6 +854,7 @@ Http Http::put(std::string url) Http Http::put2(std::string url) { Http http{ std::move(url) }; + http.p->method = "PUT"; curl_easy_setopt(http.p->curl, CURLOPT_CUSTOMREQUEST, "PUT"); return http; } @@ -786,6 +862,7 @@ Http Http::put2(std::string url) Http Http::patch(std::string url) { Http http{ std::move(url) }; + http.p->method = "PATCH"; curl_easy_setopt(http.p->curl, CURLOPT_CUSTOMREQUEST, "PATCH"); return http; } @@ -793,6 +870,7 @@ Http Http::patch(std::string url) Http Http::del(std::string url) { Http http{ std::move(url) }; + http.p->method = "DELETE"; curl_easy_setopt(http.p->curl, CURLOPT_CUSTOMREQUEST, "DELETE"); return http; } diff --git a/src/slic3r/Utils/Http.hpp b/src/slic3r/Utils/Http.hpp index d8eabb6dfd..b7c6fac4a5 100644 --- a/src/slic3r/Utils/Http.hpp +++ b/src/slic3r/Utils/Http.hpp @@ -183,6 +183,9 @@ public: // Cancels a request in progress void cancel(); + // Print the request as a curl command for debugging + void print() const; + // Tells whether current backend supports seting up a CA file using ca_file() static bool ca_file_supported(); diff --git a/src/slic3r/Utils/ICloudServiceAgent.hpp b/src/slic3r/Utils/ICloudServiceAgent.hpp new file mode 100644 index 0000000000..5a5d02cd38 --- /dev/null +++ b/src/slic3r/Utils/ICloudServiceAgent.hpp @@ -0,0 +1,477 @@ +#ifndef __I_CLOUD_SERVICE_AGENT_HPP__ +#define __I_CLOUD_SERVICE_AGENT_HPP__ + +#include "bambu_networking.hpp" +#include "../../libslic3r/ProjectTask.hpp" +#include +#include +#include +#include +#include + +namespace Slic3r { + +/** + * ICloudServiceAgent - Interface for authentication and cloud service operations. + * + * This interface encapsulates all cloud-related functionality including authentication: + * - Lifecycle methods for agent initialization + * - User session management (login/logout) + * - Token access for dependent agents (IPrinterAgent) + * - Login UI command builders for WebView integration + * - Server connectivity and subscription management + * - Settings synchronization (presets upload/download) + * - Cloud user services (messages, tasks, firmware) + * - Model mall and publishing + * - Analytics and telemetry + * - Ratings and reviews + * + * Implementations: + * - OrcaCloudServiceAgent: Native implementation for Orca Cloud (includes OAuth PKCE) + * - BBLCloudServiceAgent: Wrapper around Bambu Lab's proprietary DLL + * + * Token Sharing Pattern: + * IPrinterAgent receives an ICloudServiceAgent instance via set_cloud_agent() to + * access tokens for cloud-relay operations without coupling to a specific auth + * implementation. + */ +class ICloudServiceAgent { +public: + virtual ~ICloudServiceAgent() = default; + + // ======================================================================== + // Lifecycle Methods + // ======================================================================== + /** + * Initialize the logging backend for the agent. + * Call after set_config_dir() so logs have a destination. + */ + virtual int init_log() = 0; + + /** + * Provide the writable configuration directory for storing auth state. + * Must be called before start(). + */ + virtual int set_config_dir(std::string config_dir) = 0; + + /** + * Register the client certificate file for TLS authentication. + * May be unused by some implementations (e.g., OrcaCloudServiceAgent). + */ + virtual int set_cert_file(std::string folder, std::string filename) = 0; + + /** + * Set the country code for region-specific backend selection. + */ + virtual int set_country_code(std::string country_code) = 0; + + /** + * Start the agent, performing any expensive initialization. + * Typically regenerates PKCE bundles and attempts silent sign-in. + */ + virtual int start() = 0; + + // ======================================================================== + // User Session Management + // ======================================================================== + /** + * Authenticate the user with the provided JSON payload. + * + * Supported formats: + * 1. Traditional: {"username": "...", "password": "..."} + * 2. WebView/OAuth: {"command": "user_login", "data": {...}} + * 3. Token format: {"data": {"token": "...", "refresh_token": "...", "user": {...}}} + * + * On completion, invokes the registered OnUserLoginFn callback. + */ + virtual int change_user(std::string user_info) = 0; + + /** + * Check whether a valid authenticated session exists. + */ + virtual bool is_user_login() = 0; + + /** + * Terminate the current session. + * @param request If true, also notify the backend to invalidate the session. + */ + virtual int user_logout(bool request = false) = 0; + + /** + * Return the backend-generated user ID for the current session. + */ + virtual std::string get_user_id() = 0; + + /** + * Return the display name for the current user. + */ + virtual std::string get_user_name() = 0; + + /** + * Return the avatar URL/path for the current user. + */ + virtual std::string get_user_avatar() = 0; + + /** + * Return the nickname for the current user. + */ + virtual std::string get_user_nickname() = 0; + + // ======================================================================== + // Login UI Support + // ======================================================================== + /** + * Build a JSON command for the WebView login flow. + * Contains backend URL, API key, and PKCE parameters. + */ + virtual std::string build_login_cmd() = 0; + + /** + * Build a JSON command for WebView logout. + */ + virtual std::string build_logout_cmd() = 0; + + /** + * Return a JSON snapshot of the active session (user info, no tokens). + * Used by WebView to display current user state. + */ + virtual std::string build_login_info() = 0; + + // ======================================================================== + // Token Access (for dependent agents) + // ======================================================================== + /** + * Return the current access token for API calls. + * Cloud and printer agents use this for Authorization headers. + */ + virtual std::string get_access_token() const = 0; + + /** + * Return the current refresh token (if available). + */ + virtual std::string get_refresh_token() const = 0; + + /** + * Ensure the access token is fresh, refreshing if necessary. + * Call before making API requests to avoid 401 errors. + * + * @param reason Descriptive string for logging (e.g., "connect_server") + * @return true if the token is fresh or was successfully refreshed + */ + virtual bool ensure_token_fresh(const std::string& reason) = 0; + + // ======================================================================== + // Server Connectivity + // ======================================================================== + /** + * Return the base hostname for cloud API calls (varies by region). + * Helpful for diagnostics and when building browser URLs. + */ + virtual std::string get_cloud_service_host() = 0; + + /** + * Return the login URL for the cloud service. + * @param language Optional language code (e.g., "en-US", "zh-CN") for localized login page. + * If empty, returns the default (non-localized) login URL. + * @return The full URL to the login page, or a local file:// URL for native implementations. + */ + virtual std::string get_cloud_login_url(const std::string& language = "") = 0; + + /** + * Perform a health check against the configured backend. + * Updates is_server_connected() state and triggers OnServerConnectedFn. + */ + virtual int connect_server() = 0; + + /** + * Return whether the server is currently reachable. + */ + virtual bool is_server_connected() = 0; + + /** + * Force a server state recheck, clearing any cached state. + */ + virtual int refresh_connection() = 0; + + /** + * Subscribe to a logical module (e.g., "printer", "user"). + */ + virtual int start_subscribe(std::string module) = 0; + + /** + * Stop listening to a formerly subscribed module. + */ + virtual int stop_subscribe(std::string module) = 0; + + /** + * Subscribe to push streams for specific device identifiers. + */ + virtual int add_subscribe(std::vector dev_list) = 0; + + /** + * Remove device-level subscriptions. + */ + virtual int del_subscribe(std::vector dev_list) = 0; + + /** + * Enable or disable multi-machine mode. + */ + virtual void enable_multi_machine(bool enable) = 0; + + // ======================================================================== + // Settings Synchronization + // ======================================================================== + /** + * Fetch all presets owned by the logged-in user. + * @param user_presets Map populated with [type][setting_id] = json + */ + virtual int get_user_presets(std::map>* user_presets) = 0; + + /** + * Request a new preset identifier from the server. + */ + virtual std::string request_setting_id(std::string name, std::map* values_map, unsigned int* http_code) = 0; + + /** + * Update or create a preset with a known setting_id. + */ + virtual int put_setting(std::string setting_id, std::string name, std::map* values_map, unsigned int* http_code) = 0; + + /** + * Trigger bulk download of user presets. + */ + virtual int get_setting_list(std::string bundle_version, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr) = 0; + + /** + * Enhanced preset sync with per-item validation. + */ + virtual int get_setting_list2(std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr) = 0; + + /** + * Delete a remote preset. + */ + virtual int delete_setting(std::string setting_id) = 0; + + // ======================================================================== + // Cloud User Services + // ======================================================================== + /** + * Retrieve inbox/notification messages. + */ + virtual int get_my_message(int type, int after, int limit, unsigned int* http_code, std::string* http_body) = 0; + + /** + * Check for pending task reports. + */ + virtual int check_user_task_report(int* task_id, bool* printable) = 0; + + /** + * Fetch aggregated print statistics. + */ + virtual int get_user_print_info(unsigned int* http_code, std::string* http_body) = 0; + + /** + * Query user's tasks/prints. + */ + virtual int get_user_tasks(TaskQueryParams params, std::string* http_body) = 0; + + /** + * Fetch firmware information for a printer. + */ + virtual int get_printer_firmware(std::string dev_id, unsigned* http_code, std::string* http_body) = 0; + + /** + * Get plate index for a cloud task. + */ + virtual int get_task_plate_index(std::string task_id, int* plate_index) = 0; + + /** + * Retrieve extended user profile info. + */ + virtual int get_user_info(int* identifier) = 0; + + /** + * Fetch subtask information. + */ + virtual int get_subtask_info(std::string subtask_id, std::string* task_json, unsigned int* http_code, std::string* http_body) = 0; + + /** + * Retrieve slicing job info. + */ + virtual int get_slice_info(std::string project_id, std::string profile_id, int plate_index, std::string* slice_json) = 0; + + /** + * Query binding status for multiple devices. + */ + virtual int query_bind_status(std::vector query_list, unsigned int* http_code, std::string* http_body) = 0; + + /** + * Update printer name in cloud profile. + */ + virtual int modify_printer_name(std::string dev_id, std::string dev_name) = 0; + + // ======================================================================== + // Model Mall & Publishing + // ======================================================================== + /** + * Request live camera streaming URL. + */ + virtual int get_camera_url(std::string dev_id, std::function callback) = 0; + + /** + * Fetch staff-picked designs from model mall. + */ + virtual int get_design_staffpick(int offset, int limit, std::function callback) = 0; + + /** + * Run multi-stage publishing workflow. + */ + virtual int start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out) = 0; + + /** + * Get model publish URL. + */ + virtual int get_model_publish_url(std::string* url) = 0; + + /** + * Fetch publishing subtask information. + */ + virtual int get_subtask(BBLModelTask* task, OnGetSubTaskFn getsub_fn) = 0; + + /** + * Get model mall home URL. + */ + virtual int get_model_mall_home_url(std::string* url) = 0; + + /** + * Build model detail page URL. + */ + virtual int get_model_mall_detail_url(std::string* url, std::string id) = 0; + + /** + * Retrieve user's model mall profile. + */ + virtual int get_my_profile(std::string token, unsigned int* http_code, std::string* http_body) = 0; + + // ======================================================================== + // Analytics & Tracking + // ======================================================================== + /** + * Enable/disable telemetry. + */ + virtual int track_enable(bool enable) = 0; + + /** + * Delete telemetry files. + */ + virtual int track_remove_files() = 0; + + /** + * Report a custom analytics event. + */ + virtual int track_event(std::string evt_key, std::string content) = 0; + + /** + * Set telemetry headers. + */ + virtual int track_header(std::string header) = 0; + + /** + * Update a tracked user property. + */ + virtual int track_update_property(std::string name, std::string value, std::string type = "string") = 0; + + /** + * Read a tracked user property. + */ + virtual int track_get_property(std::string name, std::string& value, std::string type = "string") = 0; + + /** + * Check if tracking is enabled. + */ + virtual bool get_track_enable() = 0; + + // ======================================================================== + // Ratings & Reviews + // ======================================================================== + /** + * Submit a review for a marketplace design. + */ + virtual int put_model_mall_rating(int design_id, int score, std::string content, std::vector images, unsigned int& http_code, std::string& http_error) = 0; + + /** + * Get OSS configuration for image uploads. + */ + virtual int get_oss_config(std::string& config, std::string country_code, unsigned int& http_code, std::string& http_error) = 0; + + /** + * Upload rating images to OSS. + */ + virtual int put_rating_picture_oss(std::string& config, std::string& pic_oss_path, std::string model_id, int profile_id, unsigned int& http_code, std::string& http_error) = 0; + + /** + * Poll for rating result. + */ + virtual int get_model_mall_rating_result(int job_id, std::string& rating_result, unsigned int& http_code, std::string& http_error) = 0; + + // ======================================================================== + // Extra Features + // ======================================================================== + /** + * Set additional HTTP headers for all requests. + */ + virtual int set_extra_http_header(std::map extra_headers) = 0; + + /** + * Get the studio info URL. + */ + virtual std::string get_studio_info_url() = 0; + + /** + * Fetch MakerWorld user preferences. + */ + virtual int get_mw_user_preference(std::function callback) = 0; + + /** + * Retrieve MakerWorld "For You" list. + */ + virtual int get_mw_user_4ulist(int seed, int limit, std::function callback) = 0; + + /** + * Return the version of the cloud service implementation. + */ + virtual std::string get_version() = 0; + + // ======================================================================== + // Callback Registration + // ======================================================================== + /** + * Register the login status callback. + * Called after change_user() finishes or when the session expires. + */ + virtual int set_on_user_login_fn(OnUserLoginFn fn) = 0; + + /** + * Register server connection status callback. + */ + virtual int set_on_server_connected_fn(OnServerConnectedFn fn) = 0; + + /** + * Register HTTP error callback. + */ + virtual int set_on_http_error_fn(OnHttpErrorFn fn) = 0; + + /** + * Provide country code getter callback. + */ + virtual int set_get_country_code_fn(GetCountryCodeFn fn) = 0; + + /** + * Provide main thread queue callback. + */ + virtual int set_queue_on_main_fn(QueueOnMainFn fn) = 0; +}; + +} // namespace Slic3r + +#endif // __I_CLOUD_SERVICE_AGENT_HPP__ diff --git a/src/slic3r/Utils/IPrinterAgent.hpp b/src/slic3r/Utils/IPrinterAgent.hpp new file mode 100644 index 0000000000..3af9cf32f8 --- /dev/null +++ b/src/slic3r/Utils/IPrinterAgent.hpp @@ -0,0 +1,260 @@ +#ifndef __I_PRINTER_AGENT_HPP__ +#define __I_PRINTER_AGENT_HPP__ + +#include "bambu_networking.hpp" +#include +#include + +namespace Slic3r { + +class ICloudServiceAgent; + +/** + * AgentInfo - Metadata structure for printer agent information. + * + * Contains identification and descriptive information about a printer agent + * implementation, used for discovery and selection purposes. + */ +struct AgentInfo { + std::string id; ///< Unique identifier for the agent, e.g. "orca", "bbl" + std::string name; ///< Human-readable agent name, e.g. "Orca", "Bambu Lab" + std::string version; ///< Agent version string, e.g. "1.0.0" + std::string description; ///< Brief description of the agent's capabilities, e.g. "Orca printer agent" +}; + +/** + * FilamentSyncMode - Modes for filament data synchronization. + * + * Defines how filament information is obtained from the printer: + * - Subscription: Real-time push updates (e.g., MQTT subscriptions) + * - Pull: On-demand fetch via REST API (blocking call) + * - None: Filament sync unavailable + */ +enum class FilamentSyncMode { + none = 0, ///< Filament synchronization not supported + subscription, ///< Real-time push updates via subscription (e.g., MQTT) + pull ///< On-demand fetch via REST API (blocking call) +}; + +/** + * IPrinterAgent - Interface for printer operations. + * + * This interface encapsulates all printer-related functionality: + * - Direct printer communication (LAN and cloud relay) + * - Certificate management + * - Device discovery (SSDP) + * - Printer binding/unbinding + * - Print job operations + * + * Implementations: + * - OrcaPrinterAgent: Stub implementation (printer ops not yet supported) + * - BBLPrinterAgent: Wrapper around Bambu Lab's proprietary DLL + * + * Token Access: + * Printer agents receive an ICloudServiceAgent instance via set_cloud_agent() to + * access tokens for cloud-relay operations. + */ + +class IPrinterAgent { +public: + virtual ~IPrinterAgent() = default; + + // ======================================================================== + // Cloud Agent Dependency + // ======================================================================== + /** + * Set the cloud agent used for token access. + * Must be called before any cloud-relay operations. + */ + virtual void set_cloud_agent(std::shared_ptr cloud) = 0; + + // ======================================================================== + // Communication + // ======================================================================== + /** + * Publish a JSON command to a printer through cloud relay. + */ + virtual int send_message(std::string dev_id, std::string json_str, int qos, int flag) = 0; + + /** + * Establish a direct LAN connection to a printer. + */ + virtual int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) = 0; + + /** + * Tear down the active LAN printer connection. + */ + virtual int disconnect_printer() = 0; + + /** + * Send a JSON command to a LAN printer (bypassing cloud). + */ + virtual int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) = 0; + + // ======================================================================== + // Certificates + // ======================================================================== + /** + * Validate current user certificates for the printer. + */ + virtual int check_cert() = 0; + + /** + * Install or refresh device certificate for LAN TLS. + */ + virtual void install_device_cert(std::string dev_id, bool lan_only) = 0; + + // ======================================================================== + // Discovery + // ======================================================================== + /** + * Start or stop SSDP discovery. + */ + virtual bool start_discovery(bool start, bool sending) = 0; + + // ======================================================================== + // Binding + // ======================================================================== + /** + * Ping the binding endpoint to check printer readiness. + */ + virtual int ping_bind(std::string ping_code) = 0; + + /** + * Perform binding detection/handshake on a LAN printer. + */ + virtual int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) = 0; + + /** + * Execute the multi-stage printer binding workflow. + */ + virtual int bind(std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) = 0; + + /** + * Remove the association between account and printer. + */ + virtual int unbind(std::string dev_id) = 0; + + /** + * Request a one-time bind ticket from the server. + */ + virtual int request_bind_ticket(std::string* ticket) = 0; + + /** + * Register callback for fatal HTTP errors. + */ + virtual int set_server_callback(OnServerErrFn fn) = 0; + + // ======================================================================== + // Machine Selection + // ======================================================================== + /** + * Return the currently selected printer ID. + */ + virtual std::string get_user_selected_machine() = 0; + + /** + * Update the selected machine preference. + */ + virtual int set_user_selected_machine(std::string dev_id) = 0; + + // ======================================================================== + // Print Job Operations + // ======================================================================== + /** + * Start a fully managed cloud print. + */ + virtual int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) = 0; + + /** + * Start a local print with cloud record. + */ + virtual int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) = 0; + + /** + * Upload gcode to printer's SD card without starting. + */ + virtual int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) = 0; + + /** + * Start a LAN-only print. + */ + virtual int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) = 0; + + /** + * Start a print from printer's SD card. + */ + virtual int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) = 0; + + // ======================================================================== + // Callback Registration + // ======================================================================== + /** + * Register SSDP discovery callback. + */ + virtual int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) = 0; + + /** + * Register printer MQTT connection callback. + */ + virtual int set_on_printer_connected_fn(OnPrinterConnectedFn fn) = 0; + + /** + * Register subscription failure callback. + */ + virtual int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) = 0; + + /** + * Register cloud device message callback. + */ + virtual int set_on_message_fn(OnMessageFn fn) = 0; + + /** + * Register user-scoped message callback. + */ + virtual int set_on_user_message_fn(OnMessageFn fn) = 0; + + /** + * Register LAN connection status callback. + */ + virtual int set_on_local_connect_fn(OnLocalConnectedFn fn) = 0; + + /** + * Register LAN message callback. + */ + virtual int set_on_local_message_fn(OnMessageFn fn) = 0; + + /** + * Provide main thread queue callback. + */ + virtual int set_queue_on_main_fn(QueueOnMainFn fn) = 0; + + /** + * Get agent information. + */ + virtual AgentInfo get_agent_info() = 0; + + // ======================================================================== + // Filament Operations + // ======================================================================== + /** + * Get the filament synchronization mode for this agent. + * + * @return FilamentSyncMode indicating how filament data is obtained: + * - subscription: Real-time push updates via MQTT (no fetch needed) + * - pull: On-demand fetch via REST API (call fetch_filament_info()) + * - none: Filament synchronization not supported + */ + virtual FilamentSyncMode get_filament_sync_mode() const { return FilamentSyncMode::none; } + + /** + * Refresh filament info from the printer synchronously. + * Should only be called when get_filament_sync_mode() returns FilamentSyncMode::pull. + * Populates the MachineObject's DevFilaSystem with fetched filament data. + */ + virtual void fetch_filament_info(std::string dev_id) {} +}; + +} // namespace Slic3r + +#endif // __I_PRINTER_AGENT_HPP__ diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp new file mode 100644 index 0000000000..2792264830 --- /dev/null +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -0,0 +1,2031 @@ +#include "MoonrakerPrinterAgent.hpp" +#include "Http.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "slic3r/GUI/GUI_App.hpp" + +#include "nlohmann/json.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +namespace beast = boost::beast; +namespace http = beast::http; +namespace websocket = beast::websocket; +namespace net = boost::asio; +using tcp = net::ip::tcp; + +constexpr const char* k_no_api_key = "__NO_API_KEY__"; + +bool is_numeric(const std::string& value) +{ + return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c) != 0; }); +} + +std::string normalize_base_url(std::string host, const std::string& port) +{ + boost::trim(host); + if (host.empty()) { + return ""; + } + + std::string value = host; + if (is_numeric(port) && value.find("://") == std::string::npos && value.find(':') == std::string::npos) { + value += ":" + port; + } + + if (!boost::istarts_with(value, "http://") && !boost::istarts_with(value, "https://")) { + value = "http://" + value; + } + + if (value.size() > 1 && value.back() == '/') { + value.pop_back(); + } + + return value; +} + +std::string extract_host(const std::string& base_url) +{ + std::string host = base_url; + auto pos = host.find("://"); + if (pos != std::string::npos) { + host = host.substr(pos + 3); + } + pos = host.find('/'); + if (pos != std::string::npos) { + host = host.substr(0, pos); + } + return host; +} + +std::string join_url(const std::string& base_url, const std::string& path) +{ + if (base_url.empty()) { + return ""; + } + if (path.empty()) { + return base_url; + } + if (base_url.back() == '/' && path.front() == '/') { + return base_url.substr(0, base_url.size() - 1) + path; + } + if (base_url.back() != '/' && path.front() != '/') { + return base_url + "/" + path; + } + return base_url + path; +} + +std::string normalize_api_key(const std::string& api_key) +{ + if (api_key.empty() || api_key == k_no_api_key) { + return ""; + } + return api_key; +} + +// Sanitize filename to prevent path traversal attacks +// Extracts only the basename, removing any path components +std::string sanitize_filename(const std::string& filename) +{ + if (filename.empty()) { + return "print.gcode"; + } + namespace fs = boost::filesystem; + fs::path p(filename); + std::string basename = p.filename().string(); + if (basename.empty() || basename == "." || basename == "..") { + return "print.gcode"; + } + return basename; +} + +struct WsEndpoint +{ + std::string host; + std::string port; + std::string target; + bool secure = false; +}; + +bool parse_ws_endpoint(const std::string& base_url, WsEndpoint& endpoint) +{ + if (base_url.empty()) { + return false; + } + + std::string url = base_url; + if (boost::istarts_with(url, "https://")) { + endpoint.secure = true; + url = url.substr(8); + } else if (boost::istarts_with(url, "http://")) { + url = url.substr(7); + } + + auto slash = url.find('/'); + if (slash != std::string::npos) { + url = url.substr(0, slash); + } + if (url.empty()) { + return false; + } + + endpoint.host = url; + endpoint.port = endpoint.secure ? "443" : "80"; + if (auto colon = url.rfind(':'); colon != std::string::npos && url.find(']') == std::string::npos) { + endpoint.host = url.substr(0, colon); + endpoint.port = url.substr(colon + 1); + } + + endpoint.target = "/websocket"; + return !endpoint.host.empty() && !endpoint.port.empty(); +} + +std::string map_moonraker_state(std::string state) +{ + boost::algorithm::to_lower(state); + if (state == "printing") { + return "RUNNING"; + } + if (state == "paused") { + return "PAUSE"; + } + if (state == "complete") { + return "FINISH"; + } + if (state == "error" || state == "cancelled") { + return "FAILED"; + } + return "IDLE"; +} + +} // namespace + +namespace Slic3r { + +const std::string MoonrakerPrinterAgent_VERSION = "1.0.0"; + +MoonrakerPrinterAgent::MoonrakerPrinterAgent(std::string log_dir) : m_cloud_agent(nullptr) +{ + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Constructor - log_dir=" << log_dir; + (void) log_dir; +} + +MoonrakerPrinterAgent::~MoonrakerPrinterAgent() +{ + disconnect_printer(); // This will handle thread cleanup +} + +AgentInfo MoonrakerPrinterAgent::get_agent_info_static() +{ + return AgentInfo{.id = "moonraker", .name = "Moonraker Printer Agent", .version = MoonrakerPrinterAgent_VERSION, .description = "Klipper/Moonraker printer agent"}; +} + +void MoonrakerPrinterAgent::set_cloud_agent(std::shared_ptr cloud) +{ + std::lock_guard lock(state_mutex); + m_cloud_agent = cloud; + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: Cloud agent set"; +} + +int MoonrakerPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) +{ + (void) qos; + (void) flag; + return handle_request(dev_id, json_str); +} + +int MoonrakerPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) +{ + (void) qos; + (void) flag; + return handle_request(dev_id, json_str); +} + +int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) +{ + (void) username; + (void) use_ssl; + + std::string base_url = normalize_base_url(dev_ip, ""); + std::string api_key = normalize_api_key(password); + + PrinthostConfig config; + if (get_printhost_config(config)) { + if (base_url.empty()) { + base_url = config.base_url; + } + if (api_key.empty()) { + api_key = normalize_api_key(config.api_key); + } + } + + if (base_url.empty()) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: connect_printer missing host"; + dispatch_local_connect(ConnectStatusFailed, dev_id, "host_missing"); + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + if (dev_id.empty()) { + dev_id = extract_host(base_url); + } + + // Check if connection already in progress + { + std::lock_guard lock(connect_mutex); + if (connect_in_progress.load()) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Connection already in progress, waiting..."; + // Don't reject - wait for previous connection to complete + // This can happen if MonitorPanel triggers connect while previous connect is still running + } else { + connect_in_progress.store(true); + connect_stop_requested.store(false); + } + } + + // Wait for previous connection thread to finish + if (connect_thread.joinable()) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Waiting for previous connection thread..."; + connect_thread.join(); + } + + // Now we can start a new connection + { + std::lock_guard lock(connect_mutex); + connect_in_progress.store(true); + connect_stop_requested.store(false); + } + + // Stop existing status stream and clear state + stop_status_stream(); + { + std::lock_guard lock(payload_mutex); + status_cache = nlohmann::json::object(); + } + ws_last_emit_ms.store(0); + + store_host(dev_id, base_url, api_key); + + // Launch connection in background thread + connect_thread = std::thread([this, dev_id, base_url, api_key]() { + perform_connection_async(dev_id, base_url, api_key); + }); + + // Return immediately - UI is not blocked + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: connect_printer launched in background - dev_id=" << dev_id; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::disconnect_printer() +{ + // Stop connection thread if running + { + std::lock_guard lock(connect_mutex); + if (connect_in_progress.load()) { + connect_stop_requested.store(true); + // Wake up any sleeping + connect_cv.notify_all(); + } + } + + // Wait for connection thread to finish (with timeout) + if (connect_thread.joinable()) { + connect_thread.join(); + } + + stop_status_stream(); + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::check_cert() +{ + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: check_cert (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +void MoonrakerPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) +{ + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: install_device_cert (stub) - dev_id=" << dev_id << ", lan_only=" << lan_only; +} + +bool MoonrakerPrinterAgent::start_discovery(bool start, bool sending) +{ + (void) sending; + if (start) { + announce_printhost_device(); + } + return true; +} + +int MoonrakerPrinterAgent::ping_bind(std::string ping_code) +{ + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: ping_bind (stub) - ping_code=" << ping_code; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) +{ + (void) sec_link; + + std::string base_url = normalize_base_url(dev_ip, ""); + if (base_url.empty()) { + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + PrinthostConfig config; + get_printhost_config(config); + const std::string api_key = normalize_api_key(config.api_key); + + MoonrakerDeviceInfo info; + std::string error; + if (!fetch_device_info(base_url, api_key, info, error)) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: bind_detect failed: " << error; + return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; + } + + detect.dev_id = info.dev_id.empty() ? dev_ip : info.dev_id; + // Prefer fetched hostname, then preset model name, then generic fallback + std::string fallback_name = config.model_name.empty() ? "Moonraker Printer" : config.model_name; + detect.dev_name = info.dev_name.empty() ? fallback_name : info.dev_name; + detect.model_id = "moonraker"; + detect.version = info.version; + detect.connect_type = "lan"; + detect.bind_state = "free"; + + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::bind( + std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) +{ + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: bind (stub) - dev_id=" << dev_id; + (void) dev_ip; + (void) sec_link; + (void) timezone; + (void) improved; + (void) update_fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::unbind(std::string dev_id) +{ + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: unbind (stub) - dev_id=" << dev_id; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::request_bind_ticket(std::string* ticket) +{ + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: request_bind_ticket (stub)"; + if (ticket) + *ticket = ""; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_server_callback(OnServerErrFn fn) +{ + std::lock_guard lock(state_mutex); + on_server_err_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +std::string MoonrakerPrinterAgent::get_user_selected_machine() +{ + std::lock_guard lock(state_mutex); + return selected_machine; +} + +int MoonrakerPrinterAgent::set_user_selected_machine(std::string dev_id) +{ + std::lock_guard lock(state_mutex); + selected_machine = dev_id; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +{ + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: start_print (stub) - task_name=" << params.task_name; + (void) update_fn; + (void) cancel_fn; + (void) wait_fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +{ + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: start_local_print_with_record (stub)"; + (void) params; + (void) update_fn; + (void) cancel_fn; + (void) wait_fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +{ + (void) wait_fn; + + if (update_fn) update_fn(PrintingStageCreate, 0, "Preparing..."); + + const std::string base_url = resolve_host(params.dev_id); + if (base_url.empty()) { + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + const std::string api_key = resolve_api_key(params.dev_id, params.password); + + std::string filename = params.filename; + if (filename.empty()) { + filename = params.task_name; + } + if (!boost::iends_with(filename, ".gcode")) { + filename += ".gcode"; + } + + // Sanitize filename to prevent path traversal attacks + std::string safe_filename = sanitize_filename(filename); + + // Upload only, don't start print + if (!upload_gcode(params.filename, safe_filename, base_url, api_key, update_fn, cancel_fn)) { + return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; + } + + if (update_fn) update_fn(PrintingStageFinished, 100, "File uploaded"); + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) +{ + if (update_fn) update_fn(PrintingStageCreate, 0, "Preparing..."); + + // Check cancellation + if (cancel_fn && cancel_fn()) { + return BAMBU_NETWORK_ERR_CANCELED; + } + + const std::string base_url = resolve_host(params.dev_id); + if (base_url.empty()) { + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + const std::string api_key = resolve_api_key(params.dev_id, params.password); + + // Determine the G-code file to upload + // params.filename may be .3mf, params.dst_file contains actual G-code + std::string gcode_path = params.filename; + if (!params.dst_file.empty()) { + gcode_path = params.dst_file; + } + + // Check if file exists and has .gcode extension + namespace fs = boost::filesystem; + fs::path source_path(gcode_path); + if (!fs::exists(source_path)) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: G-code file does not exist: " << gcode_path; + return BAMBU_NETWORK_ERR_FILE_NOT_EXIST; + } + + // Extract filename for upload (relative to gcodes root) + std::string upload_filename = source_path.filename().string(); + if (!boost::iends_with(upload_filename, ".gcode")) { + upload_filename += ".gcode"; + } + // Sanitize filename to prevent path traversal attacks (extra safety) + upload_filename = sanitize_filename(upload_filename); + + // Upload file + if (update_fn) update_fn(PrintingStageUpload, 0, "Uploading G-code..."); + if (!upload_gcode(gcode_path, upload_filename, base_url, api_key, update_fn, cancel_fn)) { + return BAMBU_NETWORK_ERR_PRINT_LP_UPLOAD_FTP_FAILED; + } + + // Check cancellation + if (cancel_fn && cancel_fn()) { + return BAMBU_NETWORK_ERR_CANCELED; + } + + // Start print via gcode script (simpler than JSON-RPC) + if (update_fn) update_fn(PrintingStageSending, 0, "Starting print..."); + std::string gcode = "SDCARD_PRINT_FILE FILENAME=" + upload_filename; + if (!send_gcode(params.dev_id, gcode)) { + return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED; + } + + if (update_fn) update_fn(PrintingStageFinished, 100, "Print started"); + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) +{ + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: start_sdcard_print (stub)"; + (void) params; + (void) update_fn; + (void) cancel_fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_on_ssdp_msg_fn(OnMsgArrivedFn fn) +{ + { + std::lock_guard lock(state_mutex); + on_ssdp_msg_fn = fn; + } + // Call announce_printhost_device() outside the lock to avoid deadlock + // since announce_printhost_device() also acquires state_mutex + if (fn) { + announce_printhost_device(); + } + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_on_printer_connected_fn(OnPrinterConnectedFn fn) +{ + std::lock_guard lock(state_mutex); + on_printer_connected_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) +{ + std::lock_guard lock(state_mutex); + on_subscribe_failure_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_on_message_fn(OnMessageFn fn) +{ + std::lock_guard lock(state_mutex); + on_message_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_on_user_message_fn(OnMessageFn fn) +{ + std::lock_guard lock(state_mutex); + on_user_message_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_on_local_connect_fn(OnLocalConnectedFn fn) +{ + std::lock_guard lock(state_mutex); + on_local_connect_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_on_local_message_fn(OnMessageFn fn) +{ + std::lock_guard lock(state_mutex); + on_local_message_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) +{ + std::lock_guard lock(state_mutex); + queue_on_main_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +void MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) +{ + // Moonraker doesn't have standard filament tracking like Qidi + // This is a no-op for standard Moonraker installations + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: fetch_filament_info (no-op) - dev_id=" << dev_id; +} + +int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) + { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: handle_request received: " << json_str; + auto json = nlohmann::json::parse(json_str, nullptr, false); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Invalid JSON request"; + return BAMBU_NETWORK_ERR_INVALID_RESULT; + } + + // Handle info commands + if (json.contains("info") && json["info"].contains("command")) { + const auto& command = json["info"]["command"]; + if (command.is_string() && command.get() == "get_version") { + return send_version_info(dev_id); + } + } + + // Handle system commands + if (json.contains("system") && json["system"].contains("command")) { + const auto& command = json["system"]["command"]; + if (command.is_string() && command.get() == "get_access_code") { + return send_access_code(dev_id); + } + } + + // Handle print commands + if (json.contains("print") && json["print"].contains("command")) { + const auto& command = json["print"]["command"]; + if (!command.is_string()) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: print command is not a string"; + return BAMBU_NETWORK_ERR_INVALID_RESULT; + } + + const std::string cmd = command.get(); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Received print command: " << cmd; + + // Handle gcode_line command - this is how G-code commands are sent from OrcaSlicer + if (cmd == "gcode_line") { + if (!json["print"].contains("param") || !json["print"]["param"].is_string()) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: gcode_line missing param value, full json: " << json_str; + return BAMBU_NETWORK_ERR_INVALID_RESULT; + } + std::string gcode = json["print"]["param"].get(); + + // Extract sequence_id from request if present + std::string sequence_id; + if (json["print"].contains("sequence_id") && json["print"]["sequence_id"].is_string()) { + sequence_id = json["print"]["sequence_id"].get(); + } + + nlohmann::json response; + response["print"]["command"] = "gcode_line"; + if (!sequence_id.empty()) { + response["print"]["sequence_id"] = sequence_id; + } + response["print"]["param"] = gcode; + + if (send_gcode(dev_id, gcode)) { + response["print"]["result"] = "success"; + dispatch_message(dev_id, response.dump()); + return BAMBU_NETWORK_SUCCESS; + } + response["print"]["result"] = "failed"; + dispatch_message(dev_id, response.dump()); + return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; + } + + // ===== NEW: Print control commands ===== + if (cmd == "pause") { + return pause_print(dev_id); + } + if (cmd == "resume") { + return resume_print(dev_id); + } + if (cmd == "stop") { + return cancel_print(dev_id); + } + + // Bed temperature - UI sends "temp" field + if (cmd == "set_bed_temp") { + if (json["print"].contains("temp") && json["print"]["temp"].is_number()) { + int temp = json["print"]["temp"].get(); + std::string gcode = "SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=" + std::to_string(temp); + send_gcode(dev_id, gcode); + return BAMBU_NETWORK_SUCCESS; + } + } + + // Nozzle temperature - UI sends "target_temp" and "extruder_index" fields + if (cmd == "set_nozzle_temp") { + if (json["print"].contains("target_temp") && json["print"]["target_temp"].is_number()) { + int temp = json["print"]["target_temp"].get(); + int extruder_idx = 0; // Default to main extruder + if (json["print"].contains("extruder_index") && json["print"]["extruder_index"].is_number()) { + extruder_idx = json["print"]["extruder_index"].get(); + } + std::string heater = (extruder_idx == 0) ? "extruder" : "extruder" + std::to_string(extruder_idx); + std::string gcode = "SET_HEATER_TEMPERATURE HEATER=" + heater + " TARGET=" + std::to_string(temp); + send_gcode(dev_id, gcode); + return BAMBU_NETWORK_SUCCESS; + } + } + + if (cmd == "home") { + return send_gcode(dev_id, "G28") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; + } + } + + return BAMBU_NETWORK_SUCCESS; +} + +bool MoonrakerPrinterAgent::get_printhost_config(PrinthostConfig& config) const +{ + auto* preset_bundle = GUI::wxGetApp().preset_bundle; + if (!preset_bundle) { + return false; + } + + auto& preset = preset_bundle->printers.get_edited_preset(); + const auto& printer_cfg = preset.config; + const DynamicPrintConfig* host_cfg = &printer_cfg; + config.host = host_cfg->opt_string("print_host"); + if (config.host.empty()) { + if (auto* physical_cfg = preset_bundle->physical_printers.get_selected_printer_config()) { + if (!physical_cfg->opt_string("print_host").empty()) { + host_cfg = physical_cfg; + config.host = host_cfg->opt_string("print_host"); + } + } + } + if (config.host.empty()) { + return false; + } + + config.port = host_cfg->opt_string("printhost_port"); + config.api_key = host_cfg->opt_string("printhost_apikey"); + config.model_name = printer_cfg.opt_string("printer_model"); + config.base_url = normalize_base_url(config.host, config.port); + + return !config.base_url.empty(); +} + +bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, + const std::string& api_key, + MoonrakerDeviceInfo& info, + std::string& error) const +{ + auto fetch_json = [&](const std::string& url, nlohmann::json& out) { + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(10) + .timeout_max(30) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + out = nlohmann::json::parse(response_body, nullptr, false, true); + if (out.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + return true; + }; + + nlohmann::json json; + std::string url = join_url(base_url, "/server/info"); + if (!fetch_json(url, json)) { + return false; + } + + nlohmann::json result = json.contains("result") ? json["result"] : json; + info.dev_name = result.value("hostname", "Moonraker Printer"); + info.dev_id = result.value("hostname", ""); + info.version = result.value("moonraker_version", ""); + + return true; +} + +bool MoonrakerPrinterAgent::fetch_server_info(const std::string& base_url, + const std::string& api_key, + std::string& version, + std::string& error) const +{ + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(join_url(base_url, "/server/info")); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(10) + .timeout_max(30) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + + nlohmann::json result = json.contains("result") ? json["result"] : json; + if (result.contains("moonraker_version") && result["moonraker_version"].is_string()) { + version = result["moonraker_version"].get(); + } else if (result.contains("version") && result["version"].is_string()) { + version = result["version"].get(); + } + + return true; +} + +bool MoonrakerPrinterAgent::fetch_server_info_json(const std::string& base_url, + const std::string& api_key, + nlohmann::json& info, + std::string& error) const +{ + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(join_url(base_url, "/server/info")); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(10) + .timeout_max(30) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + info = nlohmann::json::parse(response_body, nullptr, false, true); + if (info.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + + return true; +} + +bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url, + const std::string& api_key, + nlohmann::json& status, + std::string& error) const +{ + std::string url = join_url(base_url, "/printer/objects/query?print_stats&virtual_sdcard&extruder&heater_bed&fan"); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(10) + .timeout_max(30) + .on_complete([&](std::string body, unsigned status_code) { + if (status_code == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status_code); + } + }) + .on_error([&](std::string body, std::string err, unsigned status_code) { + http_error = err; + if (status_code > 0) { + http_error += " (HTTP " + std::to_string(status_code) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + + if (!json.contains("result") || !json["result"].contains("status")) { + error = "Unexpected JSON structure"; + return false; + } + + status = json["result"]["status"]; + return true; +} + +bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::string& gcode) const +{ + const std::string base_url = resolve_host(dev_id); + if (base_url.empty()) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: send_gcode - empty base_url for dev_id=" << dev_id; + return false; + } + const std::string api_key = resolve_api_key(dev_id, ""); + + nlohmann::json payload; + payload["script"] = gcode; + std::string payload_str = payload.dump(); + + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: send_gcode to " << base_url << " with payload: " << payload_str; + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::post(join_url(base_url, "/printer/gcode/script")); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.header("Content-Type", "application/json") + .set_post_body(payload_str) + .timeout_connect(10) + .timeout_max(30) + .on_complete([&](std::string body, unsigned status_code) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: send_gcode response status=" << status_code << " body=" << body; + if (status_code == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status_code); + } + }) + .on_error([&](std::string body, std::string err, unsigned status_code) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: send_gcode error - body=" << body << " err=" << err << " status=" << status_code; + http_error = err; + if (status_code > 0) { + http_error += " (HTTP " + std::to_string(status_code) + ")"; + } + }) + .perform_sync(); + + if (!success) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: send_gcode failed: " << http_error; + return false; + } + + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: sent gcode successfully: " << gcode; + return true; +} + +bool MoonrakerPrinterAgent::fetch_object_list(const std::string& base_url, + const std::string& api_key, + std::set& objects, + std::string& error) const +{ + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(join_url(base_url, "/printer/objects/list")); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(10) + .timeout_max(30) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + + nlohmann::json result = json.contains("result") ? json["result"] : json; + if (!result.contains("objects") || !result["objects"].is_array()) { + error = "Unexpected JSON structure"; + return false; + } + + objects.clear(); + for (const auto& entry : result["objects"]) { + if (entry.is_string()) { + objects.insert(entry.get()); + } + } + + return !objects.empty(); +} + +int MoonrakerPrinterAgent::send_version_info(const std::string& dev_id) +{ + const std::string base_url = resolve_host(dev_id); + if (base_url.empty()) { + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + const std::string api_key = resolve_api_key(dev_id, ""); + + std::string version; + std::string error; + if (!fetch_server_info(base_url, api_key, version, error)) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Failed to fetch server info: " << error; + } + if (version.empty()) { + version = "moonraker"; + } + + nlohmann::json payload; + payload["info"]["command"] = "get_version"; + payload["info"]["result"] = "success"; + payload["info"]["module"] = nlohmann::json::array(); + + nlohmann::json module; + module["name"] = "ota"; + module["sw_ver"] = version; + module["product_name"] = "Moonraker"; + payload["info"]["module"].push_back(module); + + dispatch_message(dev_id, payload.dump()); + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::send_access_code(const std::string& dev_id) +{ + nlohmann::json payload; + payload["system"]["command"] = "get_access_code"; + payload["system"]["access_code"] = resolve_api_key(dev_id, ""); + dispatch_message(dev_id, payload.dump()); + return BAMBU_NETWORK_SUCCESS; +} + +void MoonrakerPrinterAgent::announce_printhost_device() +{ + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device() called"; + + PrinthostConfig config; + if (!get_printhost_config(config)) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - no printhost config"; + return; + } + + const std::string base_url = config.base_url; + if (base_url.empty()) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - empty base_url"; + return; + } + + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - base_url=" << base_url; + + OnMsgArrivedFn ssdp_fn; + { + std::lock_guard lock(state_mutex); + ssdp_fn = on_ssdp_msg_fn; + if (!ssdp_fn) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - no ssdp callback"; + return; + } + if (ssdp_announced_host == base_url && !ssdp_announced_id.empty()) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - already announced"; + return; + } + } + + const std::string dev_id = extract_host(base_url); + const std::string api_key = normalize_api_key(config.api_key); + + // Try to fetch actual device name from Moonraker + // Priority: 1) Moonraker hostname, 2) Preset model name, 3) Generic fallback + std::string dev_name; + MoonrakerDeviceInfo info; + std::string fetch_error; + if (fetch_device_info(base_url, api_key, info, fetch_error) && !info.dev_name.empty()) { + dev_name = info.dev_name; + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Got device name from printer: " << dev_name; + } else { + dev_name = config.model_name.empty() ? "Moonraker Printer" : config.model_name; + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Using fallback device name: " << dev_name + << " (fetch_error=" << fetch_error << ")"; + } + + if (auto* app_config = GUI::wxGetApp().app_config) { + const std::string access_code = api_key.empty() ? k_no_api_key : api_key; + app_config->set_str("access_code", dev_id, access_code); + app_config->set_str("user_access_code", dev_id, access_code); + } + + store_host(dev_id, base_url, api_key); + + nlohmann::json payload; + payload["dev_name"] = dev_name; + payload["dev_id"] = dev_id; + payload["dev_ip"] = extract_host(base_url); + payload["dev_type"] = "moonraker"; + payload["dev_signal"] = "0"; + payload["connect_type"] = "lan"; + payload["bind_state"] = "free"; + payload["sec_link"] = "secure"; + payload["ssdp_version"] = "v1"; + + ssdp_fn(payload.dump()); + + { + std::lock_guard lock(state_mutex); + ssdp_announced_host = base_url; + ssdp_announced_id = dev_id; + + // Set this as the selected machine if nothing is currently selected + // This ensures auto-connect works when MonitorPanel opens + if (selected_machine.empty()) { + selected_machine = dev_id; + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Auto-selected machine: " << dev_id; + } + } +} + +void MoonrakerPrinterAgent::dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg) +{ + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: dispatch_local_connect state=" << state + << " dev_id=" << dev_id << " msg=" << msg; + + OnLocalConnectedFn local_fn; + QueueOnMainFn queue_fn; + { + std::lock_guard lock(state_mutex); + local_fn = on_local_connect_fn; + queue_fn = queue_on_main_fn; + } + if (!local_fn) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: dispatch_local_connect - no callback registered!"; + return; + } + + auto dispatch = [state, dev_id, msg, local_fn]() { local_fn(state, dev_id, msg); }; + if (queue_fn) { + queue_fn(dispatch); + } else { + dispatch(); + } +} + +void MoonrakerPrinterAgent::dispatch_printer_connected(const std::string& dev_id) +{ + OnPrinterConnectedFn connected_fn; + QueueOnMainFn queue_fn; + { + std::lock_guard lock(state_mutex); + connected_fn = on_printer_connected_fn; + queue_fn = queue_on_main_fn; + } + if (!connected_fn) { + return; + } + + auto dispatch = [dev_id, connected_fn]() { connected_fn(dev_id); }; + if (queue_fn) { + queue_fn(dispatch); + } else { + dispatch(); + } +} + +void MoonrakerPrinterAgent::start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key) +{ + stop_status_stream(); + if (base_url.empty()) { + return; + } + + ws_stop.store(false); + ws_thread = std::thread([this, dev_id, base_url, api_key]() { + run_status_stream(dev_id, base_url, api_key); + }); +} + +void MoonrakerPrinterAgent::stop_status_stream() +{ + ws_stop.store(true); + if (ws_thread.joinable()) { + ws_thread.join(); + } +} + +void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string base_url, std::string api_key) +{ + WsEndpoint endpoint; + if (!parse_ws_endpoint(base_url, endpoint)) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket endpoint invalid for base_url=" << base_url; + return; + } + if (endpoint.secure) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket wss not supported for base_url=" << base_url; + return; + } + + // Reconnection logic + ws_reconnect_requested.store(false); // Reset reconnect flag + int retry_count = 0; + const int max_retries = 10; + const int base_delay_ms = 1000; + + while (!ws_stop.load() && retry_count < max_retries) { + bool connection_lost = false; // Flag to distinguish clean shutdown from unexpected disconnect + + try { + net::io_context ioc; + tcp::resolver resolver{ioc}; + beast::tcp_stream stream{ioc}; + + stream.expires_after(std::chrono::seconds(10)); + auto const results = resolver.resolve(endpoint.host, endpoint.port); + stream.connect(results); + + websocket::stream ws{std::move(stream)}; + ws.set_option(websocket::stream_base::decorator([&](websocket::request_type& req) { + req.set(http::field::user_agent, "OrcaSlicer"); + if (!api_key.empty()) { + req.set("X-Api-Key", api_key); + } + })); + + std::string host_header = endpoint.host; + if (!endpoint.port.empty() && endpoint.port != "80") { + host_header += ":" + endpoint.port; + } + ws.handshake(host_header, endpoint.target); + ws.text(true); + + // Send client identification + nlohmann::json identify; + identify["jsonrpc"] = "2.0"; + identify["method"] = "server.connection.identify"; + identify["params"]["client_name"] = "OrcaSlicer"; + identify["params"]["version"] = MoonrakerPrinterAgent_VERSION; + identify["params"]["type"] = "agent"; + identify["params"]["url"] = "https://github.com/SoftFever/OrcaSlicer"; + identify["id"] = 0; + ws.write(net::buffer(identify.dump())); + + std::set subscribe_objects = {"print_stats", "virtual_sdcard"}; + std::set available_objects; + std::string list_error; + if (fetch_object_list(base_url, api_key, available_objects, list_error)) { + // Store available_objects in member variable for feature detection + { + std::lock_guard lock(payload_mutex); + this->available_objects = std::move(available_objects); + } + + std::string objects_str; + for (const auto& name : this->available_objects) { + if (!objects_str.empty()) objects_str += ", "; + objects_str += name; + } + + if (this->available_objects.count("heater_bed") != 0) { + subscribe_objects.insert("heater_bed"); + } + // Only subscribe to "fan" if it exists (standard Moonraker API) + if (this->available_objects.count("fan") != 0) { + subscribe_objects.insert("fan"); + } else { + } + + // Add toolhead for homing status + if (this->available_objects.count("toolhead") != 0) { + subscribe_objects.insert("toolhead"); + } + + // Add display_status for layer info (if available) + if (this->available_objects.count("display_status") != 0) { + subscribe_objects.insert("display_status"); + } + + for (const auto& name : this->available_objects) { + if (name == "extruder" || name.rfind("extruder", 0) == 0) { + subscribe_objects.insert(name); + if (name == "extruder") { + break; + } + } + } + } else { + subscribe_objects.insert("extruder"); + subscribe_objects.insert("heater_bed"); + subscribe_objects.insert("toolhead"); // Add toolhead as fallback + subscribe_objects.insert("fan"); // Try to subscribe to fan as fallback + } + + nlohmann::json subscribe; + subscribe["jsonrpc"] = "2.0"; + subscribe["method"] = "printer.objects.subscribe"; + nlohmann::json objects = nlohmann::json::object(); + for (const auto& name : subscribe_objects) { + objects[name] = nullptr; + } + subscribe["params"]["objects"] = std::move(objects); + subscribe["id"] = 1; + ws.write(net::buffer(subscribe.dump())); + + // Read loop + while (!ws_stop.load()) { + ws.next_layer().expires_after(std::chrono::seconds(2)); + beast::flat_buffer buffer; + beast::error_code ec; + ws.read(buffer, ec); + if (ec == beast::error::timeout) { + const auto now_ms = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + const auto last_ms = ws_last_emit_ms.load(); + if (last_ms == 0 || now_ms - last_ms >= 10000) { + nlohmann::json message; + { + std::lock_guard lock(payload_mutex); + message = build_print_payload_locked(); + } + dispatch_message(dev_id, message.dump()); + ws_last_emit_ms.store(now_ms); + } + continue; + } + if (ec == websocket::error::closed) { + connection_lost = true; + break; + } + if (ec) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket read error: " << ec.message(); + connection_lost = true; + break; + } + handle_ws_message(dev_id, beast::buffers_to_string(buffer.data())); + // Check if handle_ws_message triggered reconnection request + if (ws_reconnect_requested.exchange(false)) { + connection_lost = true; + break; + } + } + + beast::error_code ec; + ws.close(websocket::close_code::normal, ec); + + // Only reset retry count on clean shutdown (not connection_lost) + if (!connection_lost && !ws_stop.load()) { + retry_count = 0; + } + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket disconnected: " << e.what(); + connection_lost = true; + } + + // Exit loop on clean shutdown + if (!connection_lost) { + break; + } + + // Check if we should stop reconnection attempts + if (ws_stop.load()) { + break; + } + + // Exponential backoff before reconnection + int delay_ms = base_delay_ms * (1 << std::min(retry_count, 5)); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Reconnecting in " << delay_ms << "ms (attempt " << (retry_count + 1) << ")"; + std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms)); + retry_count++; + } + + if (retry_count >= max_retries) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Max reconnection attempts reached"; + dispatch_local_connect(ConnectStatusLost, dev_id, "max_retries"); + } +} + +void MoonrakerPrinterAgent::handle_ws_message(const std::string& dev_id, const std::string& payload) +{ + auto json = nlohmann::json::parse(payload, nullptr, false); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Invalid WebSocket message JSON"; + return; + } + + bool updated = false; + + // Check for subscription response (has "result.status") + if (json.contains("result") && json["result"].contains("status") && + json["result"]["status"].is_object()) { + update_status_cache(json["result"]["status"]); + updated = true; + } + + // Check for status update notifications + if (json.contains("method") && json["method"].is_string()) { + const std::string method = json["method"].get(); + if (method == "notify_status_update" && json.contains("params") && + json["params"].is_array() && !json["params"].empty() && + json["params"][0].is_object()) { + update_status_cache(json["params"][0]); + updated = true; + } else if (method == "notify_klippy_ready") { + nlohmann::json updates; + updates["print_stats"]["state"] = "standby"; + update_status_cache(updates); + updated = true; + } else if (method == "notify_klippy_shutdown") { + nlohmann::json updates; + updates["print_stats"]["state"] = "error"; + update_status_cache(updates); + updated = true; + } + // Handle Klippy disconnect - update status and trigger reconnection + else if (method == "notify_klippy_disconnected") { + // Klippy disconnected - update status to reflect disconnect state + nlohmann::json updates; + updates["print_stats"]["state"] = "error"; + update_status_cache(updates); + updated = true; + // Set flag to trigger reconnection after dispatching the status update + ws_reconnect_requested.store(true); + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Klippy disconnected, triggering reconnection"; + } + } + + if (updated) { + nlohmann::json message; + { + std::lock_guard lock(payload_mutex); + message = build_print_payload_locked(); + } + + BOOST_LOG_TRIVIAL(trace) << "MoonrakerPrinterAgent: Dispatching payload: " << message.dump(); + dispatch_message(dev_id, message.dump()); + + const auto now_ms = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + ws_last_emit_ms.store(now_ms); + } +} + +void MoonrakerPrinterAgent::update_status_cache(const nlohmann::json& updates) +{ + if (!updates.is_object()) { + return; + } + + std::lock_guard lock(payload_mutex); + if (!status_cache.is_object()) { + status_cache = nlohmann::json::object(); + } + + for (const auto& item : updates.items()) { + if (item.value().is_object()) { + nlohmann::json& target = status_cache[item.key()]; + if (!target.is_object()) { + target = nlohmann::json::object(); + } + for (const auto& field : item.value().items()) { + target[field.key()] = field.value(); + } + } else { + status_cache[item.key()] = item.value(); + } + } +} + +nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const +{ + nlohmann::json payload; + payload["print"]["command"] = "push_status"; + payload["print"]["msg"] = 0; + payload["print"]["support_mqtt_alive"] = true; + + std::string state = "IDLE"; + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("state") && + status_cache["print_stats"]["state"].is_string()) { + state = map_moonraker_state(status_cache["print_stats"]["state"].get()); + } + payload["print"]["gcode_state"] = state; + + // ===== NEW: Print Stage ===== + // Map Moonraker state to Bambu stage numbers + int mc_print_stage = 0; + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("state")) { + std::string mr_state = status_cache["print_stats"]["state"].get(); + if (mr_state == "printing") mc_print_stage = 1; + else if (mr_state == "paused") mc_print_stage = 2; + else if (mr_state == "complete") mc_print_stage = 3; + else if (mr_state == "error") mc_print_stage = 4; + } + payload["print"]["mc_print_stage"] = mc_print_stage; + + // ===== NEW: Error Codes ===== + // Leave mc_print_error_code and print_error at 0 + // UI expects numeric HMS codes - setting to 1 shows generic error dialog + // Only set if real mapping from Moonraker error strings to HMS codes is defined + payload["print"]["mc_print_error_code"] = 0; + payload["print"]["print_error"] = 0; + + // ===== NEW: Home Flag ===== + // Map homed axes to bit field: X=bit0, Y=bit1, Z=bit2 + // WARNING: This only sets bits 0-2, clearing support flags (bit 3+) + // Bit 3 = 220V voltage, bit 4 = auto recovery, etc. + // This is acceptable for Moonraker (no AMS, different feature set) + int home_flag = 0; + if (status_cache.contains("toolhead") && status_cache["toolhead"].contains("homed_axes")) { + std::string homed = status_cache["toolhead"]["homed_axes"].get(); + if (homed.find('X') != std::string::npos) home_flag |= 1; // bit 0 + if (homed.find('Y') != std::string::npos) home_flag |= 2; // bit 1 + if (homed.find('Z') != std::string::npos) home_flag |= 4; // bit 2 + } + payload["print"]["home_flag"] = home_flag; + + // ===== NEW: Temperature Ranges ===== + // Moonraker doesn't provide this via API - use hardcoded defaults + payload["print"]["nozzle_temp_range"] = {100, 370}; // Typical Klipper range + payload["print"]["bed_temp_range"] = {0, 120}; // Typical bed range + + // ===== NEW: Feature Flags ===== + payload["print"]["support_send_to_sd"] = true; + // Detect bed_leveling support from available objects (bed_mesh or probe) + // Default to 0 (not supported) if neither object exists + bool has_bed_leveling = (available_objects.count("bed_mesh") != 0 || + available_objects.count("probe") != 0); + payload["print"]["support_bed_leveling"] = has_bed_leveling ? 1 : 0; + + const nlohmann::json* extruder = nullptr; + if (status_cache.contains("extruder") && status_cache["extruder"].is_object()) { + extruder = &status_cache["extruder"]; + } else { + for (const auto& item : status_cache.items()) { + if (item.value().is_object() && item.key().rfind("extruder", 0) == 0) { + extruder = &item.value(); + break; + } + } + } + + if (extruder) { + if (extruder->contains("temperature") && (*extruder)["temperature"].is_number()) { + payload["print"]["nozzle_temper"] = (*extruder)["temperature"].get(); + } + if (extruder->contains("target") && (*extruder)["target"].is_number()) { + payload["print"]["nozzle_target_temper"] = (*extruder)["target"].get(); + } + } + + if (status_cache.contains("heater_bed") && status_cache["heater_bed"].is_object()) { + const auto& bed = status_cache["heater_bed"]; + if (bed.contains("temperature") && bed["temperature"].is_number()) { + payload["print"]["bed_temper"] = bed["temperature"].get(); + } + if (bed.contains("target") && bed["target"].is_number()) { + payload["print"]["bed_target_temper"] = bed["target"].get(); + } + } + + // Handle fan speed - only if Moonraker provides "fan" object (standard API) + if (status_cache.contains("fan") && status_cache["fan"].is_object() && !status_cache["fan"].empty()) { + const auto& fan = status_cache["fan"]; + if (fan.contains("speed") && fan["speed"].is_number()) { + double speed = fan["speed"].get(); + int pwm = 0; + if (speed <= 1.0) { + pwm = static_cast(speed * 255.0 + 0.5); + } else { + pwm = static_cast(speed + 0.5); + } + pwm = std::clamp(pwm, 0, 255); + payload["print"]["fan_gear"] = pwm; + } else if (fan.contains("power") && fan["power"].is_number()) { + double power = fan["power"].get(); + int pwm = static_cast(power * 255.0 + 0.5); + pwm = std::clamp(pwm, 0, 255); + payload["print"]["fan_gear"] = pwm; + } + } + // If "fan" object doesn't exist, don't include fan_gear in payload + + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("filename") && + status_cache["print_stats"]["filename"].is_string()) { + payload["print"]["subtask_name"] = status_cache["print_stats"]["filename"].get(); + } + + // ===== NEW: G-code File Path ===== + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("filename")) { + payload["print"]["gcode_file"] = status_cache["print_stats"]["filename"]; + } + + int mc_percent = -1; + if (status_cache.contains("virtual_sdcard") && + status_cache["virtual_sdcard"].contains("progress") && + status_cache["virtual_sdcard"]["progress"].is_number()) { + const double progress = status_cache["virtual_sdcard"]["progress"].get(); + if (progress >= 0.0) { + mc_percent = std::clamp(static_cast(progress * 100.0 + 0.5), 0, 100); + } + } + if (mc_percent >= 0) { + payload["print"]["mc_percent"] = mc_percent; + } + + if (status_cache.contains("print_stats") && + status_cache["print_stats"].contains("total_duration") && + status_cache["print_stats"].contains("print_duration") && + status_cache["print_stats"]["total_duration"].is_number() && + status_cache["print_stats"]["print_duration"].is_number()) { + const double total = status_cache["print_stats"]["total_duration"].get(); + const double elapsed = status_cache["print_stats"]["print_duration"].get(); + if (total > 0.0 && elapsed >= 0.0) { + const auto remaining_minutes = std::max(0, static_cast((total - elapsed) / 60.0)); + payload["print"]["mc_remaining_time"] = remaining_minutes; + } + } + + const auto now_ms = static_cast( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count()); + payload["t_utc"] = now_ms; + + BOOST_LOG_TRIVIAL(trace) << "MoonrakerPrinterAgent: Built payload with gcode_state=" << state; + + return payload; +} + +std::string MoonrakerPrinterAgent::resolve_host(const std::string& dev_id) const +{ + { + std::lock_guard lock(state_mutex); + auto it = host_by_device.find(dev_id); + if (it != host_by_device.end()) { + return it->second; + } + } + + PrinthostConfig config; + if (get_printhost_config(config)) { + return config.base_url; + } + + return ""; +} + +std::string MoonrakerPrinterAgent::resolve_api_key(const std::string& dev_id, const std::string& fallback) const +{ + std::string api_key = normalize_api_key(fallback); + if (!api_key.empty()) { + return api_key; + } + + { + std::lock_guard lock(state_mutex); + auto it = api_key_by_device.find(dev_id); + if (it != api_key_by_device.end() && !it->second.empty()) { + return it->second; + } + } + + PrinthostConfig config; + if (get_printhost_config(config)) { + return normalize_api_key(config.api_key); + } + + return ""; +} + +void MoonrakerPrinterAgent::store_host(const std::string& dev_id, const std::string& host, const std::string& api_key) +{ + if (host.empty()) { + return; + } + std::lock_guard lock(state_mutex); + host_by_device[dev_id] = host; + if (!api_key.empty()) { + api_key_by_device[dev_id] = api_key; + } +} + +void MoonrakerPrinterAgent::dispatch_message(const std::string& dev_id, const std::string& payload) +{ + BOOST_LOG_TRIVIAL(trace) << "MoonrakerPrinterAgent: dispatch_message dev_id=" << dev_id + << " payload_size=" << payload.size(); + + OnMessageFn local_fn; + OnMessageFn cloud_fn; + QueueOnMainFn queue_fn; + { + std::lock_guard lock(state_mutex); + local_fn = on_local_message_fn; + cloud_fn = on_message_fn; + queue_fn = queue_on_main_fn; + } + + if (!local_fn && !cloud_fn) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: dispatch_message - no message callback registered!"; + return; + } + + auto dispatch = [dev_id, payload, local_fn, cloud_fn]() { + if (local_fn) { + local_fn(dev_id, payload); + return; + } + if (cloud_fn) { + cloud_fn(dev_id, payload); + } + }; + + if (queue_fn) { + queue_fn(dispatch); + } else { + dispatch(); + } +} + +bool MoonrakerPrinterAgent::upload_gcode( + const std::string& local_path, + const std::string& filename, + const std::string& base_url, + const std::string& api_key, + OnUpdateStatusFn update_fn, + WasCancelledFn cancel_fn) +{ + namespace fs = boost::filesystem; + + // Validate file exists + fs::path source_path(local_path); + if (!fs::exists(source_path)) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: File does not exist: " << local_path; + return false; + } + + // Check file size + std::uintmax_t file_size = fs::file_size(source_path); + if (file_size > 1024 * 1024 * 1024) { // 1GB limit + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: File too large: " << file_size << " bytes"; + return false; + } + + // Sanitize filename to prevent path traversal attacks + std::string safe_filename = sanitize_filename(filename); + + bool result = true; + std::string http_error; + + // Use Http::form_add and Http::form_add_file + auto http = Http::post(join_url(base_url, "/server/files/upload")); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.form_add("root", "gcodes") // Upload to gcodes directory + .form_add("print", "false") // Don't auto-start print + .form_add_file("file", source_path.string(), safe_filename) + .timeout_connect(10) + .timeout_max(300) // 5 minutes for large files + .on_complete([&](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: Upload complete: HTTP " << status << " body: " << body; + }) + .on_error([&](std::string body, std::string err, unsigned status) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Upload error: " << err << " HTTP " << status; + http_error = err; + result = false; + }) + .on_progress([&](Http::Progress progress, bool& cancel) { + // Check for cancellation via WasCancelledFn + if (cancel_fn && cancel_fn()) { + cancel = true; + result = false; + return; + } + // Report progress via OnUpdateStatusFn + if (update_fn && progress.ultotal > 0) { + int percent = static_cast((progress.ulnow * 100) / progress.ultotal); + update_fn(PrintingStageUpload, percent, "Uploading..."); + } + }) + .perform_sync(); + + if (!result) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Upload failed: " << http_error; + return false; + } + + return true; +} + +int MoonrakerPrinterAgent::pause_print(const std::string& dev_id) +{ + const std::string base_url = resolve_host(dev_id); + const std::string api_key = resolve_api_key(dev_id, ""); + + nlohmann::json request; + request["jsonrpc"] = "2.0"; + request["method"] = "printer.print.pause"; + request["id"] = next_jsonrpc_id++; + + std::string response; + // For JSON-RPC over HTTP, we need to use POST to /printer/print/pause + // But Moonraker also supports this via WebSocket + // For now, send via gcode script which is simpler + std::string gcode = "PAUSE"; + return send_gcode(dev_id, gcode) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; +} + +int MoonrakerPrinterAgent::resume_print(const std::string& dev_id) +{ + std::string gcode = "RESUME"; + return send_gcode(dev_id, gcode) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; +} + +int MoonrakerPrinterAgent::cancel_print(const std::string& dev_id) +{ + std::string gcode = "CANCEL_PRINT"; + return send_gcode(dev_id, gcode) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; +} + +bool MoonrakerPrinterAgent::send_jsonrpc_command( + const std::string& base_url, + const std::string& api_key, + const nlohmann::json& request, + std::string& response) const +{ + std::string request_str = request.dump(); + std::string url = join_url(base_url, "/printer/print/start"); + + bool success = false; + std::string http_error; + + auto http = Http::post(url); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.header("Content-Type", "application/json") + .set_post_body(request_str) + .timeout_connect(10) + .timeout_max(30) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response = body; + success = true; + } else { + http_error = "HTTP " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + }) + .perform_sync(); + + if (!success) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: JSON-RPC command failed: " << http_error; + } + + return success; +} + +void MoonrakerPrinterAgent::perform_connection_async( + const std::string& dev_id, + const std::string& base_url, + const std::string& api_key) +{ + int result = BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; + std::string error_msg; + + try { + // Check Klippy state + nlohmann::json server_info; + if (!fetch_server_info_json(base_url, api_key, server_info, error_msg)) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Failed to fetch server info: " << error_msg; + dispatch_local_connect(ConnectStatusFailed, dev_id, "server_info_failed"); + finish_connection(); + return; + } + + nlohmann::json result_json = server_info.contains("result") + ? server_info["result"] : server_info; + std::string klippy_state = result_json.value("klippy_state", ""); + + // Poll for Klippy ready state (with stop check) + if (klippy_state == "startup") { + for (int i = 0; i < 30; i++) { // 30 second max + { + std::unique_lock lock(connect_mutex); + if (connect_stop_requested.load()) { + result = BAMBU_NETWORK_ERR_CANCELED; + break; + } + } + + std::this_thread::sleep_for(std::chrono::seconds(1)); + + if (fetch_server_info_json(base_url, api_key, server_info, error_msg)) { + result_json = server_info.contains("result") + ? server_info["result"] : server_info; + klippy_state = result_json.value("klippy_state", ""); + if (klippy_state == "ready") break; + } + } + } + + // Check final state + if (klippy_state != "ready" && result == BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED) { + std::string state_message = result_json.value("state_message", "Unknown error"); + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Klippy not ready: " << klippy_state + << " - " << state_message; + error_msg = "klippy_not_ready:" + klippy_state; + dispatch_local_connect(ConnectStatusFailed, dev_id, error_msg); + finish_connection(); + return; + } + + // Query initial status + nlohmann::json initial_status; + if (query_printer_status(base_url, api_key, initial_status, error_msg)) { + { + update_status_cache(initial_status); + } + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Initial status queried successfully"; + } else { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Initial status query failed: " << error_msg; + } + + // Start WebSocket status stream + start_status_stream(dev_id, base_url, api_key); + + // Success! + result = BAMBU_NETWORK_SUCCESS; + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Connection exception: " << e.what(); + error_msg = std::string("exception: ") + e.what(); + result = BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; + } + + // Dispatch final result to UI + if (result == BAMBU_NETWORK_SUCCESS) { + dispatch_local_connect(ConnectStatusOk, dev_id, "0"); + dispatch_printer_connected(dev_id); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: connect_printer completed - dev_id=" << dev_id; + } else if (result != BAMBU_NETWORK_ERR_CANCELED) { + dispatch_local_connect(ConnectStatusFailed, dev_id, error_msg); + } + + finish_connection(); +} + +void MoonrakerPrinterAgent::finish_connection() +{ + std::lock_guard lock(connect_mutex); + connect_in_progress.store(false); +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp new file mode 100644 index 0000000000..977cfcf6a9 --- /dev/null +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -0,0 +1,182 @@ +#ifndef __MOONRAKER_PRINTER_AGENT_HPP__ +#define __MOONRAKER_PRINTER_AGENT_HPP__ + +#include "IPrinterAgent.hpp" +#include "ICloudServiceAgent.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +namespace Slic3r { + +class MoonrakerPrinterAgent final : public IPrinterAgent +{ +public: + explicit MoonrakerPrinterAgent(std::string log_dir); + ~MoonrakerPrinterAgent() override; + + static AgentInfo get_agent_info_static(); + AgentInfo get_agent_info() override { return get_agent_info_static(); } + + // Cloud Agent Dependency + void set_cloud_agent(std::shared_ptr cloud) override; + + // Communication + int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; + int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; + int disconnect_printer() override; + int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; + + // Certificates + int check_cert() override; + void install_device_cert(std::string dev_id, bool lan_only) override; + + // Discovery + bool start_discovery(bool start, bool sending) override; + + // Binding + int ping_bind(std::string ping_code) override; + int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override; + int bind(std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override; + int unbind(std::string dev_id) override; + int request_bind_ticket(std::string* ticket) override; + int set_server_callback(OnServerErrFn fn) override; + + // Machine Selection + std::string get_user_selected_machine() override; + int set_user_selected_machine(std::string dev_id) override; + + // Print Job Operations + int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; + int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; + int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; + int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override; + int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override; + + // Callbacks + int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override; + int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override; + int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override; + int set_on_message_fn(OnMessageFn fn) override; + int set_on_user_message_fn(OnMessageFn fn) override; + int set_on_local_connect_fn(OnLocalConnectedFn fn) override; + int set_on_local_message_fn(OnMessageFn fn) override; + int set_queue_on_main_fn(QueueOnMainFn fn) override; + + // Pull-mode agent (on-demand filament sync) + FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } + void fetch_filament_info(std::string dev_id) override; + +private: + struct PrinthostConfig + { + std::string host; + std::string port; + std::string api_key; + std::string base_url; + std::string model_name; + }; + + struct MoonrakerDeviceInfo + { + std::string dev_id; + std::string dev_name; + std::string version; + }; + + int handle_request(const std::string& dev_id, const std::string& json_str); + int send_version_info(const std::string& dev_id); + int send_access_code(const std::string& dev_id); + + bool get_printhost_config(PrinthostConfig& config) const; + bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; + bool fetch_server_info(const std::string& base_url, const std::string& api_key, std::string& version, std::string& error) const; + bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set& objects, std::string& error) const; + bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const; + bool send_gcode(const std::string& dev_id, const std::string& gcode) const; + + std::string resolve_host(const std::string& dev_id) const; + std::string resolve_api_key(const std::string& dev_id, const std::string& fallback) const; + void store_host(const std::string& dev_id, const std::string& host, const std::string& api_key); + + void announce_printhost_device(); + void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg); + void dispatch_printer_connected(const std::string& dev_id); + void dispatch_message(const std::string& dev_id, const std::string& payload); + void start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key); + void stop_status_stream(); + void run_status_stream(std::string dev_id, std::string base_url, std::string api_key); + void handle_ws_message(const std::string& dev_id, const std::string& payload); + void update_status_cache(const nlohmann::json& updates); + nlohmann::json build_print_payload_locked() const; + + // Print control helpers + int pause_print(const std::string& dev_id); + int resume_print(const std::string& dev_id); + int cancel_print(const std::string& dev_id); + + // File upload + bool upload_gcode(const std::string& local_path, const std::string& filename, + const std::string& base_url, const std::string& api_key, + OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); + + // JSON-RPC helper + bool send_jsonrpc_command(const std::string& base_url, const std::string& api_key, + const nlohmann::json& request, std::string& response) const; + + // Server info (returns JSON, not just version string) + bool fetch_server_info_json(const std::string& base_url, const std::string& api_key, + nlohmann::json& info, std::string& error) const; + + // Connection thread management + void perform_connection_async(const std::string& dev_id, + const std::string& base_url, + const std::string& api_key); + void finish_connection(); + + mutable std::recursive_mutex state_mutex; + std::map host_by_device; + std::map api_key_by_device; + std::string ssdp_announced_host; + std::string ssdp_announced_id; + std::shared_ptr m_cloud_agent; + std::string selected_machine; + + OnMsgArrivedFn on_ssdp_msg_fn; + OnPrinterConnectedFn on_printer_connected_fn; + GetSubscribeFailureFn on_subscribe_failure_fn; + OnMessageFn on_message_fn; + OnMessageFn on_user_message_fn; + OnLocalConnectedFn on_local_connect_fn; + OnMessageFn on_local_message_fn; + QueueOnMainFn queue_on_main_fn; + OnServerErrFn on_server_err_fn; + + mutable std::recursive_mutex payload_mutex; + nlohmann::json status_cache; + + std::atomic next_jsonrpc_id{1}; + std::set available_objects; // Track for feature detection + + std::atomic ws_stop{false}; + std::atomic ws_reconnect_requested{false}; // Flag to trigger reconnection + std::atomic ws_last_emit_ms{0}; + std::thread ws_thread; + + // Connection thread management + std::atomic connect_in_progress{false}; + std::atomic connect_stop_requested{false}; + std::thread connect_thread; + std::recursive_mutex connect_mutex; + std::condition_variable connect_cv; +}; + +} // namespace Slic3r + +#endif diff --git a/src/slic3r/Utils/NetworkAgent.cpp b/src/slic3r/Utils/NetworkAgent.cpp index 8220105f10..273612365c 100644 --- a/src/slic3r/Utils/NetworkAgent.cpp +++ b/src/slic3r/Utils/NetworkAgent.cpp @@ -2,1921 +2,861 @@ #include #include #include -#if defined(_MSC_VER) || defined(_WIN32) -#include -#else -#include -#endif #include #include "libslic3r/Utils.hpp" #include "NetworkAgent.hpp" - -#include "slic3r/Utils/FileTransferUtils.hpp" - -using namespace BBL; +#include "BBLNetworkPlugin.hpp" +#include "BBLCloudServiceAgent.hpp" +#include "BBLPrinterAgent.hpp" namespace Slic3r { -#define BAMBU_SOURCE_LIBRARY "BambuSource" - -#if defined(_MSC_VER) || defined(_WIN32) -static HMODULE netwoking_module = NULL; -static HMODULE source_module = NULL; -#else -static void* netwoking_module = NULL; -static void* source_module = NULL; -#endif - bool NetworkAgent::use_legacy_network = true; -NetworkLibraryLoadError NetworkAgent::s_load_error = {}; -typedef int (*func_start_print_legacy)(void *agent, PrintParams_Legacy params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); -typedef int (*func_start_local_print_with_record_legacy)(void *agent, PrintParams_Legacy params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); -typedef int (*func_start_send_gcode_to_sdcard_legacy)(void *agent, PrintParams_Legacy params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); -typedef int (*func_start_local_print_legacy)(void *agent, PrintParams_Legacy params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); -typedef int (*func_start_sdcard_print_legacy)(void* agent, PrintParams_Legacy params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); -typedef int (*func_send_message_legacy)(void* agent, std::string dev_id, std::string json_str, int qos); -typedef int (*func_send_message_to_printer_legacy)(void* agent, std::string dev_id, std::string json_str, int qos); - -func_check_debug_consistent NetworkAgent::check_debug_consistent_ptr = nullptr; -func_get_version NetworkAgent::get_version_ptr = nullptr; -func_create_agent NetworkAgent::create_agent_ptr = nullptr; -func_destroy_agent NetworkAgent::destroy_agent_ptr = nullptr; -func_init_log NetworkAgent::init_log_ptr = nullptr; -func_set_config_dir NetworkAgent::set_config_dir_ptr = nullptr; -func_set_cert_file NetworkAgent::set_cert_file_ptr = nullptr; -func_set_country_code NetworkAgent::set_country_code_ptr = nullptr; -func_start NetworkAgent::start_ptr = nullptr; -func_set_on_ssdp_msg_fn NetworkAgent::set_on_ssdp_msg_fn_ptr = nullptr; -func_set_on_user_login_fn NetworkAgent::set_on_user_login_fn_ptr = nullptr; -func_set_on_printer_connected_fn NetworkAgent::set_on_printer_connected_fn_ptr = nullptr; -func_set_on_server_connected_fn NetworkAgent::set_on_server_connected_fn_ptr = nullptr; -func_set_on_http_error_fn NetworkAgent::set_on_http_error_fn_ptr = nullptr; -func_set_get_country_code_fn NetworkAgent::set_get_country_code_fn_ptr = nullptr; -func_set_on_subscribe_failure_fn NetworkAgent::set_on_subscribe_failure_fn_ptr = nullptr; -func_set_on_message_fn NetworkAgent::set_on_message_fn_ptr = nullptr; -func_set_on_user_message_fn NetworkAgent::set_on_user_message_fn_ptr = nullptr; -func_set_on_local_connect_fn NetworkAgent::set_on_local_connect_fn_ptr = nullptr; -func_set_on_local_message_fn NetworkAgent::set_on_local_message_fn_ptr = nullptr; -func_set_queue_on_main_fn NetworkAgent::set_queue_on_main_fn_ptr = nullptr; -func_connect_server NetworkAgent::connect_server_ptr = nullptr; -func_is_server_connected NetworkAgent::is_server_connected_ptr = nullptr; -func_refresh_connection NetworkAgent::refresh_connection_ptr = nullptr; -func_start_subscribe NetworkAgent::start_subscribe_ptr = nullptr; -func_stop_subscribe NetworkAgent::stop_subscribe_ptr = nullptr; -func_add_subscribe NetworkAgent::add_subscribe_ptr = nullptr; -func_del_subscribe NetworkAgent::del_subscribe_ptr = nullptr; -func_enable_multi_machine NetworkAgent::enable_multi_machine_ptr = nullptr; -func_send_message NetworkAgent::send_message_ptr = nullptr; -func_connect_printer NetworkAgent::connect_printer_ptr = nullptr; -func_disconnect_printer NetworkAgent::disconnect_printer_ptr = nullptr; -func_send_message_to_printer NetworkAgent::send_message_to_printer_ptr = nullptr; -func_check_cert NetworkAgent::check_cert_ptr = nullptr; -func_install_device_cert NetworkAgent::install_device_cert_ptr = nullptr; -func_start_discovery NetworkAgent::start_discovery_ptr = nullptr; -func_change_user NetworkAgent::change_user_ptr = nullptr; -func_is_user_login NetworkAgent::is_user_login_ptr = nullptr; -func_user_logout NetworkAgent::user_logout_ptr = nullptr; -func_get_user_id NetworkAgent::get_user_id_ptr = nullptr; -func_get_user_name NetworkAgent::get_user_name_ptr = nullptr; -func_get_user_avatar NetworkAgent::get_user_avatar_ptr = nullptr; -func_get_user_nickanme NetworkAgent::get_user_nickanme_ptr = nullptr; -func_build_login_cmd NetworkAgent::build_login_cmd_ptr = nullptr; -func_build_logout_cmd NetworkAgent::build_logout_cmd_ptr = nullptr; -func_build_login_info NetworkAgent::build_login_info_ptr = nullptr; -func_ping_bind NetworkAgent::ping_bind_ptr = nullptr; -func_bind_detect NetworkAgent::bind_detect_ptr = nullptr; -func_set_server_callback NetworkAgent::set_server_callback_ptr = nullptr; -func_bind NetworkAgent::bind_ptr = nullptr; -func_unbind NetworkAgent::unbind_ptr = nullptr; -func_get_bambulab_host NetworkAgent::get_bambulab_host_ptr = nullptr; -func_get_user_selected_machine NetworkAgent::get_user_selected_machine_ptr = nullptr; -func_set_user_selected_machine NetworkAgent::set_user_selected_machine_ptr = nullptr; -func_start_print NetworkAgent::start_print_ptr = nullptr; -func_start_local_print_with_record NetworkAgent::start_local_print_with_record_ptr = nullptr; -func_start_send_gcode_to_sdcard NetworkAgent::start_send_gcode_to_sdcard_ptr = nullptr; -func_start_local_print NetworkAgent::start_local_print_ptr = nullptr; -func_start_sdcard_print NetworkAgent::start_sdcard_print_ptr = nullptr; -func_get_user_presets NetworkAgent::get_user_presets_ptr = nullptr; -func_request_setting_id NetworkAgent::request_setting_id_ptr = nullptr; -func_put_setting NetworkAgent::put_setting_ptr = nullptr; -func_get_setting_list NetworkAgent::get_setting_list_ptr = nullptr; -func_get_setting_list2 NetworkAgent::get_setting_list2_ptr = nullptr; -func_delete_setting NetworkAgent::delete_setting_ptr = nullptr; -func_get_studio_info_url NetworkAgent::get_studio_info_url_ptr = nullptr; -func_set_extra_http_header NetworkAgent::set_extra_http_header_ptr = nullptr; -func_get_my_message NetworkAgent::get_my_message_ptr = nullptr; -func_check_user_task_report NetworkAgent::check_user_task_report_ptr = nullptr; -func_get_user_print_info NetworkAgent::get_user_print_info_ptr = nullptr; -func_get_user_tasks NetworkAgent::get_user_tasks_ptr = nullptr; -func_get_printer_firmware NetworkAgent::get_printer_firmware_ptr = nullptr; -func_get_task_plate_index NetworkAgent::get_task_plate_index_ptr = nullptr; -func_get_user_info NetworkAgent::get_user_info_ptr = nullptr; -func_request_bind_ticket NetworkAgent::request_bind_ticket_ptr = nullptr; -func_get_subtask_info NetworkAgent::get_subtask_info_ptr = nullptr; -func_get_slice_info NetworkAgent::get_slice_info_ptr = nullptr; -func_query_bind_status NetworkAgent::query_bind_status_ptr = nullptr; -func_modify_printer_name NetworkAgent::modify_printer_name_ptr = nullptr; -func_get_camera_url NetworkAgent::get_camera_url_ptr = nullptr; -func_get_design_staffpick NetworkAgent::get_design_staffpick_ptr = nullptr; -func_start_pubilsh NetworkAgent::start_publish_ptr = nullptr; -func_get_model_publish_url NetworkAgent::get_model_publish_url_ptr = nullptr; -func_get_model_mall_home_url NetworkAgent::get_model_mall_home_url_ptr = nullptr; -func_get_model_mall_detail_url NetworkAgent::get_model_mall_detail_url_ptr = nullptr; -func_get_subtask NetworkAgent::get_subtask_ptr = nullptr; -func_get_my_profile NetworkAgent::get_my_profile_ptr = nullptr; -func_track_enable NetworkAgent::track_enable_ptr = nullptr; -func_track_remove_files NetworkAgent::track_remove_files_ptr = nullptr; -func_track_event NetworkAgent::track_event_ptr = nullptr; -func_track_header NetworkAgent::track_header_ptr = nullptr; -func_track_update_property NetworkAgent::track_update_property_ptr = nullptr; -func_track_get_property NetworkAgent::track_get_property_ptr = nullptr; -func_put_model_mall_rating_url NetworkAgent::put_model_mall_rating_url_ptr = nullptr; -func_get_oss_config NetworkAgent::get_oss_config_ptr = nullptr; -func_put_rating_picture_oss NetworkAgent::put_rating_picture_oss_ptr = nullptr; -func_get_model_mall_rating_result NetworkAgent::get_model_mall_rating_result_ptr = nullptr; - -func_get_mw_user_preference NetworkAgent::get_mw_user_preference_ptr = nullptr; -func_get_mw_user_4ulist NetworkAgent::get_mw_user_4ulist_ptr = nullptr; - -static PrintParams_Legacy as_legacy(PrintParams& param) -{ - PrintParams_Legacy l; - - l.dev_id = std::move(param.dev_id); - l.task_name = std::move(param.task_name); - l.project_name = std::move(param.project_name); - l.preset_name = std::move(param.preset_name); - l.filename = std::move(param.filename); - l.config_filename = std::move(param.config_filename); - l.plate_index = param.plate_index; - l.ftp_folder = std::move(param.ftp_folder); - l.ftp_file = std::move(param.ftp_file); - l.ftp_file_md5 = std::move(param.ftp_file_md5); - l.ams_mapping = std::move(param.ams_mapping); - l.ams_mapping_info = std::move(param.ams_mapping_info); - l.connection_type = std::move(param.connection_type); - l.comments = std::move(param.comments); - l.origin_profile_id = param.origin_profile_id; - l.stl_design_id = param.stl_design_id; - l.origin_model_id = std::move(param.origin_model_id); - l.print_type = std::move(param.print_type); - l.dst_file = std::move(param.dst_file); - l.dev_name = std::move(param.dev_name); - l.dev_ip = std::move(param.dev_ip); - l.use_ssl_for_ftp = param.use_ssl_for_ftp; - l.use_ssl_for_mqtt = param.use_ssl_for_mqtt; - l.username = std::move(param.username); - l.password = std::move(param.password); - l.task_bed_leveling = param.task_bed_leveling; - l.task_flow_cali = param.task_flow_cali; - l.task_vibration_cali = param.task_vibration_cali; - l.task_layer_inspect = param.task_layer_inspect; - l.task_record_timelapse = param.task_record_timelapse; - l.task_use_ams = param.task_use_ams; - l.task_bed_type = std::move(param.task_bed_type); - l.extra_options = std::move(param.extra_options); - - return l; -} - -NetworkAgent::NetworkAgent(std::string log_dir) -{ - if (create_agent_ptr) { - network_agent = create_agent_ptr(log_dir); - } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", this %1%, network_agent=%2%, create_agent_ptr=%3%, log_dir=%4%")%this %network_agent %create_agent_ptr %log_dir; -} - -NetworkAgent::~NetworkAgent() -{ - int ret = 0; - if (network_agent && destroy_agent_ptr) { - ret = destroy_agent_ptr(network_agent); - } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", this %1%, network_agent=%2%, destroy_agent_ptr=%3%, ret %4%")%this %network_agent %destroy_agent_ptr %ret; -} +// ============================================================================ +// Static methods - delegate to BBLNetworkPlugin +// ============================================================================ std::string NetworkAgent::get_libpath_in_current_directory(std::string library_name) { - std::string lib_path; -#if defined(_MSC_VER) || defined(_WIN32) - wchar_t file_name[512]; - DWORD ret = GetModuleFileNameW(NULL, file_name, 512); - if (!ret) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", GetModuleFileNameW return error, can not Load Library for %1%") % library_name; - return lib_path; - } - int size_needed = ::WideCharToMultiByte(0, 0, file_name, wcslen(file_name), nullptr, 0, nullptr, nullptr); - std::string file_name_string(size_needed, 0); - ::WideCharToMultiByte(0, 0, file_name, wcslen(file_name), file_name_string.data(), size_needed, nullptr, nullptr); - - std::size_t found = file_name_string.find("orca-slicer.exe"); - if (found == (file_name_string.size() - 16)) { - lib_path = library_name + ".dll"; - lib_path = file_name_string.replace(found, 16, lib_path); - } -#else -#endif - return lib_path; + return BBLNetworkPlugin::get_libpath_in_current_directory(library_name); } std::string NetworkAgent::get_versioned_library_path(const std::string& version) { - std::string data_dir_str = data_dir(); - boost::filesystem::path data_dir_path(data_dir_str); - auto plugin_folder = data_dir_path / "plugins"; - -#if defined(_MSC_VER) || defined(_WIN32) - return (plugin_folder / (std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".dll")).string(); -#elif defined(__WXMAC__) - return (plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".dylib")).string(); -#else - return (plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".so")).string(); -#endif + return BBLNetworkPlugin::get_versioned_library_path(version); } bool NetworkAgent::versioned_library_exists(const std::string& version) { - if (version.empty()) return false; - std::string path = get_versioned_library_path(version); - - // Check if versioned library exists - if (boost::filesystem::exists(path)) return true; - - // For legacy version, also check if unversioned legacy library exists - // (it will be auto-migrated to versioned format when loaded) - if (version == BAMBU_NETWORK_AGENT_VERSION_LEGACY) { - return legacy_library_exists(); - } - - return false; + return BBLNetworkPlugin::versioned_library_exists(version); } bool NetworkAgent::legacy_library_exists() { - std::string data_dir_str = data_dir(); - boost::filesystem::path data_dir_path(data_dir_str); - auto plugin_folder = data_dir_path / "plugins"; - -#if defined(_MSC_VER) || defined(_WIN32) - auto legacy_path = plugin_folder / (std::string(BAMBU_NETWORK_LIBRARY) + ".dll"); -#elif defined(__WXMAC__) - auto legacy_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".dylib"); -#else - auto legacy_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".so"); -#endif - return boost::filesystem::exists(legacy_path); + return BBLNetworkPlugin::legacy_library_exists(); } void NetworkAgent::remove_legacy_library() { - std::string data_dir_str = data_dir(); - boost::filesystem::path data_dir_path(data_dir_str); - auto plugin_folder = data_dir_path / "plugins"; - -#if defined(_MSC_VER) || defined(_WIN32) - auto legacy_path = plugin_folder / (std::string(BAMBU_NETWORK_LIBRARY) + ".dll"); -#elif defined(__WXMAC__) - auto legacy_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".dylib"); -#else - auto legacy_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".so"); -#endif - - if (boost::filesystem::exists(legacy_path)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": removing legacy library at " << legacy_path.string(); - boost::system::error_code ec; - boost::filesystem::remove(legacy_path, ec); - if (ec) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to remove legacy library: " << ec.message(); - } - } + BBLNetworkPlugin::remove_legacy_library(); } std::vector NetworkAgent::scan_plugin_versions() { - std::vector discovered_versions; - std::string data_dir_str = data_dir(); - boost::filesystem::path plugin_folder = boost::filesystem::path(data_dir_str) / "plugins"; - - if (!boost::filesystem::is_directory(plugin_folder)) { - return discovered_versions; - } - -#if defined(_MSC_VER) || defined(_WIN32) - std::string prefix = std::string(BAMBU_NETWORK_LIBRARY) + "_"; - std::string extension = ".dll"; -#elif defined(__WXMAC__) - std::string prefix = std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_"; - std::string extension = ".dylib"; -#else - std::string prefix = std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_"; - std::string extension = ".so"; -#endif - - boost::system::error_code ec; - for (auto& entry : boost::filesystem::directory_iterator(plugin_folder, ec)) { - if (ec) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": error iterating directory: " << ec.message(); - break; - } - if (!boost::filesystem::is_regular_file(entry.status())) - continue; - - std::string filename = entry.path().filename().string(); - - if (filename.rfind(prefix, 0) != 0) - continue; - if (filename.size() <= extension.size() || - filename.compare(filename.size() - extension.size(), extension.size(), extension) != 0) - continue; - - std::string version = filename.substr(prefix.size(), - filename.size() - prefix.size() - extension.size()); - discovered_versions.push_back(version); - } - - return discovered_versions; + return BBLNetworkPlugin::scan_plugin_versions(); } int NetworkAgent::initialize_network_module(bool using_backup, const std::string& version) { - clear_load_error(); - - std::string library; - std::string data_dir_str = data_dir(); - boost::filesystem::path data_dir_path(data_dir_str); - auto plugin_folder = data_dir_path / "plugins"; - - if (using_backup) { - plugin_folder = plugin_folder/"backup"; - } - - if (version.empty()) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": version is required but not provided"; - set_load_error( - "Network library version not specified", - "A version must be specified to load the network library", - "" - ); - return -1; - } - - // Auto-migration: If loading legacy version and versioned library doesn't exist, - // but unversioned legacy library does exist, rename it to versioned format - if (version == BAMBU_NETWORK_AGENT_VERSION_LEGACY) { - boost::filesystem::path versioned_path; - boost::filesystem::path legacy_path; -#if defined(_MSC_VER) || defined(_WIN32) - versioned_path = plugin_folder / (std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".dll"); - legacy_path = plugin_folder / (std::string(BAMBU_NETWORK_LIBRARY) + ".dll"); -#elif defined(__WXMAC__) - versioned_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".dylib"); - legacy_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".dylib"); -#else - versioned_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".so"); - legacy_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".so"); -#endif - if (!boost::filesystem::exists(versioned_path) && boost::filesystem::exists(legacy_path)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": auto-migrating unversioned legacy library to versioned format"; - - try { - // Rename unversioned to versioned in the same folder (main or backup). - boost::filesystem::rename(legacy_path, versioned_path); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": successfully renamed " << legacy_path.string() << " to " - << versioned_path.string(); - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to rename legacy library: " << e.what(); - } - } - } - - // Load versioned library -#if defined(_MSC_VER) || defined(_WIN32) - library = plugin_folder.string() + "\\" + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".dll"; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": loading versioned library at " << library; -#else - #if defined(__WXMAC__) - std::string lib_ext = ".dylib"; - #else - std::string lib_ext = ".so"; - #endif - library = plugin_folder.string() + "/" + std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + lib_ext; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": loading versioned library at " << library; -#endif - -#if defined(_MSC_VER) || defined(_WIN32) - wchar_t lib_wstr[256]; - memset(lib_wstr, 0, sizeof(lib_wstr)); - ::MultiByteToWideChar(CP_UTF8, NULL, library.c_str(), strlen(library.c_str())+1, lib_wstr, sizeof(lib_wstr) / sizeof(lib_wstr[0])); - netwoking_module = LoadLibrary(lib_wstr); - if (!netwoking_module) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": versioned library not found, trying current directory"; - std::string library_path = get_libpath_in_current_directory(std::string(BAMBU_NETWORK_LIBRARY)); - if (library_path.empty()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", can not get path in current directory for %1%") % BAMBU_NETWORK_LIBRARY; - set_load_error( - "Network library not found", - "Could not locate versioned library: " + library, - library - ); - return -1; - } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", current path %1%")%library_path; - memset(lib_wstr, 0, sizeof(lib_wstr)); - ::MultiByteToWideChar(CP_UTF8, NULL, library_path.c_str(), strlen(library_path.c_str())+1, lib_wstr, sizeof(lib_wstr) / sizeof(lib_wstr[0])); - netwoking_module = LoadLibrary(lib_wstr); - } -#else - netwoking_module = dlopen(library.c_str(), RTLD_LAZY); - if (!netwoking_module) { - char* dll_error = dlerror(); - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": dlopen failed: " << (dll_error ? dll_error : "unknown error"); - set_load_error( - "Failed to load network library", - dll_error ? std::string(dll_error) : "Unknown dlopen error", - library - ); - } - printf("after dlopen, network_module is %p\n", netwoking_module); -#endif - - if (!netwoking_module) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", can not Load Library for %1%")%library; - if (!s_load_error.has_error) { - set_load_error( - "Network library failed to load", - "LoadLibrary/dlopen returned null", - library - ); - } - return -1; - } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", successfully loaded library %1%, module %2%")%library %netwoking_module; - - // load file transfer interface - InitFTModule(netwoking_module); - - //load the functions - check_debug_consistent_ptr = reinterpret_cast(get_network_function("bambu_network_check_debug_consistent")); - get_version_ptr = reinterpret_cast(get_network_function("bambu_network_get_version")); - create_agent_ptr = reinterpret_cast(get_network_function("bambu_network_create_agent")); - destroy_agent_ptr = reinterpret_cast(get_network_function("bambu_network_destroy_agent")); - init_log_ptr = reinterpret_cast(get_network_function("bambu_network_init_log")); - set_config_dir_ptr = reinterpret_cast(get_network_function("bambu_network_set_config_dir")); - set_cert_file_ptr = reinterpret_cast(get_network_function("bambu_network_set_cert_file")); - set_country_code_ptr = reinterpret_cast(get_network_function("bambu_network_set_country_code")); - start_ptr = reinterpret_cast(get_network_function("bambu_network_start")); - set_on_ssdp_msg_fn_ptr = reinterpret_cast(get_network_function("bambu_network_set_on_ssdp_msg_fn")); - set_on_user_login_fn_ptr = reinterpret_cast(get_network_function("bambu_network_set_on_user_login_fn")); - set_on_printer_connected_fn_ptr = reinterpret_cast(get_network_function("bambu_network_set_on_printer_connected_fn")); - set_on_server_connected_fn_ptr = reinterpret_cast(get_network_function("bambu_network_set_on_server_connected_fn")); - set_on_http_error_fn_ptr = reinterpret_cast(get_network_function("bambu_network_set_on_http_error_fn")); - set_get_country_code_fn_ptr = reinterpret_cast(get_network_function("bambu_network_set_get_country_code_fn")); - set_on_subscribe_failure_fn_ptr = reinterpret_cast(get_network_function("bambu_network_set_on_subscribe_failure_fn")); - set_on_message_fn_ptr = reinterpret_cast(get_network_function("bambu_network_set_on_message_fn")); - set_on_user_message_fn_ptr = reinterpret_cast(get_network_function("bambu_network_set_on_user_message_fn")); - set_on_local_connect_fn_ptr = reinterpret_cast(get_network_function("bambu_network_set_on_local_connect_fn")); - set_on_local_message_fn_ptr = reinterpret_cast(get_network_function("bambu_network_set_on_local_message_fn")); - set_queue_on_main_fn_ptr = reinterpret_cast(get_network_function("bambu_network_set_queue_on_main_fn")); - connect_server_ptr = reinterpret_cast(get_network_function("bambu_network_connect_server")); - is_server_connected_ptr = reinterpret_cast(get_network_function("bambu_network_is_server_connected")); - refresh_connection_ptr = reinterpret_cast(get_network_function("bambu_network_refresh_connection")); - start_subscribe_ptr = reinterpret_cast(get_network_function("bambu_network_start_subscribe")); - stop_subscribe_ptr = reinterpret_cast(get_network_function("bambu_network_stop_subscribe")); - add_subscribe_ptr = reinterpret_cast(get_network_function("bambu_network_add_subscribe")); - del_subscribe_ptr = reinterpret_cast(get_network_function("bambu_network_del_subscribe")); - enable_multi_machine_ptr = reinterpret_cast(get_network_function("bambu_network_enable_multi_machine")); - send_message_ptr = reinterpret_cast(get_network_function("bambu_network_send_message")); - connect_printer_ptr = reinterpret_cast(get_network_function("bambu_network_connect_printer")); - disconnect_printer_ptr = reinterpret_cast(get_network_function("bambu_network_disconnect_printer")); - send_message_to_printer_ptr = reinterpret_cast(get_network_function("bambu_network_send_message_to_printer")); - check_cert_ptr = reinterpret_cast(get_network_function("bambu_network_update_cert")); - install_device_cert_ptr = reinterpret_cast(get_network_function("bambu_network_install_device_cert")); - start_discovery_ptr = reinterpret_cast(get_network_function("bambu_network_start_discovery")); - change_user_ptr = reinterpret_cast(get_network_function("bambu_network_change_user")); - is_user_login_ptr = reinterpret_cast(get_network_function("bambu_network_is_user_login")); - user_logout_ptr = reinterpret_cast(get_network_function("bambu_network_user_logout")); - get_user_id_ptr = reinterpret_cast(get_network_function("bambu_network_get_user_id")); - get_user_name_ptr = reinterpret_cast(get_network_function("bambu_network_get_user_name")); - get_user_avatar_ptr = reinterpret_cast(get_network_function("bambu_network_get_user_avatar")); - get_user_nickanme_ptr = reinterpret_cast(get_network_function("bambu_network_get_user_nickanme")); - build_login_cmd_ptr = reinterpret_cast(get_network_function("bambu_network_build_login_cmd")); - build_logout_cmd_ptr = reinterpret_cast(get_network_function("bambu_network_build_logout_cmd")); - build_login_info_ptr = reinterpret_cast(get_network_function("bambu_network_build_login_info")); - ping_bind_ptr = reinterpret_cast(get_network_function("bambu_network_ping_bind")); - bind_detect_ptr = reinterpret_cast(get_network_function("bambu_network_bind_detect")); - set_server_callback_ptr = reinterpret_cast(get_network_function("bambu_network_set_server_callback")); - bind_ptr = reinterpret_cast(get_network_function("bambu_network_bind")); - unbind_ptr = reinterpret_cast(get_network_function("bambu_network_unbind")); - get_bambulab_host_ptr = reinterpret_cast(get_network_function("bambu_network_get_bambulab_host")); - get_user_selected_machine_ptr = reinterpret_cast(get_network_function("bambu_network_get_user_selected_machine")); - set_user_selected_machine_ptr = reinterpret_cast(get_network_function("bambu_network_set_user_selected_machine")); - start_print_ptr = reinterpret_cast(get_network_function("bambu_network_start_print")); - start_local_print_with_record_ptr = reinterpret_cast(get_network_function("bambu_network_start_local_print_with_record")); - start_send_gcode_to_sdcard_ptr = reinterpret_cast(get_network_function("bambu_network_start_send_gcode_to_sdcard")); - start_local_print_ptr = reinterpret_cast(get_network_function("bambu_network_start_local_print")); - start_sdcard_print_ptr = reinterpret_cast(get_network_function("bambu_network_start_sdcard_print")); - get_user_presets_ptr = reinterpret_cast(get_network_function("bambu_network_get_user_presets")); - request_setting_id_ptr = reinterpret_cast(get_network_function("bambu_network_request_setting_id")); - put_setting_ptr = reinterpret_cast(get_network_function("bambu_network_put_setting")); - get_setting_list_ptr = reinterpret_cast(get_network_function("bambu_network_get_setting_list")); - get_setting_list2_ptr = reinterpret_cast(get_network_function("bambu_network_get_setting_list2")); - delete_setting_ptr = reinterpret_cast(get_network_function("bambu_network_delete_setting")); - get_studio_info_url_ptr = reinterpret_cast(get_network_function("bambu_network_get_studio_info_url")); - set_extra_http_header_ptr = reinterpret_cast(get_network_function("bambu_network_set_extra_http_header")); - get_my_message_ptr = reinterpret_cast(get_network_function("bambu_network_get_my_message")); - check_user_task_report_ptr = reinterpret_cast(get_network_function("bambu_network_check_user_task_report")); - get_user_print_info_ptr = reinterpret_cast(get_network_function("bambu_network_get_user_print_info")); - get_user_tasks_ptr = reinterpret_cast(get_network_function("bambu_network_get_user_tasks")); - get_printer_firmware_ptr = reinterpret_cast(get_network_function("bambu_network_get_printer_firmware")); - get_task_plate_index_ptr = reinterpret_cast(get_network_function("bambu_network_get_task_plate_index")); - get_user_info_ptr = reinterpret_cast(get_network_function("bambu_network_get_user_info")); - request_bind_ticket_ptr = reinterpret_cast(get_network_function("bambu_network_request_bind_ticket")); - get_subtask_info_ptr = reinterpret_cast(get_network_function("bambu_network_get_subtask_info")); - get_slice_info_ptr = reinterpret_cast(get_network_function("bambu_network_get_slice_info")); - query_bind_status_ptr = reinterpret_cast(get_network_function("bambu_network_query_bind_status")); - modify_printer_name_ptr = reinterpret_cast(get_network_function("bambu_network_modify_printer_name")); - get_camera_url_ptr = reinterpret_cast(get_network_function("bambu_network_get_camera_url")); - get_design_staffpick_ptr = reinterpret_cast(get_network_function("bambu_network_get_design_staffpick")); - start_publish_ptr = reinterpret_cast(get_network_function("bambu_network_start_publish")); - get_model_publish_url_ptr = reinterpret_cast(get_network_function("bambu_network_get_model_publish_url")); - get_subtask_ptr = reinterpret_cast(get_network_function("bambu_network_get_subtask")); - get_model_mall_home_url_ptr = reinterpret_cast(get_network_function("bambu_network_get_model_mall_home_url")); - get_model_mall_detail_url_ptr = reinterpret_cast(get_network_function("bambu_network_get_model_mall_detail_url")); - get_my_profile_ptr = reinterpret_cast(get_network_function("bambu_network_get_my_profile")); - track_enable_ptr = reinterpret_cast(get_network_function("bambu_network_track_enable")); - track_remove_files_ptr = reinterpret_cast(get_network_function("bambu_network_track_remove_files")); - track_event_ptr = reinterpret_cast(get_network_function("bambu_network_track_event")); - track_header_ptr = reinterpret_cast(get_network_function("bambu_network_track_header")); - track_update_property_ptr = reinterpret_cast(get_network_function("bambu_network_track_update_property")); - track_get_property_ptr = reinterpret_cast(get_network_function("bambu_network_track_get_property")); - put_model_mall_rating_url_ptr = reinterpret_cast(get_network_function("bambu_network_put_model_mall_rating")); - get_oss_config_ptr = reinterpret_cast(get_network_function("bambu_network_get_oss_config")); - put_rating_picture_oss_ptr = reinterpret_cast(get_network_function("bambu_network_put_rating_picture_oss")); - get_model_mall_rating_result_ptr = reinterpret_cast(get_network_function("bambu_network_get_model_mall_rating")); - - get_mw_user_preference_ptr = reinterpret_cast(get_network_function("bambu_network_get_mw_user_preference")); - get_mw_user_4ulist_ptr = reinterpret_cast(get_network_function("bambu_network_get_mw_user_4ulist")); - - if (get_version_ptr) { - std::string version = get_version_ptr(); - printf("network plugin version: %s\n", version.c_str()); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": network plugin version = " << version; - } - - return 0; + return BBLNetworkPlugin::instance().initialize(using_backup, version); } int NetworkAgent::unload_network_module() { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", network module %1%")%netwoking_module; - UnloadFTModule(); -#if defined(_MSC_VER) || defined(_WIN32) - if (netwoking_module) { - FreeLibrary(netwoking_module); - netwoking_module = NULL; - } - if (source_module) { - FreeLibrary(source_module); - source_module = NULL; - } -#else - if (netwoking_module) { - dlclose(netwoking_module); - netwoking_module = NULL; - } - if (source_module) { - dlclose(source_module); - source_module = NULL; - } -#endif - - check_debug_consistent_ptr = nullptr; - get_version_ptr = nullptr; - create_agent_ptr = nullptr; - destroy_agent_ptr = nullptr; - init_log_ptr = nullptr; - set_config_dir_ptr = nullptr; - set_cert_file_ptr = nullptr; - set_country_code_ptr = nullptr; - start_ptr = nullptr; - set_on_ssdp_msg_fn_ptr = nullptr; - set_on_user_login_fn_ptr = nullptr; - set_on_printer_connected_fn_ptr = nullptr; - set_on_server_connected_fn_ptr = nullptr; - set_on_http_error_fn_ptr = nullptr; - set_get_country_code_fn_ptr = nullptr; - set_on_subscribe_failure_fn_ptr = nullptr; - set_on_message_fn_ptr = nullptr; - set_on_user_message_fn_ptr = nullptr; - set_on_local_connect_fn_ptr = nullptr; - set_on_local_message_fn_ptr = nullptr; - set_queue_on_main_fn_ptr = nullptr; - connect_server_ptr = nullptr; - is_server_connected_ptr = nullptr; - refresh_connection_ptr = nullptr; - start_subscribe_ptr = nullptr; - stop_subscribe_ptr = nullptr; - send_message_ptr = nullptr; - connect_printer_ptr = nullptr; - disconnect_printer_ptr = nullptr; - send_message_to_printer_ptr = nullptr; - check_cert_ptr = nullptr; - start_discovery_ptr = nullptr; - change_user_ptr = nullptr; - is_user_login_ptr = nullptr; - user_logout_ptr = nullptr; - get_user_id_ptr = nullptr; - get_user_name_ptr = nullptr; - get_user_avatar_ptr = nullptr; - get_user_nickanme_ptr = nullptr; - build_login_cmd_ptr = nullptr; - build_logout_cmd_ptr = nullptr; - build_login_info_ptr = nullptr; - ping_bind_ptr = nullptr; - bind_ptr = nullptr; - unbind_ptr = nullptr; - get_bambulab_host_ptr = nullptr; - get_user_selected_machine_ptr = nullptr; - set_user_selected_machine_ptr = nullptr; - start_print_ptr = nullptr; - start_local_print_with_record_ptr = nullptr; - start_send_gcode_to_sdcard_ptr = nullptr; - start_local_print_ptr = nullptr; - start_sdcard_print_ptr = nullptr; - get_user_presets_ptr = nullptr; - request_setting_id_ptr = nullptr; - put_setting_ptr = nullptr; - get_setting_list_ptr = nullptr; - get_setting_list2_ptr = nullptr; - delete_setting_ptr = nullptr; - get_studio_info_url_ptr = nullptr; - set_extra_http_header_ptr = nullptr; - get_my_message_ptr = nullptr; - check_user_task_report_ptr = nullptr; - get_user_print_info_ptr = nullptr; - get_user_tasks_ptr = nullptr; - get_printer_firmware_ptr = nullptr; - get_task_plate_index_ptr = nullptr; - get_user_info_ptr = nullptr; - get_subtask_info_ptr = nullptr; - get_slice_info_ptr = nullptr; - query_bind_status_ptr = nullptr; - modify_printer_name_ptr = nullptr; - get_camera_url_ptr = nullptr; - get_design_staffpick_ptr = nullptr; - start_publish_ptr = nullptr; - get_model_publish_url_ptr = nullptr; - get_subtask_ptr = nullptr; - get_model_mall_home_url_ptr = nullptr; - get_model_mall_detail_url_ptr = nullptr; - get_my_profile_ptr = nullptr; - track_enable_ptr = nullptr; - track_remove_files_ptr = nullptr; - track_event_ptr = nullptr; - track_header_ptr = nullptr; - track_update_property_ptr = nullptr; - track_get_property_ptr = nullptr; - get_oss_config_ptr = nullptr; - put_rating_picture_oss_ptr = nullptr; - put_model_mall_rating_url_ptr = nullptr; - get_model_mall_rating_result_ptr = nullptr; - - get_mw_user_preference_ptr = nullptr; - get_mw_user_4ulist_ptr = nullptr; - - return 0; + return BBLNetworkPlugin::instance().unload(); } bool NetworkAgent::is_network_module_loaded() { - return netwoking_module != nullptr; + return BBLNetworkPlugin::instance().is_loaded(); } #if defined(_MSC_VER) || defined(_WIN32) HMODULE NetworkAgent::get_bambu_source_entry() +{ + return BBLNetworkPlugin::instance().get_bambu_source_entry(); +} #else void* NetworkAgent::get_bambu_source_entry() -#endif { - if ((source_module) || (!netwoking_module)) - return source_module; - - //int ret = -1; - std::string library; - std::string data_dir_str = data_dir(); - boost::filesystem::path data_dir_path(data_dir_str); - auto plugin_folder = data_dir_path / "plugins"; -#if defined(_MSC_VER) || defined(_WIN32) - wchar_t lib_wstr[128]; - - //goto load bambu source - library = plugin_folder.string() + "/" + std::string(BAMBU_SOURCE_LIBRARY) + ".dll"; - memset(lib_wstr, 0, sizeof(lib_wstr)); - ::MultiByteToWideChar(CP_UTF8, NULL, library.c_str(), strlen(library.c_str())+1, lib_wstr, sizeof(lib_wstr) / sizeof(lib_wstr[0])); - source_module = LoadLibrary(lib_wstr); - if (!source_module) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", try load BambuSource directly from current directory"); - std::string library_path = get_libpath_in_current_directory(std::string(BAMBU_SOURCE_LIBRARY)); - if (library_path.empty()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", can not get path in current directory for %1%") % BAMBU_SOURCE_LIBRARY; - return source_module; - } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", current path %1%")%library_path; - memset(lib_wstr, 0, sizeof(lib_wstr)); - ::MultiByteToWideChar(CP_UTF8, NULL, library_path.c_str(), strlen(library_path.c_str()) + 1, lib_wstr, sizeof(lib_wstr) / sizeof(lib_wstr[0])); - source_module = LoadLibrary(lib_wstr); - } -#else -#if defined(__WXMAC__) - library = plugin_folder.string() + "/" + std::string("lib") + std::string(BAMBU_SOURCE_LIBRARY) + ".dylib"; -#else - library = plugin_folder.string() + "/" + std::string("lib") + std::string(BAMBU_SOURCE_LIBRARY) + ".so"; -#endif - source_module = dlopen( library.c_str(), RTLD_LAZY); - /*if (!source_module) { -#if defined(__WXMAC__) - library = std::string("lib") + BAMBU_SOURCE_LIBRARY + ".dylib"; -#else - library = std::string("lib") + BAMBU_SOURCE_LIBRARY + ".so"; -#endif - source_module = dlopen( library.c_str(), RTLD_LAZY); - }*/ + return BBLNetworkPlugin::instance().get_bambu_source_entry(); +} #endif - return source_module; +std::string NetworkAgent::get_version() +{ + return BBLNetworkPlugin::instance().get_version(); } void* NetworkAgent::get_network_function(const char* name) { - void* function = nullptr; - - if (!netwoking_module) - return function; - -#if defined(_MSC_VER) || defined(_WIN32) - function = GetProcAddress(netwoking_module, name); -#else - function = dlsym(netwoking_module, name); -#endif - - if (!function) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", can not find function %1%")%name; - } - return function; -} - -std::string NetworkAgent::get_version() -{ - bool consistent = true; - //check the debug consistent first - if (check_debug_consistent_ptr) { -#if defined(NDEBUG) - consistent = check_debug_consistent_ptr(false); -#else - consistent = check_debug_consistent_ptr(true); -#endif - } - if (!consistent) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", inconsistent library,return 00.00.00.00!"); - return "00.00.00.00"; - } - if (get_version_ptr) { - return get_version_ptr(); - } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", get_version not supported,return 00.00.00.00!"); - return "00.00.00.00"; + return BBLNetworkPlugin::instance().get_network_function(name); } NetworkLibraryLoadError NetworkAgent::get_load_error() { - return s_load_error; + return BBLNetworkPlugin::instance().get_load_error(); } void NetworkAgent::clear_load_error() { - s_load_error = NetworkLibraryLoadError{}; + BBLNetworkPlugin::instance().clear_load_error(); } void NetworkAgent::set_load_error(const std::string& message, const std::string& technical_details, const std::string& attempted_path) { - s_load_error.has_error = true; - s_load_error.message = message; - s_load_error.technical_details = technical_details; - s_load_error.attempted_path = attempted_path; + BBLNetworkPlugin::instance().set_load_error(message, technical_details, attempted_path); } +// ============================================================================ +// Constructors +// ============================================================================ + +NetworkAgent::NetworkAgent(std::string log_dir) +{ + auto& plugin = BBLNetworkPlugin::instance(); + + if (plugin.is_loaded()) { + // Create agent if not already created + if (!plugin.has_agent()) { + plugin.create_agent(log_dir); + } + + // Create BBL sub-agents that will use the singleton + m_cloud_agent = std::make_shared(); + m_printer_agent = std::make_shared(); + m_printer_agent->set_cloud_agent(m_cloud_agent); + m_printer_agent_id = m_printer_agent->get_agent_info().id; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", this %1%, agent=%2%, log_dir=%3%") + % this % plugin.get_agent() % log_dir; + } else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": BBL network plugin not loaded"; + } +} + +NetworkAgent::NetworkAgent(std::shared_ptr cloud_agent, + std::shared_ptr printer_agent) + : m_cloud_agent(std::move(cloud_agent)) + , m_printer_agent(std::move(printer_agent)) +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format( + ", sub-agent composition mode: cloud=%1%, printer=%2%") + % (m_cloud_agent ? "yes" : "no") + % (m_printer_agent ? "yes" : "no (will be set when printer selected)"); +} + +NetworkAgent::~NetworkAgent() +{ + // Note: We don't destroy the agent here anymore since it's managed by BBLNetworkPlugin singleton + // The singleton manages the agent lifecycle + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", this %1%") % this; +} + +void NetworkAgent::set_printer_agent(std::shared_ptr printer_agent) +{ + // Local copies to allow safe access after releasing the lock. + // This pattern ensures the objects stay alive (via shared_ptr refcount) even if + // another thread modifies m_printer_agent or m_printer_callbacks after we unlock. + std::shared_ptr new_printer_agent; + PrinterCallbacks callbacks; + + { + // Critical section: protect access to shared state + std::lock_guard lock(m_agent_mutex); + + if (!printer_agent) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": null printer agent provided"; + return; + } + + // Disconnect all callbacks from the old agent + apply_printer_callbacks(m_printer_agent, callbacks); + // Take ownership of the incoming agent and update the agent ID + m_printer_agent = std::move(printer_agent); + m_printer_agent_id = m_printer_agent->get_agent_info().id; + + // Create local shared_ptr copies - this increments the reference count, + // guaranteeing the agent object stays alive even if m_printer_agent + // is modified by another thread after we unlock + new_printer_agent = m_printer_agent; + callbacks = m_printer_callbacks; + } + // Lock released here - m_agent_mutex is now free for other threads + + // Apply callbacks OUTSIDE the lock to avoid deadlock risk and minimize + // critical section duration. The local shared_ptr copy ensures the agent + // cannot be destroyed while we're using it. + apply_printer_callbacks(new_printer_agent, callbacks); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": agent switched successfully"; +} + +void* NetworkAgent::get_network_agent() +{ + return BBLNetworkPlugin::instance().get_agent(); +} + +void NetworkAgent::apply_printer_callbacks(const std::shared_ptr& printer_agent, + const PrinterCallbacks& callbacks) +{ + if (!printer_agent) { + return; + } + + printer_agent->set_on_ssdp_msg_fn(callbacks.on_ssdp_msg_fn); + printer_agent->set_on_printer_connected_fn(callbacks.on_printer_connected_fn); + printer_agent->set_on_subscribe_failure_fn(callbacks.on_subscribe_failure_fn); + printer_agent->set_on_message_fn(callbacks.on_message_fn); + printer_agent->set_on_user_message_fn(callbacks.on_user_message_fn); + printer_agent->set_on_local_connect_fn(callbacks.on_local_connect_fn); + printer_agent->set_on_local_message_fn(callbacks.on_local_message_fn); + printer_agent->set_queue_on_main_fn(callbacks.queue_on_main_fn); + printer_agent->set_server_callback(callbacks.on_server_err_fn); +} + +// ============================================================================ +// Instance methods - delegate to sub-agents +// ============================================================================ + int NetworkAgent::init_log() { - int ret = 0; - if (network_agent && init_log_ptr) { - ret = init_log_ptr(network_agent); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->init_log(); + return -1; } int NetworkAgent::set_config_dir(std::string config_dir) { - int ret = 0; - if (network_agent && set_config_dir_ptr) { - ret = set_config_dir_ptr(network_agent, config_dir); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, config_dir=%3%")%network_agent %ret %config_dir ; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->set_config_dir(config_dir); + return -1; } int NetworkAgent::set_cert_file(std::string folder, std::string filename) { - int ret = 0; - if (network_agent && set_cert_file_ptr) { - ret = set_cert_file_ptr(network_agent, folder, filename); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, folder=%3%, filename=%4%")%network_agent %ret %folder %filename; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->set_cert_file(folder, filename); + return -1; } int NetworkAgent::set_country_code(std::string country_code) { - int ret = 0; - if (network_agent && set_country_code_ptr) { - ret = set_country_code_ptr(network_agent, country_code); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, country_code=%3%")%network_agent %ret %country_code ; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->set_country_code(country_code); + return -1; } int NetworkAgent::start() { - int ret = 0; - if (network_agent && start_ptr) { - ret = start_ptr(network_agent); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->start(); + return -1; } int NetworkAgent::set_on_ssdp_msg_fn(OnMsgArrivedFn fn) { - int ret = 0; - if (network_agent && set_on_ssdp_msg_fn_ptr) { - ret = set_on_ssdp_msg_fn_ptr(network_agent, fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; + std::shared_ptr printer_agent; + { + std::lock_guard lock(m_agent_mutex); + m_printer_callbacks.on_ssdp_msg_fn = fn; + printer_agent = m_printer_agent; } - return ret; + if (printer_agent) return printer_agent->set_on_ssdp_msg_fn(fn); + return -1; } int NetworkAgent::set_on_user_login_fn(OnUserLoginFn fn) { - int ret = 0; - if (network_agent && set_on_user_login_fn_ptr) { - ret = set_on_user_login_fn_ptr(network_agent, fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->set_on_user_login_fn(fn); + return -1; } int NetworkAgent::set_on_printer_connected_fn(OnPrinterConnectedFn fn) { - int ret = 0; - if (network_agent && set_on_printer_connected_fn_ptr) { - ret = set_on_printer_connected_fn_ptr(network_agent, fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; + std::shared_ptr printer_agent; + { + std::lock_guard lock(m_agent_mutex); + m_printer_callbacks.on_printer_connected_fn = fn; + printer_agent = m_printer_agent; } - return ret; + if (printer_agent) return printer_agent->set_on_printer_connected_fn(fn); + return -1; } int NetworkAgent::set_on_server_connected_fn(OnServerConnectedFn fn) { - int ret = 0; - if (network_agent && set_on_server_connected_fn_ptr) { - ret = set_on_server_connected_fn_ptr(network_agent, fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->set_on_server_connected_fn(fn); + return -1; } int NetworkAgent::set_on_http_error_fn(OnHttpErrorFn fn) { - int ret = 0; - if (network_agent && set_on_http_error_fn_ptr) { - ret = set_on_http_error_fn_ptr(network_agent, fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->set_on_http_error_fn(fn); + return -1; } int NetworkAgent::set_get_country_code_fn(GetCountryCodeFn fn) { - int ret = 0; - if (network_agent && set_get_country_code_fn_ptr) { - ret = set_get_country_code_fn_ptr(network_agent, fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->set_get_country_code_fn(fn); + return -1; } int NetworkAgent::set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) { - int ret = 0; - if (network_agent && set_on_subscribe_failure_fn_ptr) { - ret = set_on_subscribe_failure_fn_ptr(network_agent, fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; + std::shared_ptr printer_agent; + { + std::lock_guard lock(m_agent_mutex); + m_printer_callbacks.on_subscribe_failure_fn = fn; + printer_agent = m_printer_agent; } - return ret; + if (printer_agent) return printer_agent->set_on_subscribe_failure_fn(fn); + return -1; } int NetworkAgent::set_on_message_fn(OnMessageFn fn) { - int ret = 0; - if (network_agent && set_on_message_fn_ptr) { - ret = set_on_message_fn_ptr(network_agent, fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; + std::shared_ptr printer_agent; + { + std::lock_guard lock(m_agent_mutex); + m_printer_callbacks.on_message_fn = fn; + printer_agent = m_printer_agent; } - return ret; + if (printer_agent) return printer_agent->set_on_message_fn(fn); + return -1; } int NetworkAgent::set_on_user_message_fn(OnMessageFn fn) { - int ret = 0; - if (network_agent && set_on_user_message_fn_ptr) { - ret = set_on_user_message_fn_ptr(network_agent, fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; + std::shared_ptr printer_agent; + { + std::lock_guard lock(m_agent_mutex); + m_printer_callbacks.on_user_message_fn = fn; + printer_agent = m_printer_agent; } - return ret; + if (printer_agent) return printer_agent->set_on_user_message_fn(fn); + return -1; } int NetworkAgent::set_on_local_connect_fn(OnLocalConnectedFn fn) { - int ret = 0; - if (network_agent && set_on_local_connect_fn_ptr) { - ret = set_on_local_connect_fn_ptr(network_agent, fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; + std::shared_ptr printer_agent; + { + std::lock_guard lock(m_agent_mutex); + m_printer_callbacks.on_local_connect_fn = fn; + printer_agent = m_printer_agent; } - return ret; + if (printer_agent) return printer_agent->set_on_local_connect_fn(fn); + return -1; } int NetworkAgent::set_on_local_message_fn(OnMessageFn fn) { - int ret = 0; - if (network_agent && set_on_local_message_fn_ptr) { - ret = set_on_local_message_fn_ptr(network_agent, fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; + std::shared_ptr printer_agent; + { + std::lock_guard lock(m_agent_mutex); + m_printer_callbacks.on_local_message_fn = fn; + printer_agent = m_printer_agent; } - return ret; + if (printer_agent) return printer_agent->set_on_local_message_fn(fn); + return -1; } int NetworkAgent::set_queue_on_main_fn(QueueOnMainFn fn) { - int ret = 0; - if (network_agent && set_queue_on_main_fn_ptr) { - ret = set_queue_on_main_fn_ptr(network_agent, fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; + // Set on both agents + std::shared_ptr cloud_agent; + std::shared_ptr printer_agent; + { + std::lock_guard lock(m_agent_mutex); + m_printer_callbacks.queue_on_main_fn = fn; + cloud_agent = m_cloud_agent; + printer_agent = m_printer_agent; } + + int ret = 0; + if (cloud_agent) ret = cloud_agent->set_queue_on_main_fn(fn); + if (printer_agent) printer_agent->set_queue_on_main_fn(fn); return ret; } int NetworkAgent::connect_server() { - int ret = 0; - if (network_agent && connect_server_ptr) { - ret = connect_server_ptr(network_agent); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->connect_server(); + return -1; } bool NetworkAgent::is_server_connected() { - bool ret = false; - if (network_agent && is_server_connected_ptr) { - ret = is_server_connected_ptr(network_agent); - //BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->is_server_connected(); + return false; } int NetworkAgent::refresh_connection() { - int ret = 0; - if (network_agent && refresh_connection_ptr) { - ret = refresh_connection_ptr(network_agent); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->refresh_connection(); + return -1; } int NetworkAgent::start_subscribe(std::string module) { - int ret = 0; - if (network_agent && start_subscribe_ptr) { - ret = start_subscribe_ptr(network_agent, module); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, module=%3%")%network_agent %ret %module ; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->start_subscribe(module); + return -1; } int NetworkAgent::stop_subscribe(std::string module) { - int ret = 0; - if (network_agent && stop_subscribe_ptr) { - ret = stop_subscribe_ptr(network_agent, module); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, module=%3%")%network_agent %ret %module ; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->stop_subscribe(module); + return -1; } int NetworkAgent::add_subscribe(std::vector dev_list) { - int ret = 0; - if (network_agent && add_subscribe_ptr) { - ret = add_subscribe_ptr(network_agent, dev_list); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->add_subscribe(dev_list); + return -1; } int NetworkAgent::del_subscribe(std::vector dev_list) { - int ret = 0; - if (network_agent && del_subscribe_ptr) { - ret = del_subscribe_ptr(network_agent, dev_list); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->del_subscribe(dev_list); + return -1; } void NetworkAgent::enable_multi_machine(bool enable) { - if (network_agent && enable_multi_machine_ptr) { - enable_multi_machine_ptr(network_agent, enable); - } + if (m_cloud_agent) m_cloud_agent->enable_multi_machine(enable); } int NetworkAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) { - int ret = 0; - if (network_agent && send_message_ptr) { - if (use_legacy_network) { - ret = (reinterpret_cast(send_message_ptr))(network_agent, dev_id, json_str, qos); - } else { - ret = send_message_ptr(network_agent, dev_id, json_str, qos, flag); - } - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, dev_id=%3%, json_str=%4%, qos=%5%")%network_agent %ret %dev_id %json_str %qos; - } - return ret; + if (m_printer_agent) return m_printer_agent->send_message(dev_id, json_str, qos, flag); + return -1; } int NetworkAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) { - int ret = 0; - if (network_agent && connect_printer_ptr) { - ret = connect_printer_ptr(network_agent, dev_id, dev_ip, username, password, use_ssl); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << (boost::format(" error: network_agent=%1%, ret=%2%, dev_id=%3%, dev_ip=%4%, username=%5%, password=%6%") - % network_agent % ret % dev_id % dev_ip % username % password).str(); - } - return ret; + if (m_printer_agent) return m_printer_agent->connect_printer(dev_id, dev_ip, username, password, use_ssl); + return -1; } int NetworkAgent::disconnect_printer() { - int ret = 0; - if (network_agent && disconnect_printer_ptr) { - ret = disconnect_printer_ptr(network_agent); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; - } - return ret; + if (m_printer_agent) return m_printer_agent->disconnect_printer(); + return -1; } int NetworkAgent::send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) { - int ret = 0; - if (network_agent && send_message_to_printer_ptr) { - if (use_legacy_network) { - ret = (reinterpret_cast(send_message_to_printer_ptr))(network_agent, dev_id, json_str, qos); - } else { - ret = send_message_to_printer_ptr(network_agent, dev_id, json_str, qos, flag); - } - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, dev_id=%3%, json_str=%4%, qos=%5%") - %network_agent %ret %dev_id %json_str %qos; - } - return ret; + if (m_printer_agent) return m_printer_agent->send_message_to_printer(dev_id, json_str, qos, flag); + return -1; } int NetworkAgent::check_cert() { - int ret = 0; - if (network_agent && check_cert_ptr) { - ret = check_cert_ptr(network_agent); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_printer_agent) return m_printer_agent->check_cert(); + return -1; } void NetworkAgent::install_device_cert(std::string dev_id, bool lan_only) { - if (network_agent && install_device_cert_ptr) { - install_device_cert_ptr(network_agent, dev_id, lan_only); - } + if (m_printer_agent) m_printer_agent->install_device_cert(dev_id, lan_only); } bool NetworkAgent::start_discovery(bool start, bool sending) { - bool ret = false; - if (network_agent && start_discovery_ptr) { - ret = start_discovery_ptr(network_agent, start, sending); - //BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, start=%3%, sending=%4%")%network_agent %ret %start %sending; - } - return ret; + if (m_printer_agent) return m_printer_agent->start_discovery(start, sending); + return false; } -int NetworkAgent::change_user(std::string user_info) +int NetworkAgent::change_user(std::string user_info) { - int ret = 0; - if (network_agent && change_user_ptr) { - ret = change_user_ptr(network_agent, user_info); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, user_info=%3%")%network_agent %ret %user_info ; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->change_user(user_info); + return -1; } bool NetworkAgent::is_user_login() { - bool ret = false; - if (network_agent && is_user_login_ptr) { - ret = is_user_login_ptr(network_agent); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->is_user_login(); + return false; } -int NetworkAgent::user_logout(bool request) +int NetworkAgent::user_logout(bool request) { - int ret = 0; - if (network_agent && user_logout_ptr) { - ret = user_logout_ptr(network_agent, request); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->user_logout(request); + return -1; } std::string NetworkAgent::get_user_id() { - std::string ret; - if (network_agent && get_user_id_ptr) { - ret = get_user_id_ptr(network_agent); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_user_id(); + return ""; } std::string NetworkAgent::get_user_name() { - std::string ret; - if (network_agent && get_user_name_ptr) { - ret = get_user_name_ptr(network_agent); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_user_name(); + return ""; } std::string NetworkAgent::get_user_avatar() { - std::string ret; - if (network_agent && get_user_avatar_ptr) { - ret = get_user_avatar_ptr(network_agent); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_user_avatar(); + return ""; } -std::string NetworkAgent::get_user_nickanme() +std::string NetworkAgent::get_user_nickname() { - std::string ret; - if (network_agent && get_user_nickanme_ptr) { - ret = get_user_nickanme_ptr(network_agent); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_user_nickname(); + return ""; } std::string NetworkAgent::build_login_cmd() { - std::string ret; - if (network_agent && build_login_cmd_ptr) { - ret = build_login_cmd_ptr(network_agent); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->build_login_cmd(); + return ""; } std::string NetworkAgent::build_logout_cmd() { - std::string ret; - if (network_agent && build_logout_cmd_ptr) { - ret = build_logout_cmd_ptr(network_agent); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->build_logout_cmd(); + return ""; } std::string NetworkAgent::build_login_info() { - std::string ret; - if (network_agent && build_login_info_ptr) { - ret = build_login_info_ptr(network_agent); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->build_login_info(); + return ""; } int NetworkAgent::ping_bind(std::string ping_code) { - int ret = 0; - if (network_agent && ping_bind_ptr) { - ret = ping_bind_ptr(network_agent, ping_code); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, pin code=%3%") - % network_agent % ret % ping_code; - } - return ret; + if (m_printer_agent) return m_printer_agent->ping_bind(ping_code); + return -1; } int NetworkAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) { - int ret = 0; - if (network_agent && bind_detect_ptr) { - ret = bind_detect_ptr(network_agent, dev_ip, sec_link, detect); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, dev_ip=%3%") - % network_agent % ret % dev_ip; - } - return ret; + if (m_printer_agent) return m_printer_agent->bind_detect(dev_ip, sec_link, detect); + return -1; } int NetworkAgent::set_server_callback(OnServerErrFn fn) { - int ret = 0; - if (network_agent && set_server_callback_ptr) { - ret = set_server_callback_ptr(network_agent, fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") - % network_agent % ret; + std::shared_ptr printer_agent; + { + std::lock_guard lock(m_agent_mutex); + m_printer_callbacks.on_server_err_fn = fn; + printer_agent = m_printer_agent; } - return ret; + if (printer_agent) return printer_agent->set_server_callback(fn); + return -1; } -int NetworkAgent::bind(std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) +int NetworkAgent::bind(std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) { - int ret = 0; - if (network_agent && bind_ptr) { - ret = bind_ptr(network_agent, dev_ip, dev_id, sec_link, timezone, improved, update_fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, dev_ip=%3%, timezone=%4%") - %network_agent %ret %dev_ip %timezone; - } - return ret; + if (m_printer_agent) return m_printer_agent->bind(dev_ip, dev_id, sec_link, timezone, improved, update_fn); + return -1; } int NetworkAgent::unbind(std::string dev_id) { - int ret = 0; - if (network_agent && unbind_ptr) { - ret = unbind_ptr(network_agent, dev_id); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, user_info=%3%")%network_agent %ret %dev_id ; - } - return ret; + if (m_printer_agent) return m_printer_agent->unbind(dev_id); + return -1; } -std::string NetworkAgent::get_bambulab_host() +std::string NetworkAgent::get_cloud_service_host() { - std::string ret; - if (network_agent && get_bambulab_host_ptr) { - ret = get_bambulab_host_ptr(network_agent); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_cloud_service_host(); + return ""; +} + +std::string NetworkAgent::get_cloud_login_url(const std::string& language) +{ + if (m_cloud_agent) return m_cloud_agent->get_cloud_login_url(language); + return ""; } std::string NetworkAgent::get_user_selected_machine() { - std::string ret; - if (network_agent && get_user_selected_machine_ptr) { - ret = get_user_selected_machine_ptr(network_agent); - } - return ret; + if (m_printer_agent) return m_printer_agent->get_user_selected_machine(); + return ""; } int NetworkAgent::set_user_selected_machine(std::string dev_id) { - int ret = 0; - if (network_agent && set_user_selected_machine_ptr) { - ret = set_user_selected_machine_ptr(network_agent, dev_id); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, user_info=%3%")%network_agent %ret %dev_id ; - } - return ret; + if (m_printer_agent) return m_printer_agent->set_user_selected_machine(dev_id); + return -1; } int NetworkAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { - int ret = 0; - if (network_agent && start_print_ptr) { - if (use_legacy_network) { - ret = (reinterpret_cast(start_print_ptr))(network_agent, as_legacy(params), update_fn, cancel_fn, wait_fn); - } else { - ret = start_print_ptr(network_agent, params, update_fn, cancel_fn, wait_fn); - } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" : network_agent=%1%, ret=%2%, dev_id=%3%, task_name=%4%, project_name=%5%") - %network_agent %ret %params.dev_id %params.task_name %params.project_name; - } - return ret; + if (m_printer_agent) return m_printer_agent->start_print(params, update_fn, cancel_fn, wait_fn); + return -1; } int NetworkAgent::start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { - int ret = 0; - if (network_agent && start_local_print_with_record_ptr) { - if (use_legacy_network) { - ret = (reinterpret_cast(start_local_print_with_record_ptr))(network_agent, as_legacy(params), update_fn, cancel_fn, wait_fn); - } else { - ret = start_local_print_with_record_ptr(network_agent, params, update_fn, cancel_fn, wait_fn); - } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" : network_agent=%1%, ret=%2%, dev_id=%3%, task_name=%4%, project_name=%5%") - %network_agent %ret %params.dev_id %params.task_name %params.project_name; - } - return ret; + if (m_printer_agent) return m_printer_agent->start_local_print_with_record(params, update_fn, cancel_fn, wait_fn); + return -1; } int NetworkAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { - int ret = 0; - if (network_agent && start_send_gcode_to_sdcard_ptr) { - if (use_legacy_network) { - ret = (reinterpret_cast(start_send_gcode_to_sdcard_ptr))(network_agent, as_legacy(params), update_fn, cancel_fn, wait_fn); - } else { - ret = start_send_gcode_to_sdcard_ptr(network_agent, params, update_fn, cancel_fn, wait_fn); - } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" : network_agent=%1%, ret=%2%, dev_id=%3%, task_name=%4%, project_name=%5%") - % network_agent % ret % params.dev_id % params.task_name % params.project_name; - } - return ret; + if (m_printer_agent) return m_printer_agent->start_send_gcode_to_sdcard(params, update_fn, cancel_fn, wait_fn); + return -1; } int NetworkAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) { - int ret = 0; - if (network_agent && start_local_print_ptr) { - if (use_legacy_network) { - ret = (reinterpret_cast(start_local_print_ptr))(network_agent, as_legacy(params), update_fn, cancel_fn); - } else { - ret = start_local_print_ptr(network_agent, params, update_fn, cancel_fn); - } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" : network_agent=%1%, ret=%2%, dev_id=%3%, task_name=%4%, project_name=%5%") - %network_agent %ret %params.dev_id %params.task_name %params.project_name; - } - return ret; + if (m_printer_agent) return m_printer_agent->start_local_print(params, update_fn, cancel_fn); + return -1; } int NetworkAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) { - int ret = 0; - if (network_agent && start_sdcard_print_ptr) { - if (use_legacy_network) { - ret = (reinterpret_cast(start_sdcard_print_ptr))(network_agent, as_legacy(params), update_fn, cancel_fn); - } else { - ret = start_sdcard_print_ptr(network_agent, params, update_fn, cancel_fn); - } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" : network_agent=%1%, ret=%2%, dev_id=%3%, task_name=%4%, project_name=%5%") - % network_agent % ret % params.dev_id % params.task_name % params.project_name; + if (m_printer_agent) return m_printer_agent->start_sdcard_print(params, update_fn, cancel_fn); + return -1; +} + +FilamentSyncMode NetworkAgent::get_filament_sync_mode() const +{ + if (m_printer_agent) return m_printer_agent->get_filament_sync_mode(); + return FilamentSyncMode::none; // Default when no agent +} + +void NetworkAgent::fetch_filament_info(std::string dev_id) +{ + if (m_printer_agent) { + m_printer_agent->fetch_filament_info(dev_id); } - return ret; } int NetworkAgent::get_user_presets(std::map>* user_presets) { - int ret = 0; - if (network_agent && get_user_presets_ptr) { - ret = get_user_presets_ptr(network_agent, user_presets); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" : network_agent=%1%, ret=%2%, setting_id count=%3%")%network_agent %ret %user_presets->size() ; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_user_presets(user_presets); + return -1; } std::string NetworkAgent::request_setting_id(std::string name, std::map* values_map, unsigned int* http_code) { - std::string ret; - if (network_agent && request_setting_id_ptr) { - ret = request_setting_id_ptr(network_agent, name, values_map, http_code); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" : network_agent=%1%, name=%2%, http_code=%3%, ret.setting_id=%4%") - %network_agent %name %(*http_code) %ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->request_setting_id(name, values_map, http_code); + return ""; } int NetworkAgent::put_setting(std::string setting_id, std::string name, std::map* values_map, unsigned int* http_code) { - int ret = 0; - if (network_agent && put_setting_ptr) { - ret = put_setting_ptr(network_agent, setting_id, name, values_map, http_code); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" : network_agent=%1%, setting_id=%2%, name=%3%, http_code=%4%, ret=%5%") - %network_agent %setting_id %name %(*http_code) %ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->put_setting(setting_id, name, values_map, http_code); + return -1; } int NetworkAgent::get_setting_list(std::string bundle_version, ProgressFn pro_fn, WasCancelledFn cancel_fn) { - int ret = 0; - if (network_agent && get_setting_list_ptr) { - ret = get_setting_list_ptr(network_agent, bundle_version, pro_fn, cancel_fn); - if (ret) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, bundle_version=%3%") % network_agent % ret % bundle_version; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_setting_list(bundle_version, pro_fn, cancel_fn); + return -1; } int NetworkAgent::get_setting_list2(std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn, WasCancelledFn cancel_fn) { - int ret = 0; - if (network_agent && get_setting_list2_ptr) { - ret = get_setting_list2_ptr(network_agent, bundle_version, chk_fn, pro_fn, cancel_fn); - if (ret) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, bundle_version=%3%") % network_agent % ret % bundle_version; - } else { - ret = get_setting_list(bundle_version, pro_fn, cancel_fn); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_setting_list2(bundle_version, chk_fn, pro_fn, cancel_fn); + return -1; } int NetworkAgent::delete_setting(std::string setting_id) { - int ret = 0; - if (network_agent && delete_setting_ptr) { - ret = delete_setting_ptr(network_agent, setting_id); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, setting_id=%3%")%network_agent %ret %setting_id ; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->delete_setting(setting_id); + return -1; } std::string NetworkAgent::get_studio_info_url() { - std::string ret; - if (network_agent && get_studio_info_url_ptr) { - ret = get_studio_info_url_ptr(network_agent); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_studio_info_url(); + return ""; } int NetworkAgent::set_extra_http_header(std::map extra_headers) { - int ret = 0; - if (network_agent && set_extra_http_header_ptr) { - ret = set_extra_http_header_ptr(network_agent, extra_headers); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, extra_headers count=%3%")%network_agent %ret %extra_headers.size() ; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->set_extra_http_header(extra_headers); + return -1; } int NetworkAgent::get_my_message(int type, int after, int limit, unsigned int* http_code, std::string* http_body) { - int ret = 0; - if (network_agent && get_my_message_ptr) { - ret = get_my_message_ptr(network_agent, type, after, limit, http_code, http_body); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_my_message(type, after, limit, http_code, http_body); + return -1; } int NetworkAgent::check_user_task_report(int* task_id, bool* printable) { - int ret = 0; - if (network_agent && check_user_task_report_ptr) { - ret = check_user_task_report_ptr(network_agent, task_id, printable); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, task_id=%3%, printable=%4%")%network_agent %ret %(*task_id) %(*printable); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->check_user_task_report(task_id, printable); + return -1; } int NetworkAgent::get_user_print_info(unsigned int* http_code, std::string* http_body) { - int ret = 0; - if (network_agent && get_user_print_info_ptr) { - ret = get_user_print_info_ptr(network_agent, http_code, http_body); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, http_code=%3%, http_body=%4%")%network_agent %ret %(*http_code) %(*http_body); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_user_print_info(http_code, http_body); + return -1; } int NetworkAgent::get_user_tasks(TaskQueryParams params, std::string* http_body) { - int ret = 0; - if (network_agent && get_user_tasks_ptr) { - ret = get_user_tasks_ptr(network_agent, params, http_body); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, http_body=%3%") % network_agent % ret % (*http_body); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_user_tasks(params, http_body); + return -1; } int NetworkAgent::get_printer_firmware(std::string dev_id, unsigned* http_code, std::string* http_body) { - int ret = 0; - if (network_agent && get_printer_firmware_ptr) { - ret = get_printer_firmware_ptr(network_agent, dev_id, http_code, http_body); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" : network_agent=%1%, ret=%2%, dev_id=%3%, http_code=%4%, http_body=%5%") - %network_agent %ret %dev_id %(*http_code) %(*http_body); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_printer_firmware(dev_id, http_code, http_body); + return -1; } int NetworkAgent::get_task_plate_index(std::string task_id, int* plate_index) { - int ret = 0; - if (network_agent && get_task_plate_index_ptr) { - ret = get_task_plate_index_ptr(network_agent, task_id, plate_index); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, task_id=%3%")%network_agent %ret %task_id; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_task_plate_index(task_id, plate_index); + return -1; } int NetworkAgent::get_user_info(int* identifier) { - int ret = 0; - if (network_agent && get_user_info_ptr) { - ret = get_user_info_ptr(network_agent, identifier); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_user_info(identifier); + return -1; } int NetworkAgent::request_bind_ticket(std::string* ticket) { - int ret = 0; - if (network_agent && request_bind_ticket_ptr) { - ret = request_bind_ticket_ptr(network_agent, ticket); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_printer_agent) return m_printer_agent->request_bind_ticket(ticket); + return -1; } int NetworkAgent::get_subtask_info(std::string subtask_id, std::string* task_json, unsigned int* http_code, std::string* http_body) { - int ret = 0; - if (network_agent && get_subtask_info_ptr) { - ret = get_subtask_info_ptr(network_agent, subtask_id, task_json, http_code, http_body); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format("error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_subtask_info(subtask_id, task_json, http_code, http_body); + return -1; } int NetworkAgent::get_slice_info(std::string project_id, std::string profile_id, int plate_index, std::string* slice_json) { - int ret = 0; - if (network_agent && get_slice_info_ptr) { - ret = get_slice_info_ptr(network_agent, project_id, profile_id, plate_index, slice_json); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" : network_agent=%1%, project_id=%2%, profile_id=%3%, plate_index=%4%, slice_json=%5%") - %network_agent %project_id %profile_id %plate_index %(*slice_json); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_slice_info(project_id, profile_id, plate_index, slice_json); + return -1; } int NetworkAgent::query_bind_status(std::vector query_list, unsigned int* http_code, std::string* http_body) { - int ret = 0; - if (network_agent && query_bind_status_ptr) { - ret = query_bind_status_ptr(network_agent, query_list, http_code, http_body); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, http_code=%3%, http_body=%4%") - %network_agent %ret%(*http_code) %(*http_body); - } - return ret; + if (m_cloud_agent) return m_cloud_agent->query_bind_status(query_list, http_code, http_body); + return -1; } int NetworkAgent::modify_printer_name(std::string dev_id, std::string dev_name) { - int ret = 0; - if (network_agent && modify_printer_name_ptr) { - ret = modify_printer_name_ptr(network_agent, dev_id, dev_name); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" : network_agent=%1%, ret=%2%, dev_id=%3%, dev_name=%4%")%network_agent %ret %dev_id %dev_name; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->modify_printer_name(dev_id, dev_name); + return -1; } int NetworkAgent::get_camera_url(std::string dev_id, std::function callback) { - int ret = 0; - if (network_agent && get_camera_url_ptr) { - ret = get_camera_url_ptr(network_agent, dev_id, callback); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%, dev_id=%3%")%network_agent %ret %dev_id; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_camera_url(dev_id, callback); + return -1; } int NetworkAgent::get_design_staffpick(int offset, int limit, std::function callback) { - int ret = 0; - if (network_agent && get_design_staffpick_ptr) { - ret = get_design_staffpick_ptr(network_agent, offset, limit, callback); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%")%network_agent %ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_design_staffpick(offset, limit, callback); + return -1; } -int NetworkAgent::get_mw_user_preference(std::function callback) +int NetworkAgent::start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out) { - int ret = 0; - if (network_agent && get_mw_user_preference_ptr) { - ret = get_mw_user_preference_ptr(network_agent,callback); - if (ret) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; -} - - -int NetworkAgent::get_mw_user_4ulist(int seed, int limit, std::function callback) -{ - int ret = 0; - if (network_agent && get_mw_user_4ulist_ptr) { - ret = get_mw_user_4ulist_ptr(network_agent,seed, limit, callback); - if (ret) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; -} - -int NetworkAgent::start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string *out) -{ - int ret = 0; - if (network_agent && start_publish_ptr) { - ret = start_publish_ptr(network_agent, params, update_fn, cancel_fn, out); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->start_publish(params, update_fn, cancel_fn, out); + return -1; } int NetworkAgent::get_model_publish_url(std::string* url) { - int ret = 0; - if (network_agent && get_model_publish_url_ptr) { - ret = get_model_publish_url_ptr(network_agent, url); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_model_publish_url(url); + return -1; } int NetworkAgent::get_subtask(BBLModelTask* task, OnGetSubTaskFn getsub_fn) { - int ret = 0; - if (network_agent && get_subtask_ptr) { - ret = get_subtask_ptr(network_agent, task, getsub_fn); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - - return ret; + if (m_cloud_agent) return m_cloud_agent->get_subtask(task, getsub_fn); + return -1; } int NetworkAgent::get_model_mall_home_url(std::string* url) { - int ret = 0; - if (network_agent && get_model_publish_url_ptr) { - ret = get_model_mall_home_url_ptr(network_agent, url); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_model_mall_home_url(url); + return -1; } int NetworkAgent::get_model_mall_detail_url(std::string* url, std::string id) { - int ret = 0; - if (network_agent && get_model_publish_url_ptr) { - ret = get_model_mall_detail_url_ptr(network_agent, url, id); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_model_mall_detail_url(url, id); + return -1; } -int NetworkAgent::get_my_profile(std::string token, unsigned int *http_code, std::string *http_body) +int NetworkAgent::get_my_profile(std::string token, unsigned int* http_code, std::string* http_body) { - int ret = 0; - if (network_agent && get_my_profile_ptr) { - ret = get_my_profile_ptr(network_agent, token, http_code, http_body); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format("error network_agnet=%1%, ret = %2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_my_profile(token, http_code, http_body); + return -1; } int NetworkAgent::track_enable(bool enable) { - enable_track = false; - int ret = 0; - if (network_agent && track_enable_ptr) { - ret = track_enable_ptr(network_agent, enable); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format("error network_agnet=%1%, ret = %2%") % network_agent % ret; - } - return ret; + this->enable_track = enable; + if (m_cloud_agent) return m_cloud_agent->track_enable(enable); + return -1; } int NetworkAgent::track_remove_files() { - int ret = 0; - if (network_agent && track_remove_files_ptr) { - ret = track_remove_files_ptr(network_agent); - if (ret) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format("error network_agnet=%1%, ret = %2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->track_remove_files(); + return -1; } int NetworkAgent::track_event(std::string evt_key, std::string content) { - return 0; - if (!this->enable_track) - return 0; - - int ret = 0; - if (network_agent && track_event_ptr) { - ret = track_event_ptr(network_agent, evt_key, content); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format("error network_agnet=%1%, ret = %2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->track_event(evt_key, content); + return -1; } int NetworkAgent::track_header(std::string header) { - if (!this->enable_track) - return 0; - int ret = 0; - if (network_agent && track_header_ptr) { - ret = track_header_ptr(network_agent, header); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format("error network_agnet=%1%, ret = %2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->track_header(header); + return -1; } int NetworkAgent::track_update_property(std::string name, std::string value, std::string type) { - if (!this->enable_track) - return 0; - - int ret = 0; - if (network_agent && track_update_property_ptr) { - ret = track_update_property_ptr(network_agent, name, value, type); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format("error network_agnet=%1%, ret = %2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->track_update_property(name, value, type); + return -1; } int NetworkAgent::track_get_property(std::string name, std::string& value, std::string type) { - if (!this->enable_track) - return 0; - - int ret = 0; - if (network_agent && track_get_property_ptr) { - ret = track_get_property_ptr(network_agent, name, value, type); - if (ret) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format("error network_agnet=%1%, ret = %2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->track_get_property(name, value, type); + return -1; } -int NetworkAgent::put_model_mall_rating(int rating_id, int score, std::string content, std::vector images, unsigned int &http_code, std::string &http_error) +int NetworkAgent::put_model_mall_rating(int design_id, int score, std::string content, std::vector images, unsigned int& http_code, std::string& http_error) { - int ret = 0; - if (network_agent && get_model_publish_url_ptr) { - ret = put_model_mall_rating_url_ptr(network_agent, rating_id, score, content, images, http_code, http_error); - if (ret) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->put_model_mall_rating(design_id, score, content, images, http_code, http_error); + return -1; } -int NetworkAgent::get_oss_config(std::string &config, std::string country_code, unsigned int &http_code, std::string &http_error) +int NetworkAgent::get_oss_config(std::string& config, std::string country_code, unsigned int& http_code, std::string& http_error) { - int ret = 0; - if (network_agent && get_oss_config_ptr) { - ret = get_oss_config_ptr(network_agent, config, country_code, http_code, http_error); - if (ret) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_oss_config(config, country_code, http_code, http_error); + return -1; } -int NetworkAgent::put_rating_picture_oss(std::string &config, std::string &pic_oss_path, std::string model_id, int profile_id, unsigned int &http_code, std::string &http_error) +int NetworkAgent::put_rating_picture_oss(std::string& config, std::string& pic_oss_path, std::string model_id, int profile_id, unsigned int& http_code, std::string& http_error) { - int ret = 0; - if (network_agent && put_rating_picture_oss_ptr) { - ret = put_rating_picture_oss_ptr(network_agent, config, pic_oss_path, model_id, profile_id, http_code, http_error); - if (ret) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->put_rating_picture_oss(config, pic_oss_path, model_id, profile_id, http_code, http_error); + return -1; } -int NetworkAgent::get_model_mall_rating_result(int job_id, std::string &rating_result, unsigned int &http_code, std::string &http_error) +int NetworkAgent::get_model_mall_rating_result(int job_id, std::string& rating_result, unsigned int& http_code, std::string& http_error) { - int ret = 0; - if (network_agent && get_model_mall_rating_result_ptr) { - ret = get_model_mall_rating_result_ptr(network_agent, job_id, rating_result, http_code, http_error); - if (ret) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" error: network_agent=%1%, ret=%2%") % network_agent % ret; - } - return ret; + if (m_cloud_agent) return m_cloud_agent->get_model_mall_rating_result(job_id, rating_result, http_code, http_error); + return -1; } -} //namespace - -std::vector BBL::get_all_available_versions() +int NetworkAgent::get_mw_user_preference(std::function callback) { - std::vector result; - std::set known_base_versions; - std::set all_known_versions; - - for (size_t i = 0; i < AVAILABLE_NETWORK_VERSIONS_COUNT; ++i) { - result.push_back(NetworkLibraryVersionInfo::from_static(AVAILABLE_NETWORK_VERSIONS[i])); - known_base_versions.insert(AVAILABLE_NETWORK_VERSIONS[i].version); - all_known_versions.insert(AVAILABLE_NETWORK_VERSIONS[i].version); - } - - std::vector discovered = Slic3r::NetworkAgent::scan_plugin_versions(); - - std::vector> suffixed_versions; - - for (const auto& version : discovered) { - if (all_known_versions.count(version) > 0) - continue; - - std::string base = extract_base_version(version); - std::string suffix = extract_suffix(version); - - if (suffix.empty()) - continue; - - if (known_base_versions.count(base) == 0) - continue; - - suffixed_versions.emplace_back(base, version); - all_known_versions.insert(version); - } - - std::sort(suffixed_versions.begin(), suffixed_versions.end(), - [](const auto& a, const auto& b) { - if (a.first != b.first) return a.first > b.first; - return a.second < b.second; - }); - - for (const auto& [base, full] : suffixed_versions) { - size_t insert_pos = 0; - for (size_t i = 0; i < result.size(); ++i) { - if (result[i].base_version == base) { - insert_pos = i + 1; - while (insert_pos < result.size() && - result[insert_pos].base_version == base) { - ++insert_pos; - } - break; - } - } - - std::string sfx = extract_suffix(full); - result.insert(result.begin() + insert_pos, - NetworkLibraryVersionInfo::from_discovered(full, base, sfx)); - } - - return result; + if (m_cloud_agent) return m_cloud_agent->get_mw_user_preference(callback); + return -1; } + +int NetworkAgent::get_mw_user_4ulist(int seed, int limit, std::function callback) +{ + if (m_cloud_agent) return m_cloud_agent->get_mw_user_4ulist(seed, limit, callback); + return -1; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/NetworkAgent.hpp b/src/slic3r/Utils/NetworkAgent.hpp index 50bb35474e..09cee2a45e 100644 --- a/src/slic3r/Utils/NetworkAgent.hpp +++ b/src/slic3r/Utils/NetworkAgent.hpp @@ -3,118 +3,22 @@ #include "bambu_networking.hpp" #include "libslic3r/ProjectTask.hpp" +#include "ICloudServiceAgent.hpp" +#include "IPrinterAgent.hpp" +#include -using namespace BBL; namespace Slic3r { -typedef bool (*func_check_debug_consistent)(bool is_debug); -typedef std::string (*func_get_version)(void); -typedef void* (*func_create_agent)(std::string log_dir); -typedef int (*func_destroy_agent)(void *agent); -typedef int (*func_init_log)(void *agent); -typedef int (*func_set_config_dir)(void *agent, std::string config_dir); -typedef int (*func_set_cert_file)(void *agent, std::string folder, std::string filename); -typedef int (*func_set_country_code)(void *agent, std::string country_code); -typedef int (*func_start)(void *agent); -typedef int (*func_set_on_ssdp_msg_fn)(void *agent, OnMsgArrivedFn fn); -typedef int (*func_set_on_user_login_fn)(void *agent, OnUserLoginFn fn); -typedef int (*func_set_on_printer_connected_fn)(void *agent, OnPrinterConnectedFn fn); -typedef int (*func_set_on_server_connected_fn)(void *agent, OnServerConnectedFn fn); -typedef int (*func_set_on_http_error_fn)(void *agent, OnHttpErrorFn fn); -typedef int (*func_set_get_country_code_fn)(void *agent, GetCountryCodeFn fn); -typedef int (*func_set_on_subscribe_failure_fn)(void *agent, GetSubscribeFailureFn fn); -typedef int (*func_set_on_message_fn)(void *agent, OnMessageFn fn); -typedef int (*func_set_on_user_message_fn)(void *agent, OnMessageFn fn); -typedef int (*func_set_on_local_connect_fn)(void *agent, OnLocalConnectedFn fn); -typedef int (*func_set_on_local_message_fn)(void *agent, OnMessageFn fn); -typedef int (*func_set_queue_on_main_fn)(void *agent, QueueOnMainFn fn); -typedef int (*func_connect_server)(void *agent); -typedef bool (*func_is_server_connected)(void *agent); -typedef int (*func_refresh_connection)(void *agent); -typedef int (*func_start_subscribe)(void *agent, std::string module); -typedef int (*func_stop_subscribe)(void *agent, std::string module); -typedef int (*func_add_subscribe)(void *agent, std::vector dev_list); -typedef int (*func_del_subscribe)(void *agent, std::vector dev_list); -typedef void (*func_enable_multi_machine)(void *agent, bool enable); -typedef int (*func_send_message)(void *agent, std::string dev_id, std::string json_str, int qos, int flag); -typedef int (*func_connect_printer)(void *agent, std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); -typedef int (*func_disconnect_printer)(void *agent); -typedef int (*func_send_message_to_printer)(void *agent, std::string dev_id, std::string json_str, int qos, int flag); -typedef int (*func_check_cert)(void* agent); -typedef void (*func_install_device_cert)(void* agent, std::string dev_id, bool lan_only); -typedef bool (*func_start_discovery)(void *agent, bool start, bool sending); -typedef int (*func_change_user)(void *agent, std::string user_info); -typedef bool (*func_is_user_login)(void *agent); -typedef int (*func_user_logout)(void *agent, bool request); -typedef std::string (*func_get_user_id)(void *agent); -typedef std::string (*func_get_user_name)(void *agent); -typedef std::string (*func_get_user_avatar)(void *agent); -typedef std::string (*func_get_user_nickanme)(void *agent); -typedef std::string (*func_build_login_cmd)(void *agent); -typedef std::string (*func_build_logout_cmd)(void *agent); -typedef std::string (*func_build_login_info)(void *agent); -typedef int (*func_ping_bind)(void *agent, std::string ping_code); -typedef int (*func_bind_detect)(void *agent, std::string dev_ip, std::string sec_link, detectResult& detect); -typedef int (*func_set_server_callback)(void *agent, OnServerErrFn fn); -typedef int (*func_bind)(void *agent, std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn); -typedef int (*func_unbind)(void *agent, std::string dev_id); -typedef std::string (*func_get_bambulab_host)(void *agent); -typedef std::string (*func_get_user_selected_machine)(void *agent); -typedef int (*func_set_user_selected_machine)(void *agent, std::string dev_id); -typedef int (*func_start_print)(void *agent, PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); -typedef int (*func_start_local_print_with_record)(void *agent, PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); -typedef int (*func_start_send_gcode_to_sdcard)(void *agent, PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); -typedef int (*func_start_local_print)(void *agent, PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); -typedef int (*func_start_sdcard_print)(void *agent, PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); -typedef int (*func_get_user_presets)(void *agent, std::map>* user_presets); -typedef std::string (*func_request_setting_id)(void *agent, std::string name, std::map* values_map, unsigned int* http_code); -typedef int (*func_put_setting)(void *agent, std::string setting_id, std::string name, std::map* values_map, unsigned int* http_code); -typedef int (*func_get_setting_list)(void *agent, std::string bundle_version, ProgressFn pro_fn, WasCancelledFn cancel_fn); -typedef int (*func_get_setting_list2)(void *agent, std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn, WasCancelledFn cancel_fn); -typedef int (*func_delete_setting)(void *agent, std::string setting_id); -typedef std::string (*func_get_studio_info_url)(void *agent); -typedef int (*func_set_extra_http_header)(void *agent, std::map extra_headers); -typedef int (*func_get_my_message)(void *agent, int type, int after, int limit, unsigned int* http_code, std::string* http_body); -typedef int (*func_check_user_task_report)(void *agent, int* task_id, bool* printable); -typedef int (*func_get_user_print_info)(void *agent, unsigned int* http_code, std::string* http_body); -typedef int (*func_get_user_tasks)(void *agent, TaskQueryParams params, std::string* http_body); -typedef int (*func_get_printer_firmware)(void *agent, std::string dev_id, unsigned* http_code, std::string* http_body); -typedef int (*func_get_task_plate_index)(void *agent, std::string task_id, int* plate_index); -typedef int (*func_get_user_info)(void *agent, int* identifier); -typedef int (*func_request_bind_ticket)(void *agent, std::string* ticket); -typedef int (*func_get_subtask_info)(void *agent, std::string subtask_id, std::string* task_json, unsigned int* http_code, std::string *http_body); -typedef int (*func_get_slice_info)(void *agent, std::string project_id, std::string profile_id, int plate_index, std::string* slice_json); -typedef int (*func_query_bind_status)(void *agent, std::vector query_list, unsigned int* http_code, std::string* http_body); -typedef int (*func_modify_printer_name)(void *agent, std::string dev_id, std::string dev_name); -typedef int (*func_get_camera_url)(void *agent, std::string dev_id, std::function callback); -typedef int (*func_get_design_staffpick)(void *agent, int offset, int limit, std::function callback); -typedef int (*func_start_pubilsh)(void *agent, PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out); -typedef int (*func_get_model_publish_url)(void *agent, std::string* url); -typedef int (*func_get_subtask)(void *agent, BBLModelTask* task, OnGetSubTaskFn getsub_fn); -typedef int (*func_get_model_mall_home_url)(void *agent, std::string* url); -typedef int (*func_get_model_mall_detail_url)(void *agent, std::string* url, std::string id); -typedef int (*func_get_my_profile)(void *agent, std::string token, unsigned int *http_code, std::string *http_body); -typedef int (*func_track_enable)(void *agent, bool enable); -typedef int (*func_track_remove_files)(void *agent); -typedef int (*func_track_event)(void *agent, std::string evt_key, std::string content); -typedef int (*func_track_header)(void *agent, std::string header); -typedef int (*func_track_update_property)(void *agent, std::string name, std::string value, std::string type); -typedef int (*func_track_get_property)(void *agent, std::string name, std::string& value, std::string type); -typedef int (*func_put_model_mall_rating_url)( - void *agent, int rating_id, int score, std::string content, std::vector images, unsigned int &http_code, std::string &http_error); -typedef int (*func_get_oss_config)(void *agent, std::string &config, std::string country_code, unsigned int &http_code, std::string &http_error); -typedef int (*func_put_rating_picture_oss)( - void *agent, std::string &config, std::string &pic_oss_path, std::string model_id, int profile_id, unsigned int &http_code, std::string &http_error); -typedef int (*func_get_model_mall_rating_result)(void *agent, int job_id, std::string &rating_result, unsigned int &http_code, std::string &http_error); -typedef int (*func_get_mw_user_preference)(void *agent, std::function callback); -typedef int (*func_get_mw_user_4ulist)(void *agent, int seed, int limit, std::function callback); +// Forward declaration +class BBLNetworkPlugin; //the NetworkAgent class class NetworkAgent { public: + // Static utility methods - delegate to BBLNetworkPlugin static std::string get_libpath_in_current_directory(std::string library_name); static std::string get_versioned_library_path(const std::string& version); static bool versioned_library_exists(const std::string& version); @@ -136,9 +40,24 @@ public: static NetworkLibraryLoadError get_load_error(); static void clear_load_error(); static void set_load_error(const std::string& message, const std::string& technical_details, const std::string& attempted_path); + + // Traditional constructor (uses BBL DLL via singleton) NetworkAgent(std::string log_dir); + + // Sub-agent composition constructor (uses injected sub-agents) + NetworkAgent(std::shared_ptr cloud_agent, + std::shared_ptr printer_agent); + ~NetworkAgent(); + // Sub-agent accessors + std::shared_ptr get_cloud_agent() const { return m_cloud_agent; } + std::shared_ptr get_printer_agent() const { return m_printer_agent; } + + // Set the printer agent (for dynamic agent switching) + void set_printer_agent(std::shared_ptr printer_agent); + + // Instance methods - delegate to sub-agents or BBLNetworkPlugin int init_log(); int set_config_dir(std::string config_dir); int set_cert_file(std::string folder, std::string filename); @@ -177,7 +96,7 @@ public: std::string get_user_id(); std::string get_user_name(); std::string get_user_avatar(); - std::string get_user_nickanme(); + std::string get_user_nickname(); std::string build_login_cmd(); std::string build_logout_cmd(); std::string build_login_info(); @@ -186,7 +105,8 @@ public: int set_server_callback(OnServerErrFn fn); int bind(std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn); int unbind(std::string dev_id); - std::string get_bambulab_host(); + std::string get_cloud_service_host(); + std::string get_cloud_login_url(const std::string& language = ""); std::string get_user_selected_machine(); int set_user_selected_machine(std::string dev_id); int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); @@ -194,6 +114,8 @@ public: int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); + FilamentSyncMode get_filament_sync_mode() const; + void fetch_filament_info(std::string dev_id); int get_user_presets(std::map>* user_presets); std::string request_setting_id(std::string name, std::map* values_map, unsigned int* http_code); int put_setting(std::string setting_id, std::string name, std::map* values_map, unsigned int* http_code); @@ -236,117 +158,36 @@ public: int get_mw_user_preference(std::function callback); int get_mw_user_4ulist(int seed, int limit, std::function callback); - void *get_network_agent() { return network_agent; } + + // Get underlying agent handle from BBLNetworkPlugin + void* get_network_agent(); private: + struct PrinterCallbacks { + OnMsgArrivedFn on_ssdp_msg_fn = nullptr; + OnPrinterConnectedFn on_printer_connected_fn = nullptr; + GetSubscribeFailureFn on_subscribe_failure_fn = nullptr; + OnMessageFn on_message_fn = nullptr; + OnMessageFn on_user_message_fn = nullptr; + OnLocalConnectedFn on_local_connect_fn = nullptr; + OnMessageFn on_local_message_fn = nullptr; + QueueOnMainFn queue_on_main_fn = nullptr; + OnServerErrFn on_server_err_fn = nullptr; + }; + + void apply_printer_callbacks(const std::shared_ptr& printer_agent, + const PrinterCallbacks& callbacks); + + mutable std::mutex m_agent_mutex; // Protect agent swapping + PrinterCallbacks m_printer_callbacks; bool enable_track = false; - void* network_agent { nullptr }; - static NetworkLibraryLoadError s_load_error; - - static func_check_debug_consistent check_debug_consistent_ptr; - static func_get_version get_version_ptr; - static func_create_agent create_agent_ptr; - static func_destroy_agent destroy_agent_ptr; - static func_init_log init_log_ptr; - static func_set_config_dir set_config_dir_ptr; - static func_set_cert_file set_cert_file_ptr; - static func_set_country_code set_country_code_ptr; - static func_start start_ptr; - static func_set_on_ssdp_msg_fn set_on_ssdp_msg_fn_ptr; - static func_set_on_user_login_fn set_on_user_login_fn_ptr; - static func_set_on_printer_connected_fn set_on_printer_connected_fn_ptr; - static func_set_on_server_connected_fn set_on_server_connected_fn_ptr; - static func_set_on_http_error_fn set_on_http_error_fn_ptr; - static func_set_get_country_code_fn set_get_country_code_fn_ptr; - static func_set_on_subscribe_failure_fn set_on_subscribe_failure_fn_ptr; - static func_set_on_message_fn set_on_message_fn_ptr; - static func_set_on_user_message_fn set_on_user_message_fn_ptr; - static func_set_on_local_connect_fn set_on_local_connect_fn_ptr; - static func_set_on_local_message_fn set_on_local_message_fn_ptr; - static func_set_queue_on_main_fn set_queue_on_main_fn_ptr; - static func_connect_server connect_server_ptr; - static func_is_server_connected is_server_connected_ptr; - static func_refresh_connection refresh_connection_ptr; - static func_start_subscribe start_subscribe_ptr; - static func_stop_subscribe stop_subscribe_ptr; - static func_add_subscribe add_subscribe_ptr; - static func_del_subscribe del_subscribe_ptr; - static func_enable_multi_machine enable_multi_machine_ptr; - static func_send_message send_message_ptr; - static func_connect_printer connect_printer_ptr; - static func_disconnect_printer disconnect_printer_ptr; - static func_send_message_to_printer send_message_to_printer_ptr; - static func_check_cert check_cert_ptr; - static func_install_device_cert install_device_cert_ptr; - static func_start_discovery start_discovery_ptr; - static func_change_user change_user_ptr; - static func_is_user_login is_user_login_ptr; - static func_user_logout user_logout_ptr; - static func_get_user_id get_user_id_ptr; - static func_get_user_name get_user_name_ptr; - static func_get_user_avatar get_user_avatar_ptr; - static func_get_user_nickanme get_user_nickanme_ptr; - static func_build_login_cmd build_login_cmd_ptr; - static func_build_logout_cmd build_logout_cmd_ptr; - static func_build_login_info build_login_info_ptr; - static func_ping_bind ping_bind_ptr; - static func_bind_detect bind_detect_ptr; - static func_set_server_callback set_server_callback_ptr; - static func_bind bind_ptr; - static func_unbind unbind_ptr; - static func_get_bambulab_host get_bambulab_host_ptr; - static func_get_user_selected_machine get_user_selected_machine_ptr; - static func_set_user_selected_machine set_user_selected_machine_ptr; - static func_start_print start_print_ptr; - static func_start_local_print_with_record start_local_print_with_record_ptr; - static func_start_send_gcode_to_sdcard start_send_gcode_to_sdcard_ptr; - static func_start_local_print start_local_print_ptr; - static func_start_sdcard_print start_sdcard_print_ptr; - static func_get_user_presets get_user_presets_ptr; - static func_request_setting_id request_setting_id_ptr; - static func_put_setting put_setting_ptr; - static func_get_setting_list get_setting_list_ptr; - static func_get_setting_list2 get_setting_list2_ptr; - static func_delete_setting delete_setting_ptr; - static func_get_studio_info_url get_studio_info_url_ptr; - static func_set_extra_http_header set_extra_http_header_ptr; - static func_get_my_message get_my_message_ptr; - static func_check_user_task_report check_user_task_report_ptr; - static func_get_user_print_info get_user_print_info_ptr; - static func_get_user_tasks get_user_tasks_ptr; - static func_get_printer_firmware get_printer_firmware_ptr; - static func_get_task_plate_index get_task_plate_index_ptr; - static func_get_user_info get_user_info_ptr; - static func_request_bind_ticket request_bind_ticket_ptr; - static func_get_subtask_info get_subtask_info_ptr; - static func_get_slice_info get_slice_info_ptr; - static func_query_bind_status query_bind_status_ptr; - static func_modify_printer_name modify_printer_name_ptr; - static func_get_camera_url get_camera_url_ptr; - static func_get_design_staffpick get_design_staffpick_ptr; - static func_start_pubilsh start_publish_ptr; - static func_get_model_publish_url get_model_publish_url_ptr; - static func_get_subtask get_subtask_ptr; - static func_get_model_mall_home_url get_model_mall_home_url_ptr; - static func_get_model_mall_detail_url get_model_mall_detail_url_ptr; - static func_get_my_profile get_my_profile_ptr; - static func_track_enable track_enable_ptr; - static func_track_remove_files track_remove_files_ptr; - static func_track_event track_event_ptr; - static func_track_header track_header_ptr; - static func_track_update_property track_update_property_ptr; - static func_track_get_property track_get_property_ptr; - static func_put_model_mall_rating_url put_model_mall_rating_url_ptr; - static func_get_oss_config get_oss_config_ptr; - static func_put_rating_picture_oss put_rating_picture_oss_ptr; - static func_get_model_mall_rating_result get_model_mall_rating_result_ptr; - - static func_get_mw_user_preference get_mw_user_preference_ptr; - static func_get_mw_user_4ulist get_mw_user_4ulist_ptr; + // Sub-agent composition (for Orca/BBL mixed mode) + std::shared_ptr m_cloud_agent; + std::shared_ptr m_printer_agent; + std::string m_printer_agent_id; }; } #endif - diff --git a/src/slic3r/Utils/NetworkAgentFactory.cpp b/src/slic3r/Utils/NetworkAgentFactory.cpp new file mode 100644 index 0000000000..fe65f6fc75 --- /dev/null +++ b/src/slic3r/Utils/NetworkAgentFactory.cpp @@ -0,0 +1,176 @@ +#include "NetworkAgentFactory.hpp" +#include "IPrinterAgent.hpp" +#include "ICloudServiceAgent.hpp" +#include "BBLPrinterAgent.hpp" +#include "OrcaPrinterAgent.hpp" +#include "QidiPrinterAgent.hpp" +#include "MoonrakerPrinterAgent.hpp" +#include + +namespace Slic3r { +namespace { + +static std::mutex s_registry_mutex; + +std::map& get_printer_agents() +{ + static std::map agents; + return agents; +} + +std::string& get_default_agent_id() +{ + static std::string default_id; + return default_id; +} + +} // anonymous namespace + +bool NetworkAgentFactory::register_printer_agent(const std::string& id, const std::string& display_name, PrinterAgentFactory factory) +{ + std::lock_guard lock(s_registry_mutex); + auto& agents = get_printer_agents(); + + auto result = agents.emplace(id, PrinterAgentInfo(id, display_name, std::move(factory))); + + if (result.second) { + BOOST_LOG_TRIVIAL(info) << "Registered printer agent: " << id << " (" << display_name << ")"; + + // Set as default if it's the first agent registered + auto& default_id = get_default_agent_id(); + if (default_id.empty()) { + default_id = id; + } + return true; + } else { + BOOST_LOG_TRIVIAL(warning) << "Printer agent already registered: " << id; + return false; + } +} + +bool NetworkAgentFactory::is_printer_agent_registered(const std::string& id) +{ + std::lock_guard lock(s_registry_mutex); + auto& agents = get_printer_agents(); + return agents.find(id) != agents.end(); +} + +const PrinterAgentInfo* NetworkAgentFactory::get_printer_agent_info(const std::string& id) +{ + std::lock_guard lock(s_registry_mutex); + auto& agents = get_printer_agents(); + auto it = agents.find(id); + return (it != agents.end()) ? &it->second : nullptr; +} + +std::vector NetworkAgentFactory::get_registered_printer_agents() +{ + std::lock_guard lock(s_registry_mutex); + auto& agents = get_printer_agents(); + std::vector result; + result.reserve(agents.size()); + + for (const auto& pair : agents) { + result.push_back(pair.second); + } + + return result; +} + +std::shared_ptr NetworkAgentFactory::create_printer_agent_by_id(const std::string& id, + std::shared_ptr cloud_agent, + const std::string& log_dir) +{ + std::lock_guard lock(s_registry_mutex); + auto& agents = get_printer_agents(); + auto it = agents.find(id); + + if (it == agents.end()) { + BOOST_LOG_TRIVIAL(warning) << "Unknown printer agent ID: " << id; + return nullptr; + } + + return it->second.factory(cloud_agent, log_dir); +} + +std::string NetworkAgentFactory::get_default_printer_agent_id() +{ + std::lock_guard lock(s_registry_mutex); + return get_default_agent_id(); +} + +void NetworkAgentFactory::set_default_printer_agent_id(const std::string& id) +{ + std::lock_guard lock(s_registry_mutex); + auto& agents = get_printer_agents(); + + if (agents.find(id) != agents.end()) { + get_default_agent_id() = id; + BOOST_LOG_TRIVIAL(info) << "Default printer agent set to: " << id; + } else { + BOOST_LOG_TRIVIAL(warning) << "Cannot set default to unregistered agent: " << id; + } +} + +void NetworkAgentFactory::register_all_agents() +{ + // Register Orca printer agent + { + auto info = OrcaPrinterAgent::get_agent_info_static(); + register_printer_agent(info.id, info.name, + [](std::shared_ptr cloud_agent, + const std::string& log_dir) -> std::shared_ptr { + auto agent = std::make_shared(log_dir); + if (cloud_agent) { + agent->set_cloud_agent(cloud_agent); + } + return agent; + }); + } + + // Register Qidi printer agent + { + auto info = QidiPrinterAgent::get_agent_info_static(); + register_printer_agent(info.id, info.name, + [](std::shared_ptr cloud_agent, + const std::string& log_dir) -> std::shared_ptr { + auto agent = std::make_shared(log_dir); + if (cloud_agent) { + agent->set_cloud_agent(cloud_agent); + } + return agent; + }); + } + + // Register Moonraker printer agent + { + auto info = MoonrakerPrinterAgent::get_agent_info_static(); + register_printer_agent(info.id, info.name, + [](std::shared_ptr cloud_agent, + const std::string& log_dir) -> std::shared_ptr { + auto agent = std::make_shared(log_dir); + if (cloud_agent) { + agent->set_cloud_agent(cloud_agent); + } + return agent; + }); + } + + // Register BBL printer agent (only if bbl network agent is available) + { + auto info = BBLPrinterAgent::get_agent_info_static(); + register_printer_agent(info.id, info.name, + [](std::shared_ptr cloud_agent, + const std::string& log_dir) -> std::shared_ptr { + auto agent = std::make_shared(); + if (cloud_agent) { + agent->set_cloud_agent(cloud_agent); + } + return agent; + }); + } + + BOOST_LOG_TRIVIAL(info) << "Registered " << get_printer_agents().size() << " printer agents"; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/NetworkAgentFactory.hpp b/src/slic3r/Utils/NetworkAgentFactory.hpp new file mode 100644 index 0000000000..121b8dec27 --- /dev/null +++ b/src/slic3r/Utils/NetworkAgentFactory.hpp @@ -0,0 +1,250 @@ +#ifndef __NETWORK_AGENT_FACTORY_HPP__ +#define __NETWORK_AGENT_FACTORY_HPP__ + +#include "ICloudServiceAgent.hpp" +#include "IPrinterAgent.hpp" +#include "NetworkAgent.hpp" +#include "OrcaCloudServiceAgent.hpp" +#include "BBLCloudServiceAgent.hpp" +#include "BBLNetworkPlugin.hpp" +#include "libslic3r/AppConfig.hpp" +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +// Forward declarations +class ICloudServiceAgent; +class IPrinterAgent; + +/** + * AgentProvider - Specifies which implementation to use for each agent type. + * + * - Orca: Native Orca implementations (OrcaCloudServiceAgent, OrcaPrinterAgent) + * - BBL: BBL DLL wrapper implementations (BBLCloudServiceAgent, BBLPrinterAgent) + */ +enum class AgentProvider { Orca, BBL }; + +// Factory function type for creating printer agents +using PrinterAgentFactory = + std::function(std::shared_ptr cloud_agent, const std::string& log_dir)>; + +// Information about a registered printer agent +struct PrinterAgentInfo +{ + std::string id; // e.g., "orca", "bbl" + std::string display_name; // e.g., "Orca Native", "Bambu Lab" + PrinterAgentFactory factory; // Function to create the agent + + PrinterAgentInfo(const std::string& id_, const std::string& display_name_, PrinterAgentFactory factory_) + : id(id_), display_name(display_name_), factory(std::move(factory_)) + {} +}; + +/** + * NetworkAgentFactory - Factory for creating network agent instances + * + * This factory creates cloud agents and printer agents for the networking subsystem. + * The architecture separates cloud services (authentication, project sync) from + * printer communication (device discovery, print jobs). + * + * Startup flow: + * 1. Call register_all_agents() during app initialization + * 2. Cloud agent created at startup via create_agent_from_config() + * 3. Printer agent created on-demand when a printer is selected + * + * Usage: + * // At app startup (before any agent creation) + * NetworkAgentFactory::register_all_agents(); + * + * // Create NetworkAgent with cloud agent only + * auto agent = create_agent_from_config(log_dir, app_config); + * + * // When printer is selected - create printer agent from registry + * auto printer = NetworkAgentFactory::create_printer_agent_by_id("orca", cloud, log_dir); + */ +class NetworkAgentFactory +{ +public: + // ======================================================================== + // Printer Agent Registry + // ======================================================================== + + /** + * Register all built-in printer agents. + * Must be called once during application initialization, before any + * calls to get_registered_printer_agents() or create_printer_agent_by_id(). + */ + static void register_all_agents(); + + /** + * Register a printer agent type + * + * @param id Unique identifier for the agent (e.g., "orca", "bbl") + * @param display_name Human-readable name for UI + * @param factory Factory function to create the agent + * @return true if registration succeeded, false if already registered + */ + static bool register_printer_agent(const std::string& id, const std::string& display_name, PrinterAgentFactory factory); + + /** + * Check if an agent ID is registered + */ + static bool is_printer_agent_registered(const std::string& id); + + /** + * Get info about a registered agent + */ + static const PrinterAgentInfo* get_printer_agent_info(const std::string& id); + + /** + * Get all registered printer agents (for UI population) + */ + static std::vector get_registered_printer_agents(); + + /** + * Create a printer agent by ID (using registry) + * + * @param id Agent ID to create + * @param cloud_agent Cloud agent for token access + * @param log_dir Directory for log files + * @return Shared pointer to IPrinterAgent, or nullptr if ID not found + */ + static std::shared_ptr create_printer_agent_by_id(const std::string& id, + std::shared_ptr cloud_agent, + const std::string& log_dir); + + /** + * Get default printer agent ID + */ + static std::string get_default_printer_agent_id(); + + /** + * Set a specific agent as the default + */ + static void set_default_printer_agent_id(const std::string& id); + + // ======================================================================== + // Cloud Agent Factory + // ======================================================================== + + /** + * Create a cloud service agent based on provider type. + * Handles authentication, project sync, and other cloud services. + * + * @param provider Which implementation to use (Orca or BBL) + * @param log_dir Directory for log files + * @return Shared pointer to ICloudServiceAgent implementation + */ + static std::shared_ptr create_cloud_agent(AgentProvider provider, const std::string& log_dir) + { + switch (provider) { + case AgentProvider::Orca: return std::make_shared(log_dir); + case AgentProvider::BBL: { + auto& plugin = BBLNetworkPlugin::instance(); + if (!plugin.is_loaded()) { + return nullptr; + } + if (!plugin.has_agent()) { + plugin.create_agent(log_dir); + } + if (!plugin.has_agent()) { + return nullptr; + } + return std::make_shared(); + } + default: return nullptr; + } + } + + // ======================================================================== + // NetworkAgent Facade Creation + // ======================================================================== + + /** + * Create a NetworkAgent from pre-created sub-agents + * + * @param cloud_agent Cloud service agent (required, includes auth) + * @param printer_agent Printer agent (optional, can be nullptr) + * @return Unique pointer to NetworkAgent facade + */ + static std::unique_ptr create_from_agents(std::shared_ptr cloud_agent, + std::shared_ptr printer_agent) + { + return std::make_unique(std::move(cloud_agent), std::move(printer_agent)); + } + +private: + // Factory is not instantiable + NetworkAgentFactory() = delete; + ~NetworkAgentFactory() = delete; + NetworkAgentFactory(const NetworkAgentFactory&) = delete; + NetworkAgentFactory& operator=(const NetworkAgentFactory&) = delete; +}; + +/** + * Create a NetworkAgent from AppConfig settings (main entry point) + * + * Creates a NetworkAgent with cloud agent only. The printer agent is created + * separately when a printer is selected, via create_printer_agent_by_id(). + * + * Cloud provider selection: + * - use_orca_cloud=true → OrcaCloudServiceAgent (default) + * - use_orca_cloud=false → BBLCloudServiceAgent (requires plugin) + * + * @param log_dir Directory for log files + * @param app_config Application configuration object + * @return NetworkAgent with cloud agent, or nullptr on failure + */ +inline std::unique_ptr create_agent_from_config(const std::string& log_dir, AppConfig* app_config) +{ + // Determine cloud provider from config + bool use_orca_cloud = false; + if (app_config) { + try { + use_orca_cloud = app_config->get("use_orca_cloud") == "true" || app_config->get_bool("use_orca_cloud"); + } catch (...) { + use_orca_cloud = false; + } + } + + // Create cloud agent + AgentProvider provider = use_orca_cloud ? AgentProvider::Orca : AgentProvider::BBL; + auto cloud_agent = NetworkAgentFactory::create_cloud_agent(provider, log_dir); + + // Fall back to Orca if BBL plugin not available + if (!cloud_agent && provider == AgentProvider::BBL) { + BOOST_LOG_TRIVIAL(warning) << "BBL plugin not loaded, falling back to Orca cloud agent"; + cloud_agent = NetworkAgentFactory::create_cloud_agent(AgentProvider::Orca, log_dir); + } + + if (!cloud_agent) { + BOOST_LOG_TRIVIAL(error) << "Failed to create cloud agent"; + return nullptr; + } + + // auto bbl_printer_agent = NetworkAgentFactory::create_printer_agent_by_id("bbl", cloud_agent, log_dir); + + // Create NetworkAgent with cloud agent only (printer agent added later) + // We will create the printer agent later when the printer is selected, so we pass nullptr for the printer agent here. + auto agent = NetworkAgentFactory::create_from_agents(std::move(cloud_agent), nullptr); + + // Configure URL overrides for Orca cloud + if (agent && app_config && use_orca_cloud) { + auto* orca_cloud = dynamic_cast(agent->get_cloud_agent().get()); + if (orca_cloud) { + orca_cloud->configure_urls(app_config); + } + } + + BOOST_LOG_TRIVIAL(info) << "Created NetworkAgent with cloud agent"; + return agent; +} + +} // namespace Slic3r + +#endif // __NETWORK_AGENT_FACTORY_HPP__ diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp new file mode 100644 index 0000000000..6ed3bab66c --- /dev/null +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp @@ -0,0 +1,2676 @@ +#include "OrcaCloudServiceAgent.hpp" +#include "Http.hpp" +#include "slic3r/Utils/InstanceID.hpp" +#include "libslic3r/Utils.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "libslic3r/AppConfig.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#endif + +#if defined(__APPLE__) +#include +#endif + +namespace pt = boost::property_tree; + +namespace Slic3r { + +namespace { +constexpr const char* ORCA_DEFAULT_API_URL = "https://api.orcaslicer.com"; +constexpr const char* ORCA_DEFAULT_AUTH_URL = "https://auth.orcaslicer.com"; +constexpr const char* ORCA_DEFAULT_PUB_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; +constexpr const char* ORCA_HEALTH_PATH = "/api/v1/health"; +constexpr const char* ORCA_SYNC_PULL_PATH = "/api/v1/sync/pull"; +constexpr const char* ORCA_SYNC_PUSH_PATH = "/api/v1/sync/push"; +constexpr const char* ORCA_PROFILES_PATH = "/api/v1/profiles"; +constexpr const char* ORCA_SYNC_STATE_FILE = "sync_state"; + +constexpr const char* CONFIG_ORCA_API_URL = "orca_api_url"; +constexpr const char* CONFIG_ORCA_AUTH_URL = "orca_auth_url"; +constexpr const char* CONFIG_ORCA_PUB_KEY = "orca_pub_key"; + +constexpr const char* SECRET_STORE_SERVICE = "OrcaSlicer/Auth"; +constexpr const char* SECRET_STORE_USER = "orca_refresh_token"; +constexpr std::chrono::seconds TOKEN_REFRESH_SKEW{900}; // 15 minutes + +std::string generate_uuid(const std::string& name = "") +{ + if (name.empty()) { + return ""; + } + + // Use a fixed namespace UUID for OrcaSlicer profiles + // This ensures the same name always generates the same UUID + static const boost::uuids::uuid orca_namespace = + boost::uuids::string_generator()("f47ac10b-58cc-4372-a567-0e02b2c3d479"); + + boost::uuids::name_generator_sha1 gen(orca_namespace); + boost::uuids::uuid id = gen(name); + return boost::uuids::to_string(id); +} + +std::string base64url_encode(const std::vector& data) +{ + std::string out; + out.resize(boost::beast::detail::base64::encoded_size(data.size())); + out.resize(boost::beast::detail::base64::encode(out.data(), data.data(), data.size())); + + std::replace(out.begin(), out.end(), '+', '-'); + std::replace(out.begin(), out.end(), '/', '_'); + out.erase(std::remove(out.begin(), out.end(), '='), out.end()); + return out; +} + +bool base64url_decode(const std::string& input, std::vector& out) +{ + std::string padded = input; + while (padded.size() % 4 != 0) padded.push_back('='); + std::string normalized = padded; + std::replace(normalized.begin(), normalized.end(), '-', '+'); + std::replace(normalized.begin(), normalized.end(), '_', '/'); + + out.resize(boost::beast::detail::base64::decoded_size(normalized.size())); + auto res = boost::beast::detail::base64::decode(out.data(), normalized.data(), normalized.size()); + if (!res.second) return false; + out.resize(res.first); + return true; +} + +std::vector random_bytes(size_t len) +{ + std::vector bytes(len); + if (RAND_bytes(bytes.data(), static_cast(len)) != 1) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution dist(0, 255); + for (auto& b : bytes) b = static_cast(dist(gen)); + } + return bytes; +} + +std::string generate_code_verifier() +{ + constexpr int PKCE_VERIFIER_BYTES = 32; + auto bytes = random_bytes(PKCE_VERIFIER_BYTES); + return base64url_encode(bytes); +} + +std::string generate_state_token() +{ + auto bytes = random_bytes(16); + std::stringstream ss; + for (auto b : bytes) { + ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(b); + } + return ss.str(); +} + +std::string sha256_base64url(const std::string& input) +{ + unsigned char hash[SHA256_DIGEST_LENGTH]; + SHA256(reinterpret_cast(input.data()), input.size(), hash); + std::vector hash_vec(hash, hash + sizeof(hash)); + return base64url_encode(hash_vec); +} + +std::string machine_identifier() +{ + if (auto* cfg = Slic3r::GUI::wxGetApp().app_config) { + const auto iid = Slic3r::instance_id::ensure(*cfg); + if (!iid.empty()) return iid; + } + +#if defined(__linux__) + std::ifstream f("/etc/machine-id"); + std::string id; + if (f.good()) { + std::getline(f, id); + } + if (!id.empty()) return id; +#elif defined(_WIN32) + char buffer[MAX_COMPUTERNAME_LENGTH + 1] = {0}; + DWORD size = MAX_COMPUTERNAME_LENGTH + 1; + if (GetComputerNameA(buffer, &size)) { + return std::string(buffer, size); + } +#elif defined(__APPLE__) + char uuid_str[128] = {0}; + size_t len = sizeof(uuid_str); + if (sysctlbyname("kern.uuid", uuid_str, &len, nullptr, 0) == 0 && len > 0) { + return std::string(uuid_str, len - 1); + } +#endif + return wxGetUserId().ToStdString() + "@" + wxGetHostName().ToStdString(); +} + +std::vector sha256_bytes(const std::string& input) +{ + std::vector out(SHA256_DIGEST_LENGTH, 0); + SHA256(reinterpret_cast(input.data()), input.size(), out.data()); + return out; +} + +std::string hmac_sha256_hex(const std::string& data, const std::vector& key) +{ + unsigned int len = 0; + unsigned char result[EVP_MAX_MD_SIZE]; + if (HMAC(EVP_sha256(), key.data(), static_cast(key.size()), + reinterpret_cast(data.data()), data.size(), result, &len) == nullptr) { + return {}; + } + + std::ostringstream oss; + oss << std::hex << std::setfill('0'); + for (unsigned int i = 0; i < len; ++i) { + oss << std::setw(2) << static_cast(result[i]); + } + return oss.str(); +} + +bool is_port_available(int port) +{ + if (port <= 0 || port > 65535) return false; + + using boost::asio::ip::tcp; + boost::asio::io_context ctx; + boost::system::error_code ec; + + tcp::acceptor acceptor(ctx); + tcp::endpoint endpoint(tcp::v4(), static_cast(port)); + + acceptor.open(endpoint.protocol(), ec); + if (ec) return false; + acceptor.set_option(tcp::acceptor::reuse_address(true), ec); + if (ec) return false; + acceptor.bind(endpoint, ec); + if (ec) return false; + acceptor.close(ec); + return true; +} + +int choose_loopback_port() +{ + int base_port = auth_constants::LOOPBACK_PORT; + + if (const char* env_port = std::getenv("ORCA_LOOPBACK_PORT")) { + try { + int parsed = std::stoi(env_port); + if (parsed > 0 && parsed <= 65535) { + base_port = parsed; + } + } catch (...) { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: invalid ORCA_LOOPBACK_PORT value, falling back to default"; + } + } + + std::vector candidates = {base_port, base_port + 1, base_port + 2}; + for (int port : candidates) { + if (is_port_available(port)) return port; + } + + return base_port; +} + +bool aes256gcm_encrypt(const std::string& plaintext, const std::vector& key, std::string& out_b64) +{ + const int iv_len = 12; + auto iv = random_bytes(iv_len); + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return false; + + bool ok = true; + int len = 0; + std::vector ciphertext(plaintext.size()); + std::vector tag(16); + + if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) != 1) ok = false; + if (ok && EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, nullptr) != 1) ok = false; + if (ok && EVP_EncryptInit_ex(ctx, nullptr, nullptr, key.data(), iv.data()) != 1) ok = false; + if (ok && EVP_EncryptUpdate(ctx, ciphertext.data(), &len, + reinterpret_cast(plaintext.data()), plaintext.size()) != 1) ok = false; + int ciphertext_len = len; + if (ok && EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len) != 1) ok = false; + ciphertext_len += len; + if (ok && EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, tag.size(), tag.data()) != 1) ok = false; + + EVP_CIPHER_CTX_free(ctx); + + if (!ok) return false; + ciphertext.resize(ciphertext_len); + + std::vector payload; + payload.reserve(iv.size() + tag.size() + ciphertext.size()); + payload.insert(payload.end(), iv.begin(), iv.end()); + payload.insert(payload.end(), tag.begin(), tag.end()); + payload.insert(payload.end(), ciphertext.begin(), ciphertext.end()); + + out_b64 = base64url_encode(payload); + return true; +} + +bool aes256gcm_decrypt(const std::string& b64_payload, const std::vector& key, std::string& plaintext) +{ + std::vector payload; + if (!base64url_decode(b64_payload, payload)) return false; + if (payload.size() < 12 + 16) return false; + + const size_t iv_len = 12; + const size_t tag_len = 16; + std::vector iv(payload.begin(), payload.begin() + iv_len); + std::vector tag(payload.begin() + iv_len, payload.begin() + iv_len + tag_len); + std::vector ciphertext(payload.begin() + iv_len + tag_len, payload.end()); + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return false; + + bool ok = true; + int len = 0; + std::vector plain(ciphertext.size()); + + if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) != 1) ok = false; + if (ok && EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, nullptr) != 1) ok = false; + if (ok && EVP_DecryptInit_ex(ctx, nullptr, nullptr, key.data(), iv.data()) != 1) ok = false; + if (ok && EVP_DecryptUpdate(ctx, plain.data(), &len, ciphertext.data(), ciphertext.size()) != 1) ok = false; + int plain_len = len; + if (ok && EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, tag.size(), tag.data()) != 1) ok = false; + if (ok && EVP_DecryptFinal_ex(ctx, plain.data() + len, &len) != 1) ok = false; + plain_len += len; + + EVP_CIPHER_CTX_free(ctx); + + if (!ok) return false; + plain.resize(plain_len); + plaintext.assign(reinterpret_cast(plain.data()), plain.size()); + return true; +} + +} // namespace + +// ============================================================================ +// Constructor / Destructor +// ============================================================================ + +OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir) + : log_dir(std::move(log_dir)) + , api_base_url(ORCA_DEFAULT_API_URL) + , auth_base_url(ORCA_DEFAULT_AUTH_URL) +{ + auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY; + pkce_bundle.loopback_port = choose_loopback_port(); + update_redirect_uri(); + regenerate_pkce(); + compute_fallback_path(); + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Constructor - log_dir=" << this->log_dir; +} + +OrcaCloudServiceAgent::~OrcaCloudServiceAgent() +{ + if (refresh_thread.joinable()) { + refresh_thread.join(); + } +} + +void OrcaCloudServiceAgent::configure_urls(AppConfig* app_config) +{ + if (!app_config) return; + + // Read token storage preference + m_use_encrypted_token_file = app_config->get_bool(SETTING_USE_ENCRYPTED_TOKEN_FILE); + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: token storage mode set to " + << (m_use_encrypted_token_file ? "encrypted file" : "System Keychain"); + + std::string api_url = app_config->get(CONFIG_ORCA_API_URL); + if (!api_url.empty()) { + api_base_url = api_url; + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Using custom API URL: " << api_base_url; + } + + std::string auth_url = app_config->get(CONFIG_ORCA_AUTH_URL); + if (!auth_url.empty()) { + auth_base_url = auth_url; + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Using custom Auth URL: " << auth_base_url; + } + + std::string pub_key = app_config->get(CONFIG_ORCA_PUB_KEY); + if (!pub_key.empty()) { + auth_headers["apikey"] = pub_key; + } +} + +void OrcaCloudServiceAgent::set_api_base_url(const std::string& url) +{ + api_base_url = url; +} + +void OrcaCloudServiceAgent::set_auth_base_url(const std::string& url) +{ + auth_base_url = url; +} + +void OrcaCloudServiceAgent::set_use_encrypted_token_file(bool use) +{ + m_use_encrypted_token_file = use; + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: token storage mode set to " + << (m_use_encrypted_token_file ? "encrypted file" : "System Keychain"); +} + +bool OrcaCloudServiceAgent::get_use_encrypted_token_file() const +{ + return m_use_encrypted_token_file; +} + +// ============================================================================ +// ICloudServiceAgent - Lifecycle Methods +// ============================================================================ + +int OrcaCloudServiceAgent::init_log() +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: init_log called"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::set_config_dir(std::string cfg_dir) +{ + config_dir = cfg_dir; + wxFileName fallback(wxString::FromUTF8(cfg_dir.c_str()), "orca_refresh_token.sec"); + fallback.Normalize(); + refresh_fallback_path = fallback.GetFullPath().ToStdString(); + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: set_config_dir - " << cfg_dir; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::set_cert_file(std::string folder, std::string filename) +{ + // Not used by OrcaCloudServiceAgent (OAuth doesn't need client certs) + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: set_cert_file called (unused) - " << folder << "/" << filename; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::set_country_code(std::string code) +{ + country_code = code; + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: set_country_code - " << code; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::start() +{ + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: start called"; + + // Regenerate PKCE bundle for fresh login attempts + regenerate_pkce(); + + // Attempt silent sign-in from stored refresh token + std::string stored_refresh; + if (load_refresh_token(stored_refresh) && !stored_refresh.empty()) { + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Found stored refresh token, attempting silent sign-in"; + refresh_now(stored_refresh, "refresh token", false); + } + + return BAMBU_NETWORK_SUCCESS; +} + +// ============================================================================ +// ICloudServiceAgent - User Session Management +// ============================================================================ + +bool OrcaCloudServiceAgent::exchange_auth_code(const std::string& auth_code, const std::string& state, std::string& session_payload) +{ + // Validate PKCE state + const auto expected_state = pkce_bundle.state; + if (!expected_state.empty() && state != expected_state) { + BOOST_LOG_TRIVIAL(warning) << "[auth] event=code_exchange result=failure reason=state_mismatch"; + return false; + } + + std::string url = auth_base_url + auth_constants::TOKEN_PATH; + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: exchanging auth code for tokens"; + + std::string redirect_uri; + std::string code_verifier; + std::map headers_copy; + { + std::lock_guard lock(headers_mutex); + headers_copy = extra_headers; + } + { + std::lock_guard lock(state_mutex); + redirect_uri = pkce_bundle.redirect; + code_verifier = pkce_bundle.verifier; + for (const auto& pair : auth_headers) { + headers_copy[pair.first] = pair.second; + } + } + + bool has_apikey = false; + for (const auto& pair : headers_copy) { + if (pair.first == "apikey") { + has_apikey = true; + break; + } + } + if (!has_apikey) { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: exchange_auth_code - apikey header MISSING! Token request may fail."; + } + + std::string response; + unsigned int http_code = 0; + bool success = false; + + try { + auto http = Http::post(url); + + for (const auto& pair : headers_copy) { + http.header(pair.first, pair.second); + } + + http.remove_header("Authorization"); + http.remove_header("Content-Type"); + http.form_add("grant_type", "authorization_code"); + http.form_add("code", auth_code); + http.form_add("redirect_uri", redirect_uri); + http.form_add("code_verifier", code_verifier); + + http.on_complete([&](std::string body, unsigned resp_status) { + success = true; + http_code = resp_status; + response = body; + }) + .on_error([&](std::string body, std::string error, unsigned resp_status) { + success = false; + http_code = resp_status; + response = body; + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error; + }) + .timeout_max(30) + .perform_sync(); + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: exchange_auth_code exception - " << e.what(); + } + + if (!success || http_code >= 400) { + BOOST_LOG_TRIVIAL(error) << "[auth] event=code_exchange result=failure http_code=" << http_code; + return false; + } + + session_payload = response; + BOOST_LOG_TRIVIAL(info) << "[auth] event=code_exchange result=success"; + return true; +} + +int OrcaCloudServiceAgent::change_user(std::string user_info) +{ + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: change_user invoked"; + + try { + std::stringstream ss(user_info); + pt::ptree tree; + pt::read_json(ss, tree); + + auto read_str = [](const pt::ptree& node, const std::string& path) { + return node.get(path, ""); + }; + + // Check if this is a WebView login message (PKCE flow completion) + std::string command = tree.get("command", ""); + if (command == "user_login") { + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Detected WebView login message"; + + auto data_opt = tree.get_child_optional("data"); + if (!data_opt) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: WebView login payload missing data field"; + invoke_user_login_callback(0, false); + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + pt::ptree data = *data_opt; + std::string state = read_str(data, "state"); + + // Check for auth code (PKCE authorization code flow) + std::string auth_code = read_str(data, "code"); + if (!auth_code.empty()) { + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Detected auth code, exchanging for tokens"; + std::string session_payload; + if (!exchange_auth_code(auth_code, state, session_payload)) { + invoke_user_login_callback(0, false); + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + // Recursively process the session payload (contains access_token, user, etc.) + return change_user(session_payload); + } + + // Direct token flow (tokens already obtained by WebView) + std::string token = read_str(data, "token"); + std::string user_id = read_str(data, "user_id"); + std::string username = read_str(data, "username"); + std::string name = read_str(data, "name"); + std::string nickname = read_str(data, "nickname"); + std::string avatar = read_str(data, "avatar"); + std::string refresh_token = read_str(data, "refresh_token"); + + // Validate PKCE state + const auto expected_state = pkce_bundle.state; + if (!expected_state.empty() && state != expected_state) { + BOOST_LOG_TRIVIAL(warning) << "[auth] event=login result=failure reason=state_mismatch"; + invoke_user_login_callback(0, false); + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + if (token.empty() || user_id.empty()) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: WebView login - token or user_id empty"; + invoke_user_login_callback(0, false); + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + bool success = set_user_session(token, user_id, username, name, nickname, avatar, refresh_token); + if (success) { + invoke_user_login_callback(1, true); + if (on_login_complete_handler) { + on_login_complete_handler(true, user_id); + } + } else { + invoke_user_login_callback(0, false); + } + BOOST_LOG_TRIVIAL(info) << "[auth] event=login result=" << (success ? "success" : "failure") + << " source=webview user_id=" << user_id; + return success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + // Orca cloud session payload (default flow) + const pt::ptree* session_node = nullptr; + auto data_opt = tree.get_child_optional("data"); + if (data_opt) { + if (data_opt->get_child_optional("session")) { + session_node = &data_opt->get_child("session"); + } else if (data_opt->get_optional("access_token") || + data_opt->get_optional("token")) { + session_node = &*data_opt; + } + } + if (!session_node) { + if (tree.get_child_optional("session")) { + session_node = &tree.get_child("session"); + } else if (tree.get_optional("access_token") || + tree.get_optional("token")) { + session_node = &tree; + } + } + + if (session_node) { + std::string access_token = read_str(*session_node, "access_token"); + if (access_token.empty()) { + access_token = read_str(*session_node, "token"); + } + std::string refresh_token = read_str(*session_node, "refresh_token"); + std::string user_id = read_str(*session_node, "user.id"); + std::string email = read_str(*session_node, "user.email"); + std::string full_name = read_str(*session_node, "user.user_metadata.full_name"); + std::string preferred_username = read_str(*session_node, "user.user_metadata.preferred_username"); + std::string avatar = read_str(*session_node, "user.user_metadata.avatar_url"); + std::string username = !preferred_username.empty() ? preferred_username : email; + std::string name = !full_name.empty() ? full_name : (!preferred_username.empty() ? preferred_username : email); + std::string nickname = !preferred_username.empty() ? preferred_username : username; + if (nickname.empty()) nickname = name; + if (nickname.empty()) nickname = email; + + if (access_token.empty() || user_id.empty()) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: Orca cloud login payload missing access_token or user.id"; + invoke_user_login_callback(0, false); + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Orca cloud login successful - user_id=" << user_id; + bool success = set_user_session(access_token, user_id, username, name, nickname, avatar, refresh_token); + if (success) { + invoke_user_login_callback(1, true); + if (on_login_complete_handler) { + on_login_complete_handler(true, user_id); + } + } else { + invoke_user_login_callback(0, false); + } + BOOST_LOG_TRIVIAL(info) << "[auth] event=login result=" << (success ? "success" : "failure") + << " source=session user_id=" << user_id; + return success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: Username/password login is disabled. Use the Orca cloud PKCE flow."; + invoke_user_login_callback(0, false); + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: change_user exception - " << e.what(); + invoke_user_login_callback(0, false); + return BAMBU_NETWORK_ERR_INVALID_RESULT; + } +} + +bool OrcaCloudServiceAgent::is_user_login() +{ + std::lock_guard lock(session_mutex); + return session.logged_in; +} + +int OrcaCloudServiceAgent::user_logout(bool request) +{ + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: user_logout - request=" << request; + + // Send logout request to backend if requested + if (request) { + std::string token; + std::string refresh_copy; + { + std::lock_guard lock(session_mutex); + token = session.access_token; + refresh_copy = session.refresh_token; + } + + if (!token.empty()) { + std::string response; + unsigned int http_code = 0; + pt::ptree logout_req; + if (!refresh_copy.empty()) { + logout_req.put("refresh_token", refresh_copy); + } + std::stringstream body_ss; + if (!logout_req.empty()) { + pt::write_json(body_ss, logout_req); + } else { + body_ss << "{}"; + } + + int result = http_post_auth(auth_constants::LOGOUT_PATH, body_ss.str(), &response, &http_code) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_INVALID_HANDLE; + if (result != BAMBU_NETWORK_SUCCESS || http_code >= 400) { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: Orca cloud logout request failed - http_code=" << http_code; + } + } + } + + // Clear session + clear_session(); + + // Invoke callback + invoke_user_login_callback(0, false); + + BOOST_LOG_TRIVIAL(info) << "[auth] event=logout result=success request=" << request; + + return BAMBU_NETWORK_SUCCESS; +} + +std::string OrcaCloudServiceAgent::get_user_id() +{ + std::lock_guard lock(session_mutex); + return session.user_id; +} + +std::string OrcaCloudServiceAgent::get_user_name() +{ + std::lock_guard lock(session_mutex); + return session.user_name; +} + +std::string OrcaCloudServiceAgent::get_user_avatar() +{ + std::lock_guard lock(session_mutex); + return session.user_avatar; +} + +std::string OrcaCloudServiceAgent::get_user_nickname() +{ + std::lock_guard lock(session_mutex); + return session.user_nickname; +} + +// ============================================================================ +// ICloudServiceAgent - Login UI Support +// ============================================================================ + +std::string OrcaCloudServiceAgent::build_login_cmd() +{ + // When already signed in, emit the homepage payload so the web UI + // can flip to the logged-in state without re-opening the login flow. + if (is_user_login()) { + pt::ptree cmd; + cmd.put("command", "studio_userlogin"); + + pt::ptree data; + std::string display_name = get_user_nickname(); + if (display_name.empty()) { + display_name = get_user_name(); + } + data.put("name", display_name); + data.put("avatar", get_user_avatar()); + cmd.add_child("data", data); + + std::stringstream ss; + pt::write_json(ss, cmd, false); + return ss.str(); + } + + update_redirect_uri(); + regenerate_pkce(); + const auto bundle = pkce(); + + pt::ptree tree; + tree.put("action", "login_config"); + tree.put("backend_url", auth_base_url); + + // Include API key for direct Supabase calls from JavaScript + { + std::lock_guard lock(state_mutex); + auto it = auth_headers.find("apikey"); + if (it != auth_headers.end()) { + tree.put("apikey", it->second); + } + } + + pt::ptree pkce_node; + pkce_node.put("code_challenge", bundle.challenge); + pkce_node.put("code_challenge_method", "S256"); + pkce_node.put("state", bundle.state); + pkce_node.put("redirect_uri", bundle.redirect); + pkce_node.put("code_verifier", bundle.verifier); + pkce_node.put("loopback_port", bundle.loopback_port); + tree.add_child("pkce", pkce_node); + + std::stringstream ss; + pt::write_json(ss, tree, false); + return ss.str(); +} + +std::string OrcaCloudServiceAgent::build_logout_cmd() +{ + pt::ptree tree; + tree.put("action", "logout"); + tree.put("provider", "orca"); + + std::stringstream ss; + pt::write_json(ss, tree); + return ss.str(); +} + +std::string OrcaCloudServiceAgent::build_login_info() +{ + pt::ptree tree; + { + std::lock_guard lock(session_mutex); + tree.put("user_id", session.user_id); + tree.put("user_name", session.user_name); + tree.put("nickname", session.user_nickname); + tree.put("avatar", session.user_avatar); + tree.put("logged_in", session.logged_in); + } + // Do not expose tokens to the WebView + tree.put("access_token", ""); + tree.put("refresh_token", ""); + tree.put("backend_url", api_base_url); + tree.put("auth_url", auth_base_url); + + std::stringstream ss; + pt::write_json(ss, tree); + return ss.str(); +} + +// ============================================================================ +// ICloudServiceAgent - Token Access +// ============================================================================ + +std::string OrcaCloudServiceAgent::get_access_token() const +{ + std::lock_guard lock(session_mutex); + return session.access_token; +} + +std::string OrcaCloudServiceAgent::get_refresh_token() const +{ + std::lock_guard lock(session_mutex); + return session.refresh_token; +} + +bool OrcaCloudServiceAgent::ensure_token_fresh(const std::string& reason) +{ + return refresh_if_expiring(TOKEN_REFRESH_SKEW, reason); +} + +// ============================================================================ +// ICloudServiceAgent - Server Connectivity +// ============================================================================ + +int OrcaCloudServiceAgent::connect_server() +{ + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: connect_server"; + + std::string response; + unsigned int http_code = 0; + int result = http_get(ORCA_HEALTH_PATH, &response, &http_code); + + bool connected = (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300); + { + std::lock_guard lock(state_mutex); + is_connected = connected; + } + + invoke_server_connected_callback(connected ? 0 : -1, http_code); + + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: connect_server result=" << (connected ? "connected" : "failed"); + return connected ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; +} + +bool OrcaCloudServiceAgent::is_server_connected() +{ + std::lock_guard lock(state_mutex); + return is_connected; +} + +int OrcaCloudServiceAgent::refresh_connection() +{ + return connect_server(); +} + +int OrcaCloudServiceAgent::start_subscribe(std::string module) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: start_subscribe - " << module << " (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::stop_subscribe(std::string module) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: stop_subscribe - " << module << " (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::add_subscribe(std::vector dev_list) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: add_subscribe (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::del_subscribe(std::vector dev_list) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: del_subscribe (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +void OrcaCloudServiceAgent::enable_multi_machine(bool enable) +{ + std::lock_guard lock(state_mutex); + multi_machine_enabled = enable; +} + +// ============================================================================ +// ICloudServiceAgent - Settings Synchronization +// ============================================================================ + +int OrcaCloudServiceAgent::get_user_presets(std::map>* user_presets) +{ + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: get_user_presets"; + + if (!user_presets) return BAMBU_NETWORK_ERR_INVALID_HANDLE; + + if (!is_user_login()) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: Not logged in"; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + bool success = false; + std::string error_msg; + int http_code_out = 0; + SyncState saved_state; + + { + std::lock_guard lock(state_mutex); + saved_state = sync_state; + sync_state = SyncState{}; + } + + auto on_success = [&](const SyncPullResponse& resp) { + for (const auto& upsert : resp.upserts) { + std::string preset_type = IOT_PRINT_TYPE_STRING; + if (upsert.content.contains("type") && upsert.content["type"].is_string()) { + preset_type = upsert.content["type"].get(); + } + + (*user_presets)[preset_type][upsert.id] = upsert.content.dump(); + } + + if (!resp.next_cursor.empty()) { + std::lock_guard lock(state_mutex); + sync_state.last_sync_timestamp = resp.next_cursor; + save_sync_state(); + } else { + std::lock_guard lock(state_mutex); + sync_state = saved_state; + } + success = true; + }; + + auto on_error = [&](int code, const std::string& err) { + http_code_out = code; + error_msg = err; + success = false; + }; + + sync_pull(on_success, on_error); + + if (!success) { + std::lock_guard lock(state_mutex); + sync_state = saved_state; + } + + return success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED; +} + +std::string OrcaCloudServiceAgent::request_setting_id(std::string name, std::map* values_map, unsigned int* http_code) +{ + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: request_setting_id - " << name; + + std::string new_id = generate_uuid(name); + if (new_id.empty()) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: request_setting_id failed - name is empty"; + return ""; + } + + nlohmann::json content; + content["name"] = name; + content["type"] = IOT_PRINT_TYPE_STRING; // Default type + + if (values_map && !values_map->empty()) { + for (const auto& pair : *values_map) { + // Skip updated_time - it's metadata, not content + if (pair.first == IOT_JSON_KEY_UPDATED_TIME) continue; + content[pair.first] = pair.second; + } + } + + // Use sync_push to create the profile (no original_updated_at for new profiles per spec) + auto result = sync_push(new_id, name, content, ""); + if (http_code) *http_code = result.http_code; + + if (result.success) { + // Return new_updated_at via values_map so caller can store it in Preset::updated_time + if (values_map && !result.new_updated_at.empty()) { + (*values_map)[IOT_JSON_KEY_UPDATED_TIME] = result.new_updated_at; + } + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Created preset - setting_id=" << new_id + << ", new_updated_at=" << result.new_updated_at; + return new_id; + } + + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: request_setting_id failed - " << result.error_message; + return ""; +} + +int OrcaCloudServiceAgent::put_setting(std::string setting_id, std::string name, std::map* values_map, unsigned int* http_code) +{ + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: put_setting - setting_id=" << setting_id << ", name=" << name; + + // Extract original_updated_at for Optimistic Concurrency Control (per spec section 5.2) + // If present, server will verify version before update. If absent, treated as insert. + std::string original_updated_at; + if (values_map) { + auto it = values_map->find(IOT_JSON_KEY_UPDATED_TIME); + if (it != values_map->end()) { + original_updated_at = it->second; + } + } + + // Build content JSON + nlohmann::json content; + content["name"] = name; + + if (values_map && !values_map->empty()) { + for (const auto& pair : *values_map) { + // Skip updated_time - it's used for OCC, not as content + if (pair.first == IOT_JSON_KEY_UPDATED_TIME) continue; + content[pair.first] = pair.second; + } + } + + auto result = sync_push(setting_id, name, content, original_updated_at); + if (http_code) *http_code = result.http_code; + + if (result.success) { + if (values_map && !result.new_updated_at.empty()) { + (*values_map)[IOT_JSON_KEY_UPDATED_TIME] = result.new_updated_at; + } + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Updated preset - setting_id=" << setting_id + << ", new_updated_at=" << result.new_updated_at; + return BAMBU_NETWORK_SUCCESS; + } + + if (result.http_code == 409) { + // Conflict - update values_map with server version + if (values_map && !result.server_version.updated_at.empty()) { + (*values_map)[IOT_JSON_KEY_UPDATED_TIME] = result.server_version.updated_at; + } + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: put_setting conflict - server_updated_at=" + << result.server_version.updated_at; + } + + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: put_setting failed - " << result.error_message; + return BAMBU_NETWORK_ERR_PUT_SETTING_FAILED; +} + +int OrcaCloudServiceAgent::get_setting_list(std::string bundle_version, ProgressFn pro_fn, WasCancelledFn cancel_fn) +{ + return get_setting_list2(bundle_version, nullptr, pro_fn, cancel_fn); +} + +int OrcaCloudServiceAgent::get_setting_list2(std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn, WasCancelledFn cancel_fn) +{ + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: get_setting_list2 - bundle_version=" << bundle_version; + + bool success = false; + int error_code = 0; + + auto on_success = [&](const SyncPullResponse& resp) { + int total = static_cast(resp.upserts.size() + resp.deletes.size()); + int processed = 0; + bool cancelled = false; + + for (const auto& upsert : resp.upserts) { + if (cancel_fn && cancel_fn()) { + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: get_setting_list2 cancelled"; + cancelled = true; + break; + } + + if (chk_fn) { + std::map info; + info[IOT_JSON_KEY_SETTING_ID] = upsert.id; + info[IOT_JSON_KEY_UPDATED_TIME] = upsert.updated_at; + + if (upsert.content.is_object()) { + for (auto& [key, value] : upsert.content.items()) { + if (value.is_string()) { + info[key] = value.get(); + } else { + info[key] = value.dump(); + } + } + } + + if (!info.count(IOT_JSON_KEY_NAME) && !upsert.name.empty()) { + info[IOT_JSON_KEY_NAME] = upsert.name; + } + if (!info.count(IOT_JSON_KEY_TYPE)) { + info[IOT_JSON_KEY_TYPE] = IOT_PRINT_TYPE_STRING; + } + + chk_fn(info); + } + + if (pro_fn) { + int progress = total > 0 ? (processed * 100 / total) : 100; + pro_fn(progress); + } + + processed++; + } + + if (!cancelled) { + for (const auto& deleted_id : resp.deletes) { + if (cancel_fn && cancel_fn()) { + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: get_setting_list2 cancelled"; + cancelled = true; + break; + } + + if (chk_fn) { + std::map info; + info[IOT_JSON_KEY_SETTING_ID] = deleted_id; + info["deleted"] = "true"; + chk_fn(info); + } + + if (pro_fn) { + int progress = total > 0 ? (processed * 100 / total) : 100; + pro_fn(progress); + } + + processed++; + } + } + + if (!cancelled && !resp.next_cursor.empty()) { + std::lock_guard lock(state_mutex); + sync_state.last_sync_timestamp = resp.next_cursor; + save_sync_state(); + } + + if (pro_fn) { + pro_fn(100); + } + + success = !cancelled; + }; + + auto on_error = [&](int code, const std::string& err) { + error_code = code; + success = false; + }; + + sync_pull(on_success, on_error); + + return success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED; +} + +int OrcaCloudServiceAgent::delete_setting(std::string setting_id) +{ + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: delete_setting - " << setting_id; + + std::string path = std::string(ORCA_PROFILES_PATH) + "/" + setting_id; + std::string response; + unsigned int http_code = 0; + + int result = http_delete(path, &response, &http_code); + if (result != BAMBU_NETWORK_SUCCESS || http_code >= 400) { + return BAMBU_NETWORK_ERR_DEL_SETTING_FAILED; + } + + return BAMBU_NETWORK_SUCCESS; +} + +// ============================================================================ +// Sync Protocol Implementation +// ============================================================================ + +int OrcaCloudServiceAgent::sync_pull( + std::function on_success, + std::function on_error) +{ + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: sync_pull"; + + std::string path = ORCA_SYNC_PULL_PATH; + if (!sync_state.last_sync_timestamp.empty()) { + path += "?cursor=" + sync_state.last_sync_timestamp; + } + + std::string response; + unsigned int http_code = 0; + int result = http_get(path, &response, &http_code); + + // Handle 410 Gone - cursor too old, need full resync + if (http_code == 410) { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: sync_pull returned 410 Gone - cursor too old, triggering full resync"; + clear_sync_state(); + // Retry without cursor + path = ORCA_SYNC_PULL_PATH; + result = http_get(path, &response, &http_code); + } + + if (result != BAMBU_NETWORK_SUCCESS || (http_code != 200 && http_code != 304)) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: sync_pull failed - http_code=" << http_code; + if (on_error) on_error(http_code, response); + return BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED; + } + + // Handle 304 Not Modified - no changes + if (http_code == 304) { + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: sync_pull - no changes (304)"; + if (on_success) { + SyncPullResponse empty_response; + on_success(empty_response); + } + return BAMBU_NETWORK_SUCCESS; + } + + try { + auto json = nlohmann::json::parse(response); + SyncPullResponse resp; + resp.next_cursor = json.value("next_cursor", ""); + + if (json.contains("upserts") && json["upserts"].is_array()) { + for (const auto& item : json["upserts"]) { + ProfileUpsert upsert; + upsert.id = item.value("id", ""); + upsert.name = item.value("name", ""); + upsert.updated_at = item.value(ORCA_JSON_KEY_UPDATE_TIME, ""); + upsert.created_at = item.value(ORCA_JSON_KEY_CREATED_TIME, ""); + if (item.contains("content")) { + upsert.content = item["content"]; + } + resp.upserts.push_back(upsert); + } + } + + if (json.contains("deletes") && json["deletes"].is_array()) { + for (const auto& item : json["deletes"]) { + resp.deletes.push_back(item.get()); + } + } + + if (on_success) on_success(resp); + return BAMBU_NETWORK_SUCCESS; + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: sync_pull parse error - " << e.what(); + if (on_error) on_error(http_code, e.what()); + return BAMBU_NETWORK_ERR_INVALID_RESULT; + } +} + +SyncPushResult OrcaCloudServiceAgent::sync_push( + const std::string& profile_id, + const std::string& name, + const nlohmann::json& content, + const std::string& original_updated_at) +{ + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: sync_push - " << profile_id; + + SyncPushResult result; + result.success = false; + result.http_code = 0; + result.server_deleted = false; + + nlohmann::json body; + body["id"] = profile_id; + body["name"] = name; + body["content"] = content; + if (!original_updated_at.empty()) { + body["original_updated_at"] = original_updated_at; + } + + std::string response; + unsigned int http_code = 0; + int http_result = http_post(ORCA_SYNC_PUSH_PATH, body.dump(), &response, &http_code); + + result.http_code = http_code; + + if (http_result != BAMBU_NETWORK_SUCCESS) { + result.error_message = "HTTP request failed"; + return result; + } + + if (http_code == 409) { + // Conflict - parse server version + try { + auto json = nlohmann::json::parse(response); + if (json.is_null()) { + result.server_deleted = true; + } else { + result.server_version.id = json.value("id", ""); + result.server_version.name = json.value("name", ""); + result.server_version.updated_at = json.value(ORCA_JSON_KEY_UPDATE_TIME, ""); + } + } catch (...) {} + result.error_message = response; + return result; + } + + if (http_code != 200) { + result.error_message = response; + return result; + } + + // Success + try { + auto json = nlohmann::json::parse(response); + result.new_updated_at = json.value(ORCA_JSON_KEY_UPDATE_TIME, ""); + if (!result.new_updated_at.empty()) { + result.success = true; + } else { + result.error_message = "Server response missing required updated_at timestamp"; + } + } catch (const std::exception& e) { + result.error_message = e.what(); + } + + return result; +} + +// ============================================================================ +// Sync State Management +// ============================================================================ + +void OrcaCloudServiceAgent::load_sync_state() +{ + std::lock_guard lock(state_mutex); + + if (sync_state_path.empty()) return; + + try { + std::ifstream ifs(sync_state_path); + if (ifs.good()) { + std::string line; + if (std::getline(ifs, line)) { + sync_state.last_sync_timestamp = line; + } + } + } catch (...) {} +} + +void OrcaCloudServiceAgent::save_sync_state() +{ + std::lock_guard lock(state_mutex); + + if (sync_state_path.empty()) return; + + try { + std::string tmp_path = sync_state_path + ".tmp"; + std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc); + if (ofs.good()) { + ofs << sync_state.last_sync_timestamp; + ofs.close(); + boost::filesystem::rename(tmp_path, sync_state_path); + } + } catch (...) {} +} + +void OrcaCloudServiceAgent::clear_sync_state() +{ + std::lock_guard lock(state_mutex); + sync_state = SyncState{}; + if (!sync_state_path.empty() && boost::filesystem::exists(sync_state_path)) { + boost::filesystem::remove(sync_state_path); + } +} + +// ============================================================================ +// Auth - PKCE and Session Management +// ============================================================================ + +void OrcaCloudServiceAgent::set_session_handler(SessionHandler handler) +{ + session_handler = std::move(handler); +} + +void OrcaCloudServiceAgent::set_on_login_complete_handler(OnLoginCompleteHandler handler) +{ + on_login_complete_handler = std::move(handler); +} + +const OrcaCloudServiceAgent::PkceBundle& OrcaCloudServiceAgent::pkce() +{ + if (pkce_bundle.verifier.empty() || pkce_bundle.challenge.empty() || pkce_bundle.state.empty()) { + regenerate_pkce(); + } + return pkce_bundle; +} + +void OrcaCloudServiceAgent::regenerate_pkce() +{ + pkce_bundle.verifier = generate_code_verifier(); + pkce_bundle.challenge = sha256_base64url(pkce_bundle.verifier); + pkce_bundle.state = generate_state_token(); + if (pkce_bundle.redirect.empty()) { + pkce_bundle.redirect = "http://localhost:" + std::to_string(pkce_bundle.loopback_port) + auth_constants::LOOPBACK_PATH; + } + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: regenerated PKCE bundle"; +} + +void OrcaCloudServiceAgent::update_redirect_uri() +{ + int selected_port = choose_loopback_port(); + if (selected_port != pkce_bundle.loopback_port) { + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: loopback port changed to " << selected_port; + } + pkce_bundle.loopback_port = selected_port; + pkce_bundle.redirect = "http://localhost:" + std::to_string(selected_port) + auth_constants::LOOPBACK_PATH; +} + +// ============================================================================ +// Auth - Token Persistence +// ============================================================================ + +void OrcaCloudServiceAgent::persist_refresh_token(const std::string& token) +{ + if (token.empty()) { + clear_refresh_token(); + return; + } + + bool stored = false; + + if (m_use_encrypted_token_file) { + // Use encrypted file only + auto key = sha256_bytes(machine_identifier()); + if (key.empty()) { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot derive key for refresh-token file storage"; + return; + } + + std::string payload; + if (!aes256gcm_encrypt(token, key, payload)) { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to encrypt refresh token for file storage"; + return; + } + + std::string signed_payload = payload; + if (auto mac = hmac_sha256_hex(payload, key); !mac.empty()) { + signed_payload = "v2:" + mac + ":" + payload; + } + + compute_fallback_path(); + wxFileName path(wxString::FromUTF8(refresh_fallback_path.c_str())); + path.Normalize(); + if (!wxFileName::DirExists(path.GetPath())) { + wxFileName::Mkdir(path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL); + } + + const std::string tmp_path = refresh_fallback_path + ".tmp"; + std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc | std::ios::binary); + if (ofs.good()) { + ofs << signed_payload; + ofs.flush(); + ofs.close(); + + if (wxRenameFile(wxString::FromUTF8(tmp_path.c_str()), wxString::FromUTF8(refresh_fallback_path.c_str()), true)) { + stored = true; + } else { + wxRemoveFile(wxString::FromUTF8(tmp_path.c_str())); + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to atomically replace refresh-token file"; + } + } else { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot open refresh-token file for write - " << refresh_fallback_path; + } + } else { + // Use wxSecretStore only + wxSecretStore store = wxSecretStore::GetDefault(); + if (store.IsOk()) { + wxSecretValue secret(wxString::FromUTF8(token.c_str())); + if (store.Save(SECRET_STORE_SERVICE, SECRET_STORE_USER, secret)) { + stored = true; + } else { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: System Keychain save failed"; + } + } else { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: System Keychain not available"; + } + } + + if (stored) { + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: refresh token persisted successfully"; + } +} + +bool OrcaCloudServiceAgent::load_refresh_token(std::string& out_token) +{ + out_token.clear(); + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: load_refresh_token called"; + + if (m_use_encrypted_token_file) { + // Load from encrypted file only + compute_fallback_path(); + if (wxFileExists(wxString::FromUTF8(refresh_fallback_path.c_str()))) { + std::ifstream ifs(refresh_fallback_path, std::ios::binary); + std::string payload((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + auto key = sha256_bytes(machine_identifier()); + std::string plain; + if (!key.empty()) { + std::string encoded_payload = payload; + bool integrity_ok = true; + + if (payload.rfind("v2:", 0) == 0) { + auto delim = payload.find(':', 3); + if (delim == std::string::npos) { + integrity_ok = false; + } else { + std::string stored_hmac = payload.substr(3, delim - 3); + std::string lower_stored = stored_hmac; + std::transform(lower_stored.begin(), lower_stored.end(), lower_stored.begin(), ::tolower); + encoded_payload = payload.substr(delim + 1); + + std::string computed_hmac = hmac_sha256_hex(encoded_payload, key); + std::transform(computed_hmac.begin(), computed_hmac.end(), computed_hmac.begin(), ::tolower); + if (computed_hmac.empty() || computed_hmac != lower_stored) { + integrity_ok = false; + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: refresh token integrity check failed (HMAC mismatch)"; + } + } + } + + if (integrity_ok && aes256gcm_decrypt(encoded_payload, key, plain) && !plain.empty()) { + out_token = plain; + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: loaded refresh token from encrypted file"; + + // Upgrade legacy payloads to signed format + if (payload.rfind("v2:", 0) != 0) { + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: upgrading legacy token format to v2"; + persist_refresh_token(out_token); + } + return true; + } + } + } + } else { + // Load from wxSecretStore only + wxSecretStore store = wxSecretStore::GetDefault(); + if (store.IsOk()) { + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: System Keychain is available, attempting load"; + wxString username; + wxSecretValue secret; + if (store.Load(SECRET_STORE_SERVICE, username, secret) && secret.IsOk()) { + out_token.assign(static_cast(secret.GetData()), secret.GetSize()); + if (!out_token.empty()) { + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: loaded refresh token from System Keychain"; + return true; + } + } else { + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: System Keychain load failed or data is invalid"; + } + } else { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: System Keychain is NOT available"; + } + } + + return false; +} + +void OrcaCloudServiceAgent::clear_refresh_token() +{ + wxSecretStore store = wxSecretStore::GetDefault(); + if (store.IsOk()) { + store.Delete(SECRET_STORE_SERVICE); + } + + compute_fallback_path(); + if (!refresh_fallback_path.empty() && wxFileExists(wxString::FromUTF8(refresh_fallback_path.c_str()))) { + wxRemoveFile(wxString::FromUTF8(refresh_fallback_path.c_str())); + } +} + +// ============================================================================ +// Auth - Token Refresh +// ============================================================================ + +bool OrcaCloudServiceAgent::should_refresh_locked(std::chrono::seconds skew) const +{ + if (!session.logged_in) return false; + if (session.expires_at.time_since_epoch().count() == 0) return true; + + auto now = std::chrono::system_clock::now(); + return (session.expires_at - now) <= skew; +} + +bool OrcaCloudServiceAgent::decode_jwt_expiry(const std::string& token, std::chrono::system_clock::time_point& out_tp) +{ + out_tp = {}; + if (token.empty()) return false; + + auto first = token.find('.'); + auto second = token.find('.', first == std::string::npos ? 0 : first + 1); + if (first == std::string::npos || second == std::string::npos) return false; + + std::string payload_b64 = token.substr(first + 1, second - first - 1); + std::vector payload_bytes; + if (!base64url_decode(payload_b64, payload_bytes)) return false; + + std::string payload_str(payload_bytes.begin(), payload_bytes.end()); + try { + pt::ptree payload; + std::stringstream ss(payload_str); + pt::read_json(ss, payload); + auto exp_opt = payload.get_optional("exp"); + if (exp_opt) { + out_tp = std::chrono::system_clock::time_point{std::chrono::seconds(*exp_opt)}; + return true; + } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: failed to decode JWT exp - " << e.what(); + } + return false; +} + +bool OrcaCloudServiceAgent::refresh_now(const std::string& refresh_token, const std::string& reason, bool async) +{ + if (refresh_token.empty()) return false; + + bool expected = false; + if (!refresh_running.compare_exchange_strong(expected, true)) { + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: refresh already running, skip (reason=" << reason << ")"; + return false; + } + + auto worker = [this, refresh_token, reason]() { + const std::string req_id = generate_state_token(); + BOOST_LOG_TRIVIAL(info) << "[auth] event=refresh_start source=" << reason << " rid=" << req_id; + bool ok = refresh_session_with_token(refresh_token); + if (ok) { + BOOST_LOG_TRIVIAL(info) << "[auth] event=refresh_complete result=success source=" << reason << " rid=" << req_id; + } else { + BOOST_LOG_TRIVIAL(warning) << "[auth] event=refresh_complete result=failure source=" << reason << " rid=" << req_id; + } + refresh_running.store(false); + return ok; + }; + + if (async) { + if (refresh_thread.joinable()) { + refresh_thread.join(); + } + refresh_thread = std::thread([worker]() { worker(); }); + return true; + } + + return worker(); +} + +bool OrcaCloudServiceAgent::refresh_from_storage(const std::string& reason, bool async) +{ + std::string refresh_token = get_refresh_token(); + if (refresh_token.empty()) { + load_refresh_token(refresh_token); + } + if (refresh_token.empty()) { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: no refresh token available for refresh (reason=" << reason << ")"; + return false; + } + + return refresh_now(refresh_token, reason, async); +} + +bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const std::string& reason) +{ + bool needs_refresh = false; + { + std::lock_guard lock(session_mutex); + needs_refresh = should_refresh_locked(skew); + } + + if (!needs_refresh) return true; + + if (refresh_from_storage(reason, false)) return true; + + std::this_thread::sleep_for(std::chrono::milliseconds(750)); + return refresh_from_storage(reason + "_retry", false); +} + +bool OrcaCloudServiceAgent::refresh_session_with_token(const std::string& refresh_token) +{ + std::string body = "{\"refresh_token\":\"" + refresh_token + "\"}"; + + std::string url = auth_base_url + auth_constants::TOKEN_PATH + "?grant_type=refresh_token"; + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: refresh request - token_length=" << refresh_token.size() << ", url=" << url; + + std::string response; + unsigned int http_code = 0; + if (!http_post_token(body, &response, &http_code, url) || http_code >= 400) { + std::string truncated_response = response.size() > 200 ? response.substr(0, 200) + "..." : response; + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: token refresh failed - http_code=" << http_code + << ", response_body=" << truncated_response; + return false; + } + + if (session_handler) { + return session_handler(response); + } + + // No session handler set - parse the token response directly and establish session + // This makes OrcaCloudServiceAgent self-contained without requiring external setup + try { + std::stringstream ss(response); + pt::ptree tree; + pt::read_json(ss, tree); + + auto read_str = [](const pt::ptree& node, const std::string& path) { + return node.get(path, ""); + }; + + // Token refresh response has the same structure as login response + std::string access_token = read_str(tree, "access_token"); + if (access_token.empty()) { + access_token = read_str(tree, "token"); + } + std::string new_refresh_token = read_str(tree, "refresh_token"); + std::string user_id = read_str(tree, "user.id"); + std::string email = read_str(tree, "user.email"); + std::string full_name = read_str(tree, "user.user_metadata.full_name"); + std::string preferred_username = read_str(tree, "user.user_metadata.preferred_username"); + std::string avatar = read_str(tree, "user.user_metadata.avatar_url"); + + std::string username = !preferred_username.empty() ? preferred_username : email; + std::string name = !full_name.empty() ? full_name : (!preferred_username.empty() ? preferred_username : email); + std::string nickname = !preferred_username.empty() ? preferred_username : username; + if (nickname.empty()) nickname = name; + if (nickname.empty()) nickname = email; + + if (access_token.empty() || user_id.empty()) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: token refresh response missing access_token or user.id"; + invoke_user_login_callback(0, false); + return false; + } + + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: token refresh successful - user_id=" << user_id; + bool success = set_user_session(access_token, user_id, username, name, nickname, avatar, new_refresh_token); + if (success) { + invoke_user_login_callback(0, true); + if (on_login_complete_handler) { + on_login_complete_handler(true, user_id); + } + } else { + invoke_user_login_callback(0, false); + } + BOOST_LOG_TRIVIAL(info) << "[auth] event=token_refresh result=" << (success ? "success" : "failure") + << " user_id=" << user_id; + return success; + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: token refresh parse exception - " << e.what(); + invoke_user_login_callback(0, false); + return false; + } +} + +// ============================================================================ +// Auth - Session State Helpers +// ============================================================================ + +bool OrcaCloudServiceAgent::set_user_session(const std::string& token, + const std::string& user_id, + const std::string& username, + const std::string& name, + const std::string& nickname, + const std::string& avatar, + const std::string& refresh_token) +{ + std::chrono::system_clock::time_point exp_tp{}; + decode_jwt_expiry(token, exp_tp); + + { + std::lock_guard lock(session_mutex); + session.access_token = token; + session.refresh_token = refresh_token; + session.user_id = user_id; + session.user_name = name.empty() ? username : name; + session.user_nickname = nickname.empty() ? (!username.empty() ? username : name) : nickname; + session.user_avatar = avatar; + session.expires_at = exp_tp; + session.logged_in = true; + } + + if (!refresh_token.empty()) { + persist_refresh_token(refresh_token); + } + + // Set per-user sync state path + if (!config_dir.empty() && !user_id.empty()) { + boost::filesystem::path user_dir = boost::filesystem::path(config_dir) / "user" / user_id; + if (!boost::filesystem::exists(user_dir)) { + boost::filesystem::create_directories(user_dir); + } + sync_state_path = (user_dir / ORCA_SYNC_STATE_FILE).string(); + load_sync_state(); + } + + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: set_user_session - user_id=" << user_id << ", username=" << username; + return true; +} + +void OrcaCloudServiceAgent::clear_session() +{ + { + std::lock_guard lock(session_mutex); + session = SessionInfo{}; + } + clear_refresh_token(); +} + +// ============================================================================ +// HTTP Helpers +// ============================================================================ + +bool OrcaCloudServiceAgent::attempt_refresh_after_unauthorized(const std::string& reason) +{ + if (refresh_from_storage(reason, false)) return true; + + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + if (refresh_from_storage(reason + "_retry", false)) return true; + + BOOST_LOG_TRIVIAL(warning) << "[auth] event=refresh result=failure source=" << reason << " action=logout"; + return false; +} + +std::map OrcaCloudServiceAgent::data_headers() +{ + std::scoped_lock lock(state_mutex, headers_mutex); + auto headers = auth_headers; + for (const auto& pair : extra_headers) { + headers[pair.first] = pair.second; + } + return headers; +} + +int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* response_body, unsigned int* http_code) +{ + std::string url = api_base_url + path; + BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: GET " << url; + + ensure_token_fresh("http_get_" + path); + + struct HttpResult { + bool success{false}; + unsigned int status{0}; + std::string body; + }; + + auto perform = [&]() { + HttpResult result; + try { + auto http = Http::get(url); + + std::string token = get_access_token(); + + auto headers = data_headers(); + for (const auto& pair : headers) { + http.header(pair.first, pair.second); + } + + if (!token.empty()) { + http.header("Authorization", "Bearer " + token); + } + + http.on_complete([&](std::string body, unsigned resp_status) { + result.success = true; + result.status = resp_status; + result.body = body; + }) + .on_error([&](std::string body, std::string error, unsigned resp_status) { + result.success = false; + result.status = resp_status; + result.body = body; + }) + .timeout_max(30) + .perform_sync(); + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: http_get exception - " << e.what(); + } + return result; + }; + + HttpResult res = perform(); + + // Single retry on 401 - no recursion + if (res.status == 401 && attempt_refresh_after_unauthorized("http_get_" + path)) { + res = perform(); + } + + if (response_body) *response_body = res.body; + if (http_code) *http_code = res.status; + + if (!res.success || res.status >= 400) { + invoke_http_error_callback(res.status, res.body); + } + + return res.success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; +} + +int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code) +{ + std::string url = api_base_url + path; + BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << url; + + ensure_token_fresh("http_post_" + path); + + struct HttpResult { + bool success{false}; + unsigned int status{0}; + std::string body; + }; + + auto perform = [&]() { + HttpResult result; + try { + auto http = Http::post(url); + + std::string token = get_access_token(); + + auto headers = data_headers(); + for (const auto& pair : headers) { + http.header(pair.first, pair.second); + } + + if (!token.empty()) { + http.header("Authorization", "Bearer " + token); + } + + http.header("Content-Type", "application/json"); + http.set_post_body(body); + + http.on_complete([&](std::string resp_body, unsigned resp_status) { + result.success = true; + result.status = resp_status; + result.body = resp_body; + }) + .on_error([&](std::string resp_body, std::string error, unsigned resp_status) { + result.success = false; + result.status = resp_status; + result.body = resp_body; + }) + .timeout_max(30) + .perform_sync(); + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: http_post exception - " << e.what(); + } + return result; + }; + + HttpResult res = perform(); + + // Single retry on 401 - no recursion + if (res.status == 401 && attempt_refresh_after_unauthorized("http_post_" + path)) { + res = perform(); + } + + if (response_body) *response_body = res.body; + if (http_code) *http_code = res.status; + + if (!res.success || res.status >= 400) { + invoke_http_error_callback(res.status, res.body); + } + + return res.success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; +} + +int OrcaCloudServiceAgent::http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code) +{ + std::string url = api_base_url + path; + BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: PUT " << url; + + ensure_token_fresh("http_put_" + path); + + struct HttpResult { + bool success{false}; + unsigned int status{0}; + std::string body; + }; + + auto perform = [&]() { + HttpResult result; + try { + auto http = Http::put(url); + + std::string token = get_access_token(); + + auto headers = data_headers(); + for (const auto& pair : headers) { + http.header(pair.first, pair.second); + } + + if (!token.empty()) { + http.header("Authorization", "Bearer " + token); + } + + http.header("Content-Type", "application/json"); + http.set_post_body(body); + + http.on_complete([&](std::string resp_body, unsigned resp_status) { + result.success = true; + result.status = resp_status; + result.body = resp_body; + }) + .on_error([&](std::string resp_body, std::string error, unsigned resp_status) { + result.success = false; + result.status = resp_status; + result.body = resp_body; + }) + .timeout_max(30) + .perform_sync(); + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: http_put exception - " << e.what(); + } + return result; + }; + + HttpResult res = perform(); + + // Single retry on 401 - no recursion + if (res.status == 401 && attempt_refresh_after_unauthorized("http_put_" + path)) { + res = perform(); + } + + if (response_body) *response_body = res.body; + if (http_code) *http_code = res.status; + + if (!res.success || res.status >= 400) { + invoke_http_error_callback(res.status, res.body); + } + + return res.success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; +} + +int OrcaCloudServiceAgent::http_delete(const std::string& path, std::string* response_body, unsigned int* http_code) +{ + std::string url = api_base_url + path; + BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: DELETE " << url; + + ensure_token_fresh("http_delete_" + path); + + struct HttpResult { + bool success{false}; + unsigned int status{0}; + std::string body; + }; + + auto perform = [&]() { + HttpResult result; + try { + auto http = Http::del(url); + + std::string token = get_access_token(); + + auto headers = data_headers(); + for (const auto& pair : headers) { + http.header(pair.first, pair.second); + } + + if (!token.empty()) { + http.header("Authorization", "Bearer " + token); + } + + http.on_complete([&](std::string resp_body, unsigned resp_status) { + result.success = true; + result.status = resp_status; + result.body = resp_body; + }) + .on_error([&](std::string resp_body, std::string error, unsigned resp_status) { + result.success = false; + result.status = resp_status; + result.body = resp_body; + }) + .timeout_max(30) + .perform_sync(); + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: http_delete exception - " << e.what(); + } + return result; + }; + + HttpResult res = perform(); + + // Single retry on 401 - no recursion + if (res.status == 401 && attempt_refresh_after_unauthorized("http_delete_" + path)) { + res = perform(); + } + + if (response_body) *response_body = res.body; + if (http_code) *http_code = res.status; + + if (!res.success || res.status >= 400) { + invoke_http_error_callback(res.status, res.body); + } + + return res.success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; +} + +bool OrcaCloudServiceAgent::http_post_token(const std::string& body, std::string* response_body, unsigned int* http_code, const std::string& custom_url) +{ + std::map headers_copy; + std::string url; + { + std::lock_guard lock(headers_mutex); + url = custom_url.empty() ? (auth_base_url + auth_constants::TOKEN_PATH) : custom_url; + headers_copy = extra_headers; + } + + // Add auth headers + { + std::lock_guard lock(state_mutex); + for (const auto& pair : auth_headers) { + headers_copy[pair.first] = pair.second; + } + } + + BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << url; + + bool has_apikey = false; + for (const auto& pair : headers_copy) { + if (pair.first == "apikey") + has_apikey = true; + } + if (!has_apikey) { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: http_post_token - apikey header MISSING! Token request will likely fail."; + } + + try { + auto http = Http::post(url); + + for (const auto& pair : headers_copy) { + http.header(pair.first, pair.second); + } + + http.remove_header("Authorization"); + http.remove_header("Content-Type"); + http.header("Content-Type", "application/json"); + http.set_post_body(body); + + bool success = false; + unsigned int status = 0; + std::string resp_body; + + http.on_complete([&](std::string body, unsigned resp_status) { + success = true; + status = resp_status; + resp_body = body; + }) + .on_error([&](std::string body, std::string error, unsigned resp_status) { + success = false; + status = resp_status; + resp_body = body; + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error; + }) + .timeout_max(30) + .perform_sync(); + + if (response_body) + *response_body = resp_body; + if (http_code) + *http_code = status; + return success; + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: http_post_token exception - " << e.what(); + if (http_code) + *http_code = 0; + return false; + } +} + +bool OrcaCloudServiceAgent::http_post_auth(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code) +{ + std::string url = auth_base_url + path; + std::string token; + std::map headers_copy; + { + std::lock_guard lock(session_mutex); + token = session.access_token; + } + { + std::lock_guard lock(headers_mutex); + headers_copy = extra_headers; + } + { + std::lock_guard lock(state_mutex); + for (const auto& pair : auth_headers) { + headers_copy[pair.first] = pair.second; + } + } + + BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST (auth) " << url; + + try { + auto http = Http::post(url); + + for (const auto& pair : headers_copy) { + http.header(pair.first, pair.second); + } + + if (!token.empty()) { + http.header("Authorization", "Bearer " + token); + } + + http.remove_header("Content-Type"); + http.header("Content-Type", "application/json"); + http.set_post_body(body); + + bool success = false; + unsigned int status = 0; + std::string resp_body; + + http.on_complete([&](std::string body, unsigned resp_status) { + success = true; + status = resp_status; + resp_body = body; + }) + .on_error([&](std::string body, std::string error, unsigned resp_status) { + success = false; + status = resp_status; + resp_body = body; + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP (auth) error - " << error; + }) + .timeout_max(30) + .perform_sync(); + + if (response_body) + *response_body = resp_body; + if (http_code) + *http_code = status; + return success; + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: http_post_auth exception - " << e.what(); + if (http_code) + *http_code = 0; + return false; + } +} + +void OrcaCloudServiceAgent::compute_fallback_path() +{ + if (!refresh_fallback_path.empty()) return; + wxFileName fallback(wxStandardPaths::Get().GetUserDataDir(), "orca_refresh_token.sec"); + fallback.Normalize(); + refresh_fallback_path = fallback.GetFullPath().ToStdString(); +} + +// ============================================================================ +// JSON Helpers +// ============================================================================ + +std::string OrcaCloudServiceAgent::map_to_json(const std::map& map) +{ + nlohmann::json j; + for (const auto& pair : map) { + j[pair.first] = pair.second; + } + return j.dump(); +} + +void OrcaCloudServiceAgent::json_to_map(const std::string& json, std::map& map) +{ + try { + auto j = nlohmann::json::parse(json); + for (auto it = j.begin(); it != j.end(); ++it) { + if (it.value().is_string()) { + map[it.key()] = it.value().get(); + } else { + map[it.key()] = it.value().dump(); + } + } + } catch (...) {} +} + +// ============================================================================ +// Callback Invocation +// ============================================================================ + +void OrcaCloudServiceAgent::invoke_user_login_callback(int online_login, bool login) +{ + OnUserLoginFn callback; + QueueOnMainFn queue_fn; + + { + std::lock_guard lock(callback_mutex); + callback = on_user_login_fn; + queue_fn = queue_on_main_fn; + } + + if (callback) { + if (queue_fn) { + queue_fn([callback, online_login, login]() { + callback(online_login, login); + }); + } else { + callback(online_login, login); + } + } +} + +void OrcaCloudServiceAgent::invoke_server_connected_callback(int return_code, int reason_code) +{ + OnServerConnectedFn callback; + QueueOnMainFn queue_fn; + { + std::lock_guard lock(state_mutex); + callback = on_server_connected_fn; + queue_fn = queue_on_main_fn; + } + + if (callback) { + if (queue_fn) { + queue_fn([callback, return_code, reason_code]() { + callback(return_code, reason_code); + }); + } else { + callback(return_code, reason_code); + } + } +} + +void OrcaCloudServiceAgent::invoke_http_error_callback(unsigned http_code, const std::string& http_body) +{ + OnHttpErrorFn callback; + QueueOnMainFn queue_fn; + { + std::lock_guard lock(state_mutex); + callback = on_http_error_fn; + queue_fn = queue_on_main_fn; + } + + if (callback) { + if (queue_fn) { + queue_fn([callback, http_code, http_body]() { + callback(http_code, http_body); + }); + } else { + callback(http_code, http_body); + } + } +} + +// ============================================================================ +// Callback Registration +// ============================================================================ + +int OrcaCloudServiceAgent::set_on_user_login_fn(OnUserLoginFn fn) +{ + std::lock_guard lock(callback_mutex); + on_user_login_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::set_on_server_connected_fn(OnServerConnectedFn fn) +{ + std::lock_guard lock(state_mutex); + on_server_connected_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::set_on_http_error_fn(OnHttpErrorFn fn) +{ + std::lock_guard lock(state_mutex); + on_http_error_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::set_get_country_code_fn(GetCountryCodeFn fn) +{ + std::lock_guard lock(state_mutex); + get_country_code_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::set_queue_on_main_fn(QueueOnMainFn fn) +{ + std::lock_guard lock(state_mutex); + queue_on_main_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +// ============================================================================ +// Stub Implementations (Cloud Services, Model Mall, Analytics, Ratings) +// ============================================================================ + +int OrcaCloudServiceAgent::get_my_message(int type, int after, int limit, unsigned int* http_code, std::string* http_body) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_my_message (stub)"; + if (http_code) *http_code = 200; + if (http_body) *http_body = "[]"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::check_user_task_report(int* task_id, bool* printable) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: check_user_task_report (stub)"; + if (task_id) *task_id = 0; + if (printable) *printable = false; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::string* http_body) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_print_info (stub)"; + if (http_code) *http_code = 200; + if (http_body) *http_body = "{}"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_user_tasks(TaskQueryParams params, std::string* http_body) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_tasks (stub)"; + if (http_body) *http_body = "[]"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_printer_firmware(std::string dev_id, unsigned* http_code, std::string* http_body) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_printer_firmware (stub)"; + if (http_code) *http_code = 200; + if (http_body) *http_body = "{}"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_task_plate_index(std::string task_id, int* plate_index) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_task_plate_index (stub)"; + if (plate_index) *plate_index = 0; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_user_info(int* identifier) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_info (stub)"; + if (identifier) *identifier = 0; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_subtask_info(std::string subtask_id, std::string* task_json, unsigned int* http_code, std::string* http_body) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_subtask_info (stub)"; + if (task_json) *task_json = "{}"; + if (http_code) *http_code = 200; + if (http_body) *http_body = "{}"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_slice_info(std::string project_id, std::string profile_id, int plate_index, std::string* slice_json) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_slice_info (stub)"; + if (slice_json) *slice_json = "{}"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::query_bind_status(std::vector query_list, unsigned int* http_code, std::string* http_body) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: query_bind_status (stub)"; + if (http_code) *http_code = 200; + if (http_body) *http_body = "{}"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::modify_printer_name(std::string dev_id, std::string dev_name) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: modify_printer_name (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_camera_url(std::string dev_id, std::function callback) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_camera_url (stub)"; + if (callback) callback(""); + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_design_staffpick(int offset, int limit, std::function callback) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_design_staffpick (stub)"; + if (callback) callback("[]"); + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: start_publish (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_model_publish_url(std::string* url) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_model_publish_url (stub)"; + if (url) *url = ""; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_subtask(BBLModelTask* task, OnGetSubTaskFn getsub_fn) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_subtask (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_model_mall_home_url(std::string* url) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_model_mall_home_url (stub)"; + if (url) *url = ""; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_model_mall_detail_url(std::string* url, std::string id) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_model_mall_detail_url (stub)"; + if (url) *url = ""; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_my_profile(std::string token, unsigned int* http_code, std::string* http_body) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_my_profile (stub)"; + if (http_code) *http_code = 200; + if (http_body) *http_body = "{}"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::track_enable(bool enable) +{ + std::lock_guard lock(state_mutex); + enable_track = enable; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::track_remove_files() +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: track_remove_files (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::track_event(std::string evt_key, std::string content) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: track_event (stub) - " << evt_key; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::track_header(std::string header) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: track_header (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::track_update_property(std::string name, std::string value, std::string type) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: track_update_property (stub) - " << name; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::track_get_property(std::string name, std::string& value, std::string type) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: track_get_property (stub) - " << name; + value = ""; + return BAMBU_NETWORK_SUCCESS; +} + +bool OrcaCloudServiceAgent::get_track_enable() +{ + std::lock_guard lock(state_mutex); + return enable_track; +} + +int OrcaCloudServiceAgent::put_model_mall_rating(int design_id, int score, std::string content, std::vector images, unsigned int& http_code, std::string& http_error) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: put_model_mall_rating (stub)"; + http_code = 200; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_oss_config(std::string& config, std::string country_code, unsigned int& http_code, std::string& http_error) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_oss_config (stub)"; + config = "{}"; + http_code = 200; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::put_rating_picture_oss(std::string& config, std::string& pic_oss_path, std::string model_id, int profile_id, unsigned int& http_code, std::string& http_error) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: put_rating_picture_oss (stub)"; + http_code = 200; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_model_mall_rating_result(int job_id, std::string& rating_result, unsigned int& http_code, std::string& http_error) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_model_mall_rating_result (stub)"; + rating_result = "{}"; + http_code = 200; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::set_extra_http_header(std::map headers) +{ + std::lock_guard lock(headers_mutex); + extra_headers = headers; + return BAMBU_NETWORK_SUCCESS; +} + +std::string OrcaCloudServiceAgent::get_cloud_service_host() +{ + return api_base_url; +} + +std::string OrcaCloudServiceAgent::get_cloud_login_url(const std::string& language) +{ + // Orca uses a local HTML file for the login flow + boost::filesystem::path login_path = boost::filesystem::path(resources_dir()) / "web" / "login" / "orca_login.html"; + return "file://" + login_path.make_preferred().string(); +} + +std::string OrcaCloudServiceAgent::get_studio_info_url() +{ + return api_base_url + "/studio/info"; +} + +int OrcaCloudServiceAgent::get_mw_user_preference(std::function callback) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_mw_user_preference (stub)"; + if (callback) callback("{}"); + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::get_mw_user_4ulist(int seed, int limit, std::function callback) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_mw_user_4ulist (stub)"; + if (callback) callback("[]"); + return BAMBU_NETWORK_SUCCESS; +} + +std::string OrcaCloudServiceAgent::get_version() +{ + return "OrcaCloudServiceAgent 1.0.0"; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp new file mode 100644 index 0000000000..510829c717 --- /dev/null +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp @@ -0,0 +1,359 @@ +#ifndef __ORCA_CLOUD_SERVICE_AGENT_HPP__ +#define __ORCA_CLOUD_SERVICE_AGENT_HPP__ + +#include "ICloudServiceAgent.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class wxSecretStore; + +namespace Slic3r { + +// Forward declaration +class AppConfig; + +// Constants for OAuth loopback server +namespace auth_constants { + constexpr int LOOPBACK_PORT = 41172; + constexpr const char* LOOPBACK_PATH = "/callback"; + constexpr const char* TOKEN_PATH = "/auth/v1/token"; + constexpr const char* LOGOUT_PATH = "/auth/v1/logout"; +} // namespace auth_constants + +// ============================================================================ +// Sync Protocol Data Structures (per Orca Cloud Sync Protocol Specification) +// ============================================================================ +// Note: These may also be defined in OrcaNetwork.hpp - guards prevent redefinition + +#ifndef ORCA_SYNC_STRUCTS_DEFINED +#define ORCA_SYNC_STRUCTS_DEFINED + +struct ProfileUpsert { + std::string id; + std::string name; + nlohmann::json content; + std::string updated_at; + std::string created_at; +}; + +struct SyncPullResponse { + std::string next_cursor; + std::vector upserts; + std::vector deletes; +}; + +struct SyncPushResult { + bool success; + int http_code; + std::string new_updated_at; + ProfileUpsert server_version; + bool server_deleted; + std::string error_message; +}; + +struct SyncState { + std::string last_sync_timestamp; +}; + +#endif // ORCA_SYNC_STRUCTS_DEFINED + +/** + * OrcaCloudServiceAgent - Native cloud service and authentication implementation for Orca Cloud. + * + * Implements the ICloudServiceAgent interface with: + * - Full OAuth 2.0 PKCE authentication support + * - Token storage via wxSecretStore with AES-256-GCM encrypted file fallback + * - JWT expiry decoding and proactive token refresh + * - Session management with thread-safe state access + * - Settings synchronization (sync_pull, sync_push) + * - Server connectivity management + * - HTTP helpers with automatic token injection + * + * This class combines the functionality of the former OrcaAuthAgent and OrcaCloudServiceAgent. + */ +class OrcaCloudServiceAgent : public ICloudServiceAgent { +public: + // ======================================================================== + // Auth Session Types + // ======================================================================== + struct SessionInfo { + std::string access_token; + std::string refresh_token; + std::string user_id; + std::string user_name; + std::string user_nickname; + std::string user_avatar; + std::chrono::system_clock::time_point expires_at{}; + bool logged_in = false; + }; + + struct PkceBundle { + std::string verifier; + std::string challenge; + std::string state; + std::string redirect; + int loopback_port = auth_constants::LOOPBACK_PORT; + }; + + using SessionHandler = std::function; + using OnLoginCompleteHandler = std::function; + + explicit OrcaCloudServiceAgent(std::string log_dir); + ~OrcaCloudServiceAgent() override; + + // Configuration + void configure_urls(AppConfig* app_config); + void set_api_base_url(const std::string& url); + void set_auth_base_url(const std::string& url); + void set_use_encrypted_token_file(bool use); + bool get_use_encrypted_token_file() const; + + // ======================================================================== + // ICloudServiceAgent Interface Implementation - Lifecycle Methods + // ======================================================================== + int init_log() override; + int set_config_dir(std::string config_dir) override; + int set_cert_file(std::string folder, std::string filename) override; + int set_country_code(std::string country_code) override; + int start() override; + + // ======================================================================== + // ICloudServiceAgent Interface Implementation - User Session Management + // ======================================================================== + int change_user(std::string user_info) override; + bool is_user_login() override; + int user_logout(bool request = false) override; + std::string get_user_id() override; + std::string get_user_name() override; + std::string get_user_avatar() override; + std::string get_user_nickname() override; + + // ======================================================================== + // ICloudServiceAgent Interface Implementation - Login UI Support + // ======================================================================== + std::string build_login_cmd() override; + std::string build_logout_cmd() override; + std::string build_login_info() override; + + // ======================================================================== + // ICloudServiceAgent Interface Implementation - Token Access + // ======================================================================== + std::string get_access_token() const override; + std::string get_refresh_token() const override; + bool ensure_token_fresh(const std::string& reason) override; + + // ======================================================================== + // ICloudServiceAgent Interface Implementation - Server Connectivity + // ======================================================================== + std::string get_cloud_service_host() override; + std::string get_cloud_login_url(const std::string& language = "") override; + int connect_server() override; + bool is_server_connected() override; + int refresh_connection() override; + int start_subscribe(std::string module) override; + int stop_subscribe(std::string module) override; + int add_subscribe(std::vector dev_list) override; + int del_subscribe(std::vector dev_list) override; + void enable_multi_machine(bool enable) override; + + // ======================================================================== + // ICloudServiceAgent Interface Implementation - Settings Synchronization + // ======================================================================== + int get_user_presets(std::map>* user_presets) override; + std::string request_setting_id(std::string name, std::map* values_map, unsigned int* http_code) override; + int put_setting(std::string setting_id, std::string name, std::map* values_map, unsigned int* http_code) override; + int get_setting_list(std::string bundle_version, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr) override; + int get_setting_list2(std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr) override; + int delete_setting(std::string setting_id) override; + + // ======================================================================== + // ICloudServiceAgent Interface Implementation - Cloud User Services + // ======================================================================== + int get_my_message(int type, int after, int limit, unsigned int* http_code, std::string* http_body) override; + int check_user_task_report(int* task_id, bool* printable) override; + int get_user_print_info(unsigned int* http_code, std::string* http_body) override; + int get_user_tasks(TaskQueryParams params, std::string* http_body) override; + int get_printer_firmware(std::string dev_id, unsigned* http_code, std::string* http_body) override; + int get_task_plate_index(std::string task_id, int* plate_index) override; + int get_user_info(int* identifier) override; + int get_subtask_info(std::string subtask_id, std::string* task_json, unsigned int* http_code, std::string* http_body) override; + int get_slice_info(std::string project_id, std::string profile_id, int plate_index, std::string* slice_json) override; + int query_bind_status(std::vector query_list, unsigned int* http_code, std::string* http_body) override; + int modify_printer_name(std::string dev_id, std::string dev_name) override; + + // ======================================================================== + // ICloudServiceAgent Interface Implementation - Model Mall & Publishing + // ======================================================================== + int get_camera_url(std::string dev_id, std::function callback) override; + int get_design_staffpick(int offset, int limit, std::function callback) override; + int start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out) override; + int get_model_publish_url(std::string* url) override; + int get_subtask(BBLModelTask* task, OnGetSubTaskFn getsub_fn) override; + int get_model_mall_home_url(std::string* url) override; + int get_model_mall_detail_url(std::string* url, std::string id) override; + int get_my_profile(std::string token, unsigned int* http_code, std::string* http_body) override; + + // ======================================================================== + // ICloudServiceAgent Interface Implementation - Analytics & Tracking + // ======================================================================== + int track_enable(bool enable) override; + int track_remove_files() override; + int track_event(std::string evt_key, std::string content) override; + int track_header(std::string header) override; + int track_update_property(std::string name, std::string value, std::string type = "string") override; + int track_get_property(std::string name, std::string& value, std::string type = "string") override; + bool get_track_enable() override; + + // ======================================================================== + // ICloudServiceAgent Interface Implementation - Ratings & Reviews + // ======================================================================== + int put_model_mall_rating(int design_id, int score, std::string content, std::vector images, unsigned int& http_code, std::string& http_error) override; + int get_oss_config(std::string& config, std::string country_code, unsigned int& http_code, std::string& http_error) override; + int put_rating_picture_oss(std::string& config, std::string& pic_oss_path, std::string model_id, int profile_id, unsigned int& http_code, std::string& http_error) override; + int get_model_mall_rating_result(int job_id, std::string& rating_result, unsigned int& http_code, std::string& http_error) override; + + // ======================================================================== + // ICloudServiceAgent Interface Implementation - Extra Features + // ======================================================================== + int set_extra_http_header(std::map extra_headers) override; + std::string get_studio_info_url() override; + int get_mw_user_preference(std::function callback) override; + int get_mw_user_4ulist(int seed, int limit, std::function callback) override; + std::string get_version() override; + + // ======================================================================== + // ICloudServiceAgent Interface Implementation - Callbacks + // ======================================================================== + int set_on_user_login_fn(OnUserLoginFn fn) override; + int set_on_server_connected_fn(OnServerConnectedFn fn) override; + int set_on_http_error_fn(OnHttpErrorFn fn) override; + int set_get_country_code_fn(GetCountryCodeFn fn) override; + int set_queue_on_main_fn(QueueOnMainFn fn) override; + + // Sync state management + void load_sync_state(); + void save_sync_state(); + void clear_sync_state(); + const SyncState& get_sync_state() const { return sync_state; } + + // ======================================================================== + // Additional Public Methods - Auth + // ======================================================================== + void set_session_handler(SessionHandler handler); + void set_on_login_complete_handler(OnLoginCompleteHandler handler); + + const PkceBundle& pkce(); + void regenerate_pkce(); + + void persist_refresh_token(const std::string& token); + bool load_refresh_token(std::string& out_token); + void clear_refresh_token(); + + // Token refresh helpers + bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason); + bool refresh_from_storage(const std::string& reason, bool async = false); + bool refresh_now(const std::string& refresh_token, const std::string& reason, bool async = false); + bool refresh_session_with_token(const std::string& refresh_token); + + // Session state helpers + bool set_user_session(const std::string& token, + const std::string& user_id, + const std::string& username, + const std::string& name, + const std::string& nickname, + const std::string& avatar, + const std::string& refresh_token = ""); + void clear_session(); + +private: + // Sync protocol helpers + int sync_pull( + std::function on_success, + std::function on_error + ); + + SyncPushResult sync_push( + const std::string& profile_id, + const std::string& name, + const nlohmann::json& content, + const std::string& original_updated_at = "" + ); + + // HTTP request helpers + int http_get(const std::string& path, std::string* response_body, unsigned int* http_code); + int http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code); + int http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code); + int http_delete(const std::string& path, std::string* response_body, unsigned int* http_code); + std::map data_headers(); + bool attempt_refresh_after_unauthorized(const std::string& reason); + + // Auth HTTP helpers + bool http_post_token(const std::string& body, std::string* response_body, unsigned int* http_code, const std::string& url = ""); + bool http_post_auth(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code); + bool exchange_auth_code(const std::string& auth_code, const std::string& state, std::string& session_payload); + void update_redirect_uri(); + void compute_fallback_path(); + bool decode_jwt_expiry(const std::string& token, std::chrono::system_clock::time_point& out_tp); + bool should_refresh_locked(std::chrono::seconds skew) const; + void invoke_user_login_callback(int online_login, bool login); + + // Callback invocation + void invoke_server_connected_callback(int return_code, int reason_code); + void invoke_http_error_callback(unsigned http_code, const std::string& http_body); + + // JSON helpers + std::string map_to_json(const std::map& map); + void json_to_map(const std::string& json, std::map& map); + + // Member variables - configuration + std::string log_dir; + std::string config_dir; + std::string api_base_url; + std::string auth_base_url; + std::string country_code; + std::map extra_headers; + std::map auth_headers; + mutable std::mutex headers_mutex; + bool m_use_encrypted_token_file{false}; + + // Member variables - auth state + PkceBundle pkce_bundle; + std::string refresh_fallback_path; + SessionHandler session_handler; + OnLoginCompleteHandler on_login_complete_handler; + SessionInfo session; + mutable std::mutex session_mutex; + + // Member variables - connection state + bool is_connected{false}; + bool enable_track{false}; + bool multi_machine_enabled{false}; + + // Sync state + SyncState sync_state; + std::string sync_state_path; + + // Callbacks + OnUserLoginFn on_user_login_fn; + OnServerConnectedFn on_server_connected_fn; + OnHttpErrorFn on_http_error_fn; + GetCountryCodeFn get_country_code_fn; + QueueOnMainFn queue_on_main_fn; + mutable std::mutex callback_mutex; + + // Thread safety + mutable std::recursive_mutex state_mutex; + std::thread refresh_thread; + std::atomic_bool refresh_running{false}; +}; + +} // namespace Slic3r + +#endif // __ORCA_CLOUD_SERVICE_AGENT_HPP__ diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp new file mode 100644 index 0000000000..1002e4068d --- /dev/null +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -0,0 +1,245 @@ +#include "OrcaPrinterAgent.hpp" + +#include + +namespace Slic3r { + +const std::string OrcaPrinterAgent_VERSION = "0.0.1"; + +OrcaPrinterAgent::OrcaPrinterAgent(std::string log_dir) : log_dir(std::move(log_dir)) +{ + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: Constructor - log_dir=" << this->log_dir; +} + +OrcaPrinterAgent::~OrcaPrinterAgent() = default; + +void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr cloud) +{ + std::lock_guard lock(state_mutex); + m_cloud_agent = cloud; + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: Cloud agent set"; +} + +// ============================================================================ +// Communication - All Stubs +// ============================================================================ + +int OrcaPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: send_message (stub) - dev_id=" << dev_id; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: connect_printer (stub) - dev_id=" << dev_id << ", dev_ip=" << dev_ip; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::disconnect_printer() +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: disconnect_printer (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: send_message_to_printer (stub) - dev_id=" << dev_id; + return BAMBU_NETWORK_SUCCESS; +} + +// ============================================================================ +// Certificates - All Stubs +// ============================================================================ + +int OrcaPrinterAgent::check_cert() +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: check_cert (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +void OrcaPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: install_device_cert (stub) - dev_id=" << dev_id; +} + +// ============================================================================ +// Discovery - Stub +// ============================================================================ + +bool OrcaPrinterAgent::start_discovery(bool start, bool sending) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: start_discovery (stub) - start=" << start << ", sending=" << sending; + return true; +} + +// ============================================================================ +// Binding - All Stubs +// ============================================================================ + +int OrcaPrinterAgent::ping_bind(std::string ping_code) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: ping_bind (stub) - ping_code=" << ping_code; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: bind_detect (stub) - dev_ip=" << dev_ip; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::bind( + std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: bind (stub) - dev_id=" << dev_id; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::unbind(std::string dev_id) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: unbind (stub) - dev_id=" << dev_id; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::request_bind_ticket(std::string* ticket) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: request_bind_ticket (stub)"; + if (ticket) + *ticket = ""; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::set_server_callback(OnServerErrFn fn) +{ + std::lock_guard lock(state_mutex); + on_server_err_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +// ============================================================================ +// Machine Selection +// ============================================================================ + +std::string OrcaPrinterAgent::get_user_selected_machine() +{ + std::lock_guard lock(state_mutex); + return selected_machine; +} + +int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) +{ + std::lock_guard lock(state_mutex); + selected_machine = dev_id; + return BAMBU_NETWORK_SUCCESS; +} + +// ============================================================================ +// Agent Information +// ============================================================================ +AgentInfo OrcaPrinterAgent::get_agent_info_static() +{ + return AgentInfo{.id = "orca", + .name = "Orca Printer Agent", + .version = OrcaPrinterAgent_VERSION, + .description = "Orca Printer Communication Protocol Agent"}; +} + +// ============================================================================ +// Print Job Operations - All Stubs +// ============================================================================ + +int OrcaPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: start_print (stub) - task_name=" << params.task_name; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::start_local_print_with_record(PrintParams params, + OnUpdateStatusFn update_fn, + WasCancelledFn cancel_fn, + OnWaitFn wait_fn) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: start_local_print_with_record (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: start_send_gcode_to_sdcard (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: start_local_print (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) +{ + BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: start_sdcard_print (stub)"; + return BAMBU_NETWORK_SUCCESS; +} + +// ============================================================================ +// Callback Registration +// ============================================================================ + +int OrcaPrinterAgent::set_on_ssdp_msg_fn(OnMsgArrivedFn fn) +{ + std::lock_guard lock(state_mutex); + on_ssdp_msg_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::set_on_printer_connected_fn(OnPrinterConnectedFn fn) +{ + std::lock_guard lock(state_mutex); + on_printer_connected_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) +{ + std::lock_guard lock(state_mutex); + on_subscribe_failure_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::set_on_message_fn(OnMessageFn fn) +{ + std::lock_guard lock(state_mutex); + on_message_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::set_on_user_message_fn(OnMessageFn fn) +{ + std::lock_guard lock(state_mutex); + on_user_message_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::set_on_local_connect_fn(OnLocalConnectedFn fn) +{ + std::lock_guard lock(state_mutex); + on_local_connect_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::set_on_local_message_fn(OnMessageFn fn) +{ + std::lock_guard lock(state_mutex); + on_local_message_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) +{ + std::lock_guard lock(state_mutex); + queue_on_main_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/OrcaPrinterAgent.hpp b/src/slic3r/Utils/OrcaPrinterAgent.hpp new file mode 100644 index 0000000000..4f2828f6be --- /dev/null +++ b/src/slic3r/Utils/OrcaPrinterAgent.hpp @@ -0,0 +1,100 @@ +#ifndef __ORCA_PRINTER_AGENT_HPP__ +#define __ORCA_PRINTER_AGENT_HPP__ + +#include "IPrinterAgent.hpp" +#include "ICloudServiceAgent.hpp" +#include +#include +#include + +namespace Slic3r { + +/** + * OrcaPrinterAgent - Stub implementation for printer operations. + * + * All printer-related operations are currently stubs that return success. + * Actual printer connectivity requires the BBL SDK or future Orca implementation. + */ +class OrcaPrinterAgent : public IPrinterAgent { +public: + explicit OrcaPrinterAgent(std::string log_dir); + ~OrcaPrinterAgent() override; + + // ======================================================================== + // IPrinterAgent Interface Implementation + // ======================================================================== + + void set_cloud_agent(std::shared_ptr cloud) override; + + // Communication + int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; + int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; + int disconnect_printer() override; + int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; + + // Certificates + int check_cert() override; + void install_device_cert(std::string dev_id, bool lan_only) override; + + // Discovery + bool start_discovery(bool start, bool sending) override; + + // Binding + int ping_bind(std::string ping_code) override; + int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override; + int bind(std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override; + int unbind(std::string dev_id) override; + int request_bind_ticket(std::string* ticket) override; + int set_server_callback(OnServerErrFn fn) override; + + // Machine Selection + std::string get_user_selected_machine() override; + int set_user_selected_machine(std::string dev_id) override; + + /** + * Get agent information. + * + * @return AgentInfo struct containing agent identification and descriptive information + */ + static AgentInfo get_agent_info_static(); + AgentInfo get_agent_info() override { return get_agent_info_static(); } + + // Print Job Operations + int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; + int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; + int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; + int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override; + int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override; + + // Callbacks + int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override; + int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override; + int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override; + int set_on_message_fn(OnMessageFn fn) override; + int set_on_user_message_fn(OnMessageFn fn) override; + int set_on_local_connect_fn(OnLocalConnectedFn fn) override; + int set_on_local_message_fn(OnMessageFn fn) override; + int set_queue_on_main_fn(QueueOnMainFn fn) override; + +private: + std::string log_dir; + std::string selected_machine; + std::shared_ptr m_cloud_agent; + + // Callbacks + OnMsgArrivedFn on_ssdp_msg_fn; + OnPrinterConnectedFn on_printer_connected_fn; + GetSubscribeFailureFn on_subscribe_failure_fn; + OnMessageFn on_message_fn; + OnMessageFn on_user_message_fn; + OnLocalConnectedFn on_local_connect_fn; + OnMessageFn on_local_message_fn; + QueueOnMainFn queue_on_main_fn; + OnServerErrFn on_server_err_fn; + + mutable std::mutex state_mutex; +}; + +} // namespace Slic3r + +#endif // __ORCA_PRINTER_AGENT_HPP__ diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index 26df92f7d4..945e66acfd 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -849,7 +849,7 @@ void PresetUpdater::priv::sync_plugins(std::string http_url, std::string plugin_ BOOST_LOG_TRIVIAL(info) << "non need to sync plugins for there is no plugins currently."; return; } - std::string curr_version = NetworkAgent::use_legacy_network ? BAMBU_NETWORK_AGENT_VERSION_LEGACY : BBL::get_latest_network_version(); + std::string curr_version = NetworkAgent::use_legacy_network ? BAMBU_NETWORK_AGENT_VERSION_LEGACY : get_latest_network_version(); std::string using_version = curr_version.substr(0, 9) + "00"; std::string cached_version; diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp new file mode 100644 index 0000000000..86bcdeb8c0 --- /dev/null +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -0,0 +1,1566 @@ +#include "QidiPrinterAgent.hpp" +#include "Http.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/DeviceCore/DevFilaSystem.h" +#include "slic3r/GUI/DeviceCore/DevManager.h" + +#include "nlohmann/json.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +namespace beast = boost::beast; +namespace http = beast::http; +namespace websocket = beast::websocket; +namespace net = boost::asio; +using tcp = net::ip::tcp; + +std::string to_hex_string(uint64_t value) +{ + std::ostringstream stream; + stream << std::hex << std::uppercase << value; + return stream.str(); +} + +bool looks_like_host(const std::string& value) +{ + if (value.empty()) { + return false; + } + if (value.find(' ') != std::string::npos) { + return false; + } + return value.find('.') != std::string::npos || value.find(':') != std::string::npos; +} + +constexpr const char* k_no_api_key = "__NO_API_KEY__"; + +bool is_numeric(const std::string& value) +{ + return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c) != 0; }); +} + +std::string normalize_base_url(std::string host, const std::string& port) +{ + boost::trim(host); + if (host.empty()) { + return ""; + } + + std::string value = host; + if (is_numeric(port) && value.find("://") == std::string::npos && value.find(':') == std::string::npos) { + value += ":" + port; + } + + if (!boost::istarts_with(value, "http://") && !boost::istarts_with(value, "https://")) { + value = "http://" + value; + } + + if (value.size() > 1 && value.back() == '/') { + value.pop_back(); + } + + return value; +} + +std::string extract_host(const std::string& base_url) +{ + std::string host = base_url; + auto pos = host.find("://"); + if (pos != std::string::npos) { + host = host.substr(pos + 3); + } + pos = host.find('/'); + if (pos != std::string::npos) { + host = host.substr(0, pos); + } + return host; +} + +std::string join_url(const std::string& base_url, const std::string& path) +{ + if (base_url.empty()) { + return ""; + } + if (path.empty()) { + return base_url; + } + if (base_url.back() == '/' && path.front() == '/') { + return base_url.substr(0, base_url.size() - 1) + path; + } + if (base_url.back() != '/' && path.front() != '/') { + return base_url + "/" + path; + } + return base_url + path; +} + +std::string normalize_api_key(const std::string& api_key) +{ + if (api_key.empty() || api_key == k_no_api_key) { + return ""; + } + return api_key; +} + +std::string normalize_model_key(std::string value) +{ + boost::algorithm::to_lower(value); + std::string normalized; + normalized.reserve(value.size()); + for (unsigned char c : value) { + if (std::isalnum(c)) { + normalized.push_back(static_cast(c)); + } + } + return normalized; +} + +std::string infer_series_id(const std::string& model_id, const std::string& dev_name) +{ + std::string source = model_id.empty() ? dev_name : model_id; + boost::trim(source); + if (source.empty()) { + return ""; + } + if (is_numeric(source)) { + return source; + } + + const std::string key = normalize_model_key(source); + if (key.find("q2") != std::string::npos) { + return "1"; + } + if (key.find("xmax") != std::string::npos && key.find("4") != std::string::npos) { + return "3"; + } + if ((key.find("xplus") != std::string::npos || key.find("plus") != std::string::npos) && key.find("4") != std::string::npos) { + return "0"; + } + return ""; +} + +struct WsEndpoint +{ + std::string host; + std::string port; + std::string target; + bool secure = false; +}; + +bool parse_ws_endpoint(const std::string& base_url, WsEndpoint& endpoint) +{ + if (base_url.empty()) { + return false; + } + + std::string url = base_url; + if (boost::istarts_with(url, "https://")) { + endpoint.secure = true; + url = url.substr(8); + } else if (boost::istarts_with(url, "http://")) { + url = url.substr(7); + } + + auto slash = url.find('/'); + if (slash != std::string::npos) { + url = url.substr(0, slash); + } + if (url.empty()) { + return false; + } + + endpoint.host = url; + endpoint.port = endpoint.secure ? "443" : "80"; + if (auto colon = url.rfind(':'); colon != std::string::npos && url.find(']') == std::string::npos) { + endpoint.host = url.substr(0, colon); + endpoint.port = url.substr(colon + 1); + } + + endpoint.target = "/websocket"; + return !endpoint.host.empty() && !endpoint.port.empty(); +} + +std::string map_moonraker_state(std::string state) +{ + boost::algorithm::to_lower(state); + if (state == "printing") { + return "RUNNING"; + } + if (state == "paused") { + return "PAUSE"; + } + if (state == "complete") { + return "FINISH"; + } + if (state == "error" || state == "cancelled") { + return "FAILED"; + } + return "IDLE"; +} + +std::string normalize_filament_type(const std::string& filament_type) +{ + std::string trimmed = filament_type; + boost::trim(trimmed); + std::string upper = trimmed; + std::transform(upper.begin(), upper.end(), upper.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); + + if (upper.find("PLA") != std::string::npos) + return "PLA"; + if (upper.find("ABS") != std::string::npos) + return "ABS"; + if (upper.find("PETG") != std::string::npos) + return "PETG"; + if (upper.find("TPU") != std::string::npos) + return "TPU"; + if (upper.find("ASA") != std::string::npos) + return "ASA"; + if (upper.find("PA") != std::string::npos || upper.find("NYLON") != std::string::npos) + return "PA"; + if (upper.find("PC") != std::string::npos) + return "PC"; + if (upper.find("PVA") != std::string::npos) + return "PVA"; + + return trimmed; +} +} // namespace + +namespace Slic3r { + +const std::string QidiPrinterAgent_VERSION = "0.0.1"; + +QidiPrinterAgent::QidiPrinterAgent(std::string log_dir) : OrcaPrinterAgent(std::move(log_dir)) +{ + BOOST_LOG_TRIVIAL(info) << "QidiPrinterAgent: Constructor"; +} + +QidiPrinterAgent::~QidiPrinterAgent() +{ + stop_status_stream(); +} + +AgentInfo QidiPrinterAgent::get_agent_info_static() +{ + return AgentInfo{.id = "qidi", .name = "Qidi Printer Agent", .version = QidiPrinterAgent_VERSION, .description = "Qidi printer agent"}; +} + +int QidiPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) +{ + (void) qos; + (void) flag; + return handle_request(dev_id, json_str); +} + +int QidiPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) +{ + (void) qos; + (void) flag; + return handle_request(dev_id, json_str); +} + +int QidiPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) +{ + (void) username; + (void) use_ssl; + std::string base_url = normalize_base_url(dev_ip, ""); + std::string api_key = normalize_api_key(password); + + PrinthostConfig config; + if (get_printhost_config(config)) { + if (base_url.empty()) { + base_url = config.base_url; + } + if (api_key.empty()) { + api_key = normalize_api_key(config.api_key); + } + } + + if (base_url.empty()) { + BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent: connect_printer missing host for dev_id=" << dev_id; + dispatch_local_connect(ConnectStatusFailed, dev_id, "host_missing"); + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + if (dev_id.empty()) { + dev_id = extract_host(base_url); + } + + { + std::lock_guard lock(payload_mutex); + status_cache = nlohmann::json::object(); + last_ams_payload = nlohmann::json(); + } + ws_last_emit_ms.store(0); + + store_host(dev_id, base_url, api_key); + start_status_stream(dev_id, base_url, api_key); + dispatch_local_connect(ConnectStatusOk, dev_id, "0"); + dispatch_printer_connected(dev_id); + BOOST_LOG_TRIVIAL(info) << "QidiPrinterAgent: connect_printer - dev_id=" << dev_id << ", dev_ip=" << dev_ip; + return BAMBU_NETWORK_SUCCESS; +} + +int QidiPrinterAgent::disconnect_printer() +{ + stop_status_stream(); + return BAMBU_NETWORK_SUCCESS; +} + +bool QidiPrinterAgent::start_discovery(bool start, bool sending) +{ + (void) sending; + if (start) { + announce_printhost_device(); + } + return true; +} + +int QidiPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) +{ + (void) sec_link; + + std::string base_url = normalize_base_url(dev_ip, ""); + if (base_url.empty()) { + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + PrinthostConfig config; + get_printhost_config(config); + const std::string api_key = normalize_api_key(config.api_key); + + QidiDeviceInfo info; + std::string error; + if (!fetch_device_info(base_url, api_key, info, error)) { + BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent: bind_detect failed: " << error; + return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; + } + + detect.dev_id = info.dev_id.empty() ? dev_ip : info.dev_id; + if (!info.model_id.empty()) { + detect.model_id = info.model_id; + } else if (!config.model_id.empty()) { + detect.model_id = config.model_id; + } else { + detect.model_id = config.model_name; + } + detect.dev_name = info.dev_name.empty() ? config.model_name : info.dev_name; + detect.version = info.version; + detect.connect_type = "lan"; + detect.bind_state = "free"; + + return BAMBU_NETWORK_SUCCESS; +} + +int QidiPrinterAgent::set_on_ssdp_msg_fn(OnMsgArrivedFn fn) +{ + { + std::lock_guard lock(state_mutex); + on_ssdp_msg_fn = fn; + } + if (fn) { + announce_printhost_device(); + } + return BAMBU_NETWORK_SUCCESS; +} + +int QidiPrinterAgent::set_on_printer_connected_fn(OnPrinterConnectedFn fn) +{ + std::lock_guard lock(state_mutex); + on_printer_connected_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int QidiPrinterAgent::set_on_message_fn(OnMessageFn fn) +{ + std::lock_guard lock(state_mutex); + on_message_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int QidiPrinterAgent::set_on_local_connect_fn(OnLocalConnectedFn fn) +{ + std::lock_guard lock(state_mutex); + on_local_connect_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int QidiPrinterAgent::set_on_local_message_fn(OnMessageFn fn) +{ + std::lock_guard lock(state_mutex); + on_local_message_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int QidiPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) +{ + std::lock_guard lock(state_mutex); + queue_on_main_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +void QidiPrinterAgent::fetch_filament_info(std::string dev_id) +{ + // Look up MachineObject via DeviceManager + auto* dev_manager = GUI::wxGetApp().getDeviceManager(); + if (!dev_manager) { + BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: DeviceManager is null"; + return; + } + MachineObject* obj = dev_manager->get_my_machine(dev_id); + if (!obj) { + BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: MachineObject not found for dev_id=" << dev_id; + return; + } + + const std::string base_url = resolve_host(dev_id); + if (base_url.empty()) { + BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: Missing host for dev_id=" << dev_id; + return; + } + const std::string api_key = resolve_api_key(dev_id, ""); + + std::vector slots; + int box_count = 0; + std::string error; + if (!fetch_slot_info(base_url, api_key, slots, box_count, error)) { + BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error; + return; + } + + QidiFilamentDict dict; + if (!fetch_filament_dict(base_url, api_key, dict, error)) { + BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error; + } + + std::string series_id; + { + QidiDeviceInfo info; + std::string device_error; + if (fetch_device_info(base_url, api_key, info, device_error)) { + series_id = infer_series_id(info.model_id, info.dev_name); + } + } + + auto build_setting_id = [&](const QidiSlotInfo& slot, const std::string& tray_type) { + const int vendor = (slot.vendor_type == 1) ? 1 : 0; + if (is_numeric(series_id) && slot.filament_type > 0) { + return "QD_" + series_id + "_" + std::to_string(vendor) + "_" + std::to_string(slot.filament_type); + } + return map_filament_type_to_setting_id(tray_type); + }; + + // Build BBL-format JSON for DevFilaSystemParser::ParseV1_0 + // The expected format matches BBL's print.push_status AMS subset + nlohmann::json ams_json = nlohmann::json::object(); + nlohmann::json ams_array = nlohmann::json::array(); + + // Calculate ams_exist_bits and tray_exist_bits + unsigned long ams_exist_bits = 0; + unsigned long tray_exist_bits = 0; + + for (int ams_id = 0; ams_id < box_count; ++ams_id) { + ams_exist_bits |= (1 << ams_id); + + nlohmann::json ams_unit = nlohmann::json::object(); + ams_unit["id"] = std::to_string(ams_id); + ams_unit["info"] = "2100"; // AMS_LITE type (2), main extruder (0) + + nlohmann::json tray_array = nlohmann::json::array(); + for (int slot_id = 0; slot_id < 4; ++slot_id) { + const int slot_index = ams_id * 4 + slot_id; + const QidiSlotInfo slot = slot_index < static_cast(slots.size()) ? slots[slot_index] : QidiSlotInfo{}; + + nlohmann::json tray_json = nlohmann::json::object(); + tray_json["id"] = std::to_string(slot_id); + tray_json["tag_uid"] = "0000000000000000"; + + if (slot.filament_exists) { + tray_exist_bits |= (1 << slot_index); + + std::string filament_type = "PLA"; + auto filament_it = dict.filaments.find(slot.filament_type); + if (filament_it != dict.filaments.end()) { + filament_type = filament_it->second; + } + std::string tray_type = normalize_filament_type(filament_type); + std::string setting_id = build_setting_id(slot, tray_type); + + std::string color = "FFFFFFFF"; + auto color_it = dict.colors.find(slot.color_index); + if (color_it != dict.colors.end()) { + color = normalize_color(color_it->second); + } + + tray_json["tray_info_idx"] = setting_id; + tray_json["tray_type"] = tray_type; + tray_json["tray_color"] = color; + } else { + tray_json["tray_info_idx"] = ""; + tray_json["tray_type"] = ""; + tray_json["tray_color"] = "00000000"; + } + + tray_array.push_back(tray_json); + } + ams_unit["tray"] = tray_array; + ams_array.push_back(ams_unit); + } + + // Format as hex strings (matching BBL protocol) + std::ostringstream ams_exist_ss; + ams_exist_ss << std::hex << std::uppercase << ams_exist_bits; + std::ostringstream tray_exist_ss; + tray_exist_ss << std::hex << std::uppercase << tray_exist_bits; + + ams_json["ams"] = ams_array; + ams_json["ams_exist_bits"] = ams_exist_ss.str(); + ams_json["tray_exist_bits"] = tray_exist_ss.str(); + + // Wrap in the expected structure for ParseV1_0 + nlohmann::json print_json = nlohmann::json::object(); + print_json["ams"] = ams_json; + + // Call the parser to populate DevFilaSystem + DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem(), false); + + BOOST_LOG_TRIVIAL(info) << "QidiPrinterAgent::fetch_filament_info: Populated DevFilaSystem with " + << box_count << " AMS units"; +} + +int QidiPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) +{ + auto json = nlohmann::json::parse(json_str, nullptr, false); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent: Invalid JSON request"; + return BAMBU_NETWORK_ERR_INVALID_RESULT; + } + + if (json.contains("info") && json["info"].contains("command")) { + const auto& command = json["info"]["command"]; + if (command.is_string() && command.get() == "get_version") { + return send_version_info(dev_id); + } + } + + if (json.contains("system") && json["system"].contains("command")) { + const auto& command = json["system"]["command"]; + if (command.is_string() && command.get() == "get_access_code") { + return send_access_code(dev_id); + } + } + + // if (json.contains("pushing") && json["pushing"].contains("command")) { + // const auto& command = json["pushing"]["command"]; + // if (command.is_string()) { + // const auto cmd = command.get(); + // if (cmd == "pushall" || cmd == "start") { + // return sync_filament_list(dev_id); + // } + // } + // } + + return BAMBU_NETWORK_SUCCESS; +} + +bool QidiPrinterAgent::get_printhost_config(PrinthostConfig& config) const +{ + auto* preset_bundle = GUI::wxGetApp().preset_bundle; + if (!preset_bundle) { + return false; + } + + auto& preset = preset_bundle->printers.get_edited_preset(); + const auto& printer_cfg = preset.config; + const DynamicPrintConfig* host_cfg = &printer_cfg; + config.host = host_cfg->opt_string("print_host"); + if (config.host.empty()) { + if (auto* physical_cfg = preset_bundle->physical_printers.get_selected_printer_config()) { + if (!physical_cfg->opt_string("print_host").empty()) { + host_cfg = physical_cfg; + config.host = host_cfg->opt_string("print_host"); + } + } + } + if (config.host.empty()) { + return false; + } + + config.port = host_cfg->opt_string("printhost_port"); + config.api_key = host_cfg->opt_string("printhost_apikey"); + config.model_id = preset.get_printer_type(preset_bundle); + config.model_name = printer_cfg.opt_string("printer_model"); + config.base_url = normalize_base_url(config.host, config.port); + + return !config.base_url.empty(); +} + +bool QidiPrinterAgent::fetch_device_info(const std::string& base_url, + const std::string& api_key, + QidiDeviceInfo& info, + std::string& error) const +{ + auto fetch_json = [&](const std::string& url, nlohmann::json& out) { + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(10) + .timeout_max(30) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + out = nlohmann::json::parse(response_body, nullptr, false, true); + if (out.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + return true; + }; + + nlohmann::json json; + std::string url = join_url(base_url, "/machine/device_info"); + if (!fetch_json(url, json)) { + url = join_url(base_url, "/printer/info"); + if (!fetch_json(url, json)) { + return false; + } + } + + nlohmann::json result = json.contains("result") ? json["result"] : json; + info.dev_name = result.value("machine_name", result.value("hostname", "")); + info.dev_id = result.value("machine_uuid", ""); + if (info.dev_id.empty()) { + info.dev_id = result.value("serial_number", ""); + } + info.model_id = result.value("model", ""); + info.version = result.value("software_version", result.value("firmware_version", "")); + + return true; +} + +bool QidiPrinterAgent::fetch_server_info(const std::string& base_url, + const std::string& api_key, + std::string& version, + std::string& error) const +{ + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(join_url(base_url, "/server/info")); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(10) + .timeout_max(30) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + + nlohmann::json result = json.contains("result") ? json["result"] : json; + if (result.contains("moonraker_version") && result["moonraker_version"].is_string()) { + version = result["moonraker_version"].get(); + } else if (result.contains("version") && result["version"].is_string()) { + version = result["version"].get(); + } + + return true; +} + +bool QidiPrinterAgent::fetch_object_list(const std::string& base_url, + const std::string& api_key, + std::set& objects, + std::string& error) const +{ + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(join_url(base_url, "/printer/objects/list")); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(10) + .timeout_max(30) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + + nlohmann::json result = json.contains("result") ? json["result"] : json; + if (!result.contains("objects") || !result["objects"].is_array()) { + error = "Unexpected JSON structure"; + return false; + } + + objects.clear(); + for (const auto& entry : result["objects"]) { + if (entry.is_string()) { + objects.insert(entry.get()); + } + } + + return !objects.empty(); +} + +int QidiPrinterAgent::send_version_info(const std::string& dev_id) +{ + const std::string base_url = resolve_host(dev_id); + if (base_url.empty()) { + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + const std::string api_key = resolve_api_key(dev_id, ""); + + std::string version; + std::string error; + if (!fetch_server_info(base_url, api_key, version, error)) { + BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: Failed to fetch server info: " << error; + } + if (version.empty()) { + version = "moonraker"; + } + + nlohmann::json payload; + payload["info"]["command"] = "get_version"; + payload["info"]["result"] = "success"; + payload["info"]["module"] = nlohmann::json::array(); + + nlohmann::json module; + module["name"] = "ota"; + module["sw_ver"] = version; + module["product_name"] = "Moonraker"; + payload["info"]["module"].push_back(module); + + dispatch_message(dev_id, payload.dump()); + return BAMBU_NETWORK_SUCCESS; +} + +int QidiPrinterAgent::send_access_code(const std::string& dev_id) +{ + nlohmann::json payload; + payload["system"]["command"] = "get_access_code"; + payload["system"]["access_code"] = resolve_api_key(dev_id, ""); + dispatch_message(dev_id, payload.dump()); + return BAMBU_NETWORK_SUCCESS; +} + +void QidiPrinterAgent::announce_printhost_device() +{ + PrinthostConfig config; + if (!get_printhost_config(config)) { + return; + } + + const std::string base_url = config.base_url; + if (base_url.empty()) { + return; + } + + OnMsgArrivedFn ssdp_fn; + { + std::lock_guard lock(state_mutex); + ssdp_fn = on_ssdp_msg_fn; + if (!ssdp_fn) { + return; + } + if (ssdp_announced_host == base_url && !ssdp_announced_id.empty()) { + return; + } + } + + const std::string dev_id = extract_host(base_url); + const std::string dev_name = config.model_name.empty() ? "Qidi Printer" : config.model_name; + const std::string model_id = config.model_id; + + if (auto* app_config = GUI::wxGetApp().app_config) { + const std::string access_code = normalize_api_key(config.api_key).empty() ? k_no_api_key : config.api_key; + app_config->set_str("access_code", dev_id, access_code); + app_config->set_str("user_access_code", dev_id, access_code); + } + + store_host(dev_id, base_url, normalize_api_key(config.api_key)); + + nlohmann::json payload; + payload["dev_name"] = dev_name; + payload["dev_id"] = dev_id; + payload["dev_ip"] = extract_host(base_url); + payload["dev_type"] = model_id.empty() ? dev_name : model_id; + payload["dev_signal"] = "0"; + payload["connect_type"] = "lan"; + payload["bind_state"] = "free"; + payload["sec_link"] = "secure"; + payload["ssdp_version"] = "v1"; + + ssdp_fn(payload.dump()); + + std::lock_guard lock(state_mutex); + ssdp_announced_host = base_url; + ssdp_announced_id = dev_id; +} + +void QidiPrinterAgent::dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg) +{ + OnLocalConnectedFn local_fn; + QueueOnMainFn queue_fn; + { + std::lock_guard lock(state_mutex); + local_fn = on_local_connect_fn; + queue_fn = queue_on_main_fn; + } + if (!local_fn) { + return; + } + + auto dispatch = [state, dev_id, msg, local_fn]() { local_fn(state, dev_id, msg); }; + if (queue_fn) { + queue_fn(dispatch); + } else { + dispatch(); + } +} + +void QidiPrinterAgent::dispatch_printer_connected(const std::string& dev_id) +{ + OnPrinterConnectedFn connected_fn; + QueueOnMainFn queue_fn; + { + std::lock_guard lock(state_mutex); + connected_fn = on_printer_connected_fn; + queue_fn = queue_on_main_fn; + } + if (!connected_fn) { + return; + } + + auto dispatch = [dev_id, connected_fn]() { connected_fn(dev_id); }; + if (queue_fn) { + queue_fn(dispatch); + } else { + dispatch(); + } +} + +void QidiPrinterAgent::start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key) +{ + stop_status_stream(); + if (base_url.empty()) { + return; + } + + ws_stop.store(false); + ws_thread = std::thread([this, dev_id, base_url, api_key]() { + run_status_stream(dev_id, base_url, api_key); + }); +} + +void QidiPrinterAgent::stop_status_stream() +{ + ws_stop.store(true); + if (ws_thread.joinable()) { + ws_thread.join(); + } +} + +void QidiPrinterAgent::run_status_stream(std::string dev_id, std::string base_url, std::string api_key) +{ + WsEndpoint endpoint; + if (!parse_ws_endpoint(base_url, endpoint)) { + BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: websocket endpoint invalid for base_url=" << base_url; + return; + } + if (endpoint.secure) { + BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: websocket wss not supported for base_url=" << base_url; + return; + } + + try { + net::io_context ioc; + tcp::resolver resolver{ioc}; + beast::tcp_stream stream{ioc}; + + stream.expires_after(std::chrono::seconds(10)); + auto const results = resolver.resolve(endpoint.host, endpoint.port); + stream.connect(results); + + websocket::stream ws{std::move(stream)}; + ws.set_option(websocket::stream_base::decorator([&](websocket::request_type& req) { + req.set(http::field::user_agent, "OrcaSlicer"); + if (!api_key.empty()) { + req.set("X-Api-Key", api_key); + } + })); + + std::string host_header = endpoint.host; + if (!endpoint.port.empty() && endpoint.port != "80") { + host_header += ":" + endpoint.port; + } + ws.handshake(host_header, endpoint.target); + ws.text(true); + + std::set subscribe_objects = {"print_stats", "virtual_sdcard"}; + std::set available_objects; + std::string list_error; + if (fetch_object_list(base_url, api_key, available_objects, list_error)) { + if (available_objects.count("heater_bed") != 0) { + subscribe_objects.insert("heater_bed"); + } + if (available_objects.count("fan") != 0) { + subscribe_objects.insert("fan"); + } + + for (const auto& name : available_objects) { + if (name == "extruder" || name.rfind("extruder", 0) == 0) { + subscribe_objects.insert(name); + if (name == "extruder") { + break; + } + } + } + } else { + BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: object list unavailable: " << list_error; + subscribe_objects.insert("extruder"); + subscribe_objects.insert("heater_bed"); + subscribe_objects.insert("fan"); + } + + nlohmann::json subscribe; + subscribe["jsonrpc"] = "2.0"; + subscribe["method"] = "printer.objects.subscribe"; + nlohmann::json objects = nlohmann::json::object(); + for (const auto& name : subscribe_objects) { + objects[name] = nullptr; + } + subscribe["params"]["objects"] = std::move(objects); + subscribe["id"] = 1; + ws.write(net::buffer(subscribe.dump())); + + while (!ws_stop.load()) { + ws.next_layer().expires_after(std::chrono::seconds(2)); + beast::flat_buffer buffer; + beast::error_code ec; + ws.read(buffer, ec); + if (ec == beast::error::timeout) { + const auto now_ms = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + const auto last_ms = ws_last_emit_ms.load(); + if (last_ms == 0 || now_ms - last_ms >= 10000) { + nlohmann::json message; + { + std::lock_guard lock(payload_mutex); + message = build_print_payload_locked(nullptr); + } + dispatch_message(dev_id, message.dump()); + ws_last_emit_ms.store(now_ms); + } + continue; + } + if (ec == websocket::error::closed) { + break; + } + if (ec) { + BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: websocket read error: " << ec.message(); + break; + } + handle_ws_message(dev_id, beast::buffers_to_string(buffer.data())); + } + + beast::error_code ec; + ws.close(websocket::close_code::normal, ec); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: websocket exception: " << e.what(); + } +} + +void QidiPrinterAgent::handle_ws_message(const std::string& dev_id, const std::string& payload) +{ + auto json = nlohmann::json::parse(payload, nullptr, false); + if (json.is_discarded()) { + return; + } + + bool updated = false; + if (json.contains("result") && json["result"].contains("status") && + json["result"]["status"].is_object()) { + update_status_cache(json["result"]["status"]); + updated = true; + } + + if (json.contains("method") && json["method"].is_string()) { + const std::string method = json["method"].get(); + if (method == "notify_status_update" && json.contains("params") && + json["params"].is_array() && !json["params"].empty() && + json["params"][0].is_object()) { + update_status_cache(json["params"][0]); + updated = true; + } else if (method == "notify_klippy_ready") { + nlohmann::json updates; + updates["print_stats"]["state"] = "standby"; + update_status_cache(updates); + updated = true; + } else if (method == "notify_klippy_shutdown") { + nlohmann::json updates; + updates["print_stats"]["state"] = "error"; + update_status_cache(updates); + updated = true; + } + } + + if (updated) { + nlohmann::json message; + { + std::lock_guard lock(payload_mutex); + message = build_print_payload_locked(nullptr); + } + dispatch_message(dev_id, message.dump()); + const auto now_ms = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + ws_last_emit_ms.store(now_ms); + } +} + +void QidiPrinterAgent::update_status_cache(const nlohmann::json& updates) +{ + if (!updates.is_object()) { + return; + } + + std::lock_guard lock(payload_mutex); + if (!status_cache.is_object()) { + status_cache = nlohmann::json::object(); + } + + for (const auto& item : updates.items()) { + if (item.value().is_object()) { + nlohmann::json& target = status_cache[item.key()]; + if (!target.is_object()) { + target = nlohmann::json::object(); + } + for (const auto& field : item.value().items()) { + target[field.key()] = field.value(); + } + } else { + status_cache[item.key()] = item.value(); + } + } +} + +nlohmann::json QidiPrinterAgent::build_print_payload_locked(const nlohmann::json* ams_override) const +{ + nlohmann::json payload; + payload["print"]["command"] = "push_status"; + payload["print"]["msg"] = 0; + payload["print"]["support_mqtt_alive"] = true; + + if (ams_override) { + payload["print"]["ams"] = *ams_override; + } else if (!last_ams_payload.is_null()) { + payload["print"]["ams"] = last_ams_payload; + } + + std::string state = "IDLE"; + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("state") && + status_cache["print_stats"]["state"].is_string()) { + state = map_moonraker_state(status_cache["print_stats"]["state"].get()); + } + payload["print"]["gcode_state"] = state; + + const nlohmann::json* extruder = nullptr; + if (status_cache.contains("extruder") && status_cache["extruder"].is_object()) { + extruder = &status_cache["extruder"]; + } else { + for (const auto& item : status_cache.items()) { + if (item.value().is_object() && item.key().rfind("extruder", 0) == 0) { + extruder = &item.value(); + break; + } + } + } + + if (extruder) { + if (extruder->contains("temperature") && (*extruder)["temperature"].is_number()) { + payload["print"]["nozzle_temper"] = (*extruder)["temperature"].get(); + } + if (extruder->contains("target") && (*extruder)["target"].is_number()) { + payload["print"]["nozzle_target_temper"] = (*extruder)["target"].get(); + } + } + + if (status_cache.contains("heater_bed") && status_cache["heater_bed"].is_object()) { + const auto& bed = status_cache["heater_bed"]; + if (bed.contains("temperature") && bed["temperature"].is_number()) { + payload["print"]["bed_temper"] = bed["temperature"].get(); + } + if (bed.contains("target") && bed["target"].is_number()) { + payload["print"]["bed_target_temper"] = bed["target"].get(); + } + } + + if (status_cache.contains("fan") && status_cache["fan"].is_object()) { + const auto& fan = status_cache["fan"]; + if (fan.contains("speed") && fan["speed"].is_number()) { + double speed = fan["speed"].get(); + int pwm = 0; + if (speed <= 1.0) { + pwm = static_cast(speed * 255.0 + 0.5); + } else { + pwm = static_cast(speed + 0.5); + } + pwm = std::clamp(pwm, 0, 255); + payload["print"]["fan_gear"] = pwm; + } + } + + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("filename") && + status_cache["print_stats"]["filename"].is_string()) { + payload["print"]["subtask_name"] = status_cache["print_stats"]["filename"].get(); + } + + int mc_percent = -1; + if (status_cache.contains("virtual_sdcard") && + status_cache["virtual_sdcard"].contains("progress") && + status_cache["virtual_sdcard"]["progress"].is_number()) { + const double progress = status_cache["virtual_sdcard"]["progress"].get(); + if (progress >= 0.0) { + mc_percent = std::clamp(static_cast(progress * 100.0 + 0.5), 0, 100); + } + } + if (mc_percent >= 0) { + payload["print"]["mc_percent"] = mc_percent; + } + + if (status_cache.contains("print_stats") && + status_cache["print_stats"].contains("total_duration") && + status_cache["print_stats"].contains("print_duration") && + status_cache["print_stats"]["total_duration"].is_number() && + status_cache["print_stats"]["print_duration"].is_number()) { + const double total = status_cache["print_stats"]["total_duration"].get(); + const double elapsed = status_cache["print_stats"]["print_duration"].get(); + if (total > 0.0 && elapsed >= 0.0) { + const auto remaining_minutes = std::max(0, static_cast((total - elapsed) / 60.0)); + payload["print"]["mc_remaining_time"] = remaining_minutes; + } + } + + const auto now_ms = static_cast( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count()); + payload["t_utc"] = now_ms; + + return payload; +} + +std::string QidiPrinterAgent::resolve_host(const std::string& dev_id) const +{ + { + std::lock_guard lock(state_mutex); + auto it = host_by_device.find(dev_id); + if (it != host_by_device.end()) { + return it->second; + } + } + + PrinthostConfig config; + if (get_printhost_config(config)) { + return config.base_url; + } + + return looks_like_host(dev_id) ? normalize_base_url(dev_id, "") : ""; +} + +std::string QidiPrinterAgent::resolve_api_key(const std::string& dev_id, const std::string& fallback) const +{ + std::string api_key = normalize_api_key(fallback); + if (!api_key.empty()) { + return api_key; + } + + { + std::lock_guard lock(state_mutex); + auto it = api_key_by_device.find(dev_id); + if (it != api_key_by_device.end() && !it->second.empty()) { + return it->second; + } + } + + PrinthostConfig config; + if (get_printhost_config(config)) { + return normalize_api_key(config.api_key); + } + + return ""; +} + +void QidiPrinterAgent::store_host(const std::string& dev_id, const std::string& host, const std::string& api_key) +{ + if (host.empty()) { + return; + } + std::lock_guard lock(state_mutex); + host_by_device[dev_id] = host; + if (!api_key.empty()) { + api_key_by_device[dev_id] = api_key; + } +} + +bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, + const std::string& api_key, + std::vector& slots, + int& box_count, + std::string& error) const +{ + std::string url = join_url(base_url, "/printer/objects/query?save_variables=variables"); + for (int i = 0; i < 16; ++i) { + url += "&box_stepper%20slot" + std::to_string(i) + "=runout_button"; + } + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(10) + .timeout_max(30) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + + if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("save_variables") || + !json["result"]["status"]["save_variables"].contains("variables")) { + error = "Unexpected JSON structure"; + return false; + } + + auto& variables = json["result"]["status"]["save_variables"]["variables"]; + auto& status = json["result"]["status"]; + + box_count = variables.value("box_count", 1); + if (box_count < 0) { + box_count = 0; + } + + const int max_slots = box_count * 4; + slots.clear(); + slots.reserve(max_slots); + + for (int i = 0; i < max_slots; ++i) { + QidiSlotInfo slot; + slot.slot_index = i; + slot.color_index = variables.value("color_slot" + std::to_string(i), 1); + slot.filament_type = variables.value("filament_slot" + std::to_string(i), 1); + slot.vendor_type = variables.value("vendor_slot" + std::to_string(i), 0); + + std::string box_stepper_key = "box_stepper slot" + std::to_string(i); + slot.filament_exists = false; + if (status.contains(box_stepper_key)) { + auto& box_stepper = status[box_stepper_key]; + if (box_stepper.contains("runout_button") && !box_stepper["runout_button"].is_null()) { + int runout_button = box_stepper["runout_button"].get(); + slot.filament_exists = (runout_button == 0); + } + } + slots.push_back(slot); + } + + return true; +} + +void QidiPrinterAgent::parse_ini_section(const std::string& content, const std::string& section_name, std::map& result) +{ + std::istringstream stream(content); + std::string line; + bool in_section = false; + std::string section_header = "[" + section_name + "]"; + + while (std::getline(stream, line)) { + boost::trim(line); + if (!line.empty() && line[0] == '[') { + in_section = (line == section_header); + continue; + } + if (line.empty() || line[0] == '#' || line[0] == ';') { + continue; + } + if (in_section) { + auto pos = line.find('='); + if (pos != std::string::npos) { + std::string key = line.substr(0, pos); + std::string value = line.substr(pos + 1); + boost::trim(key); + boost::trim(value); + try { + int index = std::stoi(key); + result[index] = value; + } catch (...) {} + } + } + } +} + +void QidiPrinterAgent::parse_filament_sections(const std::string& content, std::map& result) +{ + std::istringstream stream(content); + std::string line; + int current_fila_index = -1; + + while (std::getline(stream, line)) { + boost::trim(line); + if (!line.empty() && line[0] == '[') { + current_fila_index = -1; + if (line.size() > 5 && line.substr(0, 5) == "[fila" && line.back() == ']') { + std::string num_str = line.substr(5, line.size() - 6); + try { + current_fila_index = std::stoi(num_str); + } catch (...) { + current_fila_index = -1; + } + } + continue; + } + if (line.empty() || line[0] == '#' || line[0] == ';') { + continue; + } + if (current_fila_index > 0) { + auto pos = line.find('='); + if (pos != std::string::npos) { + std::string key = line.substr(0, pos); + std::string value = line.substr(pos + 1); + boost::trim(key); + boost::trim(value); + if (key == "filament") { + result[current_fila_index] = value; + } + } + } + } +} + +bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url, + const std::string& api_key, + QidiFilamentDict& dict, + std::string& error) const +{ + std::string url = join_url(base_url, "/server/files/config/officiall_filas_list.cfg"); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(10) + .timeout_max(30) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + dict.colors.clear(); + dict.filaments.clear(); + parse_ini_section(response_body, "colordict", dict.colors); + parse_filament_sections(response_body, dict.filaments); + + return !dict.colors.empty(); +} + +std::string QidiPrinterAgent::normalize_color(const std::string& color) +{ + std::string value = color; + boost::trim(value); + if (value.rfind("0x", 0) == 0 || value.rfind("0X", 0) == 0) { + value = value.substr(2); + } + if (!value.empty() && value[0] == '#') { + value = value.substr(1); + } + std::string normalized; + for (char c : value) { + if (std::isxdigit(static_cast(c))) { + normalized.push_back(static_cast(std::toupper(static_cast(c)))); + } + } + if (normalized.size() == 6) { + normalized += "FF"; + } + if (normalized.size() != 8) { + return "00000000"; + } + return normalized; +} + +std::string QidiPrinterAgent::map_filament_type_to_setting_id(const std::string& filament_type) +{ + std::string upper = filament_type; + boost::trim(upper); + std::transform(upper.begin(), upper.end(), upper.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); + + if (upper == "PLA") { + return "QD_1_0_1"; + } + if (upper == "ABS") { + return "QD_1_0_11"; + } + if (upper == "PETG") { + return "QD_1_0_41"; + } + if (upper == "TPU") { + return "QD_1_0_50"; + } + return ""; +} + +void QidiPrinterAgent::dispatch_message(const std::string& dev_id, const std::string& payload) +{ + OnMessageFn local_fn; + OnMessageFn cloud_fn; + QueueOnMainFn queue_fn; + { + std::lock_guard lock(state_mutex); + local_fn = on_local_message_fn; + cloud_fn = on_message_fn; + queue_fn = queue_on_main_fn; + } + + auto dispatch = [dev_id, payload, local_fn, cloud_fn]() { + if (local_fn) { + local_fn(dev_id, payload); + return; + } + if (cloud_fn) { + cloud_fn(dev_id, payload); + } + }; + + if (queue_fn) { + queue_fn(dispatch); + } else { + dispatch(); + } +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/QidiPrinterAgent.hpp b/src/slic3r/Utils/QidiPrinterAgent.hpp new file mode 100644 index 0000000000..0da9761dd9 --- /dev/null +++ b/src/slic3r/Utils/QidiPrinterAgent.hpp @@ -0,0 +1,137 @@ +#ifndef __QIDI_PRINTER_AGENT_HPP__ +#define __QIDI_PRINTER_AGENT_HPP__ + +#include "OrcaPrinterAgent.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +class QidiPrinterAgent final : public OrcaPrinterAgent +{ +public: + explicit QidiPrinterAgent(std::string log_dir); + ~QidiPrinterAgent() override; + + static AgentInfo get_agent_info_static(); + AgentInfo get_agent_info() override { return get_agent_info_static(); } + + int send_message(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; + int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; + int disconnect_printer() override; + bool start_discovery(bool start, bool sending) override; + int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override; + + int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override; + int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override; + int set_on_message_fn(OnMessageFn fn) override; + int set_on_local_connect_fn(OnLocalConnectedFn fn) override; + int set_on_local_message_fn(OnMessageFn fn) override; + int set_queue_on_main_fn(QueueOnMainFn fn) override; + + FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } + void fetch_filament_info(std::string dev_id) override; + +private: + struct PrinthostConfig + { + std::string host; + std::string port; + std::string api_key; + std::string base_url; + std::string model_id; + std::string model_name; + }; + + struct QidiDeviceInfo + { + std::string dev_id; + std::string dev_name; + std::string model_id; + std::string version; + }; + + struct QidiSlotInfo + { + int slot_index = 0; + int color_index = 0; + int filament_type = 0; + int vendor_type = 0; + bool filament_exists = false; + }; + + struct QidiFilamentDict + { + std::map colors; + std::map filaments; + }; + + int handle_request(const std::string& dev_id, const std::string& json_str); + int send_version_info(const std::string& dev_id); + int send_access_code(const std::string& dev_id); + + bool get_printhost_config(PrinthostConfig& config) const; + bool fetch_device_info(const std::string& base_url, const std::string& api_key, QidiDeviceInfo& info, std::string& error) const; + bool fetch_server_info(const std::string& base_url, const std::string& api_key, std::string& version, std::string& error) const; + bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set& objects, std::string& error) const; + + std::string resolve_host(const std::string& dev_id) const; + std::string resolve_api_key(const std::string& dev_id, const std::string& fallback) const; + void store_host(const std::string& dev_id, const std::string& host, const std::string& api_key); + + bool fetch_slot_info(const std::string& base_url, + const std::string& api_key, + std::vector& slots, + int& box_count, + std::string& error) const; + bool fetch_filament_dict(const std::string& base_url, const std::string& api_key, QidiFilamentDict& dict, std::string& error) const; + + static void parse_ini_section(const std::string& content, const std::string& section_name, std::map& result); + static void parse_filament_sections(const std::string& content, std::map& result); + + static std::string normalize_color(const std::string& color); + static std::string map_filament_type_to_setting_id(const std::string& filament_type); + + void announce_printhost_device(); + void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg); + void dispatch_printer_connected(const std::string& dev_id); + void dispatch_message(const std::string& dev_id, const std::string& payload); + void start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key); + void stop_status_stream(); + void run_status_stream(std::string dev_id, std::string base_url, std::string api_key); + void handle_ws_message(const std::string& dev_id, const std::string& payload); + void update_status_cache(const nlohmann::json& updates); + nlohmann::json build_print_payload_locked(const nlohmann::json* ams_override) const; + + mutable std::mutex state_mutex; + std::map host_by_device; + std::map api_key_by_device; + std::string ssdp_announced_host; + std::string ssdp_announced_id; + OnMsgArrivedFn on_ssdp_msg_fn; + OnPrinterConnectedFn on_printer_connected_fn; + OnLocalConnectedFn on_local_connect_fn; + OnMessageFn on_message_fn; + OnMessageFn on_local_message_fn; + QueueOnMainFn queue_on_main_fn; + + mutable std::mutex payload_mutex; + nlohmann::json status_cache; + nlohmann::json last_ams_payload; + + std::atomic ws_stop{false}; + std::atomic ws_last_emit_ms{0}; + std::thread ws_thread; +}; + +} // namespace Slic3r + +#endif diff --git a/src/slic3r/Utils/bambu_networking.hpp b/src/slic3r/Utils/bambu_networking.hpp index c4eafe6988..bce5331a75 100644 --- a/src/slic3r/Utils/bambu_networking.hpp +++ b/src/slic3r/Utils/bambu_networking.hpp @@ -10,7 +10,7 @@ extern std::string g_log_folder; extern std::string g_log_start_time; -namespace BBL { +namespace Slic3r { #define BAMBU_NETWORK_SUCCESS 0 #define BAMBU_NETWORK_ERR_INVALID_HANDLE -1 diff --git a/tests/libslic3r/test_bambu_networking.cpp b/tests/libslic3r/test_bambu_networking.cpp index c15f3b3d39..f71d286071 100644 --- a/tests/libslic3r/test_bambu_networking.cpp +++ b/tests/libslic3r/test_bambu_networking.cpp @@ -2,7 +2,7 @@ #include "slic3r/Utils/bambu_networking.hpp" -using namespace BBL; +using namespace Slic3r; TEST_CASE("extract_base_version", "[BambuNetworking]") { SECTION("version without suffix returns unchanged") { From 9c6b8edcb1aabbe125374f25237005b6829f2f24 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 24 Jan 2026 15:48:32 +0800 Subject: [PATCH 02/25] select machine --- src/slic3r/GUI/GUI_App.cpp | 107 ++++++++++++++++++++++- src/slic3r/GUI/GUI_App.hpp | 3 + src/slic3r/Utils/BBLPrinterAgent.cpp | 3 +- src/slic3r/Utils/NetworkAgentFactory.cpp | 45 ++++++++++ src/slic3r/Utils/NetworkAgentFactory.hpp | 62 +++---------- src/slic3r/Utils/OrcaPrinterAgent.cpp | 3 +- 6 files changed, 169 insertions(+), 54 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 73e09c5556..f891b839f1 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2201,7 +2201,9 @@ void GUI_App::init_networking_callbacks() if (MachineObject* obj = m_device_manager->get_my_machine(dev_id)) { obj->parse_json("lan", msg); - if (this->m_device_manager->get_selected_machine() == obj) { + // Orca: skip it if it doesn't support subscription based filament sync + if (this->m_device_manager->get_selected_machine() == obj && + m_agent->get_filament_sync_mode() == FilamentSyncMode::subscription) { GUI::wxGetApp().sidebar().load_ams_list(obj); } } @@ -3544,9 +3546,112 @@ void GUI_App::switch_printer_agent(const std::string& agent_id) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": m_device_manager is null, cannot check for deferred connection"; } + // Auto-switch MachineObject + select_machine(effective_agent_id); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id; } +void GUI_App::select_machine(const std::string& agent_id) +{ + // Skip for BBL agent for now - uses its own device discovery/selection + // Orca todo: revisit in future if we want to support auto-switching for BBL printers + if (agent_id == BBL_PRINTER_AGENT_ID) { + return; + } + + if (!m_device_manager || !preset_bundle) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": no device manager or preset bundle"; + return; + } + + // Get config source (preset or physical printer) + const auto& preset = preset_bundle->printers.get_edited_preset(); + const DynamicPrintConfig* host_cfg = &preset.config; + + std::string print_host = host_cfg->opt_string("print_host"); + if (print_host.empty()) { + if (auto* physical_cfg = preset_bundle->physical_printers.get_selected_printer_config()) { + if (!physical_cfg->opt_string("print_host").empty()) { + host_cfg = physical_cfg; + print_host = host_cfg->opt_string("print_host"); + } + } + } + if (print_host.empty()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": no print_host configured, skipping auto-switch"; + return; + } + + // Normalize host: strip protocol and path + std::string host = print_host; + if (host.find("http://") == 0) host = host.substr(7); + else if (host.find("https://") == 0) host = host.substr(8); + auto slash = host.find('/'); + if (slash != std::string::npos) host = host.substr(0, slash); + // Strip inline port if present (port comes from printhost_port) + auto colon = host.find(':'); + if (colon != std::string::npos) host = host.substr(0, colon); + + // Get port from separate config + std::string port = host_cfg->opt_string("printhost_port"); + + // Build full address (host:port) for dev_ip + std::string full_addr = host; + if (!port.empty()) { + full_addr += ":" + port; + } + + // Generate dev_id (replace . and : with _) + std::string dev_id = full_addr; + std::replace(dev_id.begin(), dev_id.end(), '.', '_'); + std::replace(dev_id.begin(), dev_id.end(), ':', '_'); + + // Check if already exists by dev_id + MachineObject* existing = m_device_manager->get_local_machine(dev_id); + + // If not found by dev_id, search by full_addr + if (!existing) { + auto local_machines = m_device_manager->get_local_machinelist(); + for (auto& [id, machine] : local_machines) { + if (machine && machine->get_dev_ip() == full_addr) { + existing = machine; + dev_id = existing->get_dev_id(); // Use existing dev_id + break; + } + } + } + + // If machine doesn't exist, create it first + if (!existing) { + BBLocalMachine machine; + machine.dev_id = dev_id; + machine.dev_ip = full_addr; + machine.dev_name = agent_id + " (" + full_addr + ")"; + machine.printer_type = preset.config.opt_string("printer_model"); + + existing = m_device_manager->insert_local_device( + machine, "lan", "free", "", ""); + + if (!existing) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to create machine dev_id=" << dev_id; + return; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": created new machine dev_id=" << dev_id; + } + + // Use MonitorPanel::select_machine() to trigger full selection flow + // This reuses existing logic for machine switching (UI updates, callbacks, etc.) + if (mainframe && mainframe->m_monitor) { + mainframe->m_monitor->select_machine(dev_id); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": triggered select_machine for dev_id=" << dev_id; + } else { + // Fallback if MonitorPanel not available + m_device_manager->set_selected_machine(dev_id); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": fallback set_selected_machine dev_id=" << dev_id; + } +} + bool GUI_App::dark_mode() { #ifdef SUPPORT_DARK_MODE diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index e494d0c2bb..6f0c0e028e 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -725,6 +725,9 @@ private: bool config_wizard_startup(); void check_updates(const bool verbose); + // select or add MachineObject + void select_machine(const std::string& agent_id); + bool m_init_app_config_from_older { false }; bool m_datadir_redefined { false }; std::string m_older_data_dir_path; diff --git a/src/slic3r/Utils/BBLPrinterAgent.cpp b/src/slic3r/Utils/BBLPrinterAgent.cpp index a1be053368..e547b424a5 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.cpp +++ b/src/slic3r/Utils/BBLPrinterAgent.cpp @@ -1,5 +1,6 @@ #include "BBLPrinterAgent.hpp" #include "BBLNetworkPlugin.hpp" +#include "NetworkAgentFactory.hpp" #include @@ -208,7 +209,7 @@ int BBLPrinterAgent::set_user_selected_machine(std::string dev_id) AgentInfo BBLPrinterAgent::get_agent_info_static() { return AgentInfo{ - .id = "bbl", + .id = BBL_PRINTER_AGENT_ID, .name = "Bambu Lab Printer Agent", .version = "", .description = "Bambu Lab printer agent" diff --git a/src/slic3r/Utils/NetworkAgentFactory.cpp b/src/slic3r/Utils/NetworkAgentFactory.cpp index fe65f6fc75..0f51d43e62 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.cpp +++ b/src/slic3r/Utils/NetworkAgentFactory.cpp @@ -173,4 +173,49 @@ void NetworkAgentFactory::register_all_agents() BOOST_LOG_TRIVIAL(info) << "Registered " << get_printer_agents().size() << " printer agents"; } +std::unique_ptr create_agent_from_config(const std::string& log_dir, AppConfig* app_config) +{ + // Determine cloud provider from config + bool use_orca_cloud = false; + if (app_config) { + try { + use_orca_cloud = app_config->get("use_orca_cloud") == "true" || app_config->get_bool("use_orca_cloud"); + } catch (...) { + use_orca_cloud = false; + } + } + + // Create cloud agent + CloudAgentProvider provider = use_orca_cloud ? CloudAgentProvider::Orca : CloudAgentProvider::BBL; + auto cloud_agent = NetworkAgentFactory::create_cloud_agent(provider, log_dir); + + // Fall back to Orca if BBL plugin not available + if (!cloud_agent && provider == CloudAgentProvider::BBL) { + BOOST_LOG_TRIVIAL(warning) << "BBL plugin not loaded, falling back to Orca cloud agent"; + cloud_agent = NetworkAgentFactory::create_cloud_agent(CloudAgentProvider::Orca, log_dir); + } + + if (!cloud_agent) { + BOOST_LOG_TRIVIAL(error) << "Failed to create cloud agent"; + return nullptr; + } + + // auto bbl_printer_agent = NetworkAgentFactory::create_printer_agent_by_id("bbl", cloud_agent, log_dir); + + // Create NetworkAgent with cloud agent only (printer agent added later) + // We will create the printer agent later when the printer is selected, so we pass nullptr for the printer agent here. + auto agent = NetworkAgentFactory::create_from_agents(std::move(cloud_agent), nullptr); + + // Configure URL overrides for Orca cloud + if (agent && app_config && use_orca_cloud) { + auto* orca_cloud = dynamic_cast(agent->get_cloud_agent().get()); + if (orca_cloud) { + orca_cloud->configure_urls(app_config); + } + } + + BOOST_LOG_TRIVIAL(info) << "Created NetworkAgent with cloud agent"; + return agent; +} + } // namespace Slic3r diff --git a/src/slic3r/Utils/NetworkAgentFactory.hpp b/src/slic3r/Utils/NetworkAgentFactory.hpp index 121b8dec27..aa7f54dccb 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.hpp +++ b/src/slic3r/Utils/NetworkAgentFactory.hpp @@ -22,12 +22,15 @@ class ICloudServiceAgent; class IPrinterAgent; /** - * AgentProvider - Specifies which implementation to use for each agent type. + * CloudAgentProvider - Specifies which implementation to use for each agent type. * - * - Orca: Native Orca implementations (OrcaCloudServiceAgent, OrcaPrinterAgent) - * - BBL: BBL DLL wrapper implementations (BBLCloudServiceAgent, BBLPrinterAgent) + * - Orca: Native Orca cloud implementations (OrcaCloudServiceAgent) + * - BBL: BBL DLL wrapper implementations (BBLCloudServiceAgent) */ -enum class AgentProvider { Orca, BBL }; +enum class CloudAgentProvider { Orca, BBL }; + +static constexpr char ORCA_PRINTER_AGENT_ID[] = "orca"; +static constexpr char BBL_PRINTER_AGENT_ID[] = "bbl"; // Factory function type for creating printer agents using PrinterAgentFactory = @@ -140,11 +143,11 @@ public: * @param log_dir Directory for log files * @return Shared pointer to ICloudServiceAgent implementation */ - static std::shared_ptr create_cloud_agent(AgentProvider provider, const std::string& log_dir) + static std::shared_ptr create_cloud_agent(CloudAgentProvider provider, const std::string& log_dir) { switch (provider) { - case AgentProvider::Orca: return std::make_shared(log_dir); - case AgentProvider::BBL: { + case CloudAgentProvider::Orca: return std::make_shared(log_dir); + case CloudAgentProvider::BBL: { auto& plugin = BBLNetworkPlugin::instance(); if (!plugin.is_loaded()) { return nullptr; @@ -200,50 +203,7 @@ private: * @param app_config Application configuration object * @return NetworkAgent with cloud agent, or nullptr on failure */ -inline std::unique_ptr create_agent_from_config(const std::string& log_dir, AppConfig* app_config) -{ - // Determine cloud provider from config - bool use_orca_cloud = false; - if (app_config) { - try { - use_orca_cloud = app_config->get("use_orca_cloud") == "true" || app_config->get_bool("use_orca_cloud"); - } catch (...) { - use_orca_cloud = false; - } - } - - // Create cloud agent - AgentProvider provider = use_orca_cloud ? AgentProvider::Orca : AgentProvider::BBL; - auto cloud_agent = NetworkAgentFactory::create_cloud_agent(provider, log_dir); - - // Fall back to Orca if BBL plugin not available - if (!cloud_agent && provider == AgentProvider::BBL) { - BOOST_LOG_TRIVIAL(warning) << "BBL plugin not loaded, falling back to Orca cloud agent"; - cloud_agent = NetworkAgentFactory::create_cloud_agent(AgentProvider::Orca, log_dir); - } - - if (!cloud_agent) { - BOOST_LOG_TRIVIAL(error) << "Failed to create cloud agent"; - return nullptr; - } - - // auto bbl_printer_agent = NetworkAgentFactory::create_printer_agent_by_id("bbl", cloud_agent, log_dir); - - // Create NetworkAgent with cloud agent only (printer agent added later) - // We will create the printer agent later when the printer is selected, so we pass nullptr for the printer agent here. - auto agent = NetworkAgentFactory::create_from_agents(std::move(cloud_agent), nullptr); - - // Configure URL overrides for Orca cloud - if (agent && app_config && use_orca_cloud) { - auto* orca_cloud = dynamic_cast(agent->get_cloud_agent().get()); - if (orca_cloud) { - orca_cloud->configure_urls(app_config); - } - } - - BOOST_LOG_TRIVIAL(info) << "Created NetworkAgent with cloud agent"; - return agent; -} +std::unique_ptr create_agent_from_config(const std::string& log_dir, AppConfig* app_config); } // namespace Slic3r diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index 1002e4068d..09acf3465d 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -1,4 +1,5 @@ #include "OrcaPrinterAgent.hpp" +#include "NetworkAgentFactory.hpp" #include @@ -139,7 +140,7 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) // ============================================================================ AgentInfo OrcaPrinterAgent::get_agent_info_static() { - return AgentInfo{.id = "orca", + return AgentInfo{.id = ORCA_PRINTER_AGENT_ID, .name = "Orca Printer Agent", .version = OrcaPrinterAgent_VERSION, .description = "Orca Printer Communication Protocol Agent"}; From 3da2bf1bd8f3be02fe2881ea430f87b1380cdfab Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 24 Jan 2026 22:35:43 +0800 Subject: [PATCH 03/25] refactor QidiPrinterAgent --- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 26 +- src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 36 +- src/slic3r/Utils/QidiPrinterAgent.cpp | 1165 +------------------- src/slic3r/Utils/QidiPrinterAgent.hpp | 99 +- 4 files changed, 101 insertions(+), 1225 deletions(-) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 2792264830..52cae222da 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -356,6 +356,13 @@ int MoonrakerPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, } detect.dev_id = info.dev_id.empty() ? dev_ip : info.dev_id; + if (!info.model_id.empty()) { + detect.model_id = info.model_id; + } else if (!config.model_id.empty()) { + detect.model_id = config.model_id; + } else { + detect.model_id = config.model_name; + } // Prefer fetched hostname, then preset model name, then generic fallback std::string fallback_name = config.model_name.empty() ? "Moonraker Printer" : config.model_name; detect.dev_name = info.dev_name.empty() ? fallback_name : info.dev_name; @@ -600,7 +607,9 @@ void MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) { // Moonraker doesn't have standard filament tracking like Qidi // This is a no-op for standard Moonraker installations - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: fetch_filament_info (no-op) - dev_id=" << dev_id; + // Note: QidiPrinterAgent overrides this method with actual implementation + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info (base class no-op) called for dev_id=" << dev_id + << " - if you see this for Qidi printer, virtual dispatch is broken!"; } int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) @@ -741,6 +750,7 @@ bool MoonrakerPrinterAgent::get_printhost_config(PrinthostConfig& config) const config.api_key = host_cfg->opt_string("printhost_apikey"); config.model_name = printer_cfg.opt_string("printer_model"); config.base_url = normalize_base_url(config.host, config.port); + config.model_id = preset.get_printer_type(preset_bundle); return !config.base_url.empty(); } @@ -797,9 +807,13 @@ bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, } nlohmann::json result = json.contains("result") ? json["result"] : json; - info.dev_name = result.value("hostname", "Moonraker Printer"); - info.dev_id = result.value("hostname", ""); - info.version = result.value("moonraker_version", ""); + info.dev_name = result.value("machine_name", result.value("hostname", "")); + info.dev_id = result.value("machine_uuid", ""); + if (info.dev_id.empty()) { + info.dev_id = result.value("serial_number", ""); + } + info.model_id = result.value("model", ""); + info.version = result.value("software_version", result.value("firmware_version", "")); return true; } @@ -1156,6 +1170,8 @@ void MoonrakerPrinterAgent::announce_printhost_device() << " (fetch_error=" << fetch_error << ")"; } + const std::string model_id = config.model_id; + if (auto* app_config = GUI::wxGetApp().app_config) { const std::string access_code = api_key.empty() ? k_no_api_key : api_key; app_config->set_str("access_code", dev_id, access_code); @@ -1168,7 +1184,7 @@ void MoonrakerPrinterAgent::announce_printhost_device() payload["dev_name"] = dev_name; payload["dev_id"] = dev_id; payload["dev_ip"] = extract_host(base_url); - payload["dev_type"] = "moonraker"; + payload["dev_type"] = model_id.empty() ? dev_name : model_id; payload["dev_signal"] = "0"; payload["connect_type"] = "lan"; payload["bind_state"] = "free"; diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index 977cfcf6a9..b6af532bd9 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -15,7 +15,7 @@ namespace Slic3r { -class MoonrakerPrinterAgent final : public IPrinterAgent +class MoonrakerPrinterAgent : public IPrinterAgent { public: explicit MoonrakerPrinterAgent(std::string log_dir); @@ -70,16 +70,18 @@ public: int set_queue_on_main_fn(QueueOnMainFn fn) override; // Pull-mode agent (on-demand filament sync) - FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } - void fetch_filament_info(std::string dev_id) override; + virtual FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } + virtual void fetch_filament_info(std::string dev_id) override; -private: +protected: + // Types exposed for derived classes struct PrinthostConfig { std::string host; std::string port; std::string api_key; std::string base_url; + std::string model_id; std::string model_name; }; @@ -87,24 +89,35 @@ private: { std::string dev_id; std::string dev_name; + std::string model_id; std::string version; }; + // Methods that derived classes may need to override or access + virtual bool get_printhost_config(PrinthostConfig& config) const; + virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; + virtual std::string get_dev_type() const { return "moonraker"; } + + // Host resolution methods + std::string resolve_host(const std::string& dev_id) const; + std::string resolve_api_key(const std::string& dev_id, const std::string& fallback) const; + void store_host(const std::string& dev_id, const std::string& host, const std::string& api_key); + + // State access for derived classes + mutable std::recursive_mutex state_mutex; + std::map host_by_device; + std::map api_key_by_device; + +private: int handle_request(const std::string& dev_id, const std::string& json_str); int send_version_info(const std::string& dev_id); int send_access_code(const std::string& dev_id); - bool get_printhost_config(PrinthostConfig& config) const; - bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; bool fetch_server_info(const std::string& base_url, const std::string& api_key, std::string& version, std::string& error) const; bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set& objects, std::string& error) const; bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const; bool send_gcode(const std::string& dev_id, const std::string& gcode) const; - std::string resolve_host(const std::string& dev_id) const; - std::string resolve_api_key(const std::string& dev_id, const std::string& fallback) const; - void store_host(const std::string& dev_id, const std::string& host, const std::string& api_key); - void announce_printhost_device(); void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg); void dispatch_printer_connected(const std::string& dev_id); @@ -140,9 +153,6 @@ private: const std::string& api_key); void finish_connection(); - mutable std::recursive_mutex state_mutex; - std::map host_by_device; - std::map api_key_by_device; std::string ssdp_announced_host; std::string ssdp_announced_id; std::shared_ptr m_cloud_agent; diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index 86bcdeb8c0..f3d740f209 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -8,87 +8,18 @@ #include "nlohmann/json.hpp" #include -#include -#include -#include -#include #include #include -#include -#include #include #include namespace { -namespace beast = boost::beast; -namespace http = beast::http; -namespace websocket = beast::websocket; -namespace net = boost::asio; -using tcp = net::ip::tcp; - -std::string to_hex_string(uint64_t value) -{ - std::ostringstream stream; - stream << std::hex << std::uppercase << value; - return stream.str(); -} - -bool looks_like_host(const std::string& value) -{ - if (value.empty()) { - return false; - } - if (value.find(' ') != std::string::npos) { - return false; - } - return value.find('.') != std::string::npos || value.find(':') != std::string::npos; -} - -constexpr const char* k_no_api_key = "__NO_API_KEY__"; - bool is_numeric(const std::string& value) { return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c) != 0; }); } -std::string normalize_base_url(std::string host, const std::string& port) -{ - boost::trim(host); - if (host.empty()) { - return ""; - } - - std::string value = host; - if (is_numeric(port) && value.find("://") == std::string::npos && value.find(':') == std::string::npos) { - value += ":" + port; - } - - if (!boost::istarts_with(value, "http://") && !boost::istarts_with(value, "https://")) { - value = "http://" + value; - } - - if (value.size() > 1 && value.back() == '/') { - value.pop_back(); - } - - return value; -} - -std::string extract_host(const std::string& base_url) -{ - std::string host = base_url; - auto pos = host.find("://"); - if (pos != std::string::npos) { - host = host.substr(pos + 3); - } - pos = host.find('/'); - if (pos != std::string::npos) { - host = host.substr(0, pos); - } - return host; -} - std::string join_url(const std::string& base_url, const std::string& path) { if (base_url.empty()) { @@ -106,14 +37,6 @@ std::string join_url(const std::string& base_url, const std::string& path) return base_url + path; } -std::string normalize_api_key(const std::string& api_key) -{ - if (api_key.empty() || api_key == k_no_api_key) { - return ""; - } - return api_key; -} - std::string normalize_model_key(std::string value) { boost::algorithm::to_lower(value); @@ -151,65 +74,6 @@ std::string infer_series_id(const std::string& model_id, const std::string& dev_ return ""; } -struct WsEndpoint -{ - std::string host; - std::string port; - std::string target; - bool secure = false; -}; - -bool parse_ws_endpoint(const std::string& base_url, WsEndpoint& endpoint) -{ - if (base_url.empty()) { - return false; - } - - std::string url = base_url; - if (boost::istarts_with(url, "https://")) { - endpoint.secure = true; - url = url.substr(8); - } else if (boost::istarts_with(url, "http://")) { - url = url.substr(7); - } - - auto slash = url.find('/'); - if (slash != std::string::npos) { - url = url.substr(0, slash); - } - if (url.empty()) { - return false; - } - - endpoint.host = url; - endpoint.port = endpoint.secure ? "443" : "80"; - if (auto colon = url.rfind(':'); colon != std::string::npos && url.find(']') == std::string::npos) { - endpoint.host = url.substr(0, colon); - endpoint.port = url.substr(colon + 1); - } - - endpoint.target = "/websocket"; - return !endpoint.host.empty() && !endpoint.port.empty(); -} - -std::string map_moonraker_state(std::string state) -{ - boost::algorithm::to_lower(state); - if (state == "printing") { - return "RUNNING"; - } - if (state == "paused") { - return "PAUSE"; - } - if (state == "complete") { - return "FINISH"; - } - if (state == "error" || state == "cancelled") { - return "FAILED"; - } - return "IDLE"; -} - std::string normalize_filament_type(const std::string& filament_type) { std::string trimmed = filament_type; @@ -236,181 +100,23 @@ std::string normalize_filament_type(const std::string& filament_type) return trimmed; } + } // namespace namespace Slic3r { const std::string QidiPrinterAgent_VERSION = "0.0.1"; -QidiPrinterAgent::QidiPrinterAgent(std::string log_dir) : OrcaPrinterAgent(std::move(log_dir)) +QidiPrinterAgent::QidiPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) { BOOST_LOG_TRIVIAL(info) << "QidiPrinterAgent: Constructor"; } -QidiPrinterAgent::~QidiPrinterAgent() -{ - stop_status_stream(); -} - AgentInfo QidiPrinterAgent::get_agent_info_static() { return AgentInfo{.id = "qidi", .name = "Qidi Printer Agent", .version = QidiPrinterAgent_VERSION, .description = "Qidi printer agent"}; } -int QidiPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) -{ - (void) qos; - (void) flag; - return handle_request(dev_id, json_str); -} - -int QidiPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) -{ - (void) qos; - (void) flag; - return handle_request(dev_id, json_str); -} - -int QidiPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) -{ - (void) username; - (void) use_ssl; - std::string base_url = normalize_base_url(dev_ip, ""); - std::string api_key = normalize_api_key(password); - - PrinthostConfig config; - if (get_printhost_config(config)) { - if (base_url.empty()) { - base_url = config.base_url; - } - if (api_key.empty()) { - api_key = normalize_api_key(config.api_key); - } - } - - if (base_url.empty()) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent: connect_printer missing host for dev_id=" << dev_id; - dispatch_local_connect(ConnectStatusFailed, dev_id, "host_missing"); - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - - if (dev_id.empty()) { - dev_id = extract_host(base_url); - } - - { - std::lock_guard lock(payload_mutex); - status_cache = nlohmann::json::object(); - last_ams_payload = nlohmann::json(); - } - ws_last_emit_ms.store(0); - - store_host(dev_id, base_url, api_key); - start_status_stream(dev_id, base_url, api_key); - dispatch_local_connect(ConnectStatusOk, dev_id, "0"); - dispatch_printer_connected(dev_id); - BOOST_LOG_TRIVIAL(info) << "QidiPrinterAgent: connect_printer - dev_id=" << dev_id << ", dev_ip=" << dev_ip; - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::disconnect_printer() -{ - stop_status_stream(); - return BAMBU_NETWORK_SUCCESS; -} - -bool QidiPrinterAgent::start_discovery(bool start, bool sending) -{ - (void) sending; - if (start) { - announce_printhost_device(); - } - return true; -} - -int QidiPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) -{ - (void) sec_link; - - std::string base_url = normalize_base_url(dev_ip, ""); - if (base_url.empty()) { - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - - PrinthostConfig config; - get_printhost_config(config); - const std::string api_key = normalize_api_key(config.api_key); - - QidiDeviceInfo info; - std::string error; - if (!fetch_device_info(base_url, api_key, info, error)) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent: bind_detect failed: " << error; - return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; - } - - detect.dev_id = info.dev_id.empty() ? dev_ip : info.dev_id; - if (!info.model_id.empty()) { - detect.model_id = info.model_id; - } else if (!config.model_id.empty()) { - detect.model_id = config.model_id; - } else { - detect.model_id = config.model_name; - } - detect.dev_name = info.dev_name.empty() ? config.model_name : info.dev_name; - detect.version = info.version; - detect.connect_type = "lan"; - detect.bind_state = "free"; - - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::set_on_ssdp_msg_fn(OnMsgArrivedFn fn) -{ - { - std::lock_guard lock(state_mutex); - on_ssdp_msg_fn = fn; - } - if (fn) { - announce_printhost_device(); - } - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::set_on_printer_connected_fn(OnPrinterConnectedFn fn) -{ - std::lock_guard lock(state_mutex); - on_printer_connected_fn = fn; - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::set_on_message_fn(OnMessageFn fn) -{ - std::lock_guard lock(state_mutex); - on_message_fn = fn; - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::set_on_local_connect_fn(OnLocalConnectedFn fn) -{ - std::lock_guard lock(state_mutex); - on_local_connect_fn = fn; - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::set_on_local_message_fn(OnMessageFn fn) -{ - std::lock_guard lock(state_mutex); - on_local_message_fn = fn; - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) -{ - std::lock_guard lock(state_mutex); - queue_on_main_fn = fn; - return BAMBU_NETWORK_SUCCESS; -} - void QidiPrinterAgent::fetch_filament_info(std::string dev_id) { // Look up MachineObject via DeviceManager @@ -447,7 +153,7 @@ void QidiPrinterAgent::fetch_filament_info(std::string dev_id) std::string series_id; { - QidiDeviceInfo info; + MoonrakerDeviceInfo info; std::string device_error; if (fetch_device_info(base_url, api_key, info, device_error)) { series_id = infer_series_id(info.model_id, info.dev_name); @@ -540,750 +246,6 @@ void QidiPrinterAgent::fetch_filament_info(std::string dev_id) << box_count << " AMS units"; } -int QidiPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) -{ - auto json = nlohmann::json::parse(json_str, nullptr, false); - if (json.is_discarded()) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent: Invalid JSON request"; - return BAMBU_NETWORK_ERR_INVALID_RESULT; - } - - if (json.contains("info") && json["info"].contains("command")) { - const auto& command = json["info"]["command"]; - if (command.is_string() && command.get() == "get_version") { - return send_version_info(dev_id); - } - } - - if (json.contains("system") && json["system"].contains("command")) { - const auto& command = json["system"]["command"]; - if (command.is_string() && command.get() == "get_access_code") { - return send_access_code(dev_id); - } - } - - // if (json.contains("pushing") && json["pushing"].contains("command")) { - // const auto& command = json["pushing"]["command"]; - // if (command.is_string()) { - // const auto cmd = command.get(); - // if (cmd == "pushall" || cmd == "start") { - // return sync_filament_list(dev_id); - // } - // } - // } - - return BAMBU_NETWORK_SUCCESS; -} - -bool QidiPrinterAgent::get_printhost_config(PrinthostConfig& config) const -{ - auto* preset_bundle = GUI::wxGetApp().preset_bundle; - if (!preset_bundle) { - return false; - } - - auto& preset = preset_bundle->printers.get_edited_preset(); - const auto& printer_cfg = preset.config; - const DynamicPrintConfig* host_cfg = &printer_cfg; - config.host = host_cfg->opt_string("print_host"); - if (config.host.empty()) { - if (auto* physical_cfg = preset_bundle->physical_printers.get_selected_printer_config()) { - if (!physical_cfg->opt_string("print_host").empty()) { - host_cfg = physical_cfg; - config.host = host_cfg->opt_string("print_host"); - } - } - } - if (config.host.empty()) { - return false; - } - - config.port = host_cfg->opt_string("printhost_port"); - config.api_key = host_cfg->opt_string("printhost_apikey"); - config.model_id = preset.get_printer_type(preset_bundle); - config.model_name = printer_cfg.opt_string("printer_model"); - config.base_url = normalize_base_url(config.host, config.port); - - return !config.base_url.empty(); -} - -bool QidiPrinterAgent::fetch_device_info(const std::string& base_url, - const std::string& api_key, - QidiDeviceInfo& info, - std::string& error) const -{ - auto fetch_json = [&](const std::string& url, nlohmann::json& out) { - std::string response_body; - bool success = false; - std::string http_error; - - auto http = Http::get(url); - if (!api_key.empty()) { - http.header("X-Api-Key", api_key); - } - http.timeout_connect(10) - .timeout_max(30) - .on_complete([&](std::string body, unsigned status) { - if (status == 200) { - response_body = body; - success = true; - } else { - http_error = "HTTP error: " + std::to_string(status); - } - }) - .on_error([&](std::string body, std::string err, unsigned status) { - http_error = err; - if (status > 0) { - http_error += " (HTTP " + std::to_string(status) + ")"; - } - }) - .perform_sync(); - - if (!success) { - error = http_error.empty() ? "Connection failed" : http_error; - return false; - } - - out = nlohmann::json::parse(response_body, nullptr, false, true); - if (out.is_discarded()) { - error = "Invalid JSON response"; - return false; - } - return true; - }; - - nlohmann::json json; - std::string url = join_url(base_url, "/machine/device_info"); - if (!fetch_json(url, json)) { - url = join_url(base_url, "/printer/info"); - if (!fetch_json(url, json)) { - return false; - } - } - - nlohmann::json result = json.contains("result") ? json["result"] : json; - info.dev_name = result.value("machine_name", result.value("hostname", "")); - info.dev_id = result.value("machine_uuid", ""); - if (info.dev_id.empty()) { - info.dev_id = result.value("serial_number", ""); - } - info.model_id = result.value("model", ""); - info.version = result.value("software_version", result.value("firmware_version", "")); - - return true; -} - -bool QidiPrinterAgent::fetch_server_info(const std::string& base_url, - const std::string& api_key, - std::string& version, - std::string& error) const -{ - std::string response_body; - bool success = false; - std::string http_error; - - auto http = Http::get(join_url(base_url, "/server/info")); - if (!api_key.empty()) { - http.header("X-Api-Key", api_key); - } - http.timeout_connect(10) - .timeout_max(30) - .on_complete([&](std::string body, unsigned status) { - if (status == 200) { - response_body = body; - success = true; - } else { - http_error = "HTTP error: " + std::to_string(status); - } - }) - .on_error([&](std::string body, std::string err, unsigned status) { - http_error = err; - if (status > 0) { - http_error += " (HTTP " + std::to_string(status) + ")"; - } - }) - .perform_sync(); - - if (!success) { - error = http_error.empty() ? "Connection failed" : http_error; - return false; - } - - auto json = nlohmann::json::parse(response_body, nullptr, false, true); - if (json.is_discarded()) { - error = "Invalid JSON response"; - return false; - } - - nlohmann::json result = json.contains("result") ? json["result"] : json; - if (result.contains("moonraker_version") && result["moonraker_version"].is_string()) { - version = result["moonraker_version"].get(); - } else if (result.contains("version") && result["version"].is_string()) { - version = result["version"].get(); - } - - return true; -} - -bool QidiPrinterAgent::fetch_object_list(const std::string& base_url, - const std::string& api_key, - std::set& objects, - std::string& error) const -{ - std::string response_body; - bool success = false; - std::string http_error; - - auto http = Http::get(join_url(base_url, "/printer/objects/list")); - if (!api_key.empty()) { - http.header("X-Api-Key", api_key); - } - http.timeout_connect(10) - .timeout_max(30) - .on_complete([&](std::string body, unsigned status) { - if (status == 200) { - response_body = body; - success = true; - } else { - http_error = "HTTP error: " + std::to_string(status); - } - }) - .on_error([&](std::string body, std::string err, unsigned status) { - http_error = err; - if (status > 0) { - http_error += " (HTTP " + std::to_string(status) + ")"; - } - }) - .perform_sync(); - - if (!success) { - error = http_error.empty() ? "Connection failed" : http_error; - return false; - } - - auto json = nlohmann::json::parse(response_body, nullptr, false, true); - if (json.is_discarded()) { - error = "Invalid JSON response"; - return false; - } - - nlohmann::json result = json.contains("result") ? json["result"] : json; - if (!result.contains("objects") || !result["objects"].is_array()) { - error = "Unexpected JSON structure"; - return false; - } - - objects.clear(); - for (const auto& entry : result["objects"]) { - if (entry.is_string()) { - objects.insert(entry.get()); - } - } - - return !objects.empty(); -} - -int QidiPrinterAgent::send_version_info(const std::string& dev_id) -{ - const std::string base_url = resolve_host(dev_id); - if (base_url.empty()) { - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - const std::string api_key = resolve_api_key(dev_id, ""); - - std::string version; - std::string error; - if (!fetch_server_info(base_url, api_key, version, error)) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: Failed to fetch server info: " << error; - } - if (version.empty()) { - version = "moonraker"; - } - - nlohmann::json payload; - payload["info"]["command"] = "get_version"; - payload["info"]["result"] = "success"; - payload["info"]["module"] = nlohmann::json::array(); - - nlohmann::json module; - module["name"] = "ota"; - module["sw_ver"] = version; - module["product_name"] = "Moonraker"; - payload["info"]["module"].push_back(module); - - dispatch_message(dev_id, payload.dump()); - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::send_access_code(const std::string& dev_id) -{ - nlohmann::json payload; - payload["system"]["command"] = "get_access_code"; - payload["system"]["access_code"] = resolve_api_key(dev_id, ""); - dispatch_message(dev_id, payload.dump()); - return BAMBU_NETWORK_SUCCESS; -} - -void QidiPrinterAgent::announce_printhost_device() -{ - PrinthostConfig config; - if (!get_printhost_config(config)) { - return; - } - - const std::string base_url = config.base_url; - if (base_url.empty()) { - return; - } - - OnMsgArrivedFn ssdp_fn; - { - std::lock_guard lock(state_mutex); - ssdp_fn = on_ssdp_msg_fn; - if (!ssdp_fn) { - return; - } - if (ssdp_announced_host == base_url && !ssdp_announced_id.empty()) { - return; - } - } - - const std::string dev_id = extract_host(base_url); - const std::string dev_name = config.model_name.empty() ? "Qidi Printer" : config.model_name; - const std::string model_id = config.model_id; - - if (auto* app_config = GUI::wxGetApp().app_config) { - const std::string access_code = normalize_api_key(config.api_key).empty() ? k_no_api_key : config.api_key; - app_config->set_str("access_code", dev_id, access_code); - app_config->set_str("user_access_code", dev_id, access_code); - } - - store_host(dev_id, base_url, normalize_api_key(config.api_key)); - - nlohmann::json payload; - payload["dev_name"] = dev_name; - payload["dev_id"] = dev_id; - payload["dev_ip"] = extract_host(base_url); - payload["dev_type"] = model_id.empty() ? dev_name : model_id; - payload["dev_signal"] = "0"; - payload["connect_type"] = "lan"; - payload["bind_state"] = "free"; - payload["sec_link"] = "secure"; - payload["ssdp_version"] = "v1"; - - ssdp_fn(payload.dump()); - - std::lock_guard lock(state_mutex); - ssdp_announced_host = base_url; - ssdp_announced_id = dev_id; -} - -void QidiPrinterAgent::dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg) -{ - OnLocalConnectedFn local_fn; - QueueOnMainFn queue_fn; - { - std::lock_guard lock(state_mutex); - local_fn = on_local_connect_fn; - queue_fn = queue_on_main_fn; - } - if (!local_fn) { - return; - } - - auto dispatch = [state, dev_id, msg, local_fn]() { local_fn(state, dev_id, msg); }; - if (queue_fn) { - queue_fn(dispatch); - } else { - dispatch(); - } -} - -void QidiPrinterAgent::dispatch_printer_connected(const std::string& dev_id) -{ - OnPrinterConnectedFn connected_fn; - QueueOnMainFn queue_fn; - { - std::lock_guard lock(state_mutex); - connected_fn = on_printer_connected_fn; - queue_fn = queue_on_main_fn; - } - if (!connected_fn) { - return; - } - - auto dispatch = [dev_id, connected_fn]() { connected_fn(dev_id); }; - if (queue_fn) { - queue_fn(dispatch); - } else { - dispatch(); - } -} - -void QidiPrinterAgent::start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key) -{ - stop_status_stream(); - if (base_url.empty()) { - return; - } - - ws_stop.store(false); - ws_thread = std::thread([this, dev_id, base_url, api_key]() { - run_status_stream(dev_id, base_url, api_key); - }); -} - -void QidiPrinterAgent::stop_status_stream() -{ - ws_stop.store(true); - if (ws_thread.joinable()) { - ws_thread.join(); - } -} - -void QidiPrinterAgent::run_status_stream(std::string dev_id, std::string base_url, std::string api_key) -{ - WsEndpoint endpoint; - if (!parse_ws_endpoint(base_url, endpoint)) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: websocket endpoint invalid for base_url=" << base_url; - return; - } - if (endpoint.secure) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: websocket wss not supported for base_url=" << base_url; - return; - } - - try { - net::io_context ioc; - tcp::resolver resolver{ioc}; - beast::tcp_stream stream{ioc}; - - stream.expires_after(std::chrono::seconds(10)); - auto const results = resolver.resolve(endpoint.host, endpoint.port); - stream.connect(results); - - websocket::stream ws{std::move(stream)}; - ws.set_option(websocket::stream_base::decorator([&](websocket::request_type& req) { - req.set(http::field::user_agent, "OrcaSlicer"); - if (!api_key.empty()) { - req.set("X-Api-Key", api_key); - } - })); - - std::string host_header = endpoint.host; - if (!endpoint.port.empty() && endpoint.port != "80") { - host_header += ":" + endpoint.port; - } - ws.handshake(host_header, endpoint.target); - ws.text(true); - - std::set subscribe_objects = {"print_stats", "virtual_sdcard"}; - std::set available_objects; - std::string list_error; - if (fetch_object_list(base_url, api_key, available_objects, list_error)) { - if (available_objects.count("heater_bed") != 0) { - subscribe_objects.insert("heater_bed"); - } - if (available_objects.count("fan") != 0) { - subscribe_objects.insert("fan"); - } - - for (const auto& name : available_objects) { - if (name == "extruder" || name.rfind("extruder", 0) == 0) { - subscribe_objects.insert(name); - if (name == "extruder") { - break; - } - } - } - } else { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: object list unavailable: " << list_error; - subscribe_objects.insert("extruder"); - subscribe_objects.insert("heater_bed"); - subscribe_objects.insert("fan"); - } - - nlohmann::json subscribe; - subscribe["jsonrpc"] = "2.0"; - subscribe["method"] = "printer.objects.subscribe"; - nlohmann::json objects = nlohmann::json::object(); - for (const auto& name : subscribe_objects) { - objects[name] = nullptr; - } - subscribe["params"]["objects"] = std::move(objects); - subscribe["id"] = 1; - ws.write(net::buffer(subscribe.dump())); - - while (!ws_stop.load()) { - ws.next_layer().expires_after(std::chrono::seconds(2)); - beast::flat_buffer buffer; - beast::error_code ec; - ws.read(buffer, ec); - if (ec == beast::error::timeout) { - const auto now_ms = static_cast( - std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count()); - const auto last_ms = ws_last_emit_ms.load(); - if (last_ms == 0 || now_ms - last_ms >= 10000) { - nlohmann::json message; - { - std::lock_guard lock(payload_mutex); - message = build_print_payload_locked(nullptr); - } - dispatch_message(dev_id, message.dump()); - ws_last_emit_ms.store(now_ms); - } - continue; - } - if (ec == websocket::error::closed) { - break; - } - if (ec) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: websocket read error: " << ec.message(); - break; - } - handle_ws_message(dev_id, beast::buffers_to_string(buffer.data())); - } - - beast::error_code ec; - ws.close(websocket::close_code::normal, ec); - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: websocket exception: " << e.what(); - } -} - -void QidiPrinterAgent::handle_ws_message(const std::string& dev_id, const std::string& payload) -{ - auto json = nlohmann::json::parse(payload, nullptr, false); - if (json.is_discarded()) { - return; - } - - bool updated = false; - if (json.contains("result") && json["result"].contains("status") && - json["result"]["status"].is_object()) { - update_status_cache(json["result"]["status"]); - updated = true; - } - - if (json.contains("method") && json["method"].is_string()) { - const std::string method = json["method"].get(); - if (method == "notify_status_update" && json.contains("params") && - json["params"].is_array() && !json["params"].empty() && - json["params"][0].is_object()) { - update_status_cache(json["params"][0]); - updated = true; - } else if (method == "notify_klippy_ready") { - nlohmann::json updates; - updates["print_stats"]["state"] = "standby"; - update_status_cache(updates); - updated = true; - } else if (method == "notify_klippy_shutdown") { - nlohmann::json updates; - updates["print_stats"]["state"] = "error"; - update_status_cache(updates); - updated = true; - } - } - - if (updated) { - nlohmann::json message; - { - std::lock_guard lock(payload_mutex); - message = build_print_payload_locked(nullptr); - } - dispatch_message(dev_id, message.dump()); - const auto now_ms = static_cast( - std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count()); - ws_last_emit_ms.store(now_ms); - } -} - -void QidiPrinterAgent::update_status_cache(const nlohmann::json& updates) -{ - if (!updates.is_object()) { - return; - } - - std::lock_guard lock(payload_mutex); - if (!status_cache.is_object()) { - status_cache = nlohmann::json::object(); - } - - for (const auto& item : updates.items()) { - if (item.value().is_object()) { - nlohmann::json& target = status_cache[item.key()]; - if (!target.is_object()) { - target = nlohmann::json::object(); - } - for (const auto& field : item.value().items()) { - target[field.key()] = field.value(); - } - } else { - status_cache[item.key()] = item.value(); - } - } -} - -nlohmann::json QidiPrinterAgent::build_print_payload_locked(const nlohmann::json* ams_override) const -{ - nlohmann::json payload; - payload["print"]["command"] = "push_status"; - payload["print"]["msg"] = 0; - payload["print"]["support_mqtt_alive"] = true; - - if (ams_override) { - payload["print"]["ams"] = *ams_override; - } else if (!last_ams_payload.is_null()) { - payload["print"]["ams"] = last_ams_payload; - } - - std::string state = "IDLE"; - if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("state") && - status_cache["print_stats"]["state"].is_string()) { - state = map_moonraker_state(status_cache["print_stats"]["state"].get()); - } - payload["print"]["gcode_state"] = state; - - const nlohmann::json* extruder = nullptr; - if (status_cache.contains("extruder") && status_cache["extruder"].is_object()) { - extruder = &status_cache["extruder"]; - } else { - for (const auto& item : status_cache.items()) { - if (item.value().is_object() && item.key().rfind("extruder", 0) == 0) { - extruder = &item.value(); - break; - } - } - } - - if (extruder) { - if (extruder->contains("temperature") && (*extruder)["temperature"].is_number()) { - payload["print"]["nozzle_temper"] = (*extruder)["temperature"].get(); - } - if (extruder->contains("target") && (*extruder)["target"].is_number()) { - payload["print"]["nozzle_target_temper"] = (*extruder)["target"].get(); - } - } - - if (status_cache.contains("heater_bed") && status_cache["heater_bed"].is_object()) { - const auto& bed = status_cache["heater_bed"]; - if (bed.contains("temperature") && bed["temperature"].is_number()) { - payload["print"]["bed_temper"] = bed["temperature"].get(); - } - if (bed.contains("target") && bed["target"].is_number()) { - payload["print"]["bed_target_temper"] = bed["target"].get(); - } - } - - if (status_cache.contains("fan") && status_cache["fan"].is_object()) { - const auto& fan = status_cache["fan"]; - if (fan.contains("speed") && fan["speed"].is_number()) { - double speed = fan["speed"].get(); - int pwm = 0; - if (speed <= 1.0) { - pwm = static_cast(speed * 255.0 + 0.5); - } else { - pwm = static_cast(speed + 0.5); - } - pwm = std::clamp(pwm, 0, 255); - payload["print"]["fan_gear"] = pwm; - } - } - - if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("filename") && - status_cache["print_stats"]["filename"].is_string()) { - payload["print"]["subtask_name"] = status_cache["print_stats"]["filename"].get(); - } - - int mc_percent = -1; - if (status_cache.contains("virtual_sdcard") && - status_cache["virtual_sdcard"].contains("progress") && - status_cache["virtual_sdcard"]["progress"].is_number()) { - const double progress = status_cache["virtual_sdcard"]["progress"].get(); - if (progress >= 0.0) { - mc_percent = std::clamp(static_cast(progress * 100.0 + 0.5), 0, 100); - } - } - if (mc_percent >= 0) { - payload["print"]["mc_percent"] = mc_percent; - } - - if (status_cache.contains("print_stats") && - status_cache["print_stats"].contains("total_duration") && - status_cache["print_stats"].contains("print_duration") && - status_cache["print_stats"]["total_duration"].is_number() && - status_cache["print_stats"]["print_duration"].is_number()) { - const double total = status_cache["print_stats"]["total_duration"].get(); - const double elapsed = status_cache["print_stats"]["print_duration"].get(); - if (total > 0.0 && elapsed >= 0.0) { - const auto remaining_minutes = std::max(0, static_cast((total - elapsed) / 60.0)); - payload["print"]["mc_remaining_time"] = remaining_minutes; - } - } - - const auto now_ms = static_cast( - std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count()); - payload["t_utc"] = now_ms; - - return payload; -} - -std::string QidiPrinterAgent::resolve_host(const std::string& dev_id) const -{ - { - std::lock_guard lock(state_mutex); - auto it = host_by_device.find(dev_id); - if (it != host_by_device.end()) { - return it->second; - } - } - - PrinthostConfig config; - if (get_printhost_config(config)) { - return config.base_url; - } - - return looks_like_host(dev_id) ? normalize_base_url(dev_id, "") : ""; -} - -std::string QidiPrinterAgent::resolve_api_key(const std::string& dev_id, const std::string& fallback) const -{ - std::string api_key = normalize_api_key(fallback); - if (!api_key.empty()) { - return api_key; - } - - { - std::lock_guard lock(state_mutex); - auto it = api_key_by_device.find(dev_id); - if (it != api_key_by_device.end() && !it->second.empty()) { - return it->second; - } - } - - PrinthostConfig config; - if (get_printhost_config(config)) { - return normalize_api_key(config.api_key); - } - - return ""; -} - -void QidiPrinterAgent::store_host(const std::string& dev_id, const std::string& host, const std::string& api_key) -{ - if (host.empty()) { - return; - } - std::lock_guard lock(state_mutex); - host_by_device[dev_id] = host; - if (!api_key.empty()) { - api_key_by_device[dev_id] = api_key; - } -} - bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, const std::string& api_key, std::vector& slots, @@ -1372,6 +334,52 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, return true; } +bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url, + const std::string& api_key, + QidiFilamentDict& dict, + std::string& error) const +{ + std::string url = join_url(base_url, "/server/files/config/officiall_filas_list.cfg"); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(10) + .timeout_max(30) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + dict.colors.clear(); + dict.filaments.clear(); + parse_ini_section(response_body, "colordict", dict.colors); + parse_filament_sections(response_body, dict.filaments); + + return !dict.colors.empty(); +} + void QidiPrinterAgent::parse_ini_section(const std::string& content, const std::string& section_name, std::map& result) { std::istringstream stream(content); @@ -1442,52 +450,6 @@ void QidiPrinterAgent::parse_filament_sections(const std::string& content, std:: } } -bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url, - const std::string& api_key, - QidiFilamentDict& dict, - std::string& error) const -{ - std::string url = join_url(base_url, "/server/files/config/officiall_filas_list.cfg"); - - std::string response_body; - bool success = false; - std::string http_error; - - auto http = Http::get(url); - if (!api_key.empty()) { - http.header("X-Api-Key", api_key); - } - http.timeout_connect(10) - .timeout_max(30) - .on_complete([&](std::string body, unsigned status) { - if (status == 200) { - response_body = body; - success = true; - } else { - http_error = "HTTP error: " + std::to_string(status); - } - }) - .on_error([&](std::string body, std::string err, unsigned status) { - http_error = err; - if (status > 0) { - http_error += " (HTTP " + std::to_string(status) + ")"; - } - }) - .perform_sync(); - - if (!success) { - error = http_error.empty() ? "Connection failed" : http_error; - return false; - } - - dict.colors.clear(); - dict.filaments.clear(); - parse_ini_section(response_body, "colordict", dict.colors); - parse_filament_sections(response_body, dict.filaments); - - return !dict.colors.empty(); -} - std::string QidiPrinterAgent::normalize_color(const std::string& color) { std::string value = color; @@ -1534,33 +496,4 @@ std::string QidiPrinterAgent::map_filament_type_to_setting_id(const std::string& return ""; } -void QidiPrinterAgent::dispatch_message(const std::string& dev_id, const std::string& payload) -{ - OnMessageFn local_fn; - OnMessageFn cloud_fn; - QueueOnMainFn queue_fn; - { - std::lock_guard lock(state_mutex); - local_fn = on_local_message_fn; - cloud_fn = on_message_fn; - queue_fn = queue_on_main_fn; - } - - auto dispatch = [dev_id, payload, local_fn, cloud_fn]() { - if (local_fn) { - local_fn(dev_id, payload); - return; - } - if (cloud_fn) { - cloud_fn(dev_id, payload); - } - }; - - if (queue_fn) { - queue_fn(dispatch); - } else { - dispatch(); - } -} - } // namespace Slic3r diff --git a/src/slic3r/Utils/QidiPrinterAgent.hpp b/src/slic3r/Utils/QidiPrinterAgent.hpp index 0da9761dd9..a2a51c423b 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.hpp +++ b/src/slic3r/Utils/QidiPrinterAgent.hpp @@ -1,64 +1,28 @@ #ifndef __QIDI_PRINTER_AGENT_HPP__ #define __QIDI_PRINTER_AGENT_HPP__ -#include "OrcaPrinterAgent.hpp" +#include "MoonrakerPrinterAgent.hpp" -#include -#include #include -#include -#include #include -#include #include namespace Slic3r { -class QidiPrinterAgent final : public OrcaPrinterAgent +class QidiPrinterAgent final : public MoonrakerPrinterAgent { public: explicit QidiPrinterAgent(std::string log_dir); - ~QidiPrinterAgent() override; + ~QidiPrinterAgent() override = default; static AgentInfo get_agent_info_static(); AgentInfo get_agent_info() override { return get_agent_info_static(); } - int send_message(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; - int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; - int disconnect_printer() override; - bool start_discovery(bool start, bool sending) override; - int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override; - - int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override; - int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override; - int set_on_message_fn(OnMessageFn fn) override; - int set_on_local_connect_fn(OnLocalConnectedFn fn) override; - int set_on_local_message_fn(OnMessageFn fn) override; - int set_queue_on_main_fn(QueueOnMainFn fn) override; - - FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } + // Override filament sync (Qidi-specific implementation) void fetch_filament_info(std::string dev_id) override; private: - struct PrinthostConfig - { - std::string host; - std::string port; - std::string api_key; - std::string base_url; - std::string model_id; - std::string model_name; - }; - - struct QidiDeviceInfo - { - std::string dev_id; - std::string dev_name; - std::string model_id; - std::string version; - }; - + // Qidi-specific device info (extends base MoonrakerDeviceInfo with model_id) struct QidiSlotInfo { int slot_index = 0; @@ -74,62 +38,15 @@ private: std::map filaments; }; - int handle_request(const std::string& dev_id, const std::string& json_str); - int send_version_info(const std::string& dev_id); - int send_access_code(const std::string& dev_id); - - bool get_printhost_config(PrinthostConfig& config) const; - bool fetch_device_info(const std::string& base_url, const std::string& api_key, QidiDeviceInfo& info, std::string& error) const; - bool fetch_server_info(const std::string& base_url, const std::string& api_key, std::string& version, std::string& error) const; - bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set& objects, std::string& error) const; - - std::string resolve_host(const std::string& dev_id) const; - std::string resolve_api_key(const std::string& dev_id, const std::string& fallback) const; - void store_host(const std::string& dev_id, const std::string& host, const std::string& api_key); - - bool fetch_slot_info(const std::string& base_url, - const std::string& api_key, - std::vector& slots, - int& box_count, - std::string& error) const; + // Qidi-specific methods + bool fetch_slot_info(const std::string& base_url, const std::string& api_key, std::vector& slots, int& box_count, std::string& error) const; bool fetch_filament_dict(const std::string& base_url, const std::string& api_key, QidiFilamentDict& dict, std::string& error) const; + // Static helpers static void parse_ini_section(const std::string& content, const std::string& section_name, std::map& result); static void parse_filament_sections(const std::string& content, std::map& result); - static std::string normalize_color(const std::string& color); static std::string map_filament_type_to_setting_id(const std::string& filament_type); - - void announce_printhost_device(); - void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg); - void dispatch_printer_connected(const std::string& dev_id); - void dispatch_message(const std::string& dev_id, const std::string& payload); - void start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key); - void stop_status_stream(); - void run_status_stream(std::string dev_id, std::string base_url, std::string api_key); - void handle_ws_message(const std::string& dev_id, const std::string& payload); - void update_status_cache(const nlohmann::json& updates); - nlohmann::json build_print_payload_locked(const nlohmann::json* ams_override) const; - - mutable std::mutex state_mutex; - std::map host_by_device; - std::map api_key_by_device; - std::string ssdp_announced_host; - std::string ssdp_announced_id; - OnMsgArrivedFn on_ssdp_msg_fn; - OnPrinterConnectedFn on_printer_connected_fn; - OnLocalConnectedFn on_local_connect_fn; - OnMessageFn on_message_fn; - OnMessageFn on_local_message_fn; - QueueOnMainFn queue_on_main_fn; - - mutable std::mutex payload_mutex; - nlohmann::json status_cache; - nlohmann::json last_ams_payload; - - std::atomic ws_stop{false}; - std::atomic ws_last_emit_ms{0}; - std::thread ws_thread; }; } // namespace Slic3r From 1a696173c072ca5037884ca89fae4a61b4e9baed Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sun, 25 Jan 2026 12:17:25 +0800 Subject: [PATCH 04/25] 1 --- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 51 +++++++++++++++++++--- src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 6 +++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 52cae222da..36341235fc 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -275,6 +275,8 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i status_cache = nlohmann::json::object(); } ws_last_emit_ms.store(0); + ws_last_dispatch_ms.store(0); + last_print_state.clear(); store_host(dev_id, base_url, api_key); @@ -1475,12 +1477,14 @@ void MoonrakerPrinterAgent::handle_ws_message(const std::string& dev_id, const s } bool updated = false; + bool is_critical = false; // Track if this is a critical update that bypasses throttle - // Check for subscription response (has "result.status") + // Check for subscription response (has "result.status") - initial subscription is critical if (json.contains("result") && json["result"].contains("status") && json["result"]["status"].is_object()) { update_status_cache(json["result"]["status"]); updated = true; + is_critical = true; // Initial subscription response - dispatch immediately } // Check for status update notifications @@ -1491,16 +1495,19 @@ void MoonrakerPrinterAgent::handle_ws_message(const std::string& dev_id, const s json["params"][0].is_object()) { update_status_cache(json["params"][0]); updated = true; + // Note: is_critical stays false for regular status updates (telemetry) } else if (method == "notify_klippy_ready") { nlohmann::json updates; updates["print_stats"]["state"] = "standby"; update_status_cache(updates); updated = true; + is_critical = true; // Klippy events are critical } else if (method == "notify_klippy_shutdown") { nlohmann::json updates; updates["print_stats"]["state"] = "error"; update_status_cache(updates); updated = true; + is_critical = true; // Klippy events are critical } // Handle Klippy disconnect - update status and trigger reconnection else if (method == "notify_klippy_disconnected") { @@ -1509,26 +1516,56 @@ void MoonrakerPrinterAgent::handle_ws_message(const std::string& dev_id, const s updates["print_stats"]["state"] = "error"; update_status_cache(updates); updated = true; + is_critical = true; // Klippy events are critical // Set flag to trigger reconnection after dispatching the status update ws_reconnect_requested.store(true); BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Klippy disconnected, triggering reconnection"; } } - if (updated) { - nlohmann::json message; + // Check for print state changes (critical - always dispatch immediately) + if (updated && !is_critical) { + std::string current_state; { std::lock_guard lock(payload_mutex); - message = build_print_payload_locked(); + if (status_cache.contains("print_stats") && + status_cache["print_stats"].contains("state") && + status_cache["print_stats"]["state"].is_string()) { + current_state = status_cache["print_stats"]["state"].get(); + } } - BOOST_LOG_TRIVIAL(trace) << "MoonrakerPrinterAgent: Dispatching payload: " << message.dump(); - dispatch_message(dev_id, message.dump()); + if (!current_state.empty() && current_state != last_print_state) { + is_critical = true; + last_print_state = current_state; + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Print state changed to " << current_state << ", dispatching immediately"; + } + } + if (updated) { const auto now_ms = static_cast( std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count()); - ws_last_emit_ms.store(now_ms); + const auto last_dispatch_ms = ws_last_dispatch_ms.load(); + + // Dispatch if: critical change OR throttle interval elapsed + const bool should_dispatch = is_critical || + last_dispatch_ms == 0 || + now_ms - last_dispatch_ms >= STATUS_UPDATE_INTERVAL_MS; + + if (should_dispatch) { + nlohmann::json message; + { + std::lock_guard lock(payload_mutex); + message = build_print_payload_locked(); + } + + BOOST_LOG_TRIVIAL(trace) << "MoonrakerPrinterAgent: Dispatching payload (critical=" << is_critical << "): " << message.dump(); + dispatch_message(dev_id, message.dump()); + ws_last_dispatch_ms.store(now_ms); + ws_last_emit_ms.store(now_ms); // Also update heartbeat timer + } + // else: skip dispatch, cache is updated for next dispatch cycle } } diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index b6af532bd9..cfdd663810 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -179,6 +179,12 @@ private: std::atomic ws_last_emit_ms{0}; std::thread ws_thread; + // Throttling configuration for WebSocket updates + // Critical changes (state transitions) dispatch immediately; telemetry is throttled + static constexpr uint64_t STATUS_UPDATE_INTERVAL_MS = 1000; // 1 update/sec for telemetry + std::atomic ws_last_dispatch_ms{0}; + std::string last_print_state; // Track state for immediate dispatch on change + // Connection thread management std::atomic connect_in_progress{false}; std::atomic connect_stop_requested{false}; From 978e3b79b541d43f901664d441bb159c3192a374 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 26 Jan 2026 19:34:14 +0800 Subject: [PATCH 05/25] refactor --- src/slic3r/GUI/DeviceCore/DevManager.cpp | 4 +- src/slic3r/GUI/DeviceManager.cpp | 24 + src/slic3r/GUI/DeviceManager.hpp | 6 +- src/slic3r/GUI/GUI_App.cpp | 68 +-- src/slic3r/GUI/Jobs/PrintJob.cpp | 2 +- src/slic3r/GUI/Jobs/PrintJob.hpp | 2 +- src/slic3r/GUI/Jobs/SendJob.cpp | 2 +- src/slic3r/GUI/Jobs/SendJob.hpp | 2 +- src/slic3r/GUI/ReleaseNote.cpp | 4 +- src/slic3r/GUI/SelectMachine.cpp | 4 +- src/slic3r/GUI/SendToPrinter.cpp | 4 +- src/slic3r/Utils/CalibUtils.cpp | 8 +- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 482 ++++++--------------- src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 32 +- src/slic3r/Utils/OrcaPrinterAgent.cpp | 21 - src/slic3r/Utils/QidiPrinterAgent.cpp | 170 +++----- src/slic3r/Utils/QidiPrinterAgent.hpp | 9 +- 17 files changed, 276 insertions(+), 568 deletions(-) diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index 5c32f42c2b..6f4885f4cc 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -518,7 +518,7 @@ namespace Slic3r #if !BBL_RELEASE_TO_PUBLIC it->second->connect(Slic3r::GUI::wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false); #else - it->second->connect(it->second->local_use_ssl_for_mqtt); + it->second->connect(it->second->local_use_ssl); #endif it->second->set_lan_mode_connection_state(true); } @@ -542,7 +542,7 @@ namespace Slic3r #if !BBL_RELEASE_TO_PUBLIC it->second->connect(Slic3r::GUI::wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false); #else - it->second->connect(it->second->local_use_ssl_for_mqtt); + it->second->connect(it->second->local_use_ssl); #endif it->second->set_lan_mode_connection_state(true); } diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index bbf0b50ca0..1542b5fbef 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -363,6 +363,30 @@ std::string MachineObject::get_ftp_folder() return DevPrinterConfigUtil::get_ftp_folder(printer_type); } +std::string MachineObject::dev_id_from_address(const std::string& host, const std::string& port) +{ + std::string result = host; + // Normalize host: strip protocol and path + if (result.find("http://") == 0) + result = result.substr(7); + else if (result.find("https://") == 0) + result = result.substr(8); + auto slash = result.find('/'); + if (slash != std::string::npos) + result = result.substr(0, slash); + + // Build full address (host:port) + if (!port.empty()) { + // Strip inline port if present (port comes from printhost_port) + auto colon = result.find(':'); + if (colon != std::string::npos) + result = result.substr(0, colon); + + result += ":" + port; + } + return result; +} + bool MachineObject::HasRecentCloudMessage() { auto curr_time = std::chrono::system_clock::now(); diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 4a871e6bdb..5454c62ca9 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -164,7 +164,11 @@ public: std::string get_dev_id() const { return dev_id; } void set_dev_id(std::string val) { dev_id = val; } - bool local_use_ssl_for_mqtt { true }; + // Generate consistent dev_id from host address and optional port + // Returns "host:port" or "host" if port is empty + static std::string dev_id_from_address(const std::string& host, const std::string& port = ""); + + bool local_use_ssl { true }; bool local_use_ssl_for_ftp { true }; std::string get_ftp_folder(); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index f891b839f1..1376e4ce24 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3520,32 +3520,6 @@ void GUI_App::switch_printer_agent(const std::string& agent_id) // Swap the agent m_agent->set_printer_agent(new_printer_agent); - // Update dependent managers - if (m_device_manager) { - m_device_manager->set_agent(m_agent); - - // If there's a selected machine that was deferred due to no printer agent, - // trigger a connection now that the agent is ready - MachineObject* selected = m_device_manager->get_selected_machine(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": checking for deferred connection - selected=" - << (selected ? selected->get_dev_id() : "null"); - if (selected) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": selected machine - is_lan_mode=" << selected->is_lan_mode_printer() - << " is_connected=" << selected->is_connected(); - } - if (selected && selected->is_lan_mode_printer() && !selected->is_connected()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": connecting deferred LAN machine dev_id=" << selected->get_dev_id(); -#if !BBL_RELEASE_TO_PUBLIC - selected->connect(app_config->get("enable_ssl_for_mqtt") == "true" ? true : false); -#else - selected->connect(selected->local_use_ssl_for_mqtt); -#endif - selected->set_lan_mode_connection_state(true); - } - } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": m_device_manager is null, cannot check for deferred connection"; - } - // Auto-switch MachineObject select_machine(effective_agent_id); @@ -3571,41 +3545,12 @@ void GUI_App::select_machine(const std::string& agent_id) std::string print_host = host_cfg->opt_string("print_host"); if (print_host.empty()) { - if (auto* physical_cfg = preset_bundle->physical_printers.get_selected_printer_config()) { - if (!physical_cfg->opt_string("print_host").empty()) { - host_cfg = physical_cfg; - print_host = host_cfg->opt_string("print_host"); - } - } - } - if (print_host.empty()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": no print_host configured, skipping auto-switch"; return; } - - // Normalize host: strip protocol and path - std::string host = print_host; - if (host.find("http://") == 0) host = host.substr(7); - else if (host.find("https://") == 0) host = host.substr(8); - auto slash = host.find('/'); - if (slash != std::string::npos) host = host.substr(0, slash); - // Strip inline port if present (port comes from printhost_port) - auto colon = host.find(':'); - if (colon != std::string::npos) host = host.substr(0, colon); - - // Get port from separate config std::string port = host_cfg->opt_string("printhost_port"); - // Build full address (host:port) for dev_ip - std::string full_addr = host; - if (!port.empty()) { - full_addr += ":" + port; - } - - // Generate dev_id (replace . and : with _) - std::string dev_id = full_addr; - std::replace(dev_id.begin(), dev_id.end(), '.', '_'); - std::replace(dev_id.begin(), dev_id.end(), ':', '_'); + // Generate dev_id from host and port + std::string dev_id = MachineObject::dev_id_from_address(print_host, port); // Check if already exists by dev_id MachineObject* existing = m_device_manager->get_local_machine(dev_id); @@ -3614,9 +3559,8 @@ void GUI_App::select_machine(const std::string& agent_id) if (!existing) { auto local_machines = m_device_manager->get_local_machinelist(); for (auto& [id, machine] : local_machines) { - if (machine && machine->get_dev_ip() == full_addr) { + if (machine && machine->get_dev_ip() == dev_id) { existing = machine; - dev_id = existing->get_dev_id(); // Use existing dev_id break; } } @@ -3626,8 +3570,9 @@ void GUI_App::select_machine(const std::string& agent_id) if (!existing) { BBLocalMachine machine; machine.dev_id = dev_id; - machine.dev_ip = full_addr; - machine.dev_name = agent_id + " (" + full_addr + ")"; + // We use dev_id as dev_ip to store the address (host:port) + machine.dev_ip = dev_id; + machine.dev_name = dev_id; machine.printer_type = preset.config.opt_string("printer_model"); existing = m_device_manager->insert_local_device( @@ -3639,6 +3584,7 @@ void GUI_App::select_machine(const std::string& agent_id) } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": created new machine dev_id=" << dev_id; } + existing->local_use_ssl = boost::istarts_with(print_host, "https://"); // Use MonitorPanel::select_machine() to trigger full selection flow // This reuses existing logic for machine switching (UI updates, callbacks, etc.) diff --git a/src/slic3r/GUI/Jobs/PrintJob.cpp b/src/slic3r/GUI/Jobs/PrintJob.cpp index a09bca61fe..a5f1b4833c 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.cpp +++ b/src/slic3r/GUI/Jobs/PrintJob.cpp @@ -202,7 +202,7 @@ void PrintJob::process(Ctl &ctl) // local print access params.dev_ip = m_dev_ip; params.use_ssl_for_ftp = m_local_use_ssl_for_ftp; - params.use_ssl_for_mqtt = m_local_use_ssl_for_mqtt; + params.use_ssl_for_mqtt = m_local_use_ssl; params.username = "bblp"; params.password = m_access_code; diff --git a/src/slic3r/GUI/Jobs/PrintJob.hpp b/src/slic3r/GUI/Jobs/PrintJob.hpp index c93761a303..f186cda202 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.hpp +++ b/src/slic3r/GUI/Jobs/PrintJob.hpp @@ -74,7 +74,7 @@ public: int m_print_from_sdc_plate_idx = 0; - bool m_local_use_ssl_for_mqtt { true }; + bool m_local_use_ssl { true }; bool m_local_use_ssl_for_ftp { true }; bool task_bed_leveling; bool task_flow_cali; diff --git a/src/slic3r/GUI/Jobs/SendJob.cpp b/src/slic3r/GUI/Jobs/SendJob.cpp index 578d0d804b..e7fe82eecd 100644 --- a/src/slic3r/GUI/Jobs/SendJob.cpp +++ b/src/slic3r/GUI/Jobs/SendJob.cpp @@ -183,7 +183,7 @@ void SendJob::process(Ctl &ctl) params.username = "bblp"; params.password = m_access_code; params.use_ssl_for_ftp = m_local_use_ssl_for_ftp; - params.use_ssl_for_mqtt = m_local_use_ssl_for_mqtt; + params.use_ssl_for_mqtt = m_local_use_ssl; wxString error_text; std::string msg_text; diff --git a/src/slic3r/GUI/Jobs/SendJob.hpp b/src/slic3r/GUI/Jobs/SendJob.hpp index e9330ad33d..1d4e025d6e 100644 --- a/src/slic3r/GUI/Jobs/SendJob.hpp +++ b/src/slic3r/GUI/Jobs/SendJob.hpp @@ -42,7 +42,7 @@ public: std::string connection_type; bool m_local_use_ssl_for_ftp{true}; - bool m_local_use_ssl_for_mqtt{true}; + bool m_local_use_ssl{true}; bool cloud_print_only { false }; bool has_sdcard { false }; bool task_use_ams { true }; diff --git a/src/slic3r/GUI/ReleaseNote.cpp b/src/slic3r/GUI/ReleaseNote.cpp index 9b156c5228..4af803d17d 100644 --- a/src/slic3r/GUI/ReleaseNote.cpp +++ b/src/slic3r/GUI/ReleaseNote.cpp @@ -1844,10 +1844,10 @@ void InputIpAddressDialog::on_send_retry() m_send_job->m_access_code = str_access_code.ToStdString(); #if !BBL_RELEASE_TO_PUBLIC - m_send_job->m_local_use_ssl_for_mqtt = wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false; + m_send_job->m_local_use_ssl = wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false; m_send_job->m_local_use_ssl_for_ftp = wxGetApp().app_config->get("enable_ssl_for_ftp") == "true" ? true : false; #else - m_send_job->m_local_use_ssl_for_mqtt = m_obj->local_use_ssl_for_mqtt; + m_send_job->m_local_use_ssl = m_obj->local_use_ssl; m_send_job->m_local_use_ssl_for_ftp = m_obj->local_use_ssl_for_ftp; #endif diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index eeb472e98a..e0e9ec3b79 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -2464,10 +2464,10 @@ void SelectMachineDialog::on_send_print() m_print_job->m_access_code = obj_->get_access_code(); #if !BBL_RELEASE_TO_PUBLIC m_print_job->m_local_use_ssl_for_ftp = wxGetApp().app_config->get("enable_ssl_for_ftp") == "true" ? true : false; - m_print_job->m_local_use_ssl_for_mqtt = wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false; + m_print_job->m_local_use_ssl = wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false; #else m_print_job->m_local_use_ssl_for_ftp = obj_->local_use_ssl_for_ftp; - m_print_job->m_local_use_ssl_for_mqtt = obj_->local_use_ssl_for_mqtt; + m_print_job->m_local_use_ssl = obj_->local_use_ssl; #endif m_print_job->connection_type = obj_->connection_type(); m_print_job->cloud_print_only = obj_->is_support_cloud_print_only; diff --git a/src/slic3r/GUI/SendToPrinter.cpp b/src/slic3r/GUI/SendToPrinter.cpp index 670a8bcad4..54bdf3a458 100644 --- a/src/slic3r/GUI/SendToPrinter.cpp +++ b/src/slic3r/GUI/SendToPrinter.cpp @@ -958,10 +958,10 @@ void SendToPrinterDialog::on_ok(wxCommandEvent &event) #if !BBL_RELEASE_TO_PUBLIC m_send_job->m_local_use_ssl_for_ftp = wxGetApp().app_config->get("enable_ssl_for_ftp") == "true" ? true : false; - m_send_job->m_local_use_ssl_for_mqtt = wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false; + m_send_job->m_local_use_ssl = wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false; #else m_send_job->m_local_use_ssl_for_ftp = obj_->local_use_ssl_for_ftp; - m_send_job->m_local_use_ssl_for_mqtt = obj_->local_use_ssl_for_mqtt; + m_send_job->m_local_use_ssl = obj_->local_use_ssl; #endif m_send_job->connection_type = obj_->connection_type(); diff --git a/src/slic3r/Utils/CalibUtils.cpp b/src/slic3r/Utils/CalibUtils.cpp index 3650e48acb..bba613e2c4 100644 --- a/src/slic3r/Utils/CalibUtils.cpp +++ b/src/slic3r/Utils/CalibUtils.cpp @@ -1791,10 +1791,10 @@ void CalibUtils::send_to_print(const CalibInfo &calib_info, wxString &error_mess #if !BBL_RELEASE_TO_PUBLIC print_job->m_local_use_ssl_for_ftp = wxGetApp().app_config->get("enable_ssl_for_ftp") == "true" ? true : false; - print_job->m_local_use_ssl_for_mqtt = wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false; + print_job->m_local_use_ssl = wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false; #else print_job->m_local_use_ssl_for_ftp = obj_->local_use_ssl_for_ftp; - print_job->m_local_use_ssl_for_mqtt = obj_->local_use_ssl_for_mqtt; + print_job->m_local_use_ssl = obj_->local_use_ssl; #endif print_job->connection_type = obj_->connection_type(); @@ -1896,10 +1896,10 @@ void CalibUtils::send_to_print(const std::vector &calib_infos, wxStri #if !BBL_RELEASE_TO_PUBLIC print_job->m_local_use_ssl_for_ftp = wxGetApp().app_config->get("enable_ssl_for_ftp") == "true" ? true : false; - print_job->m_local_use_ssl_for_mqtt = wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false; + print_job->m_local_use_ssl = wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false; #else print_job->m_local_use_ssl_for_ftp = obj_->local_use_ssl_for_ftp; - print_job->m_local_use_ssl_for_mqtt = obj_->local_use_ssl_for_mqtt; + print_job->m_local_use_ssl = obj_->local_use_ssl; #endif print_job->connection_type = obj_->connection_type(); diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 36341235fc..3c24ffe4db 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -3,6 +3,7 @@ #include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/DeviceManager.hpp" #include "nlohmann/json.hpp" #include @@ -27,90 +28,6 @@ namespace websocket = beast::websocket; namespace net = boost::asio; using tcp = net::ip::tcp; -constexpr const char* k_no_api_key = "__NO_API_KEY__"; - -bool is_numeric(const std::string& value) -{ - return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c) != 0; }); -} - -std::string normalize_base_url(std::string host, const std::string& port) -{ - boost::trim(host); - if (host.empty()) { - return ""; - } - - std::string value = host; - if (is_numeric(port) && value.find("://") == std::string::npos && value.find(':') == std::string::npos) { - value += ":" + port; - } - - if (!boost::istarts_with(value, "http://") && !boost::istarts_with(value, "https://")) { - value = "http://" + value; - } - - if (value.size() > 1 && value.back() == '/') { - value.pop_back(); - } - - return value; -} - -std::string extract_host(const std::string& base_url) -{ - std::string host = base_url; - auto pos = host.find("://"); - if (pos != std::string::npos) { - host = host.substr(pos + 3); - } - pos = host.find('/'); - if (pos != std::string::npos) { - host = host.substr(0, pos); - } - return host; -} - -std::string join_url(const std::string& base_url, const std::string& path) -{ - if (base_url.empty()) { - return ""; - } - if (path.empty()) { - return base_url; - } - if (base_url.back() == '/' && path.front() == '/') { - return base_url.substr(0, base_url.size() - 1) + path; - } - if (base_url.back() != '/' && path.front() != '/') { - return base_url + "/" + path; - } - return base_url + path; -} - -std::string normalize_api_key(const std::string& api_key) -{ - if (api_key.empty() || api_key == k_no_api_key) { - return ""; - } - return api_key; -} - -// Sanitize filename to prevent path traversal attacks -// Extracts only the basename, removing any path components -std::string sanitize_filename(const std::string& filename) -{ - if (filename.empty()) { - return "print.gcode"; - } - namespace fs = boost::filesystem; - fs::path p(filename); - std::string basename = p.filename().string(); - if (basename.empty() || basename == "." || basename == "..") { - return "print.gcode"; - } - return basename; -} struct WsEndpoint { @@ -197,7 +114,6 @@ void MoonrakerPrinterAgent::set_cloud_agent(std::shared_ptr { std::lock_guard lock(state_mutex); m_cloud_agent = cloud; - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: Cloud agent set"; } int MoonrakerPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) @@ -216,37 +132,25 @@ int MoonrakerPrinterAgent::send_message_to_printer(std::string dev_id, std::stri int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) { - (void) username; - (void) use_ssl; - std::string base_url = normalize_base_url(dev_ip, ""); - std::string api_key = normalize_api_key(password); - - PrinthostConfig config; - if (get_printhost_config(config)) { - if (base_url.empty()) { - base_url = config.base_url; - } - if (api_key.empty()) { - api_key = normalize_api_key(config.api_key); - } - } - - if (base_url.empty()) { - BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: connect_printer missing host"; - dispatch_local_connect(ConnectStatusFailed, dev_id, "host_missing"); + if (dev_id.empty() || dev_ip.empty()) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: connect_printer missing dev_id or dev_ip"; return BAMBU_NETWORK_ERR_INVALID_HANDLE; } - if (dev_id.empty()) { - dev_id = extract_host(base_url); + init_device_info(dev_id, dev_ip, username, password, use_ssl); + + if (device_info.dev_id != dev_id) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: connect_printer dev_id mismatch: expected " << device_info.dev_id << ", got " << dev_id; + dispatch_local_connect(ConnectStatusFailed, dev_id, "dev_id_mismatch"); + return BAMBU_NETWORK_ERR_INVALID_HANDLE; } // Check if connection already in progress { std::lock_guard lock(connect_mutex); if (connect_in_progress.load()) { - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Connection already in progress, waiting..."; + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: Connection already in progress, waiting..."; // Don't reject - wait for previous connection to complete // This can happen if MonitorPanel triggers connect while previous connect is still running } else { @@ -278,15 +182,12 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i ws_last_dispatch_ms.store(0); last_print_state.clear(); - store_host(dev_id, base_url, api_key); - // Launch connection in background thread - connect_thread = std::thread([this, dev_id, base_url, api_key]() { - perform_connection_async(dev_id, base_url, api_key); + connect_thread = std::thread([this, dev_id]() { + perform_connection_async(dev_id, device_info.base_url, device_info.api_key); }); - // Return immediately - UI is not blocked - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: connect_printer launched in background - dev_id=" << dev_id; + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: connect_printer launched in background - dev_id=" << dev_id; return BAMBU_NETWORK_SUCCESS; } @@ -346,30 +247,18 @@ int MoonrakerPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, return BAMBU_NETWORK_ERR_INVALID_HANDLE; } - PrinthostConfig config; - get_printhost_config(config); - const std::string api_key = normalize_api_key(config.api_key); - - MoonrakerDeviceInfo info; std::string error; - if (!fetch_device_info(base_url, api_key, info, error)) { + if (!fetch_device_info(base_url, device_info.api_key, device_info, error)) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: bind_detect failed: " << error; return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; } - detect.dev_id = info.dev_id.empty() ? dev_ip : info.dev_id; - if (!info.model_id.empty()) { - detect.model_id = info.model_id; - } else if (!config.model_id.empty()) { - detect.model_id = config.model_id; - } else { - detect.model_id = config.model_name; - } + detect.dev_id = device_info.dev_id.empty() ? dev_ip : device_info.dev_id; + detect.model_id = device_info.model_id.empty() ? device_info.model_name : device_info.model_id; // Prefer fetched hostname, then preset model name, then generic fallback - std::string fallback_name = config.model_name.empty() ? "Moonraker Printer" : config.model_name; - detect.dev_name = info.dev_name.empty() ? fallback_name : info.dev_name; - detect.model_id = "moonraker"; - detect.version = info.version; + detect.dev_name = device_info.dev_name; + detect.model_id = device_info.model_id; + detect.version = device_info.version; detect.connect_type = "lan"; detect.bind_state = "free"; @@ -447,12 +336,6 @@ int MoonrakerPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpda if (update_fn) update_fn(PrintingStageCreate, 0, "Preparing..."); - const std::string base_url = resolve_host(params.dev_id); - if (base_url.empty()) { - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - const std::string api_key = resolve_api_key(params.dev_id, params.password); - std::string filename = params.filename; if (filename.empty()) { filename = params.task_name; @@ -465,7 +348,7 @@ int MoonrakerPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpda std::string safe_filename = sanitize_filename(filename); // Upload only, don't start print - if (!upload_gcode(params.filename, safe_filename, base_url, api_key, update_fn, cancel_fn)) { + if (!upload_gcode(params.filename, safe_filename, device_info.base_url, device_info.api_key, update_fn, cancel_fn)) { return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; } @@ -481,13 +364,6 @@ int MoonrakerPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusF if (cancel_fn && cancel_fn()) { return BAMBU_NETWORK_ERR_CANCELED; } - - const std::string base_url = resolve_host(params.dev_id); - if (base_url.empty()) { - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - const std::string api_key = resolve_api_key(params.dev_id, params.password); - // Determine the G-code file to upload // params.filename may be .3mf, params.dst_file contains actual G-code std::string gcode_path = params.filename; @@ -513,7 +389,7 @@ int MoonrakerPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusF // Upload file if (update_fn) update_fn(PrintingStageUpload, 0, "Uploading G-code..."); - if (!upload_gcode(gcode_path, upload_filename, base_url, api_key, update_fn, cancel_fn)) { + if (!upload_gcode(gcode_path, upload_filename, device_info.base_url, device_info.api_key, update_fn, cancel_fn)) { return BAMBU_NETWORK_ERR_PRINT_LP_UPLOAD_FTP_FAILED; } @@ -525,7 +401,7 @@ int MoonrakerPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusF // Start print via gcode script (simpler than JSON-RPC) if (update_fn) update_fn(PrintingStageSending, 0, "Starting print..."); std::string gcode = "SDCARD_PRINT_FILE FILENAME=" + upload_filename; - if (!send_gcode(params.dev_id, gcode)) { + if (!send_gcode(device_info.dev_id, gcode)) { return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED; } @@ -548,11 +424,6 @@ int MoonrakerPrinterAgent::set_on_ssdp_msg_fn(OnMsgArrivedFn fn) std::lock_guard lock(state_mutex); on_ssdp_msg_fn = fn; } - // Call announce_printhost_device() outside the lock to avoid deadlock - // since announce_printhost_device() also acquires state_mutex - if (fn) { - announce_printhost_device(); - } return BAMBU_NETWORK_SUCCESS; } @@ -607,16 +478,11 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) void MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) { - // Moonraker doesn't have standard filament tracking like Qidi - // This is a no-op for standard Moonraker installations - // Note: QidiPrinterAgent overrides this method with actual implementation - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info (base class no-op) called for dev_id=" << dev_id - << " - if you see this for Qidi printer, virtual dispatch is broken!"; + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info (base class no-op) called for dev_id=" << dev_id; } int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) { - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: handle_request received: " << json_str; auto json = nlohmann::json::parse(json_str, nullptr, false); if (json.is_discarded()) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Invalid JSON request"; @@ -648,7 +514,7 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: } const std::string cmd = command.get(); - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Received print command: " << cmd; + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: Received print command: " << cmd; // Handle gcode_line command - this is how G-code commands are sent from OrcaSlicer if (cmd == "gcode_line") { @@ -681,7 +547,7 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; } - // ===== NEW: Print control commands ===== + // Print control commands if (cmd == "pause") { return pause_print(dev_id); } @@ -725,36 +591,27 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: return BAMBU_NETWORK_SUCCESS; } -bool MoonrakerPrinterAgent::get_printhost_config(PrinthostConfig& config) const +bool MoonrakerPrinterAgent::init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) { + device_info = MoonrakerDeviceInfo{}; auto* preset_bundle = GUI::wxGetApp().preset_bundle; if (!preset_bundle) { return false; } - auto& preset = preset_bundle->printers.get_edited_preset(); + auto& preset = preset_bundle->printers.get_edited_preset(); const auto& printer_cfg = preset.config; - const DynamicPrintConfig* host_cfg = &printer_cfg; - config.host = host_cfg->opt_string("print_host"); - if (config.host.empty()) { - if (auto* physical_cfg = preset_bundle->physical_printers.get_selected_printer_config()) { - if (!physical_cfg->opt_string("print_host").empty()) { - host_cfg = physical_cfg; - config.host = host_cfg->opt_string("print_host"); - } - } - } - if (config.host.empty()) { - return false; - } + device_info.dev_ip = dev_ip; - config.port = host_cfg->opt_string("printhost_port"); - config.api_key = host_cfg->opt_string("printhost_apikey"); - config.model_name = printer_cfg.opt_string("printer_model"); - config.base_url = normalize_base_url(config.host, config.port); - config.model_id = preset.get_printer_type(preset_bundle); + device_info.api_key = password; + device_info.model_name = printer_cfg.opt_string("printer_model"); + device_info.model_id = preset.get_printer_type(preset_bundle); + device_info.base_url = use_ssl ? "https://" + dev_ip : "http://" + dev_ip; + device_info.dev_id = dev_id; + device_info.version = ""; + device_info.dev_name = device_info.dev_id; - return !config.base_url.empty(); + return true; } bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, @@ -771,8 +628,8 @@ bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, if (!api_key.empty()) { http.header("X-Api-Key", api_key); } - http.timeout_connect(10) - .timeout_max(30) + http.timeout_connect(5) + .timeout_max(10) .on_complete([&](std::string body, unsigned status) { if (status == 200) { response_body = body; @@ -809,12 +666,7 @@ bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, } nlohmann::json result = json.contains("result") ? json["result"] : json; - info.dev_name = result.value("machine_name", result.value("hostname", "")); - info.dev_id = result.value("machine_uuid", ""); - if (info.dev_id.empty()) { - info.dev_id = result.value("serial_number", ""); - } - info.model_id = result.value("model", ""); + info.dev_name = result.value("machine_name", result.value("hostname", "")); info.version = result.value("software_version", result.value("firmware_version", "")); return true; @@ -833,8 +685,8 @@ bool MoonrakerPrinterAgent::fetch_server_info(const std::string& base_url, if (!api_key.empty()) { http.header("X-Api-Key", api_key); } - http.timeout_connect(10) - .timeout_max(30) + http.timeout_connect(5) + .timeout_max(10) .on_complete([&](std::string body, unsigned status) { if (status == 200) { response_body = body; @@ -885,8 +737,8 @@ bool MoonrakerPrinterAgent::fetch_server_info_json(const std::string& base_url, if (!api_key.empty()) { http.header("X-Api-Key", api_key); } - http.timeout_connect(10) - .timeout_max(30) + http.timeout_connect(5) + .timeout_max(10) .on_complete([&](std::string body, unsigned status) { if (status == 200) { response_body = body; @@ -932,8 +784,8 @@ bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url, if (!api_key.empty()) { http.header("X-Api-Key", api_key); } - http.timeout_connect(10) - .timeout_max(30) + http.timeout_connect(5) + .timeout_max(10) .on_complete([&](std::string body, unsigned status_code) { if (status_code == 200) { response_body = body; @@ -972,31 +824,24 @@ bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url, bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::string& gcode) const { - const std::string base_url = resolve_host(dev_id); - if (base_url.empty()) { - BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: send_gcode - empty base_url for dev_id=" << dev_id; - return false; - } - const std::string api_key = resolve_api_key(dev_id, ""); - nlohmann::json payload; payload["script"] = gcode; std::string payload_str = payload.dump(); - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: send_gcode to " << base_url << " with payload: " << payload_str; + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: send_gcode to " << device_info.base_url << " with payload: " << payload_str; std::string response_body; bool success = false; std::string http_error; - auto http = Http::post(join_url(base_url, "/printer/gcode/script")); - if (!api_key.empty()) { - http.header("X-Api-Key", api_key); + auto http = Http::post(join_url(device_info.base_url, "/printer/gcode/script")); + if (!device_info.api_key.empty()) { + http.header("X-Api-Key", device_info.api_key); } http.header("Content-Type", "application/json") .set_post_body(payload_str) - .timeout_connect(10) - .timeout_max(30) + .timeout_connect(5) + .timeout_max(10) .on_complete([&](std::string body, unsigned status_code) { BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: send_gcode response status=" << status_code << " body=" << body; if (status_code == 200) { @@ -1020,7 +865,7 @@ bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::str return false; } - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: sent gcode successfully: " << gcode; + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: sent gcode successfully: " << gcode; return true; } @@ -1037,8 +882,8 @@ bool MoonrakerPrinterAgent::fetch_object_list(const std::string& base_url, if (!api_key.empty()) { http.header("X-Api-Key", api_key); } - http.timeout_connect(10) - .timeout_max(30) + http.timeout_connect(5) + .timeout_max(10) .on_complete([&](std::string body, unsigned status) { if (status == 200) { response_body = body; @@ -1084,15 +929,9 @@ bool MoonrakerPrinterAgent::fetch_object_list(const std::string& base_url, int MoonrakerPrinterAgent::send_version_info(const std::string& dev_id) { - const std::string base_url = resolve_host(dev_id); - if (base_url.empty()) { - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - const std::string api_key = resolve_api_key(dev_id, ""); - std::string version; std::string error; - if (!fetch_server_info(base_url, api_key, version, error)) { + if (!fetch_server_info(device_info.base_url, device_info.api_key, version, error)) { BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Failed to fetch server info: " << error; } if (version.empty()) { @@ -1118,7 +957,7 @@ int MoonrakerPrinterAgent::send_access_code(const std::string& dev_id) { nlohmann::json payload; payload["system"]["command"] = "get_access_code"; - payload["system"]["access_code"] = resolve_api_key(dev_id, ""); + payload["system"]["access_code"] = device_info.api_key; dispatch_message(dev_id, payload.dump()); return BAMBU_NETWORK_SUCCESS; } @@ -1127,20 +966,6 @@ void MoonrakerPrinterAgent::announce_printhost_device() { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device() called"; - PrinthostConfig config; - if (!get_printhost_config(config)) { - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - no printhost config"; - return; - } - - const std::string base_url = config.base_url; - if (base_url.empty()) { - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - empty base_url"; - return; - } - - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - base_url=" << base_url; - OnMsgArrivedFn ssdp_fn; { std::lock_guard lock(state_mutex); @@ -1149,43 +974,38 @@ void MoonrakerPrinterAgent::announce_printhost_device() BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - no ssdp callback"; return; } - if (ssdp_announced_host == base_url && !ssdp_announced_id.empty()) { + if (ssdp_announced_host == device_info.base_url && !ssdp_announced_id.empty()) { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - already announced"; return; } } - const std::string dev_id = extract_host(base_url); - const std::string api_key = normalize_api_key(config.api_key); - // Try to fetch actual device name from Moonraker // Priority: 1) Moonraker hostname, 2) Preset model name, 3) Generic fallback std::string dev_name; MoonrakerDeviceInfo info; std::string fetch_error; - if (fetch_device_info(base_url, api_key, info, fetch_error) && !info.dev_name.empty()) { + if (fetch_device_info(device_info.base_url, device_info.api_key, info, fetch_error) && !info.dev_name.empty()) { dev_name = info.dev_name; BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Got device name from printer: " << dev_name; } else { - dev_name = config.model_name.empty() ? "Moonraker Printer" : config.model_name; + dev_name = device_info.model_name.empty() ? "Moonraker Printer" : device_info.model_name; BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Using fallback device name: " << dev_name << " (fetch_error=" << fetch_error << ")"; } - const std::string model_id = config.model_id; + const std::string model_id = device_info.model_id; if (auto* app_config = GUI::wxGetApp().app_config) { - const std::string access_code = api_key.empty() ? k_no_api_key : api_key; - app_config->set_str("access_code", dev_id, access_code); - app_config->set_str("user_access_code", dev_id, access_code); + const std::string access_code = device_info.api_key; + app_config->set_str("access_code", device_info.dev_id, access_code); + app_config->set_str("user_access_code", device_info.dev_id, access_code); } - store_host(dev_id, base_url, api_key); - nlohmann::json payload; payload["dev_name"] = dev_name; - payload["dev_id"] = dev_id; - payload["dev_ip"] = extract_host(base_url); + payload["dev_id"] = device_info.dev_id; + payload["dev_ip"] = device_info.dev_ip; payload["dev_type"] = model_id.empty() ? dev_name : model_id; payload["dev_signal"] = "0"; payload["connect_type"] = "lan"; @@ -1197,14 +1017,14 @@ void MoonrakerPrinterAgent::announce_printhost_device() { std::lock_guard lock(state_mutex); - ssdp_announced_host = base_url; - ssdp_announced_id = dev_id; + ssdp_announced_host = device_info.base_url; + ssdp_announced_id = device_info.dev_id; // Set this as the selected machine if nothing is currently selected // This ensures auto-connect works when MonitorPanel opens if (selected_machine.empty()) { - selected_machine = dev_id; - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Auto-selected machine: " << dev_id; + selected_machine = device_info.dev_id; + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Auto-selected machine: " << device_info.dev_id; } } } @@ -1560,7 +1380,6 @@ void MoonrakerPrinterAgent::handle_ws_message(const std::string& dev_id, const s message = build_print_payload_locked(); } - BOOST_LOG_TRIVIAL(trace) << "MoonrakerPrinterAgent: Dispatching payload (critical=" << is_critical << "): " << message.dump(); dispatch_message(dev_id, message.dump()); ws_last_dispatch_ms.store(now_ms); ws_last_emit_ms.store(now_ms); // Also update heartbeat timer @@ -1609,7 +1428,6 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const } payload["print"]["gcode_state"] = state; - // ===== NEW: Print Stage ===== // Map Moonraker state to Bambu stage numbers int mc_print_stage = 0; if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("state")) { @@ -1621,14 +1439,12 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const } payload["print"]["mc_print_stage"] = mc_print_stage; - // ===== NEW: Error Codes ===== // Leave mc_print_error_code and print_error at 0 // UI expects numeric HMS codes - setting to 1 shows generic error dialog // Only set if real mapping from Moonraker error strings to HMS codes is defined payload["print"]["mc_print_error_code"] = 0; payload["print"]["print_error"] = 0; - // ===== NEW: Home Flag ===== // Map homed axes to bit field: X=bit0, Y=bit1, Z=bit2 // WARNING: This only sets bits 0-2, clearing support flags (bit 3+) // Bit 3 = 220V voltage, bit 4 = auto recovery, etc. @@ -1642,12 +1458,10 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const } payload["print"]["home_flag"] = home_flag; - // ===== NEW: Temperature Ranges ===== - // Moonraker doesn't provide this via API - use hardcoded defaults + // Moonraker doesn't provide temperature ranges via API - use hardcoded defaults payload["print"]["nozzle_temp_range"] = {100, 370}; // Typical Klipper range payload["print"]["bed_temp_range"] = {0, 120}; // Typical bed range - // ===== NEW: Feature Flags ===== payload["print"]["support_send_to_sd"] = true; // Detect bed_leveling support from available objects (bed_mesh or probe) // Default to 0 (not supported) if neither object exists @@ -1713,7 +1527,6 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const payload["print"]["subtask_name"] = status_cache["print_stats"]["filename"].get(); } - // ===== NEW: G-code File Path ===== if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("filename")) { payload["print"]["gcode_file"] = status_cache["print_stats"]["filename"]; } @@ -1749,69 +1562,11 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const std::chrono::system_clock::now().time_since_epoch()).count()); payload["t_utc"] = now_ms; - BOOST_LOG_TRIVIAL(trace) << "MoonrakerPrinterAgent: Built payload with gcode_state=" << state; - return payload; } -std::string MoonrakerPrinterAgent::resolve_host(const std::string& dev_id) const -{ - { - std::lock_guard lock(state_mutex); - auto it = host_by_device.find(dev_id); - if (it != host_by_device.end()) { - return it->second; - } - } - - PrinthostConfig config; - if (get_printhost_config(config)) { - return config.base_url; - } - - return ""; -} - -std::string MoonrakerPrinterAgent::resolve_api_key(const std::string& dev_id, const std::string& fallback) const -{ - std::string api_key = normalize_api_key(fallback); - if (!api_key.empty()) { - return api_key; - } - - { - std::lock_guard lock(state_mutex); - auto it = api_key_by_device.find(dev_id); - if (it != api_key_by_device.end() && !it->second.empty()) { - return it->second; - } - } - - PrinthostConfig config; - if (get_printhost_config(config)) { - return normalize_api_key(config.api_key); - } - - return ""; -} - -void MoonrakerPrinterAgent::store_host(const std::string& dev_id, const std::string& host, const std::string& api_key) -{ - if (host.empty()) { - return; - } - std::lock_guard lock(state_mutex); - host_by_device[dev_id] = host; - if (!api_key.empty()) { - api_key_by_device[dev_id] = api_key; - } -} - void MoonrakerPrinterAgent::dispatch_message(const std::string& dev_id, const std::string& payload) { - BOOST_LOG_TRIVIAL(trace) << "MoonrakerPrinterAgent: dispatch_message dev_id=" << dev_id - << " payload_size=" << payload.size(); - OnMessageFn local_fn; OnMessageFn cloud_fn; QueueOnMainFn queue_fn; @@ -1882,7 +1637,7 @@ bool MoonrakerPrinterAgent::upload_gcode( http.form_add("root", "gcodes") // Upload to gcodes directory .form_add("print", "false") // Don't auto-start print .form_add_file("file", source_path.string(), safe_filename) - .timeout_connect(10) + .timeout_connect(5) .timeout_max(300) // 5 minutes for large files .on_complete([&](std::string body, unsigned status) { BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: Upload complete: HTTP " << status << " body: " << body; @@ -1917,9 +1672,6 @@ bool MoonrakerPrinterAgent::upload_gcode( int MoonrakerPrinterAgent::pause_print(const std::string& dev_id) { - const std::string base_url = resolve_host(dev_id); - const std::string api_key = resolve_api_key(dev_id, ""); - nlohmann::json request; request["jsonrpc"] = "2.0"; request["method"] = "printer.print.pause"; @@ -1963,8 +1715,8 @@ bool MoonrakerPrinterAgent::send_jsonrpc_command( } http.header("Content-Type", "application/json") .set_post_body(request_str) - .timeout_connect(10) - .timeout_max(30) + .timeout_connect(5) + .timeout_max(10) .on_complete([&](std::string body, unsigned status) { if (status == 200) { response = body; @@ -2007,29 +1759,7 @@ void MoonrakerPrinterAgent::perform_connection_async( ? server_info["result"] : server_info; std::string klippy_state = result_json.value("klippy_state", ""); - // Poll for Klippy ready state (with stop check) - if (klippy_state == "startup") { - for (int i = 0; i < 30; i++) { // 30 second max - { - std::unique_lock lock(connect_mutex); - if (connect_stop_requested.load()) { - result = BAMBU_NETWORK_ERR_CANCELED; - break; - } - } - - std::this_thread::sleep_for(std::chrono::seconds(1)); - - if (fetch_server_info_json(base_url, api_key, server_info, error_msg)) { - result_json = server_info.contains("result") - ? server_info["result"] : server_info; - klippy_state = result_json.value("klippy_state", ""); - if (klippy_state == "ready") break; - } - } - } - - // Check final state + // Check state if (klippy_state != "ready" && result == BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED) { std::string state_message = result_json.value("state_message", "Unknown error"); BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Klippy not ready: " << klippy_state @@ -2081,4 +1811,66 @@ void MoonrakerPrinterAgent::finish_connection() connect_in_progress.store(false); } +bool MoonrakerPrinterAgent::is_numeric(const std::string& value) +{ + return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c) != 0; }); +} + +std::string MoonrakerPrinterAgent::normalize_base_url(std::string host, const std::string& port) +{ + boost::trim(host); + if (host.empty()) { + return ""; + } + + std::string value = host; + if (is_numeric(port) && value.find("://") == std::string::npos && value.find(':') == std::string::npos) { + value += ":" + port; + } + + if (!boost::istarts_with(value, "http://") && !boost::istarts_with(value, "https://")) { + value = "http://" + value; + } + + if (value.size() > 1 && value.back() == '/') { + value.pop_back(); + } + + return value; +} + +std::string MoonrakerPrinterAgent::join_url(const std::string& base_url, const std::string& path) const +{ + if (base_url.empty()) { + return ""; + } + if (path.empty()) { + return base_url; + } + if (base_url.back() == '/' && path.front() == '/') { + return base_url.substr(0, base_url.size() - 1) + path; + } + if (base_url.back() != '/' && path.front() != '/') { + return base_url + "/" + path; + } + return base_url + path; +} + + +// Sanitize filename to prevent path traversal attacks +// Extracts only the basename, removing any path components +std::string MoonrakerPrinterAgent::sanitize_filename(const std::string& filename) +{ + if (filename.empty()) { + return "print.gcode"; + } + namespace fs = boost::filesystem; + fs::path p(filename); + std::string basename = p.filename().string(); + if (basename.empty() || basename == "." || basename == "..") { + return "print.gcode"; + } + return basename; +} + } // namespace Slic3r diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index cfdd663810..94d32a4016 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -74,39 +74,31 @@ public: virtual void fetch_filament_info(std::string dev_id) override; protected: - // Types exposed for derived classes - struct PrinthostConfig + struct MoonrakerDeviceInfo { - std::string host; - std::string port; + std::string dev_id; + std::string dev_ip; std::string api_key; std::string base_url; std::string model_id; std::string model_name; - }; - - struct MoonrakerDeviceInfo - { - std::string dev_id; std::string dev_name; - std::string model_id; std::string version; - }; + bool use_ssl = false; + } device_info; // Methods that derived classes may need to override or access - virtual bool get_printhost_config(PrinthostConfig& config) const; + virtual bool init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; - virtual std::string get_dev_type() const { return "moonraker"; } - - // Host resolution methods - std::string resolve_host(const std::string& dev_id) const; - std::string resolve_api_key(const std::string& dev_id, const std::string& fallback) const; - void store_host(const std::string& dev_id, const std::string& host, const std::string& api_key); // State access for derived classes mutable std::recursive_mutex state_mutex; - std::map host_by_device; - std::map api_key_by_device; + + // Helpers + bool is_numeric(const std::string& value); + std::string normalize_base_url(std::string host, const std::string& port); + std::string sanitize_filename(const std::string& filename); + std::string join_url(const std::string& base_url, const std::string& path) const; private: int handle_request(const std::string& dev_id, const std::string& json_str); diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index 09acf3465d..712024a286 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -1,15 +1,12 @@ #include "OrcaPrinterAgent.hpp" #include "NetworkAgentFactory.hpp" -#include - namespace Slic3r { const std::string OrcaPrinterAgent_VERSION = "0.0.1"; OrcaPrinterAgent::OrcaPrinterAgent(std::string log_dir) : log_dir(std::move(log_dir)) { - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: Constructor - log_dir=" << this->log_dir; } OrcaPrinterAgent::~OrcaPrinterAgent() = default; @@ -18,7 +15,6 @@ void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr cloud { std::lock_guard lock(state_mutex); m_cloud_agent = cloud; - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: Cloud agent set"; } // ============================================================================ @@ -27,25 +23,21 @@ 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) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: send_message (stub) - dev_id=" << dev_id; return BAMBU_NETWORK_SUCCESS; } int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: connect_printer (stub) - dev_id=" << dev_id << ", dev_ip=" << dev_ip; return BAMBU_NETWORK_SUCCESS; } int OrcaPrinterAgent::disconnect_printer() { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: disconnect_printer (stub)"; return BAMBU_NETWORK_SUCCESS; } int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: send_message_to_printer (stub) - dev_id=" << dev_id; return BAMBU_NETWORK_SUCCESS; } @@ -55,13 +47,11 @@ int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string js int OrcaPrinterAgent::check_cert() { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: check_cert (stub)"; return BAMBU_NETWORK_SUCCESS; } void OrcaPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: install_device_cert (stub) - dev_id=" << dev_id; } // ============================================================================ @@ -70,7 +60,6 @@ void OrcaPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) bool OrcaPrinterAgent::start_discovery(bool start, bool sending) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: start_discovery (stub) - start=" << start << ", sending=" << sending; return true; } @@ -80,32 +69,27 @@ bool OrcaPrinterAgent::start_discovery(bool start, bool sending) int OrcaPrinterAgent::ping_bind(std::string ping_code) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: ping_bind (stub) - ping_code=" << ping_code; return BAMBU_NETWORK_SUCCESS; } int OrcaPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: bind_detect (stub) - dev_ip=" << dev_ip; return BAMBU_NETWORK_SUCCESS; } int OrcaPrinterAgent::bind( std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: bind (stub) - dev_id=" << dev_id; return BAMBU_NETWORK_SUCCESS; } int OrcaPrinterAgent::unbind(std::string dev_id) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: unbind (stub) - dev_id=" << dev_id; return BAMBU_NETWORK_SUCCESS; } int OrcaPrinterAgent::request_bind_ticket(std::string* ticket) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: request_bind_ticket (stub)"; if (ticket) *ticket = ""; return BAMBU_NETWORK_SUCCESS; @@ -152,7 +136,6 @@ AgentInfo OrcaPrinterAgent::get_agent_info_static() int OrcaPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: start_print (stub) - task_name=" << params.task_name; return BAMBU_NETWORK_SUCCESS; } @@ -161,25 +144,21 @@ int OrcaPrinterAgent::start_local_print_with_record(PrintParams params, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: start_local_print_with_record (stub)"; return BAMBU_NETWORK_SUCCESS; } int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: start_send_gcode_to_sdcard (stub)"; return BAMBU_NETWORK_SUCCESS; } int OrcaPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: start_local_print (stub)"; return BAMBU_NETWORK_SUCCESS; } int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) { - BOOST_LOG_TRIVIAL(debug) << "OrcaPrinterAgent: start_sdcard_print (stub)"; return BAMBU_NETWORK_SUCCESS; } diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index f3d740f209..5f16ab9339 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -1,7 +1,5 @@ #include "QidiPrinterAgent.hpp" #include "Http.hpp" -#include "libslic3r/Preset.hpp" -#include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/DeviceCore/DevFilaSystem.h" #include "slic3r/GUI/DeviceCore/DevManager.h" @@ -13,103 +11,12 @@ #include #include -namespace { - -bool is_numeric(const std::string& value) -{ - return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c) != 0; }); -} - -std::string join_url(const std::string& base_url, const std::string& path) -{ - if (base_url.empty()) { - return ""; - } - if (path.empty()) { - return base_url; - } - if (base_url.back() == '/' && path.front() == '/') { - return base_url.substr(0, base_url.size() - 1) + path; - } - if (base_url.back() != '/' && path.front() != '/') { - return base_url + "/" + path; - } - return base_url + path; -} - -std::string normalize_model_key(std::string value) -{ - boost::algorithm::to_lower(value); - std::string normalized; - normalized.reserve(value.size()); - for (unsigned char c : value) { - if (std::isalnum(c)) { - normalized.push_back(static_cast(c)); - } - } - return normalized; -} - -std::string infer_series_id(const std::string& model_id, const std::string& dev_name) -{ - std::string source = model_id.empty() ? dev_name : model_id; - boost::trim(source); - if (source.empty()) { - return ""; - } - if (is_numeric(source)) { - return source; - } - - const std::string key = normalize_model_key(source); - if (key.find("q2") != std::string::npos) { - return "1"; - } - if (key.find("xmax") != std::string::npos && key.find("4") != std::string::npos) { - return "3"; - } - if ((key.find("xplus") != std::string::npos || key.find("plus") != std::string::npos) && key.find("4") != std::string::npos) { - return "0"; - } - return ""; -} - -std::string normalize_filament_type(const std::string& filament_type) -{ - std::string trimmed = filament_type; - boost::trim(trimmed); - std::string upper = trimmed; - std::transform(upper.begin(), upper.end(), upper.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); - - if (upper.find("PLA") != std::string::npos) - return "PLA"; - if (upper.find("ABS") != std::string::npos) - return "ABS"; - if (upper.find("PETG") != std::string::npos) - return "PETG"; - if (upper.find("TPU") != std::string::npos) - return "TPU"; - if (upper.find("ASA") != std::string::npos) - return "ASA"; - if (upper.find("PA") != std::string::npos || upper.find("NYLON") != std::string::npos) - return "PA"; - if (upper.find("PC") != std::string::npos) - return "PC"; - if (upper.find("PVA") != std::string::npos) - return "PVA"; - - return trimmed; -} - -} // namespace - namespace Slic3r { const std::string QidiPrinterAgent_VERSION = "0.0.1"; QidiPrinterAgent::QidiPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) { - BOOST_LOG_TRIVIAL(info) << "QidiPrinterAgent: Constructor"; } AgentInfo QidiPrinterAgent::get_agent_info_static() @@ -131,23 +38,16 @@ void QidiPrinterAgent::fetch_filament_info(std::string dev_id) return; } - const std::string base_url = resolve_host(dev_id); - if (base_url.empty()) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: Missing host for dev_id=" << dev_id; - return; - } - const std::string api_key = resolve_api_key(dev_id, ""); - std::vector slots; int box_count = 0; std::string error; - if (!fetch_slot_info(base_url, api_key, slots, box_count, error)) { + if (!fetch_slot_info(device_info.base_url, device_info.api_key, slots, box_count, error)) { BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error; return; } QidiFilamentDict dict; - if (!fetch_filament_dict(base_url, api_key, dict, error)) { + if (!fetch_filament_dict(device_info.base_url, device_info.api_key, dict, error)) { BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error; } @@ -155,7 +55,7 @@ void QidiPrinterAgent::fetch_filament_info(std::string dev_id) { MoonrakerDeviceInfo info; std::string device_error; - if (fetch_device_info(base_url, api_key, info, device_error)) { + if (fetch_device_info(device_info.base_url, device_info.api_key, info, device_error)) { series_id = infer_series_id(info.model_id, info.dev_name); } } @@ -496,4 +396,68 @@ std::string QidiPrinterAgent::map_filament_type_to_setting_id(const std::string& return ""; } +std::string QidiPrinterAgent::normalize_model_key(std::string value) +{ + boost::algorithm::to_lower(value); + std::string normalized; + normalized.reserve(value.size()); + for (unsigned char c : value) { + if (std::isalnum(c)) { + normalized.push_back(static_cast(c)); + } + } + return normalized; +} + +std::string QidiPrinterAgent::infer_series_id(const std::string& model_id, const std::string& dev_name) +{ + std::string source = model_id.empty() ? dev_name : model_id; + boost::trim(source); + if (source.empty()) { + return ""; + } + if (is_numeric(source)) { + return source; + } + + const std::string key = normalize_model_key(source); + if (key.find("q2") != std::string::npos) { + return "1"; + } + if (key.find("xmax") != std::string::npos && key.find("4") != std::string::npos) { + return "3"; + } + if ((key.find("xplus") != std::string::npos || key.find("plus") != std::string::npos) && key.find("4") != std::string::npos) { + return "0"; + } + return ""; +} + +std::string QidiPrinterAgent::normalize_filament_type(const std::string& filament_type) +{ + std::string trimmed = filament_type; + boost::trim(trimmed); + std::string upper = trimmed; + std::transform(upper.begin(), upper.end(), upper.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); + + if (upper.find("PLA") != std::string::npos) + return "PLA"; + if (upper.find("ABS") != std::string::npos) + return "ABS"; + if (upper.find("PETG") != std::string::npos) + return "PETG"; + if (upper.find("TPU") != std::string::npos) + return "TPU"; + if (upper.find("ASA") != std::string::npos) + return "ASA"; + if (upper.find("PA") != std::string::npos || upper.find("NYLON") != std::string::npos) + return "PA"; + if (upper.find("PC") != std::string::npos) + return "PC"; + if (upper.find("PVA") != std::string::npos) + return "PVA"; + + return trimmed; +} + } // namespace Slic3r diff --git a/src/slic3r/Utils/QidiPrinterAgent.hpp b/src/slic3r/Utils/QidiPrinterAgent.hpp index a2a51c423b..8657443288 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.hpp +++ b/src/slic3r/Utils/QidiPrinterAgent.hpp @@ -39,8 +39,15 @@ private: }; // Qidi-specific methods - bool fetch_slot_info(const std::string& base_url, const std::string& api_key, std::vector& slots, int& box_count, std::string& error) const; + bool fetch_slot_info(const std::string& base_url, + const std::string& api_key, + std::vector& slots, + int& box_count, + std::string& error) const; bool fetch_filament_dict(const std::string& base_url, const std::string& api_key, QidiFilamentDict& dict, std::string& error) const; + std::string normalize_filament_type(const std::string& filament_type); + std::string infer_series_id(const std::string& model_id, const std::string& dev_name); + std::string normalize_model_key(std::string value); // Static helpers static void parse_ini_section(const std::string& content, const std::string& section_name, std::map& result); From e93f50702e85536b96cef48e2c84708c53d82ce5 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 26 Jan 2026 20:12:55 +0800 Subject: [PATCH 06/25] clean up --- src/slic3r/GUI/WebUserLoginDialog.cpp | 15 --- src/slic3r/Utils/BBLCloudServiceAgent.cpp | 5 +- src/slic3r/Utils/BBLNetworkPlugin.cpp | 35 +------ src/slic3r/Utils/BBLPrinterAgent.cpp | 5 +- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 65 ++----------- src/slic3r/Utils/NetworkAgent.cpp | 14 --- src/slic3r/Utils/NetworkAgentFactory.cpp | 12 --- src/slic3r/Utils/OrcaCloudServiceAgent.cpp | 104 +++------------------ src/slic3r/Utils/QidiPrinterAgent.cpp | 3 - 9 files changed, 24 insertions(+), 234 deletions(-) diff --git a/src/slic3r/GUI/WebUserLoginDialog.cpp b/src/slic3r/GUI/WebUserLoginDialog.cpp index 58f7b575ef..2e479f4cf7 100644 --- a/src/slic3r/GUI/WebUserLoginDialog.cpp +++ b/src/slic3r/GUI/WebUserLoginDialog.cpp @@ -264,15 +264,10 @@ void ZUserLogin::OnFullScreenChanged(wxWebViewEvent &evt) void ZUserLogin::OnScriptMessage(wxWebViewEvent &evt) { wxString str_input = evt.GetString(); - BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] OnScriptMessage received: " << str_input.ToStdString(); try { json j = json::parse(into_u8(str_input)); - - BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] Parsed JSON successfully"; - wxString strCmd = j["command"]; - BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] Command: " << strCmd.ToStdString(); NetworkAgent* agent = wxGetApp().getAgent(); if (agent && strCmd == "get_login_cmd" && agent->get_cloud_agent()) { @@ -323,30 +318,21 @@ void ZUserLogin::OnScriptMessage(wxWebViewEvent &evt) if (strCmd == "autotest_token") { m_AutotestToken = j["data"]["token"]; - BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] Stored autotest_token"; } if (strCmd == "user_login") { - BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] Processing user_login command"; - BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] User data: " << j["data"].dump(); - j["data"]["autotest_token"] = m_AutotestToken; std::string message_json = j.dump(); - BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] Calling handle_script_message with: " << message_json; - // End modal dialog first to unblock event loop before processing callbacks - BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] Ending modal dialog"; EndModal(wxID_OK); // Handle message after modal dialog ends to avoid deadlock // Use wxTheApp->CallAfter to ensure it runs after modal loop exits wxTheApp->CallAfter([message_json]() { - BOOST_LOG_TRIVIAL(debug) << "[WebUserLoginDialog] Processing login message after modal ended"; wxGetApp().handle_script_message(message_json); }); } else if (strCmd == "get_localhost_url") { - BOOST_LOG_TRIVIAL(debug) << "thirdparty_login: get_localhost_url"; int loopback_port = m_loopback_port > 0 ? m_loopback_port : LOCALHOST_PORT; wxGetApp().start_http_server(loopback_port); std::string sequence_id = j["sequence_id"].get(); @@ -362,7 +348,6 @@ void ZUserLogin::OnScriptMessage(wxWebViewEvent &evt) }); } else if (strCmd == "thirdparty_login") { - BOOST_LOG_TRIVIAL(info) << "thirdparty_login: thirdparty_login"; if (j["data"].contains("url")) { std::string jump_url = j["data"]["url"].get(); int loopback_port = m_loopback_port > 0 ? m_loopback_port : LOCALHOST_PORT; diff --git a/src/slic3r/Utils/BBLCloudServiceAgent.cpp b/src/slic3r/Utils/BBLCloudServiceAgent.cpp index ee7fcf4355..5f20549db2 100644 --- a/src/slic3r/Utils/BBLCloudServiceAgent.cpp +++ b/src/slic3r/Utils/BBLCloudServiceAgent.cpp @@ -5,10 +5,7 @@ namespace Slic3r { -BBLCloudServiceAgent::BBLCloudServiceAgent() -{ - BOOST_LOG_TRIVIAL(info) << "BBLCloudServiceAgent: Constructor - using BBLNetworkPlugin singleton"; -} +BBLCloudServiceAgent::BBLCloudServiceAgent() = default; BBLCloudServiceAgent::~BBLCloudServiceAgent() = default; diff --git a/src/slic3r/Utils/BBLNetworkPlugin.cpp b/src/slic3r/Utils/BBLNetworkPlugin.cpp index e584bed17e..290de9c32b 100644 --- a/src/slic3r/Utils/BBLNetworkPlugin.cpp +++ b/src/slic3r/Utils/BBLNetworkPlugin.cpp @@ -92,12 +92,8 @@ int BBLNetworkPlugin::initialize(bool using_backup, const std::string& version) legacy_path = plugin_folder / (std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".so"); #endif if (!boost::filesystem::exists(versioned_path) && boost::filesystem::exists(legacy_path)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": auto-migrating unversioned legacy library to versioned format"; - try { boost::filesystem::rename(legacy_path, versioned_path); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": successfully renamed " << legacy_path.string() << " to " - << versioned_path.string(); } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to rename legacy library: " << e.what(); } @@ -107,7 +103,6 @@ int BBLNetworkPlugin::initialize(bool using_backup, const std::string& version) // Load versioned library #if defined(_MSC_VER) || defined(_WIN32) library = plugin_folder.string() + "\\" + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".dll"; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": loading versioned library at " << library; #else #if defined(__WXMAC__) std::string lib_ext = ".dylib"; @@ -115,7 +110,6 @@ int BBLNetworkPlugin::initialize(bool using_backup, const std::string& version) std::string lib_ext = ".so"; #endif library = plugin_folder.string() + "/" + std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + lib_ext; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": loading versioned library at " << library; #endif #if defined(_MSC_VER) || defined(_WIN32) @@ -124,10 +118,8 @@ int BBLNetworkPlugin::initialize(bool using_backup, const std::string& version) ::MultiByteToWideChar(CP_UTF8, NULL, library.c_str(), strlen(library.c_str())+1, lib_wstr, sizeof(lib_wstr) / sizeof(lib_wstr[0])); m_networking_module = LoadLibrary(lib_wstr); if (!m_networking_module) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": versioned library not found, trying current directory"; std::string library_path = get_libpath_in_current_directory(std::string(BAMBU_NETWORK_LIBRARY)); if (library_path.empty()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", can not get path in current directory for %1%") % BAMBU_NETWORK_LIBRARY; set_load_error( "Network library not found", "Could not locate versioned library: " + library, @@ -135,7 +127,6 @@ int BBLNetworkPlugin::initialize(bool using_backup, const std::string& version) ); return -1; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", current path %1%")%library_path; memset(lib_wstr, 0, sizeof(lib_wstr)); ::MultiByteToWideChar(CP_UTF8, NULL, library_path.c_str(), strlen(library_path.c_str())+1, lib_wstr, sizeof(lib_wstr) / sizeof(lib_wstr[0])); m_networking_module = LoadLibrary(lib_wstr); @@ -151,11 +142,9 @@ int BBLNetworkPlugin::initialize(bool using_backup, const std::string& version) library ); } - printf("after dlopen, network_module is %p\n", m_networking_module); #endif if (!m_networking_module) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", can not Load Library for %1%")%library; if (!m_load_error.has_error) { set_load_error( "Network library failed to load", @@ -165,7 +154,6 @@ int BBLNetworkPlugin::initialize(bool using_backup, const std::string& version) } return -1; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", successfully loaded library %1%, module %2%")%library %m_networking_module; // Load file transfer interface InitFTModule(m_networking_module); @@ -174,9 +162,7 @@ int BBLNetworkPlugin::initialize(bool using_backup, const std::string& version) load_all_function_pointers(); if (m_get_version) { - std::string ver = m_get_version(); - printf("network plugin version: %s\n", ver.c_str()); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": network plugin version = " << ver; + (void) m_get_version(); } return 0; @@ -184,8 +170,6 @@ int BBLNetworkPlugin::initialize(bool using_backup, const std::string& version) int BBLNetworkPlugin::unload() { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", network module %1%")%m_networking_module; - UnloadFTModule(); #if defined(_MSC_VER) || defined(_WIN32) @@ -247,7 +231,6 @@ std::string BBLNetworkPlugin::get_version() const void* BBLNetworkPlugin::create_agent(const std::string& log_dir) { if (m_agent) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": agent already exists"; return m_agent; } @@ -255,9 +238,6 @@ void* BBLNetworkPlugin::create_agent(const std::string& log_dir) m_agent = m_create_agent(log_dir); } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", agent=%1%, create_agent=%2%, log_dir=%3%") - % m_agent % (m_create_agent ? "yes" : "no") % log_dir; - return m_agent; } @@ -267,8 +247,6 @@ int BBLNetworkPlugin::destroy_agent() if (m_agent && m_destroy_agent) { ret = m_destroy_agent(m_agent); } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", agent=%1%, destroy_agent=%2%, ret=%3%") - % m_agent % (m_destroy_agent ? "yes" : "no") % ret; m_agent = nullptr; return ret; } @@ -299,13 +277,10 @@ void* BBLNetworkPlugin::get_source_module() ::MultiByteToWideChar(CP_UTF8, NULL, library.c_str(), strlen(library.c_str())+1, lib_wstr, sizeof(lib_wstr) / sizeof(lib_wstr[0])); m_source_module = LoadLibrary(lib_wstr); if (!m_source_module) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", try load BambuSource directly from current directory"); std::string library_path = get_libpath_in_current_directory(std::string(BAMBU_SOURCE_LIBRARY)); if (library_path.empty()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", can not get path in current directory for %1%") % BAMBU_SOURCE_LIBRARY; return m_source_module; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", current path %1%")%library_path; memset(lib_wstr, 0, sizeof(lib_wstr)); ::MultiByteToWideChar(CP_UTF8, NULL, library_path.c_str(), strlen(library_path.c_str()) + 1, lib_wstr, sizeof(lib_wstr) / sizeof(lib_wstr[0])); m_source_module = LoadLibrary(lib_wstr); @@ -335,9 +310,6 @@ void* BBLNetworkPlugin::get_function(const char* name) function = dlsym(m_networking_module, name); #endif - if (!function) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", can not find function %1%")%name; - } return function; } @@ -352,7 +324,6 @@ std::string BBLNetworkPlugin::get_libpath_in_current_directory(const std::string wchar_t file_name[512]; DWORD ret = GetModuleFileNameW(NULL, file_name, 512); if (!ret) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", GetModuleFileNameW return error, can not Load Library for %1%") % library_name; return lib_path; } int size_needed = ::WideCharToMultiByte(0, 0, file_name, wcslen(file_name), nullptr, 0, nullptr, nullptr); @@ -430,12 +401,8 @@ void BBLNetworkPlugin::remove_legacy_library() #endif if (boost::filesystem::exists(legacy_path)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": removing legacy library at " << legacy_path.string(); boost::system::error_code ec; boost::filesystem::remove(legacy_path, ec); - if (ec) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to remove legacy library: " << ec.message(); - } } } diff --git a/src/slic3r/Utils/BBLPrinterAgent.cpp b/src/slic3r/Utils/BBLPrinterAgent.cpp index e547b424a5..22ce2c24a5 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.cpp +++ b/src/slic3r/Utils/BBLPrinterAgent.cpp @@ -6,10 +6,7 @@ namespace Slic3r { -BBLPrinterAgent::BBLPrinterAgent() -{ - BOOST_LOG_TRIVIAL(info) << "BBLPrinterAgent: Constructor - using BBLNetworkPlugin singleton"; -} +BBLPrinterAgent::BBLPrinterAgent() = default; BBLPrinterAgent::~BBLPrinterAgent() = default; diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 3c24ffe4db..e5c10bb74b 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -96,7 +96,6 @@ const std::string MoonrakerPrinterAgent_VERSION = "1.0.0"; MoonrakerPrinterAgent::MoonrakerPrinterAgent(std::string log_dir) : m_cloud_agent(nullptr) { - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Constructor - log_dir=" << log_dir; (void) log_dir; } @@ -150,7 +149,6 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i { std::lock_guard lock(connect_mutex); if (connect_in_progress.load()) { - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: Connection already in progress, waiting..."; // Don't reject - wait for previous connection to complete // This can happen if MonitorPanel triggers connect while previous connect is still running } else { @@ -161,7 +159,6 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i // Wait for previous connection thread to finish if (connect_thread.joinable()) { - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Waiting for previous connection thread..."; connect_thread.join(); } @@ -187,7 +184,6 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i perform_connection_async(dev_id, device_info.base_url, device_info.api_key); }); - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: connect_printer launched in background - dev_id=" << dev_id; return BAMBU_NETWORK_SUCCESS; } @@ -214,13 +210,13 @@ int MoonrakerPrinterAgent::disconnect_printer() int MoonrakerPrinterAgent::check_cert() { - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: check_cert (stub)"; return BAMBU_NETWORK_SUCCESS; } void MoonrakerPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) { - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: install_device_cert (stub) - dev_id=" << dev_id << ", lan_only=" << lan_only; + (void) dev_id; + (void) lan_only; } bool MoonrakerPrinterAgent::start_discovery(bool start, bool sending) @@ -234,7 +230,7 @@ bool MoonrakerPrinterAgent::start_discovery(bool start, bool sending) int MoonrakerPrinterAgent::ping_bind(std::string ping_code) { - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: ping_bind (stub) - ping_code=" << ping_code; + (void) ping_code; return BAMBU_NETWORK_SUCCESS; } @@ -268,8 +264,8 @@ int MoonrakerPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, int MoonrakerPrinterAgent::bind( std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) { - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: bind (stub) - dev_id=" << dev_id; (void) dev_ip; + (void) dev_id; (void) sec_link; (void) timezone; (void) improved; @@ -279,13 +275,12 @@ int MoonrakerPrinterAgent::bind( int MoonrakerPrinterAgent::unbind(std::string dev_id) { - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: unbind (stub) - dev_id=" << dev_id; + (void) dev_id; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::request_bind_ticket(std::string* ticket) { - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: request_bind_ticket (stub)"; if (ticket) *ticket = ""; return BAMBU_NETWORK_SUCCESS; @@ -313,7 +308,7 @@ int MoonrakerPrinterAgent::set_user_selected_machine(std::string dev_id) int MoonrakerPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: start_print (stub) - task_name=" << params.task_name; + (void) params; (void) update_fn; (void) cancel_fn; (void) wait_fn; @@ -322,7 +317,6 @@ int MoonrakerPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn upda int MoonrakerPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: start_local_print_with_record (stub)"; (void) params; (void) update_fn; (void) cancel_fn; @@ -411,7 +405,6 @@ int MoonrakerPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusF int MoonrakerPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) { - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: start_sdcard_print (stub)"; (void) params; (void) update_fn; (void) cancel_fn; @@ -478,7 +471,7 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) void MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) { - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info (base class no-op) called for dev_id=" << dev_id; + (void) dev_id; } int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) @@ -514,7 +507,6 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: } const std::string cmd = command.get(); - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: Received print command: " << cmd; // Handle gcode_line command - this is how G-code commands are sent from OrcaSlicer if (cmd == "gcode_line") { @@ -828,8 +820,6 @@ bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::str payload["script"] = gcode; std::string payload_str = payload.dump(); - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: send_gcode to " << device_info.base_url << " with payload: " << payload_str; - std::string response_body; bool success = false; std::string http_error; @@ -843,7 +833,6 @@ bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::str .timeout_connect(5) .timeout_max(10) .on_complete([&](std::string body, unsigned status_code) { - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: send_gcode response status=" << status_code << " body=" << body; if (status_code == 200) { response_body = body; success = true; @@ -852,7 +841,6 @@ bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::str } }) .on_error([&](std::string body, std::string err, unsigned status_code) { - BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: send_gcode error - body=" << body << " err=" << err << " status=" << status_code; http_error = err; if (status_code > 0) { http_error += " (HTTP " + std::to_string(status_code) + ")"; @@ -865,7 +853,6 @@ bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::str return false; } - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: sent gcode successfully: " << gcode; return true; } @@ -964,18 +951,14 @@ int MoonrakerPrinterAgent::send_access_code(const std::string& dev_id) void MoonrakerPrinterAgent::announce_printhost_device() { - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device() called"; - OnMsgArrivedFn ssdp_fn; { std::lock_guard lock(state_mutex); ssdp_fn = on_ssdp_msg_fn; if (!ssdp_fn) { - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - no ssdp callback"; return; } if (ssdp_announced_host == device_info.base_url && !ssdp_announced_id.empty()) { - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - already announced"; return; } } @@ -987,11 +970,8 @@ void MoonrakerPrinterAgent::announce_printhost_device() std::string fetch_error; if (fetch_device_info(device_info.base_url, device_info.api_key, info, fetch_error) && !info.dev_name.empty()) { dev_name = info.dev_name; - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Got device name from printer: " << dev_name; } else { dev_name = device_info.model_name.empty() ? "Moonraker Printer" : device_info.model_name; - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Using fallback device name: " << dev_name - << " (fetch_error=" << fetch_error << ")"; } const std::string model_id = device_info.model_id; @@ -1021,19 +1001,14 @@ void MoonrakerPrinterAgent::announce_printhost_device() ssdp_announced_id = device_info.dev_id; // Set this as the selected machine if nothing is currently selected - // This ensures auto-connect works when MonitorPanel opens if (selected_machine.empty()) { selected_machine = device_info.dev_id; - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Auto-selected machine: " << device_info.dev_id; } } } void MoonrakerPrinterAgent::dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg) { - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: dispatch_local_connect state=" << state - << " dev_id=" << dev_id << " msg=" << msg; - OnLocalConnectedFn local_fn; QueueOnMainFn queue_fn; { @@ -1042,7 +1017,6 @@ void MoonrakerPrinterAgent::dispatch_local_connect(int state, const std::string& queue_fn = queue_on_main_fn; } if (!local_fn) { - BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: dispatch_local_connect - no callback registered!"; return; } @@ -1156,25 +1130,16 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba std::set available_objects; std::string list_error; if (fetch_object_list(base_url, api_key, available_objects, list_error)) { - // Store available_objects in member variable for feature detection { std::lock_guard lock(payload_mutex); this->available_objects = std::move(available_objects); } - std::string objects_str; - for (const auto& name : this->available_objects) { - if (!objects_str.empty()) objects_str += ", "; - objects_str += name; - } - if (this->available_objects.count("heater_bed") != 0) { subscribe_objects.insert("heater_bed"); } - // Only subscribe to "fan" if it exists (standard Moonraker API) if (this->available_objects.count("fan") != 0) { subscribe_objects.insert("fan"); - } else { } // Add toolhead for homing status @@ -1358,7 +1323,6 @@ void MoonrakerPrinterAgent::handle_ws_message(const std::string& dev_id, const s if (!current_state.empty() && current_state != last_print_state) { is_critical = true; last_print_state = current_state; - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Print state changed to " << current_state << ", dispatching immediately"; } } @@ -1640,7 +1604,8 @@ bool MoonrakerPrinterAgent::upload_gcode( .timeout_connect(5) .timeout_max(300) // 5 minutes for large files .on_complete([&](std::string body, unsigned status) { - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: Upload complete: HTTP " << status << " body: " << body; + (void) body; + (void) status; }) .on_error([&](std::string body, std::string err, unsigned status) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Upload error: " << err << " HTTP " << status; @@ -1672,17 +1637,7 @@ bool MoonrakerPrinterAgent::upload_gcode( int MoonrakerPrinterAgent::pause_print(const std::string& dev_id) { - nlohmann::json request; - request["jsonrpc"] = "2.0"; - request["method"] = "printer.print.pause"; - request["id"] = next_jsonrpc_id++; - - std::string response; - // For JSON-RPC over HTTP, we need to use POST to /printer/print/pause - // But Moonraker also supports this via WebSocket - // For now, send via gcode script which is simpler - std::string gcode = "PAUSE"; - return send_gcode(dev_id, gcode) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; + return send_gcode(dev_id, "PAUSE") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; } int MoonrakerPrinterAgent::resume_print(const std::string& dev_id) diff --git a/src/slic3r/Utils/NetworkAgent.cpp b/src/slic3r/Utils/NetworkAgent.cpp index 273612365c..b893593ce2 100644 --- a/src/slic3r/Utils/NetworkAgent.cpp +++ b/src/slic3r/Utils/NetworkAgent.cpp @@ -114,16 +114,10 @@ NetworkAgent::NetworkAgent(std::string log_dir) plugin.create_agent(log_dir); } - // Create BBL sub-agents that will use the singleton m_cloud_agent = std::make_shared(); m_printer_agent = std::make_shared(); m_printer_agent->set_cloud_agent(m_cloud_agent); m_printer_agent_id = m_printer_agent->get_agent_info().id; - - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", this %1%, agent=%2%, log_dir=%3%") - % this % plugin.get_agent() % log_dir; - } else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": BBL network plugin not loaded"; } } @@ -132,17 +126,12 @@ NetworkAgent::NetworkAgent(std::shared_ptr cloud_agent, : m_cloud_agent(std::move(cloud_agent)) , m_printer_agent(std::move(printer_agent)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format( - ", sub-agent composition mode: cloud=%1%, printer=%2%") - % (m_cloud_agent ? "yes" : "no") - % (m_printer_agent ? "yes" : "no (will be set when printer selected)"); } NetworkAgent::~NetworkAgent() { // Note: We don't destroy the agent here anymore since it's managed by BBLNetworkPlugin singleton // The singleton manages the agent lifecycle - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", this %1%") % this; } void NetworkAgent::set_printer_agent(std::shared_ptr printer_agent) @@ -158,7 +147,6 @@ void NetworkAgent::set_printer_agent(std::shared_ptr printer_agen std::lock_guard lock(m_agent_mutex); if (!printer_agent) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": null printer agent provided"; return; } @@ -180,8 +168,6 @@ void NetworkAgent::set_printer_agent(std::shared_ptr printer_agen // critical section duration. The local shared_ptr copy ensures the agent // cannot be destroyed while we're using it. apply_printer_callbacks(new_printer_agent, callbacks); - - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": agent switched successfully"; } void* NetworkAgent::get_network_agent() diff --git a/src/slic3r/Utils/NetworkAgentFactory.cpp b/src/slic3r/Utils/NetworkAgentFactory.cpp index 0f51d43e62..faafe136b2 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.cpp +++ b/src/slic3r/Utils/NetworkAgentFactory.cpp @@ -34,16 +34,12 @@ bool NetworkAgentFactory::register_printer_agent(const std::string& id, const st auto result = agents.emplace(id, PrinterAgentInfo(id, display_name, std::move(factory))); if (result.second) { - BOOST_LOG_TRIVIAL(info) << "Registered printer agent: " << id << " (" << display_name << ")"; - - // Set as default if it's the first agent registered auto& default_id = get_default_agent_id(); if (default_id.empty()) { default_id = id; } return true; } else { - BOOST_LOG_TRIVIAL(warning) << "Printer agent already registered: " << id; return false; } } @@ -106,9 +102,6 @@ void NetworkAgentFactory::set_default_printer_agent_id(const std::string& id) if (agents.find(id) != agents.end()) { get_default_agent_id() = id; - BOOST_LOG_TRIVIAL(info) << "Default printer agent set to: " << id; - } else { - BOOST_LOG_TRIVIAL(warning) << "Cannot set default to unregistered agent: " << id; } } @@ -170,7 +163,6 @@ void NetworkAgentFactory::register_all_agents() }); } - BOOST_LOG_TRIVIAL(info) << "Registered " << get_printer_agents().size() << " printer agents"; } std::unique_ptr create_agent_from_config(const std::string& log_dir, AppConfig* app_config) @@ -189,9 +181,7 @@ std::unique_ptr create_agent_from_config(const std::string& log_di CloudAgentProvider provider = use_orca_cloud ? CloudAgentProvider::Orca : CloudAgentProvider::BBL; auto cloud_agent = NetworkAgentFactory::create_cloud_agent(provider, log_dir); - // Fall back to Orca if BBL plugin not available if (!cloud_agent && provider == CloudAgentProvider::BBL) { - BOOST_LOG_TRIVIAL(warning) << "BBL plugin not loaded, falling back to Orca cloud agent"; cloud_agent = NetworkAgentFactory::create_cloud_agent(CloudAgentProvider::Orca, log_dir); } @@ -206,7 +196,6 @@ std::unique_ptr create_agent_from_config(const std::string& log_di // We will create the printer agent later when the printer is selected, so we pass nullptr for the printer agent here. auto agent = NetworkAgentFactory::create_from_agents(std::move(cloud_agent), nullptr); - // Configure URL overrides for Orca cloud if (agent && app_config && use_orca_cloud) { auto* orca_cloud = dynamic_cast(agent->get_cloud_agent().get()); if (orca_cloud) { @@ -214,7 +203,6 @@ std::unique_ptr create_agent_from_config(const std::string& log_di } } - BOOST_LOG_TRIVIAL(info) << "Created NetworkAgent with cloud agent"; return agent; } diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp index 6ed3bab66c..da518f1e10 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp @@ -332,7 +332,6 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir) update_redirect_uri(); regenerate_pkce(); compute_fallback_path(); - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Constructor - log_dir=" << this->log_dir; } OrcaCloudServiceAgent::~OrcaCloudServiceAgent() @@ -348,19 +347,15 @@ void OrcaCloudServiceAgent::configure_urls(AppConfig* app_config) // Read token storage preference m_use_encrypted_token_file = app_config->get_bool(SETTING_USE_ENCRYPTED_TOKEN_FILE); - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: token storage mode set to " - << (m_use_encrypted_token_file ? "encrypted file" : "System Keychain"); std::string api_url = app_config->get(CONFIG_ORCA_API_URL); if (!api_url.empty()) { api_base_url = api_url; - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Using custom API URL: " << api_base_url; } std::string auth_url = app_config->get(CONFIG_ORCA_AUTH_URL); if (!auth_url.empty()) { auth_base_url = auth_url; - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Using custom Auth URL: " << auth_base_url; } std::string pub_key = app_config->get(CONFIG_ORCA_PUB_KEY); @@ -382,8 +377,6 @@ void OrcaCloudServiceAgent::set_auth_base_url(const std::string& url) void OrcaCloudServiceAgent::set_use_encrypted_token_file(bool use) { m_use_encrypted_token_file = use; - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: token storage mode set to " - << (m_use_encrypted_token_file ? "encrypted file" : "System Keychain"); } bool OrcaCloudServiceAgent::get_use_encrypted_token_file() const @@ -397,7 +390,6 @@ bool OrcaCloudServiceAgent::get_use_encrypted_token_file() const int OrcaCloudServiceAgent::init_log() { - BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: init_log called"; return BAMBU_NETWORK_SUCCESS; } @@ -407,35 +399,30 @@ int OrcaCloudServiceAgent::set_config_dir(std::string cfg_dir) wxFileName fallback(wxString::FromUTF8(cfg_dir.c_str()), "orca_refresh_token.sec"); fallback.Normalize(); refresh_fallback_path = fallback.GetFullPath().ToStdString(); - BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: set_config_dir - " << cfg_dir; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::set_cert_file(std::string folder, std::string filename) { // Not used by OrcaCloudServiceAgent (OAuth doesn't need client certs) - BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: set_cert_file called (unused) - " << folder << "/" << filename; + (void) folder; + (void) filename; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::set_country_code(std::string code) { country_code = code; - BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: set_country_code - " << code; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::start() { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: start called"; - - // Regenerate PKCE bundle for fresh login attempts regenerate_pkce(); // Attempt silent sign-in from stored refresh token std::string stored_refresh; if (load_refresh_token(stored_refresh) && !stored_refresh.empty()) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Found stored refresh token, attempting silent sign-in"; refresh_now(stored_refresh, "refresh token", false); } @@ -456,7 +443,6 @@ bool OrcaCloudServiceAgent::exchange_auth_code(const std::string& auth_code, con } std::string url = auth_base_url + auth_constants::TOKEN_PATH; - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: exchanging auth code for tokens"; std::string redirect_uri; std::string code_verifier; @@ -533,8 +519,6 @@ bool OrcaCloudServiceAgent::exchange_auth_code(const std::string& auth_code, con int OrcaCloudServiceAgent::change_user(std::string user_info) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: change_user invoked"; - try { std::stringstream ss(user_info); pt::ptree tree; @@ -547,7 +531,6 @@ int OrcaCloudServiceAgent::change_user(std::string user_info) // Check if this is a WebView login message (PKCE flow completion) std::string command = tree.get("command", ""); if (command == "user_login") { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Detected WebView login message"; auto data_opt = tree.get_child_optional("data"); if (!data_opt) { @@ -562,7 +545,6 @@ int OrcaCloudServiceAgent::change_user(std::string user_info) // Check for auth code (PKCE authorization code flow) std::string auth_code = read_str(data, "code"); if (!auth_code.empty()) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Detected auth code, exchanging for tokens"; std::string session_payload; if (!exchange_auth_code(auth_code, state, session_payload)) { invoke_user_login_callback(0, false); @@ -652,7 +634,6 @@ int OrcaCloudServiceAgent::change_user(std::string user_info) return BAMBU_NETWORK_ERR_INVALID_HANDLE; } - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Orca cloud login successful - user_id=" << user_id; bool success = set_user_session(access_token, user_id, username, name, nickname, avatar, refresh_token); if (success) { invoke_user_login_callback(1, true); @@ -662,8 +643,6 @@ int OrcaCloudServiceAgent::change_user(std::string user_info) } else { invoke_user_login_callback(0, false); } - BOOST_LOG_TRIVIAL(info) << "[auth] event=login result=" << (success ? "success" : "failure") - << " source=session user_id=" << user_id; return success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_INVALID_HANDLE; } @@ -686,7 +665,6 @@ bool OrcaCloudServiceAgent::is_user_login() int OrcaCloudServiceAgent::user_logout(bool request) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: user_logout - request=" << request; // Send logout request to backend if requested if (request) { @@ -719,14 +697,8 @@ int OrcaCloudServiceAgent::user_logout(bool request) } } - // Clear session clear_session(); - - // Invoke callback invoke_user_login_callback(0, false); - - BOOST_LOG_TRIVIAL(info) << "[auth] event=logout result=success request=" << request; - return BAMBU_NETWORK_SUCCESS; } @@ -871,8 +843,6 @@ bool OrcaCloudServiceAgent::ensure_token_fresh(const std::string& reason) int OrcaCloudServiceAgent::connect_server() { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: connect_server"; - std::string response; unsigned int http_code = 0; int result = http_get(ORCA_HEALTH_PATH, &response, &http_code); @@ -884,8 +854,6 @@ int OrcaCloudServiceAgent::connect_server() } invoke_server_connected_callback(connected ? 0 : -1, http_code); - - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: connect_server result=" << (connected ? "connected" : "failed"); return connected ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; } @@ -902,25 +870,25 @@ int OrcaCloudServiceAgent::refresh_connection() int OrcaCloudServiceAgent::start_subscribe(std::string module) { - BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: start_subscribe - " << module << " (stub)"; + (void) module; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::stop_subscribe(std::string module) { - BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: stop_subscribe - " << module << " (stub)"; + (void) module; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::add_subscribe(std::vector dev_list) { - BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: add_subscribe (stub)"; + (void) dev_list; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::del_subscribe(std::vector dev_list) { - BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: del_subscribe (stub)"; + (void) dev_list; return BAMBU_NETWORK_SUCCESS; } @@ -936,8 +904,6 @@ void OrcaCloudServiceAgent::enable_multi_machine(bool enable) int OrcaCloudServiceAgent::get_user_presets(std::map>* user_presets) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: get_user_presets"; - if (!user_presets) return BAMBU_NETWORK_ERR_INVALID_HANDLE; if (!is_user_login()) { @@ -995,8 +961,6 @@ int OrcaCloudServiceAgent::get_user_presets(std::map* values_map, unsigned int* http_code) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: request_setting_id - " << name; - std::string new_id = generate_uuid(name); if (new_id.empty()) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: request_setting_id failed - name is empty"; @@ -1020,12 +984,9 @@ std::string OrcaCloudServiceAgent::request_setting_id(std::string name, std::map if (http_code) *http_code = result.http_code; if (result.success) { - // Return new_updated_at via values_map so caller can store it in Preset::updated_time if (values_map && !result.new_updated_at.empty()) { (*values_map)[IOT_JSON_KEY_UPDATED_TIME] = result.new_updated_at; } - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Created preset - setting_id=" << new_id - << ", new_updated_at=" << result.new_updated_at; return new_id; } @@ -1035,9 +996,8 @@ std::string OrcaCloudServiceAgent::request_setting_id(std::string name, std::map int OrcaCloudServiceAgent::put_setting(std::string setting_id, std::string name, std::map* values_map, unsigned int* http_code) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: put_setting - setting_id=" << setting_id << ", name=" << name; - - // Extract original_updated_at for Optimistic Concurrency Control (per spec section 5.2) + // Extract original_updated_at for Optimistic Concurrency Control + // If present, server will verify version before update. If absent, treated as insert. // If present, server will verify version before update. If absent, treated as insert. std::string original_updated_at; if (values_map) { @@ -1066,8 +1026,6 @@ int OrcaCloudServiceAgent::put_setting(std::string setting_id, std::string name, if (values_map && !result.new_updated_at.empty()) { (*values_map)[IOT_JSON_KEY_UPDATED_TIME] = result.new_updated_at; } - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: Updated preset - setting_id=" << setting_id - << ", new_updated_at=" << result.new_updated_at; return BAMBU_NETWORK_SUCCESS; } @@ -1091,8 +1049,7 @@ int OrcaCloudServiceAgent::get_setting_list(std::string bundle_version, Progress int OrcaCloudServiceAgent::get_setting_list2(std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn, WasCancelledFn cancel_fn) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: get_setting_list2 - bundle_version=" << bundle_version; - + (void) bundle_version; bool success = false; int error_code = 0; @@ -1103,7 +1060,6 @@ int OrcaCloudServiceAgent::get_setting_list2(std::string bundle_version, CheckFn for (const auto& upsert : resp.upserts) { if (cancel_fn && cancel_fn()) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: get_setting_list2 cancelled"; cancelled = true; break; } @@ -1144,7 +1100,6 @@ int OrcaCloudServiceAgent::get_setting_list2(std::string bundle_version, CheckFn if (!cancelled) { for (const auto& deleted_id : resp.deletes) { if (cancel_fn && cancel_fn()) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: get_setting_list2 cancelled"; cancelled = true; break; } @@ -1190,8 +1145,6 @@ int OrcaCloudServiceAgent::get_setting_list2(std::string bundle_version, CheckFn int OrcaCloudServiceAgent::delete_setting(std::string setting_id) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: delete_setting - " << setting_id; - std::string path = std::string(ORCA_PROFILES_PATH) + "/" + setting_id; std::string response; unsigned int http_code = 0; @@ -1212,8 +1165,6 @@ int OrcaCloudServiceAgent::sync_pull( std::function on_success, std::function on_error) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: sync_pull"; - std::string path = ORCA_SYNC_PULL_PATH; if (!sync_state.last_sync_timestamp.empty()) { path += "?cursor=" + sync_state.last_sync_timestamp; @@ -1238,9 +1189,7 @@ int OrcaCloudServiceAgent::sync_pull( return BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED; } - // Handle 304 Not Modified - no changes if (http_code == 304) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: sync_pull - no changes (304)"; if (on_success) { SyncPullResponse empty_response; on_success(empty_response); @@ -1289,8 +1238,6 @@ SyncPushResult OrcaCloudServiceAgent::sync_push( const nlohmann::json& content, const std::string& original_updated_at) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: sync_push - " << profile_id; - SyncPushResult result; result.success = false; result.http_code = 0; @@ -1429,15 +1376,11 @@ void OrcaCloudServiceAgent::regenerate_pkce() if (pkce_bundle.redirect.empty()) { pkce_bundle.redirect = "http://localhost:" + std::to_string(pkce_bundle.loopback_port) + auth_constants::LOOPBACK_PATH; } - BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: regenerated PKCE bundle"; } void OrcaCloudServiceAgent::update_redirect_uri() { int selected_port = choose_loopback_port(); - if (selected_port != pkce_bundle.loopback_port) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: loopback port changed to " << selected_port; - } pkce_bundle.loopback_port = selected_port; pkce_bundle.redirect = "http://localhost:" + std::to_string(selected_port) + auth_constants::LOOPBACK_PATH; } @@ -1512,15 +1455,12 @@ void OrcaCloudServiceAgent::persist_refresh_token(const std::string& token) } } - if (stored) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: refresh token persisted successfully"; - } + (void) stored; } bool OrcaCloudServiceAgent::load_refresh_token(std::string& out_token) { out_token.clear(); - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: load_refresh_token called"; if (m_use_encrypted_token_file) { // Load from encrypted file only @@ -1555,11 +1495,8 @@ bool OrcaCloudServiceAgent::load_refresh_token(std::string& out_token) if (integrity_ok && aes256gcm_decrypt(encoded_payload, key, plain) && !plain.empty()) { out_token = plain; - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: loaded refresh token from encrypted file"; - // Upgrade legacy payloads to signed format if (payload.rfind("v2:", 0) != 0) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: upgrading legacy token format to v2"; persist_refresh_token(out_token); } return true; @@ -1567,23 +1504,16 @@ bool OrcaCloudServiceAgent::load_refresh_token(std::string& out_token) } } } else { - // Load from wxSecretStore only wxSecretStore store = wxSecretStore::GetDefault(); if (store.IsOk()) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: System Keychain is available, attempting load"; wxString username; wxSecretValue secret; if (store.Load(SECRET_STORE_SERVICE, username, secret) && secret.IsOk()) { out_token.assign(static_cast(secret.GetData()), secret.GetSize()); if (!out_token.empty()) { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: loaded refresh token from System Keychain"; return true; } - } else { - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: System Keychain load failed or data is invalid"; } - } else { - BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: System Keychain is NOT available"; } } @@ -1656,14 +1586,8 @@ bool OrcaCloudServiceAgent::refresh_now(const std::string& refresh_token, const } auto worker = [this, refresh_token, reason]() { - const std::string req_id = generate_state_token(); - BOOST_LOG_TRIVIAL(info) << "[auth] event=refresh_start source=" << reason << " rid=" << req_id; + (void) reason; bool ok = refresh_session_with_token(refresh_token); - if (ok) { - BOOST_LOG_TRIVIAL(info) << "[auth] event=refresh_complete result=success source=" << reason << " rid=" << req_id; - } else { - BOOST_LOG_TRIVIAL(warning) << "[auth] event=refresh_complete result=failure source=" << reason << " rid=" << req_id; - } refresh_running.store(false); return ok; }; @@ -1712,10 +1636,7 @@ bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const bool OrcaCloudServiceAgent::refresh_session_with_token(const std::string& refresh_token) { std::string body = "{\"refresh_token\":\"" + refresh_token + "\"}"; - std::string url = auth_base_url + auth_constants::TOKEN_PATH + "?grant_type=refresh_token"; - BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: refresh request - token_length=" << refresh_token.size() << ", url=" << url; - std::string response; unsigned int http_code = 0; if (!http_post_token(body, &response, &http_code, url) || http_code >= 400) { @@ -1764,7 +1685,6 @@ bool OrcaCloudServiceAgent::refresh_session_with_token(const std::string& refres return false; } - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: token refresh successful - user_id=" << user_id; bool success = set_user_session(access_token, user_id, username, name, nickname, avatar, new_refresh_token); if (success) { invoke_user_login_callback(0, true); @@ -1774,8 +1694,6 @@ bool OrcaCloudServiceAgent::refresh_session_with_token(const std::string& refres } else { invoke_user_login_callback(0, false); } - BOOST_LOG_TRIVIAL(info) << "[auth] event=token_refresh result=" << (success ? "success" : "failure") - << " user_id=" << user_id; return success; } catch (const std::exception& e) { diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index 5f16ab9339..b5dce54c3c 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -141,9 +141,6 @@ void QidiPrinterAgent::fetch_filament_info(std::string dev_id) // Call the parser to populate DevFilaSystem DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem(), false); - - BOOST_LOG_TRIVIAL(info) << "QidiPrinterAgent::fetch_filament_info: Populated DevFilaSystem with " - << box_count << " AMS units"; } bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, From 997b2154c81952a4fed2a0d48126d50b9bd443be Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 26 Jan 2026 21:11:52 +0800 Subject: [PATCH 07/25] disable api --- src/slic3r/Utils/OrcaCloudServiceAgent.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp index da518f1e10..524ac72ed8 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp @@ -47,9 +47,9 @@ namespace pt = boost::property_tree; namespace Slic3r { namespace { -constexpr const char* ORCA_DEFAULT_API_URL = "https://api.orcaslicer.com"; -constexpr const char* ORCA_DEFAULT_AUTH_URL = "https://auth.orcaslicer.com"; -constexpr const char* ORCA_DEFAULT_PUB_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; +constexpr const char* ORCA_DEFAULT_API_URL = "https://xxx.orcaslicer.com"; +constexpr const char* ORCA_DEFAULT_AUTH_URL = "https://xxx.orcaslicer.com"; +constexpr const char* ORCA_DEFAULT_PUB_KEY = "xxxxxxxxxxxxx"; constexpr const char* ORCA_HEALTH_PATH = "/api/v1/health"; constexpr const char* ORCA_SYNC_PULL_PATH = "/api/v1/sync/pull"; constexpr const char* ORCA_SYNC_PUSH_PATH = "/api/v1/sync/push"; From 722d5f8aa772aed6047ebe6581be6d5fc0c3deb9 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 27 Jan 2026 00:42:23 +0800 Subject: [PATCH 08/25] refactor --- src/slic3r/GUI/Plater.cpp | 13 +- src/slic3r/Utils/IPrinterAgent.hpp | 2 +- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 458 +++++++-------------- src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 8 +- src/slic3r/Utils/NetworkAgent.cpp | 5 +- src/slic3r/Utils/NetworkAgent.hpp | 2 +- src/slic3r/Utils/QidiPrinterAgent.cpp | 17 +- src/slic3r/Utils/QidiPrinterAgent.hpp | 2 +- 8 files changed, 182 insertions(+), 325 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 8a412f6a9b..eaa5b1edcd 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3208,7 +3208,9 @@ std::map Sidebar::build_filament_ams_list(MachineObject // For pull-mode agents (e.g., HTTP REST API), refresh DevFilaSystem first auto* agent = wxGetApp().getDeviceManager()->get_agent(); if (agent && agent->get_filament_sync_mode() == FilamentSyncMode::pull) { - agent->fetch_filament_info(obj->get_dev_id()); + if (!agent->fetch_filament_info(obj->get_dev_id())) { + return filament_ams_list; + } } auto build_tray_config = [](DevAmsTray const &tray, std::string const &name, std::string ams_id, std::string slot_id) { @@ -16631,8 +16633,15 @@ void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWar { printer_name.Replace("Bambu Lab", "", false); wxString content; + bool device_page = (wxGetApp().mainframe == nullptr) && (wxGetApp().mainframe->m_monitor->IsShown()); if (type == PrinterWarningType::NOT_CONNECTED) { - content = wxString::Format(_L("Printer not connected. Please go to the device page to connect %s before syncing."), printer_name); + if (device_page) { + content = wxString::Format(_L("Printer not connected. Please go to the device page to connect %s before syncing."), + printer_name); + } else { + content = wxString::Format( + _L("OrcaSlicer can't connect to %s. Please check if the printer is powered on and connected to the network."), printer_name); + } } else if (type == PrinterWarningType::INCONSISTENT) { content = wxString::Format(_L("The currently connected printer on the device page is not %s. Please switch to %s before syncing."), printer_name, printer_name); } else if (type == PrinterWarningType::UNINSTALL_FILAMENT) { diff --git a/src/slic3r/Utils/IPrinterAgent.hpp b/src/slic3r/Utils/IPrinterAgent.hpp index 3af9cf32f8..76c082af50 100644 --- a/src/slic3r/Utils/IPrinterAgent.hpp +++ b/src/slic3r/Utils/IPrinterAgent.hpp @@ -252,7 +252,7 @@ public: * Should only be called when get_filament_sync_mode() returns FilamentSyncMode::pull. * Populates the MachineObject's DevFilaSystem with fetched filament data. */ - virtual void fetch_filament_info(std::string dev_id) {} + virtual bool fetch_filament_info(std::string dev_id) { return false; } }; } // namespace Slic3r diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index e5c10bb74b..768d0c2f31 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -3,8 +3,6 @@ #include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/GUI/DeviceManager.hpp" - #include "nlohmann/json.hpp" #include #include @@ -17,17 +15,15 @@ #include #include #include -#include #include namespace { -namespace beast = boost::beast; -namespace http = beast::http; +namespace beast = boost::beast; +namespace http = beast::http; namespace websocket = beast::websocket; -namespace net = boost::asio; -using tcp = net::ip::tcp; - +namespace net = boost::asio; +using tcp = net::ip::tcp; struct WsEndpoint { @@ -94,19 +90,19 @@ namespace Slic3r { const std::string MoonrakerPrinterAgent_VERSION = "1.0.0"; -MoonrakerPrinterAgent::MoonrakerPrinterAgent(std::string log_dir) : m_cloud_agent(nullptr) -{ - (void) log_dir; -} +MoonrakerPrinterAgent::MoonrakerPrinterAgent(std::string log_dir) : m_cloud_agent(nullptr) { (void) log_dir; } MoonrakerPrinterAgent::~MoonrakerPrinterAgent() { - disconnect_printer(); // This will handle thread cleanup + disconnect_printer(); // This will handle thread cleanup } AgentInfo MoonrakerPrinterAgent::get_agent_info_static() { - return AgentInfo{.id = "moonraker", .name = "Moonraker Printer Agent", .version = MoonrakerPrinterAgent_VERSION, .description = "Klipper/Moonraker printer agent"}; + return AgentInfo{.id = "moonraker", + .name = "Moonraker Printer Agent", + .version = MoonrakerPrinterAgent_VERSION, + .description = "Klipper/Moonraker printer agent"}; } void MoonrakerPrinterAgent::set_cloud_agent(std::shared_ptr cloud) @@ -131,23 +127,15 @@ int MoonrakerPrinterAgent::send_message_to_printer(std::string dev_id, std::stri int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) { - if (dev_id.empty() || dev_ip.empty()) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: connect_printer missing dev_id or dev_ip"; return BAMBU_NETWORK_ERR_INVALID_HANDLE; } - init_device_info(dev_id, dev_ip, username, password, use_ssl); - - if (device_info.dev_id != dev_id) { - BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: connect_printer dev_id mismatch: expected " << device_info.dev_id << ", got " << dev_id; - dispatch_local_connect(ConnectStatusFailed, dev_id, "dev_id_mismatch"); - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - // Check if connection already in progress { std::lock_guard lock(connect_mutex); + init_device_info(dev_id, dev_ip, username, password, use_ssl); if (connect_in_progress.load()) { // Don't reject - wait for previous connection to complete // This can happen if MonitorPanel triggers connect while previous connect is still running @@ -180,9 +168,7 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i last_print_state.clear(); // Launch connection in background thread - connect_thread = std::thread([this, dev_id]() { - perform_connection_async(dev_id, device_info.base_url, device_info.api_key); - }); + connect_thread = std::thread([this, dev_id]() { perform_connection_async(dev_id, device_info.base_url, device_info.api_key); }); return BAMBU_NETWORK_SUCCESS; } @@ -192,6 +178,7 @@ int MoonrakerPrinterAgent::disconnect_printer() // Stop connection thread if running { std::lock_guard lock(connect_mutex); + device_info = MoonrakerDeviceInfo{}; if (connect_in_progress.load()) { connect_stop_requested.store(true); // Wake up any sleeping @@ -208,10 +195,7 @@ int MoonrakerPrinterAgent::disconnect_printer() return BAMBU_NETWORK_SUCCESS; } -int MoonrakerPrinterAgent::check_cert() -{ - return BAMBU_NETWORK_SUCCESS; -} +int MoonrakerPrinterAgent::check_cert() { return BAMBU_NETWORK_SUCCESS; } void MoonrakerPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) { @@ -238,17 +222,6 @@ int MoonrakerPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, { (void) sec_link; - std::string base_url = normalize_base_url(dev_ip, ""); - if (base_url.empty()) { - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - - std::string error; - if (!fetch_device_info(base_url, device_info.api_key, device_info, error)) { - BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: bind_detect failed: " << error; - return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; - } - detect.dev_id = device_info.dev_id.empty() ? dev_ip : device_info.dev_id; detect.model_id = device_info.model_id.empty() ? device_info.model_name : device_info.model_id; // Prefer fetched hostname, then preset model name, then generic fallback @@ -315,7 +288,10 @@ int MoonrakerPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn upda return BAMBU_NETWORK_SUCCESS; } -int MoonrakerPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +int MoonrakerPrinterAgent::start_local_print_with_record(PrintParams params, + OnUpdateStatusFn update_fn, + WasCancelledFn cancel_fn, + OnWaitFn wait_fn) { (void) params; (void) update_fn; @@ -324,11 +300,15 @@ int MoonrakerPrinterAgent::start_local_print_with_record(PrintParams params, OnU return BAMBU_NETWORK_SUCCESS; } -int MoonrakerPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +int MoonrakerPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, + OnUpdateStatusFn update_fn, + WasCancelledFn cancel_fn, + OnWaitFn wait_fn) { (void) wait_fn; - if (update_fn) update_fn(PrintingStageCreate, 0, "Preparing..."); + if (update_fn) + update_fn(PrintingStageCreate, 0, "Preparing..."); std::string filename = params.filename; if (filename.empty()) { @@ -346,13 +326,15 @@ int MoonrakerPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpda return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; } - if (update_fn) update_fn(PrintingStageFinished, 100, "File uploaded"); + if (update_fn) + update_fn(PrintingStageFinished, 100, "File uploaded"); return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) { - if (update_fn) update_fn(PrintingStageCreate, 0, "Preparing..."); + if (update_fn) + update_fn(PrintingStageCreate, 0, "Preparing..."); // Check cancellation if (cancel_fn && cancel_fn()) { @@ -382,7 +364,8 @@ int MoonrakerPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusF upload_filename = sanitize_filename(upload_filename); // Upload file - if (update_fn) update_fn(PrintingStageUpload, 0, "Uploading G-code..."); + if (update_fn) + update_fn(PrintingStageUpload, 0, "Uploading G-code..."); if (!upload_gcode(gcode_path, upload_filename, device_info.base_url, device_info.api_key, update_fn, cancel_fn)) { return BAMBU_NETWORK_ERR_PRINT_LP_UPLOAD_FTP_FAILED; } @@ -393,13 +376,15 @@ int MoonrakerPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusF } // Start print via gcode script (simpler than JSON-RPC) - if (update_fn) update_fn(PrintingStageSending, 0, "Starting print..."); + if (update_fn) + update_fn(PrintingStageSending, 0, "Starting print..."); std::string gcode = "SDCARD_PRINT_FILE FILENAME=" + upload_filename; if (!send_gcode(device_info.dev_id, gcode)) { return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED; } - if (update_fn) update_fn(PrintingStageFinished, 100, "Print started"); + if (update_fn) + update_fn(PrintingStageFinished, 100, "Print started"); return BAMBU_NETWORK_SUCCESS; } @@ -469,14 +454,11 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) return BAMBU_NETWORK_SUCCESS; } -void MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) -{ - (void) dev_id; -} +bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) { return false; } int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) - { - auto json = nlohmann::json::parse(json_str, nullptr, false); +{ + auto json = nlohmann::json::parse(json_str, nullptr, false); if (json.is_discarded()) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Invalid JSON request"; return BAMBU_NETWORK_ERR_INVALID_RESULT; @@ -553,7 +535,7 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: // Bed temperature - UI sends "temp" field if (cmd == "set_bed_temp") { if (json["print"].contains("temp") && json["print"]["temp"].is_number()) { - int temp = json["print"]["temp"].get(); + int temp = json["print"]["temp"].get(); std::string gcode = "SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=" + std::to_string(temp); send_gcode(dev_id, gcode); return BAMBU_NETWORK_SUCCESS; @@ -563,13 +545,13 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: // Nozzle temperature - UI sends "target_temp" and "extruder_index" fields if (cmd == "set_nozzle_temp") { if (json["print"].contains("target_temp") && json["print"]["target_temp"].is_number()) { - int temp = json["print"]["target_temp"].get(); - int extruder_idx = 0; // Default to main extruder + int temp = json["print"]["target_temp"].get(); + int extruder_idx = 0; // Default to main extruder if (json["print"].contains("extruder_index") && json["print"]["extruder_index"].is_number()) { extruder_idx = json["print"]["extruder_index"].get(); } std::string heater = (extruder_idx == 0) ? "extruder" : "extruder" + std::to_string(extruder_idx); - std::string gcode = "SET_HEATER_TEMPERATURE HEATER=" + heater + " TARGET=" + std::to_string(temp); + std::string gcode = "SET_HEATER_TEMPERATURE HEATER=" + heater + " TARGET=" + std::to_string(temp); send_gcode(dev_id, gcode); return BAMBU_NETWORK_SUCCESS; } @@ -606,10 +588,10 @@ bool MoonrakerPrinterAgent::init_device_info(std::string dev_id, std::string dev return true; } -bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, - const std::string& api_key, - MoonrakerDeviceInfo& info, - std::string& error) const +bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, + const std::string& api_key, + MoonrakerDeviceInfo& info, + std::string& error) const { auto fetch_json = [&](const std::string& url, nlohmann::json& out) { std::string response_body; @@ -658,113 +640,17 @@ bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, } nlohmann::json result = json.contains("result") ? json["result"] : json; - info.dev_name = result.value("machine_name", result.value("hostname", "")); - info.version = result.value("software_version", result.value("firmware_version", "")); - - return true; -} - -bool MoonrakerPrinterAgent::fetch_server_info(const std::string& base_url, - const std::string& api_key, - std::string& version, - std::string& error) const -{ - std::string response_body; - bool success = false; - std::string http_error; - - auto http = Http::get(join_url(base_url, "/server/info")); - if (!api_key.empty()) { - http.header("X-Api-Key", api_key); - } - http.timeout_connect(5) - .timeout_max(10) - .on_complete([&](std::string body, unsigned status) { - if (status == 200) { - response_body = body; - success = true; - } else { - http_error = "HTTP error: " + std::to_string(status); - } - }) - .on_error([&](std::string body, std::string err, unsigned status) { - http_error = err; - if (status > 0) { - http_error += " (HTTP " + std::to_string(status) + ")"; - } - }) - .perform_sync(); - - if (!success) { - error = http_error.empty() ? "Connection failed" : http_error; - return false; - } - - auto json = nlohmann::json::parse(response_body, nullptr, false, true); - if (json.is_discarded()) { - error = "Invalid JSON response"; - return false; - } - - nlohmann::json result = json.contains("result") ? json["result"] : json; - if (result.contains("moonraker_version") && result["moonraker_version"].is_string()) { - version = result["moonraker_version"].get(); - } else if (result.contains("version") && result["version"].is_string()) { - version = result["version"].get(); - } - - return true; -} - -bool MoonrakerPrinterAgent::fetch_server_info_json(const std::string& base_url, - const std::string& api_key, - nlohmann::json& info, - std::string& error) const -{ - std::string response_body; - bool success = false; - std::string http_error; - - auto http = Http::get(join_url(base_url, "/server/info")); - if (!api_key.empty()) { - http.header("X-Api-Key", api_key); - } - http.timeout_connect(5) - .timeout_max(10) - .on_complete([&](std::string body, unsigned status) { - if (status == 200) { - response_body = body; - success = true; - } else { - http_error = "HTTP error: " + std::to_string(status); - } - }) - .on_error([&](std::string body, std::string err, unsigned status) { - http_error = err; - if (status > 0) { - http_error += " (HTTP " + std::to_string(status) + ")"; - } - }) - .perform_sync(); - - if (!success) { - error = http_error.empty() ? "Connection failed" : http_error; - return false; - } - - info = nlohmann::json::parse(response_body, nullptr, false, true); - if (info.is_discarded()) { - error = "Invalid JSON response"; - return false; - } + info.dev_name = result.value("machine_name", result.value("hostname", "")); + info.version = result.value("moonraker_version", ""); + info.klippy_state = result.value("klippy_state", ""); return true; } bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url, - const std::string& api_key, - nlohmann::json& status, - std::string& error) const + const std::string& api_key, + nlohmann::json& status, + std::string& error) const { std::string url = join_url(base_url, "/printer/objects/query?print_stats&virtual_sdcard&extruder&heater_bed&fan"); @@ -817,7 +703,7 @@ bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url, bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::string& gcode) const { nlohmann::json payload; - payload["script"] = gcode; + payload["script"] = gcode; std::string payload_str = payload.dump(); std::string response_body; @@ -856,10 +742,10 @@ bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::str return true; } -bool MoonrakerPrinterAgent::fetch_object_list(const std::string& base_url, - const std::string& api_key, - std::set& objects, - std::string& error) const +bool MoonrakerPrinterAgent::fetch_object_list(const std::string& base_url, + const std::string& api_key, + std::set& objects, + std::string& error) const { std::string response_body; bool success = false; @@ -916,15 +802,6 @@ bool MoonrakerPrinterAgent::fetch_object_list(const std::string& base_url, int MoonrakerPrinterAgent::send_version_info(const std::string& dev_id) { - std::string version; - std::string error; - if (!fetch_server_info(device_info.base_url, device_info.api_key, version, error)) { - BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Failed to fetch server info: " << error; - } - if (version.empty()) { - version = "moonraker"; - } - nlohmann::json payload; payload["info"]["command"] = "get_version"; payload["info"]["result"] = "success"; @@ -932,7 +809,7 @@ int MoonrakerPrinterAgent::send_version_info(const std::string& dev_id) nlohmann::json module; module["name"] = "ota"; - module["sw_ver"] = version; + module["sw_ver"] = device_info.version; module["product_name"] = "Moonraker"; payload["info"]["module"].push_back(module); @@ -965,9 +842,9 @@ void MoonrakerPrinterAgent::announce_printhost_device() // Try to fetch actual device name from Moonraker // Priority: 1) Moonraker hostname, 2) Preset model name, 3) Generic fallback - std::string dev_name; + std::string dev_name; MoonrakerDeviceInfo info; - std::string fetch_error; + std::string fetch_error; if (fetch_device_info(device_info.base_url, device_info.api_key, info, fetch_error) && !info.dev_name.empty()) { dev_name = info.dev_name; } else { @@ -1057,9 +934,7 @@ void MoonrakerPrinterAgent::start_status_stream(const std::string& dev_id, const } ws_stop.store(false); - ws_thread = std::thread([this, dev_id, base_url, api_key]() { - run_status_stream(dev_id, base_url, api_key); - }); + ws_thread = std::thread([this, dev_id, base_url, api_key]() { run_status_stream(dev_id, base_url, api_key); }); } void MoonrakerPrinterAgent::stop_status_stream() @@ -1083,17 +958,17 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba } // Reconnection logic - ws_reconnect_requested.store(false); // Reset reconnect flag - int retry_count = 0; - const int max_retries = 10; + ws_reconnect_requested.store(false); // Reset reconnect flag + int retry_count = 0; + const int max_retries = 10; const int base_delay_ms = 1000; while (!ws_stop.load() && retry_count < max_retries) { - bool connection_lost = false; // Flag to distinguish clean shutdown from unexpected disconnect + bool connection_lost = false; // Flag to distinguish clean shutdown from unexpected disconnect try { - net::io_context ioc; - tcp::resolver resolver{ioc}; + net::io_context ioc; + tcp::resolver resolver{ioc}; beast::tcp_stream stream{ioc}; stream.expires_after(std::chrono::seconds(10)); @@ -1117,13 +992,13 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba // Send client identification nlohmann::json identify; - identify["jsonrpc"] = "2.0"; - identify["method"] = "server.connection.identify"; + identify["jsonrpc"] = "2.0"; + identify["method"] = "server.connection.identify"; identify["params"]["client_name"] = "OrcaSlicer"; - identify["params"]["version"] = MoonrakerPrinterAgent_VERSION; - identify["params"]["type"] = "agent"; - identify["params"]["url"] = "https://github.com/SoftFever/OrcaSlicer"; - identify["id"] = 0; + identify["params"]["version"] = MoonrakerPrinterAgent_VERSION; + identify["params"]["type"] = "agent"; + identify["params"]["url"] = "https://github.com/SoftFever/OrcaSlicer"; + identify["id"] = 0; ws.write(net::buffer(identify.dump())); std::set subscribe_objects = {"print_stats", "virtual_sdcard"}; @@ -1163,19 +1038,19 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba } else { subscribe_objects.insert("extruder"); subscribe_objects.insert("heater_bed"); - subscribe_objects.insert("toolhead"); // Add toolhead as fallback - subscribe_objects.insert("fan"); // Try to subscribe to fan as fallback + subscribe_objects.insert("toolhead"); // Add toolhead as fallback + subscribe_objects.insert("fan"); // Try to subscribe to fan as fallback } nlohmann::json subscribe; - subscribe["jsonrpc"] = "2.0"; - subscribe["method"] = "printer.objects.subscribe"; + subscribe["jsonrpc"] = "2.0"; + subscribe["method"] = "printer.objects.subscribe"; nlohmann::json objects = nlohmann::json::object(); for (const auto& name : subscribe_objects) { objects[name] = nullptr; } subscribe["params"]["objects"] = std::move(objects); - subscribe["id"] = 1; + subscribe["id"] = 1; ws.write(net::buffer(subscribe.dump())); // Read loop @@ -1186,8 +1061,7 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba ws.read(buffer, ec); if (ec == beast::error::timeout) { const auto now_ms = static_cast( - std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count()); + std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); const auto last_ms = ws_last_emit_ms.load(); if (last_ms == 0 || now_ms - last_ms >= 10000) { nlohmann::json message; @@ -1261,22 +1135,20 @@ void MoonrakerPrinterAgent::handle_ws_message(const std::string& dev_id, const s return; } - bool updated = false; - bool is_critical = false; // Track if this is a critical update that bypasses throttle + bool updated = false; + bool is_critical = false; // Track if this is a critical update that bypasses throttle // Check for subscription response (has "result.status") - initial subscription is critical - if (json.contains("result") && json["result"].contains("status") && - json["result"]["status"].is_object()) { + if (json.contains("result") && json["result"].contains("status") && json["result"]["status"].is_object()) { update_status_cache(json["result"]["status"]); - updated = true; - is_critical = true; // Initial subscription response - dispatch immediately + updated = true; + is_critical = true; // Initial subscription response - dispatch immediately } // Check for status update notifications if (json.contains("method") && json["method"].is_string()) { const std::string method = json["method"].get(); - if (method == "notify_status_update" && json.contains("params") && - json["params"].is_array() && !json["params"].empty() && + if (method == "notify_status_update" && json.contains("params") && json["params"].is_array() && !json["params"].empty() && json["params"][0].is_object()) { update_status_cache(json["params"][0]); updated = true; @@ -1285,14 +1157,14 @@ void MoonrakerPrinterAgent::handle_ws_message(const std::string& dev_id, const s nlohmann::json updates; updates["print_stats"]["state"] = "standby"; update_status_cache(updates); - updated = true; - is_critical = true; // Klippy events are critical + updated = true; + is_critical = true; // Klippy events are critical } else if (method == "notify_klippy_shutdown") { nlohmann::json updates; updates["print_stats"]["state"] = "error"; update_status_cache(updates); - updated = true; - is_critical = true; // Klippy events are critical + updated = true; + is_critical = true; // Klippy events are critical } // Handle Klippy disconnect - update status and trigger reconnection else if (method == "notify_klippy_disconnected") { @@ -1300,8 +1172,8 @@ void MoonrakerPrinterAgent::handle_ws_message(const std::string& dev_id, const s nlohmann::json updates; updates["print_stats"]["state"] = "error"; update_status_cache(updates); - updated = true; - is_critical = true; // Klippy events are critical + updated = true; + is_critical = true; // Klippy events are critical // Set flag to trigger reconnection after dispatching the status update ws_reconnect_requested.store(true); BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Klippy disconnected, triggering reconnection"; @@ -1313,29 +1185,25 @@ void MoonrakerPrinterAgent::handle_ws_message(const std::string& dev_id, const s std::string current_state; { std::lock_guard lock(payload_mutex); - if (status_cache.contains("print_stats") && - status_cache["print_stats"].contains("state") && + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("state") && status_cache["print_stats"]["state"].is_string()) { current_state = status_cache["print_stats"]["state"].get(); } } if (!current_state.empty() && current_state != last_print_state) { - is_critical = true; + is_critical = true; last_print_state = current_state; } } if (updated) { const auto now_ms = static_cast( - std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count()); + std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); const auto last_dispatch_ms = ws_last_dispatch_ms.load(); // Dispatch if: critical change OR throttle interval elapsed - const bool should_dispatch = is_critical || - last_dispatch_ms == 0 || - now_ms - last_dispatch_ms >= STATUS_UPDATE_INTERVAL_MS; + const bool should_dispatch = is_critical || last_dispatch_ms == 0 || now_ms - last_dispatch_ms >= STATUS_UPDATE_INTERVAL_MS; if (should_dispatch) { nlohmann::json message; @@ -1346,7 +1214,7 @@ void MoonrakerPrinterAgent::handle_ws_message(const std::string& dev_id, const s dispatch_message(dev_id, message.dump()); ws_last_dispatch_ms.store(now_ms); - ws_last_emit_ms.store(now_ms); // Also update heartbeat timer + ws_last_emit_ms.store(now_ms); // Also update heartbeat timer } // else: skip dispatch, cache is updated for next dispatch cycle } @@ -1396,10 +1264,14 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const int mc_print_stage = 0; if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("state")) { std::string mr_state = status_cache["print_stats"]["state"].get(); - if (mr_state == "printing") mc_print_stage = 1; - else if (mr_state == "paused") mc_print_stage = 2; - else if (mr_state == "complete") mc_print_stage = 3; - else if (mr_state == "error") mc_print_stage = 4; + if (mr_state == "printing") + mc_print_stage = 1; + else if (mr_state == "paused") + mc_print_stage = 2; + else if (mr_state == "complete") + mc_print_stage = 3; + else if (mr_state == "error") + mc_print_stage = 4; } payload["print"]["mc_print_stage"] = mc_print_stage; @@ -1407,7 +1279,7 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const // UI expects numeric HMS codes - setting to 1 shows generic error dialog // Only set if real mapping from Moonraker error strings to HMS codes is defined payload["print"]["mc_print_error_code"] = 0; - payload["print"]["print_error"] = 0; + payload["print"]["print_error"] = 0; // Map homed axes to bit field: X=bit0, Y=bit1, Z=bit2 // WARNING: This only sets bits 0-2, clearing support flags (bit 3+) @@ -1416,21 +1288,23 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const int home_flag = 0; if (status_cache.contains("toolhead") && status_cache["toolhead"].contains("homed_axes")) { std::string homed = status_cache["toolhead"]["homed_axes"].get(); - if (homed.find('X') != std::string::npos) home_flag |= 1; // bit 0 - if (homed.find('Y') != std::string::npos) home_flag |= 2; // bit 1 - if (homed.find('Z') != std::string::npos) home_flag |= 4; // bit 2 + if (homed.find('X') != std::string::npos) + home_flag |= 1; // bit 0 + if (homed.find('Y') != std::string::npos) + home_flag |= 2; // bit 1 + if (homed.find('Z') != std::string::npos) + home_flag |= 4; // bit 2 } payload["print"]["home_flag"] = home_flag; // Moonraker doesn't provide temperature ranges via API - use hardcoded defaults - payload["print"]["nozzle_temp_range"] = {100, 370}; // Typical Klipper range - payload["print"]["bed_temp_range"] = {0, 120}; // Typical bed range + payload["print"]["nozzle_temp_range"] = {100, 370}; // Typical Klipper range + payload["print"]["bed_temp_range"] = {0, 120}; // Typical bed range payload["print"]["support_send_to_sd"] = true; // Detect bed_leveling support from available objects (bed_mesh or probe) // Default to 0 (not supported) if neither object exists - bool has_bed_leveling = (available_objects.count("bed_mesh") != 0 || - available_objects.count("probe") != 0); + bool has_bed_leveling = (available_objects.count("bed_mesh") != 0 || available_objects.count("probe") != 0); payload["print"]["support_bed_leveling"] = has_bed_leveling ? 1 : 0; const nlohmann::json* extruder = nullptr; @@ -1475,12 +1349,12 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const } else { pwm = static_cast(speed + 0.5); } - pwm = std::clamp(pwm, 0, 255); + pwm = std::clamp(pwm, 0, 255); payload["print"]["fan_gear"] = pwm; } else if (fan.contains("power") && fan["power"].is_number()) { - double power = fan["power"].get(); - int pwm = static_cast(power * 255.0 + 0.5); - pwm = std::clamp(pwm, 0, 255); + double power = fan["power"].get(); + int pwm = static_cast(power * 255.0 + 0.5); + pwm = std::clamp(pwm, 0, 255); payload["print"]["fan_gear"] = pwm; } } @@ -1496,8 +1370,7 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const } int mc_percent = -1; - if (status_cache.contains("virtual_sdcard") && - status_cache["virtual_sdcard"].contains("progress") && + if (status_cache.contains("virtual_sdcard") && status_cache["virtual_sdcard"].contains("progress") && status_cache["virtual_sdcard"]["progress"].is_number()) { const double progress = status_cache["virtual_sdcard"]["progress"].get(); if (progress >= 0.0) { @@ -1508,22 +1381,19 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const payload["print"]["mc_percent"] = mc_percent; } - if (status_cache.contains("print_stats") && - status_cache["print_stats"].contains("total_duration") && - status_cache["print_stats"].contains("print_duration") && - status_cache["print_stats"]["total_duration"].is_number() && + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("total_duration") && + status_cache["print_stats"].contains("print_duration") && status_cache["print_stats"]["total_duration"].is_number() && status_cache["print_stats"]["print_duration"].is_number()) { const double total = status_cache["print_stats"]["total_duration"].get(); const double elapsed = status_cache["print_stats"]["print_duration"].get(); if (total > 0.0 && elapsed >= 0.0) { - const auto remaining_minutes = std::max(0, static_cast((total - elapsed) / 60.0)); + const auto remaining_minutes = std::max(0, static_cast((total - elapsed) / 60.0)); payload["print"]["mc_remaining_time"] = remaining_minutes; } } const auto now_ms = static_cast( - std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count()); + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()); payload["t_utc"] = now_ms; return payload; @@ -1563,13 +1433,12 @@ void MoonrakerPrinterAgent::dispatch_message(const std::string& dev_id, const st } } -bool MoonrakerPrinterAgent::upload_gcode( - const std::string& local_path, - const std::string& filename, - const std::string& base_url, - const std::string& api_key, - OnUpdateStatusFn update_fn, - WasCancelledFn cancel_fn) +bool MoonrakerPrinterAgent::upload_gcode(const std::string& local_path, + const std::string& filename, + const std::string& base_url, + const std::string& api_key, + OnUpdateStatusFn update_fn, + WasCancelledFn cancel_fn) { namespace fs = boost::filesystem; @@ -1582,7 +1451,7 @@ bool MoonrakerPrinterAgent::upload_gcode( // Check file size std::uintmax_t file_size = fs::file_size(source_path); - if (file_size > 1024 * 1024 * 1024) { // 1GB limit + if (file_size > 1024 * 1024 * 1024) { // 1GB limit BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: File too large: " << file_size << " bytes"; return false; } @@ -1590,7 +1459,7 @@ bool MoonrakerPrinterAgent::upload_gcode( // Sanitize filename to prevent path traversal attacks std::string safe_filename = sanitize_filename(filename); - bool result = true; + bool result = true; std::string http_error; // Use Http::form_add and Http::form_add_file @@ -1598,11 +1467,11 @@ bool MoonrakerPrinterAgent::upload_gcode( if (!api_key.empty()) { http.header("X-Api-Key", api_key); } - http.form_add("root", "gcodes") // Upload to gcodes directory - .form_add("print", "false") // Don't auto-start print + http.form_add("root", "gcodes") // Upload to gcodes directory + .form_add("print", "false") // Don't auto-start print .form_add_file("file", source_path.string(), safe_filename) .timeout_connect(5) - .timeout_max(300) // 5 minutes for large files + .timeout_max(300) // 5 minutes for large files .on_complete([&](std::string body, unsigned status) { (void) body; (void) status; @@ -1610,7 +1479,7 @@ bool MoonrakerPrinterAgent::upload_gcode( .on_error([&](std::string body, std::string err, unsigned status) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Upload error: " << err << " HTTP " << status; http_error = err; - result = false; + result = false; }) .on_progress([&](Http::Progress progress, bool& cancel) { // Check for cancellation via WasCancelledFn @@ -1652,16 +1521,15 @@ int MoonrakerPrinterAgent::cancel_print(const std::string& dev_id) return send_gcode(dev_id, gcode) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; } -bool MoonrakerPrinterAgent::send_jsonrpc_command( - const std::string& base_url, - const std::string& api_key, - const nlohmann::json& request, - std::string& response) const +bool MoonrakerPrinterAgent::send_jsonrpc_command(const std::string& base_url, + const std::string& api_key, + const nlohmann::json& request, + std::string& response) const { std::string request_str = request.dump(); - std::string url = join_url(base_url, "/printer/print/start"); + std::string url = join_url(base_url, "/printer/print/start"); - bool success = false; + bool success = false; std::string http_error; auto http = Http::post(url); @@ -1675,14 +1543,12 @@ bool MoonrakerPrinterAgent::send_jsonrpc_command( .on_complete([&](std::string body, unsigned status) { if (status == 200) { response = body; - success = true; + success = true; } else { http_error = "HTTP " + std::to_string(status); } }) - .on_error([&](std::string body, std::string err, unsigned status) { - http_error = err; - }) + .on_error([&](std::string body, std::string err, unsigned status) { http_error = err; }) .perform_sync(); if (!success) { @@ -1692,39 +1558,22 @@ bool MoonrakerPrinterAgent::send_jsonrpc_command( return success; } -void MoonrakerPrinterAgent::perform_connection_async( - const std::string& dev_id, - const std::string& base_url, - const std::string& api_key) +void MoonrakerPrinterAgent::perform_connection_async(const std::string& dev_id, const std::string& base_url, const std::string& api_key) { - int result = BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; + int result = BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; std::string error_msg; try { - // Check Klippy state - nlohmann::json server_info; - if (!fetch_server_info_json(base_url, api_key, server_info, error_msg)) { + if (!fetch_device_info(base_url, api_key, device_info, error_msg)) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Failed to fetch server info: " << error_msg; - dispatch_local_connect(ConnectStatusFailed, dev_id, "server_info_failed"); - finish_connection(); - return; - } - - nlohmann::json result_json = server_info.contains("result") - ? server_info["result"] : server_info; - std::string klippy_state = result_json.value("klippy_state", ""); - - // Check state - if (klippy_state != "ready" && result == BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED) { - std::string state_message = result_json.value("state_message", "Unknown error"); - BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Klippy not ready: " << klippy_state - << " - " << state_message; - error_msg = "klippy_not_ready:" + klippy_state; - dispatch_local_connect(ConnectStatusFailed, dev_id, error_msg); + // Orca todo: revist here, for now don't send error, this is set current MachineObject to null + // dispatch_local_connect(ConnectStatusFailed, dev_id, "server_info_failed"); finish_connection(); return; } +// Orca todo: disable websocket for now, as we don't use MonitorPanel for Moonraker printers yet +#if 0 // Query initial status nlohmann::json initial_status; if (query_printer_status(base_url, api_key, initial_status, error_msg)) { @@ -1738,6 +1587,7 @@ void MoonrakerPrinterAgent::perform_connection_async( // Start WebSocket status stream start_status_stream(dev_id, base_url, api_key); +#endif // Success! result = BAMBU_NETWORK_SUCCESS; @@ -1745,7 +1595,7 @@ void MoonrakerPrinterAgent::perform_connection_async( } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Connection exception: " << e.what(); error_msg = std::string("exception: ") + e.what(); - result = BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; + result = BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; } // Dispatch final result to UI @@ -1754,7 +1604,8 @@ void MoonrakerPrinterAgent::perform_connection_async( dispatch_printer_connected(dev_id); BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: connect_printer completed - dev_id=" << dev_id; } else if (result != BAMBU_NETWORK_ERR_CANCELED) { - dispatch_local_connect(ConnectStatusFailed, dev_id, error_msg); + // Orca todo: revist here, for now don't send error, this is set current MachineObject to null + // dispatch_local_connect(ConnectStatusFailed, dev_id, error_msg); } finish_connection(); @@ -1811,7 +1662,6 @@ std::string MoonrakerPrinterAgent::join_url(const std::string& base_url, const s return base_url + path; } - // Sanitize filename to prevent path traversal attacks // Extracts only the basename, removing any path components std::string MoonrakerPrinterAgent::sanitize_filename(const std::string& filename) @@ -1820,7 +1670,7 @@ std::string MoonrakerPrinterAgent::sanitize_filename(const std::string& filename return "print.gcode"; } namespace fs = boost::filesystem; - fs::path p(filename); + fs::path p(filename); std::string basename = p.filename().string(); if (basename.empty() || basename == "." || basename == "..") { return "print.gcode"; diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index 94d32a4016..29b7f98715 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -71,7 +71,7 @@ public: // Pull-mode agent (on-demand filament sync) virtual FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } - virtual void fetch_filament_info(std::string dev_id) override; + virtual bool fetch_filament_info(std::string dev_id) override; protected: struct MoonrakerDeviceInfo @@ -84,6 +84,7 @@ protected: std::string model_name; std::string dev_name; std::string version; + std::string klippy_state; bool use_ssl = false; } device_info; @@ -105,7 +106,6 @@ private: int send_version_info(const std::string& dev_id); int send_access_code(const std::string& dev_id); - bool fetch_server_info(const std::string& base_url, const std::string& api_key, std::string& version, std::string& error) const; bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set& objects, std::string& error) const; bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const; bool send_gcode(const std::string& dev_id, const std::string& gcode) const; @@ -135,10 +135,6 @@ private: bool send_jsonrpc_command(const std::string& base_url, const std::string& api_key, const nlohmann::json& request, std::string& response) const; - // Server info (returns JSON, not just version string) - bool fetch_server_info_json(const std::string& base_url, const std::string& api_key, - nlohmann::json& info, std::string& error) const; - // Connection thread management void perform_connection_async(const std::string& dev_id, const std::string& base_url, diff --git a/src/slic3r/Utils/NetworkAgent.cpp b/src/slic3r/Utils/NetworkAgent.cpp index b893593ce2..16d82ba8dc 100644 --- a/src/slic3r/Utils/NetworkAgent.cpp +++ b/src/slic3r/Utils/NetworkAgent.cpp @@ -597,11 +597,12 @@ FilamentSyncMode NetworkAgent::get_filament_sync_mode() const return FilamentSyncMode::none; // Default when no agent } -void NetworkAgent::fetch_filament_info(std::string dev_id) +bool NetworkAgent::fetch_filament_info(std::string dev_id) { if (m_printer_agent) { - m_printer_agent->fetch_filament_info(dev_id); + return m_printer_agent->fetch_filament_info(dev_id); } + return false; } int NetworkAgent::get_user_presets(std::map>* user_presets) diff --git a/src/slic3r/Utils/NetworkAgent.hpp b/src/slic3r/Utils/NetworkAgent.hpp index 09cee2a45e..402497fc19 100644 --- a/src/slic3r/Utils/NetworkAgent.hpp +++ b/src/slic3r/Utils/NetworkAgent.hpp @@ -115,7 +115,7 @@ public: int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); FilamentSyncMode get_filament_sync_mode() const; - void fetch_filament_info(std::string dev_id); + bool fetch_filament_info(std::string dev_id); int get_user_presets(std::map>* user_presets); std::string request_setting_id(std::string name, std::map* values_map, unsigned int* http_code); int put_setting(std::string setting_id, std::string name, std::map* values_map, unsigned int* http_code); diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index b5dce54c3c..b0f4847c4c 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -24,18 +24,18 @@ AgentInfo QidiPrinterAgent::get_agent_info_static() return AgentInfo{.id = "qidi", .name = "Qidi Printer Agent", .version = QidiPrinterAgent_VERSION, .description = "Qidi printer agent"}; } -void QidiPrinterAgent::fetch_filament_info(std::string dev_id) +bool QidiPrinterAgent::fetch_filament_info(std::string dev_id) { // Look up MachineObject via DeviceManager auto* dev_manager = GUI::wxGetApp().getDeviceManager(); if (!dev_manager) { BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: DeviceManager is null"; - return; + return false; } MachineObject* obj = dev_manager->get_my_machine(dev_id); if (!obj) { BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: MachineObject not found for dev_id=" << dev_id; - return; + return false; } std::vector slots; @@ -43,7 +43,7 @@ void QidiPrinterAgent::fetch_filament_info(std::string dev_id) std::string error; if (!fetch_slot_info(device_info.base_url, device_info.api_key, slots, box_count, error)) { BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error; - return; + return false; } QidiFilamentDict dict; @@ -141,6 +141,7 @@ void QidiPrinterAgent::fetch_filament_info(std::string dev_id) // Call the parser to populate DevFilaSystem DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem(), false); + return true; } bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, @@ -162,8 +163,8 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, if (!api_key.empty()) { http.header("X-Api-Key", api_key); } - http.timeout_connect(10) - .timeout_max(30) + http.timeout_connect(5) + .timeout_max(10) .on_complete([&](std::string body, unsigned status) { if (status == 200) { response_body = body; @@ -246,8 +247,8 @@ bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url, if (!api_key.empty()) { http.header("X-Api-Key", api_key); } - http.timeout_connect(10) - .timeout_max(30) + http.timeout_connect(5) + .timeout_max(10) .on_complete([&](std::string body, unsigned status) { if (status == 200) { response_body = body; diff --git a/src/slic3r/Utils/QidiPrinterAgent.hpp b/src/slic3r/Utils/QidiPrinterAgent.hpp index 8657443288..27556ebd92 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.hpp +++ b/src/slic3r/Utils/QidiPrinterAgent.hpp @@ -19,7 +19,7 @@ public: AgentInfo get_agent_info() override { return get_agent_info_static(); } // Override filament sync (Qidi-specific implementation) - void fetch_filament_info(std::string dev_id) override; + bool fetch_filament_info(std::string dev_id) override; private: // Qidi-specific device info (extends base MoonrakerDeviceInfo with model_id) From 9233005725b1f0ad76cfc523423cf986f660b43b Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 27 Jan 2026 17:41:29 +0800 Subject: [PATCH 09/25] refactor --- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 236 ++++++++++++++++++++- src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 14 ++ src/slic3r/Utils/QidiPrinterAgent.cpp | 216 ++++++------------- src/slic3r/Utils/QidiPrinterAgent.hpp | 23 +- 4 files changed, 323 insertions(+), 166 deletions(-) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 768d0c2f31..ac5602c569 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -3,6 +3,8 @@ #include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/DeviceCore/DevFilaSystem.h" +#include "slic3r/GUI/DeviceCore/DevManager.h" #include "nlohmann/json.hpp" #include #include @@ -454,7 +456,239 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) return BAMBU_NETWORK_SUCCESS; } -bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) { return false; } +void MoonrakerPrinterAgent::build_ams_payload(int ams_count, const std::vector& trays) +{ + + // Look up MachineObject via DeviceManager + auto* dev_manager = GUI::wxGetApp().getDeviceManager(); + if (!dev_manager) { + return; + } + MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id); + if (!obj) { + return; + } + + + // Color normalization helper (handles #RRGGBB, 0xRRGGBB -> RRGGBBAA) + auto normalize_color = [](const std::string& color) -> std::string { + std::string value = color; + boost::trim(value); + + // Remove 0x or 0X prefix if present + if (value.size() >= 2 && (value.rfind("0x", 0) == 0 || value.rfind("0X", 0) == 0)) { + value = value.substr(2); + } + // Remove # prefix if present + if (!value.empty() && value[0] == '#') { + value = value.substr(1); + } + + // Extract only hex digits + std::string normalized; + for (char c : value) { + if (std::isxdigit(static_cast(c))) { + normalized.push_back(static_cast(std::toupper(static_cast(c)))); + } + } + + // If 6 hex digits, add FF alpha + if (normalized.size() == 6) { + normalized += "FF"; + } + + // Validate length - return default if invalid + if (normalized.size() != 8) { + return "00000000"; + } + + return normalized; + }; + + // Build BBL-format JSON for DevFilaSystemParser::ParseV1_0 + nlohmann::json ams_json = nlohmann::json::object(); + nlohmann::json ams_array = nlohmann::json::array(); + + // Calculate ams_exist_bits and tray_exist_bits + unsigned long ams_exist_bits = 0; + unsigned long tray_exist_bits = 0; + + for (int ams_id = 0; ams_id < ams_count; ++ams_id) { + ams_exist_bits |= (1 << ams_id); + + nlohmann::json ams_unit = nlohmann::json::object(); + ams_unit["id"] = std::to_string(ams_id); + ams_unit["info"] = "2100"; // AMS_LITE type (2), main extruder (0) + + nlohmann::json tray_array = nlohmann::json::array(); + for (int slot_id = 0; slot_id < 4; ++slot_id) { + int slot_index = ams_id * 4 + slot_id; + + // Find tray with matching slot_index + const AmsTrayData* tray = nullptr; + for (const auto& t : trays) { + if (t.slot_index == slot_index) { + tray = &t; + break; + } + } + + nlohmann::json tray_json = nlohmann::json::object(); + tray_json["id"] = std::to_string(slot_id); + tray_json["tag_uid"] = "0000000000000000"; + + if (tray && tray->has_filament) { + tray_exist_bits |= (1 << slot_index); + + tray_json["tray_info_idx"] = tray->tray_info_idx; + tray_json["tray_type"] = tray->tray_type; + tray_json["tray_color"] = normalize_color(tray->tray_color); + + // Add temperature data if provided + if (tray->bed_temp > 0) { + tray_json["bed_temp"] = std::to_string(tray->bed_temp); + } + if (tray->nozzle_temp > 0) { + tray_json["nozzle_temp_max"] = std::to_string(tray->nozzle_temp); + } + } else { + tray_json["tray_info_idx"] = ""; + tray_json["tray_type"] = ""; + tray_json["tray_color"] = "00000000"; + } + + tray_array.push_back(tray_json); + } + ams_unit["tray"] = tray_array; + ams_array.push_back(ams_unit); + } + + // Format as hex strings (matching BBL protocol) + std::ostringstream ams_exist_ss; + ams_exist_ss << std::hex << std::uppercase << ams_exist_bits; + std::ostringstream tray_exist_ss; + tray_exist_ss << std::hex << std::uppercase << tray_exist_bits; + + ams_json["ams"] = ams_array; + ams_json["ams_exist_bits"] = ams_exist_ss.str(); + ams_json["tray_exist_bits"] = tray_exist_ss.str(); + + // Wrap in the expected structure for ParseV1_0 + nlohmann::json print_json = nlohmann::json::object(); + print_json["ams"] = ams_json; + + // Call the parser to populate DevFilaSystem + DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem(), false); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays"; + + // Set push counters so is_info_ready() returns true for pull-mode agents. + if (obj->m_push_count == 0) { + obj->m_push_count = 1; + } + if (obj->m_full_msg_count == 0) { + obj->m_full_msg_count = 1; + } + obj->last_push_time = std::chrono::system_clock::now(); +} + +bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) +{ + // Fetch AFC lane data from Moonraker database (inline) + std::string url = join_url(device_info.base_url, "/server/database/item?namespace=lane_data"); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!device_info.api_key.empty()) { + http.header("X-Api-Key", device_info.api_key); + } + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_filament_info: Failed to fetch lane data: " << http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_filament_info: Invalid JSON response"; + return false; + } + + // Expected structure: { "result": { "namespace": "lane_data", "value": { "lane1": {...}, ... } } } + if (!json.contains("result") || !json["result"].contains("value") || !json["result"]["value"].is_object()) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_filament_info: Unexpected JSON structure or no lane_data found"; + return false; + } + + // Parse response into AmsTrayData + const auto& value = json["result"]["value"]; + std::vector trays; + int max_lane_index = 0; + + for (const auto& [lane_key, lane_obj] : value.items()) { + if (!lane_obj.is_object()) { + continue; + } + + // Extract lane index from the "lane" field (tool number, 1-based) + std::string lane_str = lane_obj.value("lane", ""); + int lane_index = -1; + if (!lane_str.empty()) { + try { + lane_index = std::stoi(lane_str) - 1; // Convert to 0-based + } catch (...) { + lane_index = -1; + } + } + + if (lane_index < 0) { + continue; + } + + AmsTrayData tray; + tray.slot_index = lane_index; + tray.tray_color = lane_obj.value("color", ""); + tray.tray_type = lane_obj.value("material", ""); + tray.bed_temp = lane_obj.value("bed_temp", 0); + tray.nozzle_temp = lane_obj.value("nozzle_temp", 0); + tray.has_filament = !tray.tray_type.empty(); + tray.tray_info_idx = ""; // AFC doesn't provide setting IDs + + max_lane_index = std::max(max_lane_index, lane_index); + trays.push_back(tray); + } + + if (trays.empty()) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: No AFC lanes found"; + return false; + } + + // Calculate AMS count from max lane index (4 trays per AMS unit) + int ams_count = (max_lane_index + 4) / 4; + + // Build and parse the AMS payload + build_ams_payload(ams_count, trays); + return true; +} int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) { diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index 29b7f98715..b301ae0fd9 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -88,6 +88,20 @@ protected: bool use_ssl = false; } device_info; + // Shared tray data for AMS payload building (used by derived classes like QidiPrinterAgent) + struct AmsTrayData { + int slot_index = 0; // 0-based slot index + bool has_filament = false; + std::string tray_type; // Material type (e.g., "PLA", "ASA") + std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA) + std::string tray_info_idx; // Setting ID (optional) + int bed_temp = 0; // Optional + int nozzle_temp = 0; // Optional + }; + + // Build ams JSON and call parser + void build_ams_payload(int ams_count, const std::vector& trays); + // Methods that derived classes may need to override or access virtual bool init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index b0f4847c4c..46aa623610 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -1,8 +1,5 @@ #include "QidiPrinterAgent.hpp" #include "Http.hpp" -#include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/GUI/DeviceCore/DevFilaSystem.h" -#include "slic3r/GUI/DeviceCore/DevManager.h" #include "nlohmann/json.hpp" #include @@ -26,129 +23,43 @@ AgentInfo QidiPrinterAgent::get_agent_info_static() bool QidiPrinterAgent::fetch_filament_info(std::string dev_id) { - // Look up MachineObject via DeviceManager - auto* dev_manager = GUI::wxGetApp().getDeviceManager(); - if (!dev_manager) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: DeviceManager is null"; - return false; - } - MachineObject* obj = dev_manager->get_my_machine(dev_id); - if (!obj) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: MachineObject not found for dev_id=" << dev_id; - return false; - } + std::string error; - std::vector slots; - int box_count = 0; - std::string error; - if (!fetch_slot_info(device_info.base_url, device_info.api_key, slots, box_count, error)) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error; - return false; + // 1. Fetch device info and infer series_id + std::string series_id; + { + MoonrakerDeviceInfo info; + if (fetch_device_info(device_info.base_url, device_info.api_key, info, error)) { + series_id = infer_series_id(info.model_id, info.dev_name); + } } + // 2. Fetch filament dictionary QidiFilamentDict dict; if (!fetch_filament_dict(device_info.base_url, device_info.api_key, dict, error)) { BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error; } - std::string series_id; - { - MoonrakerDeviceInfo info; - std::string device_error; - if (fetch_device_info(device_info.base_url, device_info.api_key, info, device_error)) { - series_id = infer_series_id(info.model_id, info.dev_name); - } + // 3. Fetch slot info and build AmsTrayData directly + std::vector trays; + int box_count = 0; + if (!fetch_slot_info(device_info.base_url, device_info.api_key, dict, series_id, trays, box_count, error)) { + BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error; + return false; } - auto build_setting_id = [&](const QidiSlotInfo& slot, const std::string& tray_type) { - const int vendor = (slot.vendor_type == 1) ? 1 : 0; - if (is_numeric(series_id) && slot.filament_type > 0) { - return "QD_" + series_id + "_" + std::to_string(vendor) + "_" + std::to_string(slot.filament_type); - } - return map_filament_type_to_setting_id(tray_type); - }; - - // Build BBL-format JSON for DevFilaSystemParser::ParseV1_0 - // The expected format matches BBL's print.push_status AMS subset - nlohmann::json ams_json = nlohmann::json::object(); - nlohmann::json ams_array = nlohmann::json::array(); - - // Calculate ams_exist_bits and tray_exist_bits - unsigned long ams_exist_bits = 0; - unsigned long tray_exist_bits = 0; - - for (int ams_id = 0; ams_id < box_count; ++ams_id) { - ams_exist_bits |= (1 << ams_id); - - nlohmann::json ams_unit = nlohmann::json::object(); - ams_unit["id"] = std::to_string(ams_id); - ams_unit["info"] = "2100"; // AMS_LITE type (2), main extruder (0) - - nlohmann::json tray_array = nlohmann::json::array(); - for (int slot_id = 0; slot_id < 4; ++slot_id) { - const int slot_index = ams_id * 4 + slot_id; - const QidiSlotInfo slot = slot_index < static_cast(slots.size()) ? slots[slot_index] : QidiSlotInfo{}; - - nlohmann::json tray_json = nlohmann::json::object(); - tray_json["id"] = std::to_string(slot_id); - tray_json["tag_uid"] = "0000000000000000"; - - if (slot.filament_exists) { - tray_exist_bits |= (1 << slot_index); - - std::string filament_type = "PLA"; - auto filament_it = dict.filaments.find(slot.filament_type); - if (filament_it != dict.filaments.end()) { - filament_type = filament_it->second; - } - std::string tray_type = normalize_filament_type(filament_type); - std::string setting_id = build_setting_id(slot, tray_type); - - std::string color = "FFFFFFFF"; - auto color_it = dict.colors.find(slot.color_index); - if (color_it != dict.colors.end()) { - color = normalize_color(color_it->second); - } - - tray_json["tray_info_idx"] = setting_id; - tray_json["tray_type"] = tray_type; - tray_json["tray_color"] = color; - } else { - tray_json["tray_info_idx"] = ""; - tray_json["tray_type"] = ""; - tray_json["tray_color"] = "00000000"; - } - - tray_array.push_back(tray_json); - } - ams_unit["tray"] = tray_array; - ams_array.push_back(ams_unit); - } - - // Format as hex strings (matching BBL protocol) - std::ostringstream ams_exist_ss; - ams_exist_ss << std::hex << std::uppercase << ams_exist_bits; - std::ostringstream tray_exist_ss; - tray_exist_ss << std::hex << std::uppercase << tray_exist_bits; - - ams_json["ams"] = ams_array; - ams_json["ams_exist_bits"] = ams_exist_ss.str(); - ams_json["tray_exist_bits"] = tray_exist_ss.str(); - - // Wrap in the expected structure for ParseV1_0 - nlohmann::json print_json = nlohmann::json::object(); - print_json["ams"] = ams_json; - - // Call the parser to populate DevFilaSystem - DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem(), false); + // 4. Build the AMS payload + build_ams_payload(box_count, trays); return true; } -bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, - const std::string& api_key, - std::vector& slots, - int& box_count, - std::string& error) const +bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, + const std::string& api_key, + const QidiFilamentDict& dict, + const std::string& series_id, + std::vector& trays, + int& box_count, + std::string& error) { std::string url = join_url(base_url, "/printer/objects/query?save_variables=variables"); for (int i = 0; i < 16; ++i) { @@ -207,26 +118,58 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, } const int max_slots = box_count * 4; - slots.clear(); - slots.reserve(max_slots); + trays.clear(); + trays.reserve(max_slots); + + // Lambda to build setting_id from slot data + auto build_setting_id = [&](int filament_type_idx, int vendor_type, const std::string& tray_type) { + const int vendor = (vendor_type == 1) ? 1 : 0; + if (is_numeric(series_id) && filament_type_idx > 0) { + return "QD_" + series_id + "_" + std::to_string(vendor) + "_" + std::to_string(filament_type_idx); + } + return map_filament_type_to_setting_id(tray_type); + }; for (int i = 0; i < max_slots; ++i) { - QidiSlotInfo slot; - slot.slot_index = i; - slot.color_index = variables.value("color_slot" + std::to_string(i), 1); - slot.filament_type = variables.value("filament_slot" + std::to_string(i), 1); - slot.vendor_type = variables.value("vendor_slot" + std::to_string(i), 0); + AmsTrayData tray; + tray.slot_index = i; + // Read slot variables + const int color_index = variables.value("color_slot" + std::to_string(i), 1); + const int filament_type = variables.value("filament_slot" + std::to_string(i), 1); + const int vendor_type = variables.value("vendor_slot" + std::to_string(i), 0); + + // Check filament presence via runout sensor std::string box_stepper_key = "box_stepper slot" + std::to_string(i); - slot.filament_exists = false; + tray.has_filament = false; if (status.contains(box_stepper_key)) { auto& box_stepper = status[box_stepper_key]; if (box_stepper.contains("runout_button") && !box_stepper["runout_button"].is_null()) { - int runout_button = box_stepper["runout_button"].get(); - slot.filament_exists = (runout_button == 0); + int runout_button = box_stepper["runout_button"].template get(); + tray.has_filament = (runout_button == 0); } } - slots.push_back(slot); + + if (tray.has_filament) { + // Look up filament type name from dictionary + std::string filament_name = "PLA"; + auto filament_it = dict.filaments.find(filament_type); + if (filament_it != dict.filaments.end()) { + filament_name = filament_it->second; + } + tray.tray_type = normalize_filament_type(filament_name); + tray.tray_info_idx = build_setting_id(filament_type, vendor_type, tray.tray_type); + + // Look up color from dictionary + auto color_it = dict.colors.find(color_index); + if (color_it != dict.colors.end()) { + tray.tray_color = color_it->second; + } else { + tray.tray_color = "FFFFFFFF"; + } + } + + trays.push_back(tray); } return true; @@ -348,31 +291,6 @@ void QidiPrinterAgent::parse_filament_sections(const std::string& content, std:: } } -std::string QidiPrinterAgent::normalize_color(const std::string& color) -{ - std::string value = color; - boost::trim(value); - if (value.rfind("0x", 0) == 0 || value.rfind("0X", 0) == 0) { - value = value.substr(2); - } - if (!value.empty() && value[0] == '#') { - value = value.substr(1); - } - std::string normalized; - for (char c : value) { - if (std::isxdigit(static_cast(c))) { - normalized.push_back(static_cast(std::toupper(static_cast(c)))); - } - } - if (normalized.size() == 6) { - normalized += "FF"; - } - if (normalized.size() != 8) { - return "00000000"; - } - return normalized; -} - std::string QidiPrinterAgent::map_filament_type_to_setting_id(const std::string& filament_type) { std::string upper = filament_type; diff --git a/src/slic3r/Utils/QidiPrinterAgent.hpp b/src/slic3r/Utils/QidiPrinterAgent.hpp index 27556ebd92..a8326edb53 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.hpp +++ b/src/slic3r/Utils/QidiPrinterAgent.hpp @@ -22,16 +22,6 @@ public: bool fetch_filament_info(std::string dev_id) override; private: - // Qidi-specific device info (extends base MoonrakerDeviceInfo with model_id) - struct QidiSlotInfo - { - int slot_index = 0; - int color_index = 0; - int filament_type = 0; - int vendor_type = 0; - bool filament_exists = false; - }; - struct QidiFilamentDict { std::map colors; @@ -39,11 +29,13 @@ private: }; // Qidi-specific methods - bool fetch_slot_info(const std::string& base_url, - const std::string& api_key, - std::vector& slots, - int& box_count, - std::string& error) const; + bool fetch_slot_info(const std::string& base_url, + const std::string& api_key, + const QidiFilamentDict& dict, + const std::string& series_id, + std::vector& trays, + int& box_count, + std::string& error); bool fetch_filament_dict(const std::string& base_url, const std::string& api_key, QidiFilamentDict& dict, std::string& error) const; std::string normalize_filament_type(const std::string& filament_type); std::string infer_series_id(const std::string& model_id, const std::string& dev_name); @@ -52,7 +44,6 @@ private: // Static helpers static void parse_ini_section(const std::string& content, const std::string& section_name, std::map& result); static void parse_filament_sections(const std::string& content, std::map& result); - static std::string normalize_color(const std::string& color); static std::string map_filament_type_to_setting_id(const std::string& filament_type); }; From 9c21744cd13f8a0273ab9489fba5ce80611f57d6 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 27 Jan 2026 21:21:44 +0800 Subject: [PATCH 10/25] fix Windows build --- src/slic3r/Utils/BBLPrinterAgent.cpp | 7 +------ src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 5 +---- src/slic3r/Utils/OrcaPrinterAgent.cpp | 5 +---- src/slic3r/Utils/QidiPrinterAgent.cpp | 2 +- 4 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/slic3r/Utils/BBLPrinterAgent.cpp b/src/slic3r/Utils/BBLPrinterAgent.cpp index 22ce2c24a5..bf13aa84ee 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.cpp +++ b/src/slic3r/Utils/BBLPrinterAgent.cpp @@ -205,12 +205,7 @@ int BBLPrinterAgent::set_user_selected_machine(std::string dev_id) // ============================================================================ AgentInfo BBLPrinterAgent::get_agent_info_static() { - return AgentInfo{ - .id = BBL_PRINTER_AGENT_ID, - .name = "Bambu Lab Printer Agent", - .version = "", - .description = "Bambu Lab printer agent" - }; + return AgentInfo{BBL_PRINTER_AGENT_ID, "Bambu Lab Printer Agent", "", "Bambu Lab printer agent"}; } // ============================================================================ diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index ac5602c569..93536d6be5 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -101,10 +101,7 @@ MoonrakerPrinterAgent::~MoonrakerPrinterAgent() AgentInfo MoonrakerPrinterAgent::get_agent_info_static() { - return AgentInfo{.id = "moonraker", - .name = "Moonraker Printer Agent", - .version = MoonrakerPrinterAgent_VERSION, - .description = "Klipper/Moonraker printer agent"}; + return AgentInfo{"moonraker", "Moonraker Printer Agent", MoonrakerPrinterAgent_VERSION, "Klipper/Moonraker printer agent"}; } void MoonrakerPrinterAgent::set_cloud_agent(std::shared_ptr cloud) diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index 712024a286..7f14c71696 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -124,10 +124,7 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) // ============================================================================ AgentInfo OrcaPrinterAgent::get_agent_info_static() { - return AgentInfo{.id = ORCA_PRINTER_AGENT_ID, - .name = "Orca Printer Agent", - .version = OrcaPrinterAgent_VERSION, - .description = "Orca Printer Communication Protocol Agent"}; + return AgentInfo{ORCA_PRINTER_AGENT_ID, "Orca Printer Agent", OrcaPrinterAgent_VERSION, "Orca Printer Communication Protocol Agent"}; } // ============================================================================ diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index 46aa623610..dc0262e3c2 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -18,7 +18,7 @@ QidiPrinterAgent::QidiPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent( AgentInfo QidiPrinterAgent::get_agent_info_static() { - return AgentInfo{.id = "qidi", .name = "Qidi Printer Agent", .version = QidiPrinterAgent_VERSION, .description = "Qidi printer agent"}; + return AgentInfo{"qidi", "Qidi Printer Agent", QidiPrinterAgent_VERSION, "Qidi printer agent"}; } bool QidiPrinterAgent::fetch_filament_info(std::string dev_id) From 006e7d6c28fe573c3ab49a59841e1a148ab72b74 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 28 Jan 2026 18:58:17 +0800 Subject: [PATCH 11/25] fix a few bugs --- src/slic3r/GUI/GUI_App.cpp | 252 ++++++++++----------- src/slic3r/GUI/WebUserLoginDialog.cpp | 3 +- src/slic3r/GUI/WebViewDialog.cpp | 5 +- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 73 +++++- src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 3 + src/slic3r/Utils/NetworkAgentFactory.cpp | 19 +- 6 files changed, 209 insertions(+), 146 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 1376e4ce24..e490374c95 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3304,131 +3304,127 @@ void GUI_App::copy_network_if_available() bool GUI_App::on_init_network(bool try_backup) { - bool create_network_agent = false; auto should_load_networking_plugin = app_config->get_bool("installed_networking"); std::string config_version = app_config->get_network_plugin_version(); - if(!should_load_networking_plugin) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "Don't load plugin as installed_networking is false"; - } else { - if (config_version.empty()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": no version configured, need to download"; - m_networking_need_update = true; + if (should_load_networking_plugin) { + if (config_version.empty()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": no version configured, need to download"; + m_networking_need_update = true; - if (!m_device_manager) - m_device_manager = new Slic3r::DeviceManager(); - if (!m_user_manager) - m_user_manager = new Slic3r::UserManager(); + if (!m_device_manager) + m_device_manager = new Slic3r::DeviceManager(); + if (!m_user_manager) + m_user_manager = new Slic3r::UserManager(); - return false; - } - int load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version); -__retry: - if (!load_agent_dll) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll ok"; - - std::string loaded_version = Slic3r::NetworkAgent::get_version(); - if (app_config && !loaded_version.empty() && loaded_version != "00.00.00.00") { - std::string config_version = app_config->get_network_plugin_version(); - std::string config_base = extract_base_version(config_version); - if (config_base != loaded_version) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": syncing config version from " << config_version << " to loaded " << loaded_version; - app_config->set(SETTING_NETWORK_PLUGIN_VERSION, loaded_version); - app_config->save(); - } + return false; } - if (check_networking_version()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, compatibility version"; - auto bambu_source = Slic3r::NetworkAgent::get_bambu_source_entry(); - if (!bambu_source) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": can not get bambu source module!"; - m_networking_compatible = false; + int load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version); + __retry: + if (!load_agent_dll) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll ok"; + + std::string loaded_version = Slic3r::NetworkAgent::get_version(); + if (app_config && !loaded_version.empty() && loaded_version != "00.00.00.00") { + std::string config_version = app_config->get_network_plugin_version(); + std::string config_base = extract_base_version(config_version); + if (config_base != loaded_version) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": syncing config version from " << config_version << " to loaded " + << loaded_version; + app_config->set(SETTING_NETWORK_PLUGIN_VERSION, loaded_version); + app_config->save(); + } + } + + if (check_networking_version()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, compatibility version"; + auto bambu_source = Slic3r::NetworkAgent::get_bambu_source_entry(); + if (!bambu_source) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": can not get bambu source module!"; + m_networking_compatible = false; + if (should_load_networking_plugin) { + m_networking_need_update = true; + } + } + } else { + if (try_backup) { + int result = Slic3r::NetworkAgent::unload_network_module(); + BOOST_LOG_TRIVIAL(info) << "on_init_network, version mismatch, unload_network_module, result = " << result; + load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(true, config_version); + try_backup = false; + goto __retry; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, version dismatch, need upload network module"; if (should_load_networking_plugin) { m_networking_need_update = true; } } - else - create_network_agent = true; } else { - if (try_backup) { - int result = Slic3r::NetworkAgent::unload_network_module(); - BOOST_LOG_TRIVIAL(info) << "on_init_network, version mismatch, unload_network_module, result = " << result; - load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(true, config_version); - try_backup = false; - goto __retry; - } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, version dismatch, need upload network module"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll failed"; if (should_load_networking_plugin) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, need upload network module"; m_networking_need_update = true; } } + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", create network agent..."); + //std::string data_dir = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); + std::string data_directory = data_dir(); + + // Register all printer agents before creating the network agent + Slic3r::NetworkAgentFactory::register_all_agents(); + + // m_agent = new Slic3r::NetworkAgent(data_directory); + std::unique_ptr agent_ptr = Slic3r::create_agent_from_config(data_directory, app_config); + m_agent = agent_ptr.release(); + + if (!m_device_manager) + m_device_manager = new Slic3r::DeviceManager(m_agent); + else + m_device_manager->set_agent(m_agent); + + if (!m_user_manager) + m_user_manager = new Slic3r::UserManager(m_agent); + else + m_user_manager->set_agent(m_agent); + + if (this->is_enable_multi_machine()) { + if (!m_task_manager) { + m_task_manager = new Slic3r::TaskManager(m_agent); + m_task_manager->start(); + } + + m_device_manager->EnableMultiMachine(true); } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll failed"; - if (should_load_networking_plugin) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, need upload network module"; - m_networking_need_update = true; - } - } + m_device_manager->EnableMultiMachine(false); } - if (create_network_agent) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", create network agent..."); - //std::string data_dir = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); - std::string data_directory = data_dir(); - - // Register all printer agents before creating the network agent - Slic3r::NetworkAgentFactory::register_all_agents(); - - // m_agent = new Slic3r::NetworkAgent(data_directory); - std::unique_ptr agent_ptr = Slic3r::create_agent_from_config(data_directory, app_config); - m_agent = agent_ptr.release(); - - if (!m_device_manager) - m_device_manager = new Slic3r::DeviceManager(m_agent); - else - m_device_manager->set_agent(m_agent); - - if (!m_user_manager) - m_user_manager = new Slic3r::UserManager(m_agent); - else - m_user_manager->set_agent(m_agent); - - if (this->is_enable_multi_machine()) { - if (!m_task_manager) { - m_task_manager = new Slic3r::TaskManager(m_agent); - m_task_manager->start(); - } - - m_device_manager->EnableMultiMachine(true); - } else { - m_device_manager->EnableMultiMachine(false); - } - - //BBS set config dir - if (m_agent) { - m_agent->set_config_dir(data_directory); - } - //BBS start http log - if (m_agent) { - m_agent->init_log(); - } - - //BBS set cert dir - if (m_agent) - m_agent->set_cert_file(resources_dir() + "/cert", "slicer_base64.cer"); - - init_http_extra_header(); - - if (m_agent) { - init_networking_callbacks(); - std::string country_code = app_config->get_country_code(); - m_agent->set_country_code(country_code); - m_agent->start(); - } + //BBS set config dir + if (m_agent) { + m_agent->set_config_dir(data_directory); } - else { + //BBS start http log + if (m_agent) { + m_agent->init_log(); + } + + //BBS set cert dir + if (m_agent) + m_agent->set_cert_file(resources_dir() + "/cert", "slicer_base64.cer"); + + init_http_extra_header(); + + if (m_agent) { + init_networking_callbacks(); + std::string country_code = app_config->get_country_code(); + m_agent->set_country_code(country_code); + m_agent->start(); + } + + if (!should_load_networking_plugin) { int result = Slic3r::NetworkAgent::unload_network_module(); BOOST_LOG_TRIVIAL(info) << "on_init_network, unload_network_module, result = " << result; @@ -3439,7 +3435,7 @@ __retry: m_user_manager = new Slic3r::UserManager(); } - if (create_network_agent && m_networking_compatible && !NetworkAgent::use_legacy_network) { + if (should_load_networking_plugin && m_networking_compatible && !NetworkAgent::use_legacy_network) { app_config->clear_remind_network_update_later(); if (has_network_update_available()) { @@ -3495,31 +3491,22 @@ void GUI_App::switch_printer_agent(const std::string& agent_id) std::string current_agent_id; if (m_agent && m_agent->get_printer_agent()) current_agent_id = m_agent->get_printer_agent()->get_agent_info().id; - - if (!current_agent_id.empty() && current_agent_id == effective_agent_id) { - return; + + if (current_agent_id.empty() || current_agent_id != effective_agent_id) { + std::string log_dir = data_dir(); + std::shared_ptr cloud_agent = m_agent->get_cloud_agent(); + // Create new printer agent via registry + std::shared_ptr new_printer_agent = NetworkAgentFactory::create_printer_agent_by_id(effective_agent_id, cloud_agent, + log_dir); + + if (!new_printer_agent) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id << "', keeping current agent"; + return; + } + + // Swap the agent + m_agent->set_printer_agent(new_printer_agent); } - - std::string log_dir = data_dir(); - std::shared_ptr cloud_agent = m_agent->get_cloud_agent(); - if (!cloud_agent) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": no cloud agent available"; - return; - } - - // Create new printer agent via registry - std::shared_ptr new_printer_agent = - NetworkAgentFactory::create_printer_agent_by_id(effective_agent_id, cloud_agent, log_dir); - - if (!new_printer_agent) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id - << "', keeping current agent"; - return; - } - - // Swap the agent - m_agent->set_printer_agent(new_printer_agent); - // Auto-switch MachineObject select_machine(effective_agent_id); @@ -3574,9 +3561,14 @@ void GUI_App::select_machine(const std::string& agent_id) machine.dev_ip = dev_id; machine.dev_name = dev_id; machine.printer_type = preset.config.opt_string("printer_model"); + auto access_code = preset.config.opt_string("printhost_apikey"); + // Orca expect non empty access code + if (access_code.empty()) { + access_code = "88888888"; + } existing = m_device_manager->insert_local_device( - machine, "lan", "free", "", ""); + machine, "lan", "free", "", access_code); if (!existing) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to create machine dev_id=" << dev_id; diff --git a/src/slic3r/GUI/WebUserLoginDialog.cpp b/src/slic3r/GUI/WebUserLoginDialog.cpp index 2e479f4cf7..743401626d 100644 --- a/src/slic3r/GUI/WebUserLoginDialog.cpp +++ b/src/slic3r/GUI/WebUserLoginDialog.cpp @@ -43,9 +43,10 @@ int ZUserLogin::web_sequence_id = 20000; ZUserLogin::ZUserLogin() : wxDialog((wxWindow *) (wxGetApp().mainframe), wxID_ANY, "OrcaSlicer") { SetBackgroundColour(*wxWHITE); + const auto bblnetwork_enabled =wxGetApp().app_config->get_bool("installed_networking"); // Url NetworkAgent* agent = wxGetApp().getAgent(); - if (!agent) { + if (!agent && bblnetwork_enabled) { SetBackgroundColour(*wxWHITE); diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index ddcc46da0e..5d9301b7ca 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -488,7 +488,10 @@ void WebViewPanel::SendLoginInfo() void WebViewPanel::ShowNetpluginTip() { // Install Network Plugin - //std::string NP_Installed = wxGetApp().app_config->get("installed_networking"); + const auto bblnetwork_enabled =wxGetApp().app_config->get_bool("installed_networking"); + if(!bblnetwork_enabled) { + return; + } bool bValid = wxGetApp().is_compatibility_version(); int nShow = 0; diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 93536d6be5..5e64567735 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -5,6 +5,8 @@ #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/DeviceCore/DevFilaSystem.h" #include "slic3r/GUI/DeviceCore/DevManager.h" +#include "../GUI/DeviceCore/DevStorage.h" +#include "../GUI/DeviceCore/DevFirmware.h" #include "nlohmann/json.hpp" #include #include @@ -586,6 +588,19 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, const std::vectorm_full_msg_count = 1; } obj->last_push_time = std::chrono::system_clock::now(); + + // Set storage state - Moonraker printers use virtual_sdcard, storage is always available. + // This is required for SelectMachineDialog to allow printing (otherwise it blocks with "No SD card"). + obj->GetStorage()->set_sdcard_state(DevStorage::HAS_SDCARD_NORMAL); + + // Populate module_vers so is_info_ready() passes the version check. + // Moonraker printers don't have BBL-style version info, but we need a non-empty map. + if (obj->module_vers.empty()) { + DevFirmwareVersionInfo ota_info; + ota_info.name = "ota"; + ota_info.sw_ver = "1.0.0"; // Placeholder version for Moonraker printers + obj->module_vers.emplace("ota", ota_info); + } } bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) @@ -668,7 +683,7 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) tray.bed_temp = lane_obj.value("bed_temp", 0); tray.nozzle_temp = lane_obj.value("nozzle_temp", 0); tray.has_filament = !tray.tray_type.empty(); - tray.tray_info_idx = ""; // AFC doesn't provide setting IDs + tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); max_lane_index = std::max(max_lane_index, lane_index); trays.push_back(tray); @@ -687,6 +702,60 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) return true; } +std::string MoonrakerPrinterAgent::map_filament_type_to_generic_id(const std::string& filament_type) +{ + std::string upper = filament_type; + boost::trim(upper); + std::transform(upper.begin(), upper.end(), upper.begin(), + [](unsigned char c) { return static_cast(std::toupper(c)); }); + + // Map to OrcaFilamentLibrary preset IDs (compatible with all printers) + // Source: resources/profiles/OrcaFilamentLibrary/filament/ + + // PLA variants + if (upper == "PLA") return "OGFL99"; + if (upper == "PLA-CF") return "OGFL98"; + if (upper == "PLA SILK" || upper == "PLA-SILK") return "OGFL96"; + if (upper == "PLA HIGH SPEED" || upper == "PLA-HS" || upper == "PLA HS") return "OGFL95"; + + // ABS/ASA variants + if (upper == "ABS") return "OGFB99"; + if (upper == "ASA") return "OGFB98"; + + // PETG/PET variants + if (upper == "PETG" || upper == "PET") return "OGFG99"; + if (upper == "PCTG") return "OGFG97"; + + // PA/Nylon variants + if (upper == "PA" || upper == "NYLON") return "OGFN99"; + if (upper == "PA-CF") return "OGFN98"; + if (upper == "PPA" || upper == "PPA-CF") return "OGFN97"; + if (upper == "PPA-GF") return "OGFN96"; + + // PC variants + if (upper == "PC") return "OGFC99"; + + // PP/PE variants + if (upper == "PE") return "OGFP99"; + if (upper == "PP") return "OGFP97"; + + // Support materials + if (upper == "PVA") return "OGFS99"; + if (upper == "HIPS") return "OGFS98"; + if (upper == "BVOH") return "OGFS97"; + + // TPU variants + if (upper == "TPU") return "OGFU99"; + + // Other materials + if (upper == "EVA") return "OGFR99"; + if (upper == "PHA") return "OGFR98"; + if (upper == "COPE") return "OGFLC99"; + if (upper == "SBS") return "OFLSBS99"; + + return ""; // Unknown material - will fall back to type-based name matching +} + int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) { auto json = nlohmann::json::parse(json_str, nullptr, false); @@ -1085,7 +1154,7 @@ void MoonrakerPrinterAgent::announce_printhost_device() const std::string model_id = device_info.model_id; if (auto* app_config = GUI::wxGetApp().app_config) { - const std::string access_code = device_info.api_key; + const std::string access_code = device_info.api_key.empty() ? "88888888" : device_info.api_key; app_config->set_str("access_code", device_info.dev_id, access_code); app_config->set_str("user_access_code", device_info.dev_id, access_code); } diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index b301ae0fd9..38badfaec0 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -115,6 +115,9 @@ protected: std::string sanitize_filename(const std::string& filename); std::string join_url(const std::string& base_url, const std::string& path) const; + // Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility + static std::string map_filament_type_to_generic_id(const std::string& filament_type); + private: int handle_request(const std::string& dev_id, const std::string& json_str); int send_version_info(const std::string& dev_id); diff --git a/src/slic3r/Utils/NetworkAgentFactory.cpp b/src/slic3r/Utils/NetworkAgentFactory.cpp index faafe136b2..1e23291399 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.cpp +++ b/src/slic3r/Utils/NetworkAgentFactory.cpp @@ -178,20 +178,15 @@ std::unique_ptr create_agent_from_config(const std::string& log_di } // Create cloud agent - CloudAgentProvider provider = use_orca_cloud ? CloudAgentProvider::Orca : CloudAgentProvider::BBL; - auto cloud_agent = NetworkAgentFactory::create_cloud_agent(provider, log_dir); - - if (!cloud_agent && provider == CloudAgentProvider::BBL) { - cloud_agent = NetworkAgentFactory::create_cloud_agent(CloudAgentProvider::Orca, log_dir); + std::shared_ptr cloud_agent = nullptr; + if (use_orca_cloud || app_config->get_bool("installed_networking")) { + CloudAgentProvider provider = use_orca_cloud ? CloudAgentProvider::Orca : CloudAgentProvider::BBL; + cloud_agent = NetworkAgentFactory::create_cloud_agent(provider, log_dir); + if (!cloud_agent) { + BOOST_LOG_TRIVIAL(error) << "Failed to create cloud agent"; + } } - if (!cloud_agent) { - BOOST_LOG_TRIVIAL(error) << "Failed to create cloud agent"; - return nullptr; - } - - // auto bbl_printer_agent = NetworkAgentFactory::create_printer_agent_by_id("bbl", cloud_agent, log_dir); - // Create NetworkAgent with cloud agent only (printer agent added later) // We will create the printer agent later when the printer is selected, so we pass nullptr for the printer agent here. auto agent = NetworkAgentFactory::create_from_agents(std::move(cloud_agent), nullptr); From 95ea712f3cb05da90c2e5b6a0fb6c0f63a2fd6f3 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Thu, 29 Jan 2026 15:35:52 +0800 Subject: [PATCH 12/25] fix issues with AFC --- scripts/test_moonraker_lane_data.py | 477 +++++++++++++++++++++ src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 34 +- 2 files changed, 502 insertions(+), 9 deletions(-) create mode 100755 scripts/test_moonraker_lane_data.py diff --git a/scripts/test_moonraker_lane_data.py b/scripts/test_moonraker_lane_data.py new file mode 100755 index 0000000000..44a0a7e30e --- /dev/null +++ b/scripts/test_moonraker_lane_data.py @@ -0,0 +1,477 @@ +#!/usr/bin/env python3 +""" +Test script for MoonrakerPrinterAgent filament sync feature. +Inserts/deletes/modifies random lane data in Moonraker database, +then reads back and displays with colored output. +""" + +import requests +import random +import argparse +import json +import time +import sys + +# Configuration +DEFAULT_HOST = "192.168.88.9" +DEFAULT_PORT = 7125 +NAMESPACE = "lane_data" +LANE_KEYS = [f"lane{i}" for i in range(1, 9)] # lane1-lane8 +MATERIALS = ["PLA", "ABS", "PETG", "ASA", "ASA Sparkle", "TPU", ""] + +# Material default temperatures (None = use null) +MATERIAL_TEMPS = { + "PLA": {"nozzle": 210, "bed": 60}, + "ABS": {"nozzle": 240, "bed": 100}, + "PETG": {"nozzle": 235, "bed": 80}, + "ASA": {"nozzle": 245, "bed": 105}, + "ASA Sparkle":{"nozzle": 245, "bed": 105}, + "TPU": {"nozzle": 220, "bed": 50}, + "": {"nozzle": None, "bed": None}, +} + +def test_connection(host, port, api_key=None, verbose=False): + """Test basic connectivity to Moonraker.""" + url = f"http://{host}:{port}/server/info" + headers = {"X-Api-Key": api_key} if api_key else {} + + if verbose: + print(f" Testing: GET {url}") + + try: + resp = requests.get(url, headers=headers, timeout=10) + if verbose: + print(f" Response: HTTP {resp.status_code}") + if resp.status_code == 200: + data = resp.json() + if verbose: + print(f" Moonraker version: {data.get('result', {}).get('moonraker_version', 'unknown')}") + return True + else: + print(f" Server returned HTTP {resp.status_code}") + if verbose: + print(f" Response: {resp.text[:500]}") + return False + except requests.exceptions.ConnectionError as e: + print(f" Connection error: {e}") + return False + except requests.exceptions.Timeout: + print(f" Connection timed out") + return False + except Exception as e: + print(f" Error: {type(e).__name__}: {e}") + return False + +def hex_to_rgb(hex_color): + """Convert hex color to RGB tuple.""" + hex_color = hex_color.lstrip('#') + if hex_color.startswith('0x') or hex_color.startswith('0X'): + hex_color = hex_color[2:] + if len(hex_color) == 6: + return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + return (128, 128, 128) # Default gray + +def color_block(hex_color): + """Return ANSI color block for terminal display.""" + r, g, b = hex_to_rgb(hex_color) + return f"\033[48;2;{r};{g};{b}m \033[0m" + +def random_color(): + """Generate random hex color, occasionally returning empty or '#None' like real data.""" + r = random.random() + if r < 0.1: + return "" # Empty color (empty lane) + if r < 0.15: + return "#None" # Observed in real data for unknown colors + return "#{:06x}".format(random.randint(0, 0xFFFFFF)) + +def get_lane_data(host, port, api_key=None): + """Fetch all lane data from Moonraker database.""" + url = f"http://{host}:{port}/server/database/item" + params = {"namespace": NAMESPACE} + headers = {"X-Api-Key": api_key} if api_key else {} + + try: + resp = requests.get(url, params=params, headers=headers, timeout=5) + if resp.status_code == 200: + data = resp.json() + return data.get("result", {}).get("value", {}) + elif resp.status_code == 404: + return {} # Namespace doesn't exist yet + else: + print(f"Error fetching lane data: HTTP {resp.status_code}") + return None + except Exception as e: + print(f"Error fetching lane data: {e}") + return None + +def set_lane_data(host, port, lane_key, lane_data, api_key=None): + """Set lane data in Moonraker database.""" + url = f"http://{host}:{port}/server/database/item" + headers = {"Content-Type": "application/json"} + if api_key: + headers["X-Api-Key"] = api_key + + payload = { + "namespace": NAMESPACE, + "key": lane_key, + "value": lane_data + } + + try: + resp = requests.post(url, json=payload, headers=headers, timeout=5) + return resp.status_code == 200 + except Exception as e: + print(f"Error setting lane data: {e}") + return False + +def delete_lane_data(host, port, lane_key, api_key=None): + """Delete lane data from Moonraker database.""" + url = f"http://{host}:{port}/server/database/item" + params = {"namespace": NAMESPACE, "key": lane_key} + headers = {"X-Api-Key": api_key} if api_key else {} + + try: + resp = requests.delete(url, params=params, headers=headers, timeout=5) + return resp.status_code == 200 + except Exception as e: + print(f"Error deleting lane data: {e}") + return False + +def display_lanes(lanes): + """Display lane data with color blocks.""" + print("\n" + "="*70) + print("CURRENT LANE DATA") + print("="*70) + + if not lanes: + print(" (no lanes configured)") + return + + # Sort by lane number + sorted_lanes = sorted(lanes.items(), + key=lambda x: int(x[1].get("lane", "0")) if x[1].get("lane", "").isdigit() else 0) + + for lane_key, data in sorted_lanes: + lane_num = data.get("lane", "?") + material = data.get("material", "") or "(empty)" + color = data.get("color", "") + bed_temp = data.get("bed_temp") + nozzle_temp = data.get("nozzle_temp") + spool_id = data.get("spool_id") + + # Show color block only for valid hex colors + if color and color.startswith("#") and color != "#None" and len(color) == 7: + block = color_block(color) + else: + block = " " # No color block + + bed_str = f"{bed_temp}°C" if bed_temp is not None else "-" + noz_str = f"{nozzle_temp}°C" if nozzle_temp is not None else "-" + spool_str = f" Spool: {spool_id}" if spool_id is not None else "" + color_str = color if color else "(none)" + + print(f" {lane_key} (T{lane_num}): {block} {color_str:10s} {material:12s} " + f"Nozzle: {noz_str:6s} Bed: {bed_str:5s}{spool_str}") + + print("="*70 + "\n") + +def make_lane_entry(tool_number, material=None): + """Generate a lane data entry matching real Moonraker AFC structure.""" + if material is None: + material = random.choice(MATERIALS) + temps = MATERIAL_TEMPS[material] + color = random_color() + + bed = None + nozzle = None + if temps["bed"] is not None: + bed = temps["bed"] + random.randint(-5, 5) + if temps["nozzle"] is not None: + nozzle = temps["nozzle"] + random.randint(-10, 10) + + spool_id = random.choice([None, random.randint(1, 50)]) + + return { + "color": color, + "material": material, + "bed_temp": bed, + "nozzle_temp": nozzle, + "scan_time": "", + "td": "", + "lane": str(tool_number), + "spool_id": spool_id, + } + +def get_used_tool_numbers(host, port, api_key=None, exclude_key=None): + """Get set of tool numbers currently in use.""" + lanes = get_lane_data(host, port, api_key) or {} + used = set() + for key, data in lanes.items(): + if key == exclude_key: + continue + lane_val = data.get("lane", "") + if lane_val.isdigit(): + used.add(int(lane_val)) + return used + +def pick_available_tool_number(used_tool_numbers): + """Pick a random tool number (0-7) not already in use. Returns None if all taken.""" + available = [n for n in range(8) if n not in used_tool_numbers] + if not available: + return None + return random.choice(available) + +def fix_duplicate_lanes(host, port, lanes, api_key=None): + """Detect and fix duplicate tool numbers in existing lane data. + + Returns the updated lane data after fixes. + """ + if not lanes: + return lanes + + # Map tool number -> list of lane keys using it + tool_to_keys = {} + for key, data in lanes.items(): + tool = data.get("lane", "") + if tool == "": + continue + tool_to_keys.setdefault(tool, []).append(key) + + # Find duplicates + duplicates = {tool: keys for tool, keys in tool_to_keys.items() if len(keys) > 1} + if not duplicates: + return lanes + + print("DUPLICATE TOOL NUMBERS DETECTED:") + for tool, keys in duplicates.items(): + print(f" Tool T{tool} used by: {', '.join(keys)}") + + # Collect all used tool numbers + used = set() + for tool, keys in tool_to_keys.items(): + if tool.isdigit(): + used.add(int(tool)) + + # Fix: keep the first key for each tool, reassign the rest + print("\nFixing duplicates...") + for tool, keys in duplicates.items(): + # Keep the first one, reassign the rest + for key in keys[1:]: + available = [n for n in range(8) if n not in used] + if not available: + print(f" {key}: cannot fix, no available tool numbers!") + continue + + new_tool = available[0] + used.add(new_tool) + + lanes[key]["lane"] = str(new_tool) + if set_lane_data(host, port, key, lanes[key], api_key): + print(f" {key}: T{tool} -> T{new_tool}") + else: + print(f" {key}: FAILED to update") + + print() + return lanes + +def perform_random_operations(host, port, api_key=None, num_ops=5): + """Perform random insert/modify/delete operations.""" + operations = ["insert", "modify", "delete"] + + print(f"\nPerforming {num_ops} random operations...") + print("-"*50) + + for i in range(num_ops): + op = random.choice(operations) + lane_key = random.choice(LANE_KEYS) + + if op in ("insert", "modify"): + # Get currently used tool numbers, excluding this key (ok to reuse its own) + used = get_used_tool_numbers(host, port, api_key, exclude_key=lane_key) + tool_num = pick_available_tool_number(used) + if tool_num is None: + print(f" [{op.upper()}] {lane_key}: SKIPPED (all tool numbers in use)") + continue + + lane_data = make_lane_entry(tool_num) + action = "INSERT" if op == "insert" else "MODIFY" + color = lane_data["color"] + material = lane_data["material"] or "(empty)" + tool = lane_data["lane"] + + if color and color.startswith("#") and color != "#None" and len(color) == 7: + block = color_block(color) + else: + block = " " + + if set_lane_data(host, port, lane_key, lane_data, api_key): + print(f" [{action}] {lane_key} (T{tool}): {block} {color or '(none)'} " + f"{material} spool={lane_data['spool_id']}") + else: + print(f" [{action}] {lane_key}: FAILED") + + elif op == "delete": + if delete_lane_data(host, port, lane_key, api_key): + print(f" [DELETE] {lane_key}") + else: + print(f" [DELETE] {lane_key}: FAILED (may not exist)") + + time.sleep(0.1) # Small delay between operations + + print("-"*50) + +def load_lanes_from_file(filepath, host, port, api_key=None): + """Load lane data from a JSON file and overwrite all lanes on the printer. + + Accepts either the raw Moonraker response format: + {"result": {"namespace": "lane_data", "value": {"lane1": {...}, ...}}} + or the plain value object: + {"lane1": {...}, "lane2": {...}, ...} + """ + try: + with open(filepath, "r") as f: + data = json.load(f) + except FileNotFoundError: + print(f"Error: file not found: {filepath}") + return False + except json.JSONDecodeError as e: + print(f"Error: invalid JSON in {filepath}: {e}") + return False + + # Accept both wrapped and unwrapped formats + if "result" in data and "value" in data.get("result", {}): + lanes = data["result"]["value"] + else: + lanes = data + + if not isinstance(lanes, dict): + print(f"Error: expected object with lane keys, got {type(lanes).__name__}") + return False + + # Validate no duplicate tool numbers + tool_to_keys = {} + for key, entry in lanes.items(): + tool = entry.get("lane", "") + if tool: + tool_to_keys.setdefault(tool, []).append(key) + dupes = {t: keys for t, keys in tool_to_keys.items() if len(keys) > 1} + if dupes: + print("Error: input JSON has duplicate tool numbers:") + for tool, keys in dupes.items(): + print(f" Tool T{tool} used by: {', '.join(keys)}") + return False + + print(f"Loading {len(lanes)} lane(s) from {filepath}...") + + # Clear all existing lanes first + print(" Clearing existing lanes...") + for lane_key in LANE_KEYS: + delete_lane_data(host, port, lane_key, api_key) + + # Write each lane from the file + ok = True + for lane_key, lane_data in lanes.items(): + if set_lane_data(host, port, lane_key, lane_data, api_key): + tool = lane_data.get("lane", "?") + material = lane_data.get("material", "") or "(empty)" + color = lane_data.get("color", "") + if color and color.startswith("#") and color != "#None" and len(color) == 7: + block = color_block(color) + else: + block = " " + print(f" [LOAD] {lane_key} (T{tool}): {block} {color or '(none)'} {material}") + else: + print(f" [LOAD] {lane_key}: FAILED") + ok = False + + return ok + +def main(): + parser = argparse.ArgumentParser( + description="Test Moonraker lane data for MoonrakerPrinterAgent filament sync" + ) + parser.add_argument("--host", default=DEFAULT_HOST, + help=f"Moonraker host (default: {DEFAULT_HOST})") + parser.add_argument("--port", type=int, default=DEFAULT_PORT, + help=f"Moonraker port (default: {DEFAULT_PORT})") + parser.add_argument("--api-key", help="Moonraker API key (if required)") + parser.add_argument("--ops", type=int, default=5, + help="Number of random operations (default: 5)") + parser.add_argument("--clear", action="store_true", + help="Clear all lane data before starting") + parser.add_argument("--read-only", action="store_true", + help="Only read and display current lane data") + parser.add_argument("--load", metavar="FILE", + help="Load lane data from JSON file and overwrite printer lanes") + parser.add_argument("--verbose", "-v", action="store_true", + help="Verbose output for debugging") + + args = parser.parse_args() + + print(f"\nConnecting to Moonraker at {args.host}:{args.port}...") + + # First test basic connectivity + if not test_connection(args.host, args.port, args.api_key, args.verbose): + print("\nFailed to connect to Moonraker!") + print("\nTroubleshooting:") + print(f" 1. Check if Moonraker is running on {args.host}") + print(f" 2. Verify port {args.port} is correct (default Moonraker port is 7125)") + print(f" 3. Try: curl http://{args.host}:{args.port}/server/info") + print(f" 4. Check if API key is required (--api-key)") + return 1 + + print("Connected!") + + # Now fetch lane data + current = get_lane_data(args.host, args.port, args.api_key) + if current is None: + print("Connected to Moonraker but failed to fetch lane data!") + return 1 + + # Check for and fix duplicate tool numbers + current = fix_duplicate_lanes(args.host, args.port, current, args.api_key) + + # Show current state + display_lanes(current) + + if args.read_only: + return 0 + + # Load from JSON file if requested + if args.load: + if not load_lanes_from_file(args.load, args.host, args.port, args.api_key): + return 1 + final = get_lane_data(args.host, args.port, args.api_key) + display_lanes(final) + if final is not None: + print("RAW JSON:") + print(json.dumps({"result": {"namespace": NAMESPACE, "key": None, "value": final}}, indent=2)) + print() + return 0 + + # Clear if requested + if args.clear: + print("Clearing all lane data...") + for lane_key in LANE_KEYS: + delete_lane_data(args.host, args.port, lane_key, args.api_key) + print("Cleared!") + display_lanes({}) + + # Perform random operations + perform_random_operations(args.host, args.port, args.api_key, args.ops) + + # Read back and display final state + final = get_lane_data(args.host, args.port, args.api_key) + display_lanes(final) + + # Print raw JSON + if final is not None: + print("RAW JSON:") + print(json.dumps({"result": {"namespace": NAMESPACE, "key": None, "value": final}}, indent=2)) + print() + + return 0 + +if __name__ == "__main__": + exit(main()) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 5e64567735..7097523f19 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -517,7 +517,7 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, const std::vector trays; int max_lane_index = 0; + // Null-safe JSON accessors: nlohmann::json::value() throws type_error + // when the key exists but the value is null (type mismatch). + auto safe_string = [](const nlohmann::json& obj, const char* key) -> std::string { + auto it = obj.find(key); + if (it != obj.end() && it->is_string()) + return it->get(); + return ""; + }; + auto safe_int = [](const nlohmann::json& obj, const char* key) -> int { + auto it = obj.find(key); + if (it != obj.end() && it->is_number()) + return it->get(); + return 0; + }; + for (const auto& [lane_key, lane_obj] : value.items()) { if (!lane_obj.is_object()) { continue; } - // Extract lane index from the "lane" field (tool number, 1-based) - std::string lane_str = lane_obj.value("lane", ""); + // Extract lane index from the "lane" field (tool number, 0-based) + std::string lane_str = safe_string(lane_obj, "lane"); int lane_index = -1; if (!lane_str.empty()) { try { - lane_index = std::stoi(lane_str) - 1; // Convert to 0-based + lane_index = std::stoi(lane_str); } catch (...) { lane_index = -1; } @@ -678,10 +693,10 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) AmsTrayData tray; tray.slot_index = lane_index; - tray.tray_color = lane_obj.value("color", ""); - tray.tray_type = lane_obj.value("material", ""); - tray.bed_temp = lane_obj.value("bed_temp", 0); - tray.nozzle_temp = lane_obj.value("nozzle_temp", 0); + tray.tray_color = safe_string(lane_obj, "color"); + tray.tray_type = safe_string(lane_obj, "material"); + tray.bed_temp = safe_int(lane_obj, "bed_temp"); + tray.nozzle_temp = safe_int(lane_obj, "nozzle_temp"); tray.has_filament = !tray.tray_type.empty(); tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); @@ -753,7 +768,8 @@ std::string MoonrakerPrinterAgent::map_filament_type_to_generic_id(const std::st if (upper == "COPE") return "OGFLC99"; if (upper == "SBS") return "OFLSBS99"; - return ""; // Unknown material - will fall back to type-based name matching + // Unknown material + return "__unknown__"; } int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) From 7494c6da7d66682de2eafefa9dcceae42cf8018e Mon Sep 17 00:00:00 2001 From: SoftFever Date: Thu, 29 Jan 2026 19:10:44 +0800 Subject: [PATCH 13/25] fix a crash bug when trying to add a filament after sync ams list --- src/libslic3r/PresetBundle.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 0c25f8ae72..6a3844ce09 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -2199,8 +2199,14 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); ConfigOptionInts* filament_map = project_config.option("filament_map"); + + filament_color->resize(n); - filament_multi_color->resize(n); + // Sync filament multi colour + filament_multi_color->values.resize(n); + for (size_t i = 0; i < n; i++) { + filament_multi_color->values[i] = filament_color->values[i]; + } filament_color_type->resize(n); filament_map->values.resize(n, 1); ams_multi_color_filment.resize(n); From a6719041a53fff38361546b4b5d0626a5e45729e Mon Sep 17 00:00:00 2001 From: SoftFever Date: Thu, 29 Jan 2026 19:19:37 +0800 Subject: [PATCH 14/25] try to fix flatpak --- src/libslic3r/Time.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/Time.cpp b/src/libslic3r/Time.cpp index de94b20853..81b9af49e2 100644 --- a/src/libslic3r/Time.cpp +++ b/src/libslic3r/Time.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #ifdef _MSC_VER #include From 01eed59ee279eb11d3de4784c25fd762fb506e45 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Thu, 29 Jan 2026 22:28:25 +0800 Subject: [PATCH 15/25] try to fix flatpak 2 --- src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index 38badfaec0..8903e37b33 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -4,7 +4,7 @@ #include "IPrinterAgent.hpp" #include "ICloudServiceAgent.hpp" -#include +#include #include #include #include From 7059789578e886423bd0d20366079ef1289d291a Mon Sep 17 00:00:00 2001 From: SoftFever Date: Fri, 30 Jan 2026 16:14:41 +0800 Subject: [PATCH 16/25] fix badge mark --- src/slic3r/GUI/Plater.cpp | 18 ++++++++++++------ src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 4 ++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index eaa5b1edcd..76ecc16727 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1985,6 +1985,8 @@ Sidebar::Sidebar(Plater *parent) } { + + // Orca: Sidebar - Filament titlebar UI // add filament title p->m_panel_filament_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); p->m_panel_filament_title->SetBackgroundColor(title_bg); @@ -2094,7 +2096,8 @@ Sidebar::Sidebar(Plater *parent) //wxBoxSizer* bSizer_filament_content; //bSizer_filament_content = new wxBoxSizer( wxHORIZONTAL ); - // BBS: filament double columns + // Orca: Sidebar - Filament content UI: setup filament selection combos panel layout + // Creates a two-column grid layout for filament selection dropdowns within the scrollable panel p->sizer_filaments = new wxBoxSizer(wxHORIZONTAL); p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); @@ -3501,6 +3504,14 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) auto_calc_flushing_volumes(i); } } + Layout(); + + // Perform preset selection and list update first — these may rebuild combo widgets, + // which clears any badge state. Badges must be set AFTER these calls to persist. + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0]); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + update_dynamic_filament_list(); + auto badge_combox_filament = [](PlaterPresetComboBox *c) { auto tip = _L("Filament type and color information have been synchronized, but slot information is not included."); c->SetToolTip(tip); @@ -3567,11 +3578,6 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) } } } - Layout(); - - wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0]); - wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); - update_dynamic_filament_list(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "begin pop_finsish_sync_ams_dialog"; pop_finsish_sync_ams_dialog(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "finish pop_finsish_sync_ams_dialog"; diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 7097523f19..127e2d19bb 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -580,6 +580,10 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, const std::vectorGetFilaSystem(), false); BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays"; + // Set printer_type so update_sync_status() can match it against the preset's printer type. + // Without this, the comparison fails and all sync badges are cleared. + obj->printer_type = device_info.model_id; + // Set push counters so is_info_ready() returns true for pull-mode agents. if (obj->m_push_count == 0) { obj->m_push_count = 1; From 8d22fad30b6cf6bd99b2a69951e410322973fd41 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 31 Jan 2026 01:04:21 +0800 Subject: [PATCH 17/25] badge support --- CLAUDE.md | 58 ++-------- src/libslic3r/PresetBundle.cpp | 72 +++++++++++-- src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp | 3 + src/slic3r/GUI/DeviceCore/DevFilaSystem.h | 1 + src/slic3r/GUI/GUI_App.cpp | 13 ++- src/slic3r/GUI/GUI_App.hpp | 2 +- src/slic3r/GUI/PhysicalPrinterDialog.cpp | 4 +- src/slic3r/GUI/Plater.cpp | 12 ++- src/slic3r/GUI/Tab.cpp | 15 --- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 8 +- src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 4 +- src/slic3r/Utils/NetworkAgentFactory.cpp | 114 +++++--------------- src/slic3r/Utils/NetworkAgentFactory.hpp | 34 ------ src/slic3r/Utils/QidiPrinterAgent.cpp | 2 +- 14 files changed, 134 insertions(+), 208 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index eb35833082..a6e48cd862 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,69 +9,25 @@ OrcaSlicer is an open-source 3D slicer application forked from Bambu Studio, bui ## Build Commands ### Building on Windows +**Always use this command to build the project when testing build issues on Windows.** ```bash -# Build everything -build_release_vs2022.bat - -# Build with debug symbols -build_release_vs2022.bat debug - -# Build only dependencies -build_release_vs2022.bat deps - -# Build only slicer (after deps are built) -build_release_vs2022.bat slicer - - +cmake --build . --config %build_type% --target ALL_BUILD -- -m ``` ### Building on macOS +**Always use this command to build the project when testing build issues on macOS.** ```bash -# Build everything (dependencies and slicer) -./build_release_macos.sh - -# Build only dependencies -./build_release_macos.sh -d - -# Build only slicer (after deps are built) -./build_release_macos.sh -s - -# Use Ninja generator for faster builds -./build_release_macos.sh -x - -# Build for specific architecture -./build_release_macos.sh -a arm64 # or x86_64 or universal - -# Build for specific macOS version target -./build_release_macos.sh -t 11.3 +cmake --build build/arm64 --config RelWithDebInfo --target all -- ``` ### Building on Linux + **Always use this command to build the project when testing build issues on Linux.** ```bash -# First time setup - install system dependencies -./build_linux.sh -u +cmake --build build/arm64 --config RelWithDebInfo --target all -- -# Build dependencies and slicer -./build_linux.sh -dsi - -# Build everything (alternative) -./build_linux.sh -dsi - -# Individual options: -./build_linux.sh -d # dependencies only -./build_linux.sh -s # slicer only -./build_linux.sh -i # build AppImage - -# Performance and debug options: -./build_linux.sh -j N # limit to N cores -./build_linux.sh -1 # single core build -./build_linux.sh -b # Debug build -./build_linux.sh -e # RelWithDebInfo build -./build_linux.sh -c # clean build -./build_linux.sh -r # skip RAM/disk checks -./build_linux.sh -l # use Clang instead of GCC ``` + ### Build System - Uses CMake with minimum version 3.13 (maximum 3.31.x on Windows) - Primary build directory: `build/` diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 6a3844ce09..f45c4e0653 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -2342,6 +2342,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector("filament_multi_colour")->values; auto ams_id = ams.opt_string("ams_id", 0u); auto slot_id = ams.opt_string("slot_id", 0u); - ams_infos.push_back({filament_id.empty() ? false : true,false, filament_color}); + auto is_placeholder = ams.has("filament_slot_placeholder") && ams.opt_bool("filament_slot_placeholder", 0u); + ams_infos.push_back({filament_id.empty() ? false : true, false, is_placeholder, filament_color}); AMSMapInfo temp = {ams_id, slot_id}; ams_array_maps.push_back(temp); index++; @@ -2378,6 +2380,12 @@ unsigned int PresetBundle::sync_ams_list(std::vectorvalues.resize(exist_filament_presets.size(), 1); } else {//overwrite; - filament_color->values = ams_filament_colors; - filament_color_type->values = ams_filament_color_types; - this->filament_presets = ams_filament_presets; - filament_map->values.resize(ams_filament_colors.size(), 1); + bool has_placeholders = std::any_of(ams_infos.begin(), ams_infos.end(), + [](const AmsInfo& a) { return a.is_placeholder; }); + if (has_placeholders) { + // Orca: merge — keep existing filaments for empty slots + auto exist_colors = filament_color->values; + auto exist_color_types = filament_color_type->values; + auto exist_presets = this->filament_presets; + + size_t tray_count = ams_filament_presets.size(); + size_t total = std::max(tray_count, exist_presets.size()); + + std::vector result_colors; + std::vector result_color_types; + std::vector result_presets; + std::vector> result_multi_colors; + + for (size_t i = 0; i < total; i++) { + bool is_loaded = (i < ams_infos.size() && ams_infos[i].valid); + + if (is_loaded) { + // Loaded tray: use tray's filament data + result_colors.push_back(ams_filament_colors[i]); + result_color_types.push_back(ams_filament_color_types[i]); + result_presets.push_back(ams_filament_presets[i]); + result_multi_colors.push_back( + i < ams_multi_color_filment.size() ? ams_multi_color_filment[i] + : std::vector{ams_filament_colors[i]}); + } else if (i < exist_presets.size()) { + // Empty tray or beyond tray count: keep existing filament + result_colors.push_back(exist_colors[i]); + result_color_types.push_back(exist_color_types[i]); + result_presets.push_back(exist_presets[i]); + result_multi_colors.push_back({exist_colors[i]}); + } else { + // New slot beyond existing count: use first visible filament as fallback + result_colors.push_back("#CECECE"); + result_color_types.push_back("1"); + result_presets.push_back(this->filaments.first_visible().name); + result_multi_colors.push_back({"#CECECE"}); + } + } + + filament_color->values = result_colors; + filament_color_type->values = result_color_types; + this->filament_presets = result_presets; + ams_multi_color_filment = result_multi_colors; + filament_map->values.resize(total, 1); + } else { + // BBL: existing wholesale replace + filament_color->values = ams_filament_colors; + filament_color_type->values = ams_filament_color_types; + this->filament_presets = ams_filament_presets; + filament_map->values.resize(ams_filament_colors.size(), 1); + } auto& print_config = this->prints.get_edited_preset().config; auto support_filament_opt = print_config.option("support_filament"); auto support_interface_filament_opt = print_config.option("support_interface_filament"); - if (support_filament_opt->value > ams_filament_color_types.size()) + if (support_filament_opt->value > filament_color_type->values.size()) support_filament_opt->value = 0; - if (support_interface_filament_opt->value > ams_filament_color_types.size()) + if (support_interface_filament_opt->value > filament_color_type->values.size()) support_interface_filament_opt->value = 0; } // Update ams_multi_color_filment diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp index 5cf3f6b885..8f35ccf454 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp @@ -612,6 +612,9 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS { curr_tray->remain = -1; } + if (tray_it->contains("tray_slot_placeholder")) { + curr_tray->is_slot_placeholder = true; + } int ams_id_int = 0; int tray_id_int = 0; try diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h index f6c1a2d420..e76834aca9 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h @@ -53,6 +53,7 @@ public: wxColour wx_color; bool is_bbl; bool is_exists = false; + bool is_slot_placeholder = false; // Orca: True for empty tray slots from pull-mode agents int hold_count = 0; int remain = 0; // filament remain: 0 ~ 100 diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index e490374c95..50f76fd2c0 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3469,16 +3469,25 @@ unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) )); } -void GUI_App::switch_printer_agent(const std::string& agent_id) +void GUI_App::switch_printer_agent() { if (!m_agent) { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": no agent exists"; return; } + std::string agent_id; + + const DynamicPrintConfig& config = preset_bundle->printers.get_edited_preset().config; + if (config.has("printer_agent")) { + std::string value = config.option("printer_agent")->value; + if (!value.empty()) { + agent_id = value; + } + } // Use registry to validate and create agent // If empty, use default - std::string effective_agent_id = agent_id.empty() ? NetworkAgentFactory::get_default_printer_agent_id() : agent_id; + std::string effective_agent_id = agent_id.empty() ? ORCA_PRINTER_AGENT_ID : agent_id; // Check if agent is registered if (!NetworkAgentFactory::is_printer_agent_registered(effective_agent_id)) { diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 6f0c0e028e..ecdab5a676 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -342,7 +342,7 @@ public: NetworkAgent* getAgent() { return m_agent; } // Dynamic printer agent switching - void switch_printer_agent(const std::string& agent_id); + void switch_printer_agent(); FilamentColorCodeQuery* get_filament_color_code_query(); bool is_editor() const { return m_app_mode == EAppMode::Editor; } diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index d2fa243919..3806215e56 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -177,13 +177,13 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr std::string selected_agent = current_agent; if (selected_agent.empty()) { - selected_agent = NetworkAgentFactory::get_default_printer_agent_id(); + selected_agent = ORCA_PRINTER_AGENT_ID; } // Verify selected agent is valid auto it = std::find_if(agents.begin(), agents.end(), [&selected_agent](const auto& a) { return a.id == selected_agent; }); if (it == agents.end()) { - selected_agent = NetworkAgentFactory::get_default_printer_agent_id(); + selected_agent = ORCA_PRINTER_AGENT_ID; } // Set default value for the enum (using the index) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 76ecc16727..7bfd3da5dc 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3230,6 +3230,7 @@ std::map Sidebar::build_filament_ams_list(MachineObject tray_config.set_key_value("filament_multi_colour", new ConfigOptionStrings{}); tray_config.set_key_value("filament_colour_type", new ConfigOptionStrings{std::to_string(tray.ctype)}); tray_config.set_key_value("filament_exist", new ConfigOptionBools{tray.is_exists}); + tray_config.set_key_value("filament_slot_placeholder", new ConfigOptionBools{tray.is_slot_placeholder}); std::optional info; if (wxGetApp().preset_bundle) { info = wxGetApp().preset_bundle->get_filament_by_filament_id(tray.setting_id); @@ -3520,8 +3521,15 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) { // badge ams filament clear_combos_filament_badge(); if (sync_result.direct_sync) { - for (auto &c : p->combos_filament) { - badge_combox_filament(c); + auto& ams_list = wxGetApp().preset_bundle->filament_ams_list; + size_t tray_idx = 0; + for (auto& entry : ams_list) { + if (tray_idx >= p->combos_filament.size()) break; + auto filament_id = entry.second.opt_string("filament_id", 0u); + if (!filament_id.empty()) { + badge_combox_filament(p->combos_filament[tray_idx]); + } + tray_idx++; } } } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 0487c2840e..08656a01ed 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2136,22 +2136,7 @@ void Tab::on_presets_changed() void Tab::update_printer_agent_if_needed() { - std::string agent_id = "orca"; - if (m_preset_bundle) { - if (wxGetApp().preset_bundle->is_bbl_vendor()) { - agent_id = "bbl"; - } else if (wxGetApp().preset_bundle->is_qidi_vendor()) { - agent_id = "qidi"; - } - } - const DynamicPrintConfig& config = m_preset_bundle->printers.get_edited_preset().config; - if (config.has("printer_agent")) { - std::string value = config.option("printer_agent")->value; - if (!value.empty()) { - agent_id = value; - } - } // Switch agent in GUI_App wxGetApp().switch_printer_agent(agent_id); } diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 127e2d19bb..b150b7df96 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -455,7 +455,7 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) return BAMBU_NETWORK_SUCCESS; } -void MoonrakerPrinterAgent::build_ams_payload(int ams_count, const std::vector& trays) +void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays) { // Look up MachineObject via DeviceManager @@ -520,7 +520,8 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, const std::vector& trays); + void build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays); // Methods that derived classes may need to override or access virtual bool init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); diff --git a/src/slic3r/Utils/NetworkAgentFactory.cpp b/src/slic3r/Utils/NetworkAgentFactory.cpp index 1e23291399..63e30dc0c8 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.cpp +++ b/src/slic3r/Utils/NetworkAgentFactory.cpp @@ -6,6 +6,8 @@ #include "QidiPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp" #include +#include +#include namespace Slic3r { namespace { @@ -18,10 +20,22 @@ std::map& get_printer_agents() return agents; } -std::string& get_default_agent_id() +// Helper to register a printer agent type with the standard factory pattern. +// AgentTypes that take a log_dir constructor arg use the default; BBLPrinterAgent +// (no log_dir) is registered separately. +template +void register_agent() { - static std::string default_id; - return default_id; + auto info = T::get_agent_info_static(); + NetworkAgentFactory::register_printer_agent( + info.id, info.name, + [](std::shared_ptr cloud_agent, + const std::string& log_dir) -> std::shared_ptr { + auto agent = std::make_shared(log_dir); + if (cloud_agent) + agent->set_cloud_agent(cloud_agent); + return agent; + }); } } // anonymous namespace @@ -30,18 +44,7 @@ bool NetworkAgentFactory::register_printer_agent(const std::string& id, const st { std::lock_guard lock(s_registry_mutex); auto& agents = get_printer_agents(); - - auto result = agents.emplace(id, PrinterAgentInfo(id, display_name, std::move(factory))); - - if (result.second) { - auto& default_id = get_default_agent_id(); - if (default_id.empty()) { - default_id = id; - } - return true; - } else { - return false; - } + return agents.emplace(id, PrinterAgentInfo(id, display_name, std::move(factory))).second; } bool NetworkAgentFactory::is_printer_agent_registered(const std::string& id) @@ -51,14 +54,6 @@ bool NetworkAgentFactory::is_printer_agent_registered(const std::string& id) return agents.find(id) != agents.end(); } -const PrinterAgentInfo* NetworkAgentFactory::get_printer_agent_info(const std::string& id) -{ - std::lock_guard lock(s_registry_mutex); - auto& agents = get_printer_agents(); - auto it = agents.find(id); - return (it != agents.end()) ? &it->second : nullptr; -} - std::vector NetworkAgentFactory::get_registered_printer_agents() { std::lock_guard lock(s_registry_mutex); @@ -89,80 +84,24 @@ std::shared_ptr NetworkAgentFactory::create_printer_agent_by_id(c return it->second.factory(cloud_agent, log_dir); } -std::string NetworkAgentFactory::get_default_printer_agent_id() -{ - std::lock_guard lock(s_registry_mutex); - return get_default_agent_id(); -} - -void NetworkAgentFactory::set_default_printer_agent_id(const std::string& id) -{ - std::lock_guard lock(s_registry_mutex); - auto& agents = get_printer_agents(); - - if (agents.find(id) != agents.end()) { - get_default_agent_id() = id; - } -} - void NetworkAgentFactory::register_all_agents() { - // Register Orca printer agent - { - auto info = OrcaPrinterAgent::get_agent_info_static(); - register_printer_agent(info.id, info.name, - [](std::shared_ptr cloud_agent, - const std::string& log_dir) -> std::shared_ptr { - auto agent = std::make_shared(log_dir); - if (cloud_agent) { - agent->set_cloud_agent(cloud_agent); - } - return agent; - }); - } + register_agent(); + register_agent(); + register_agent(); - // Register Qidi printer agent - { - auto info = QidiPrinterAgent::get_agent_info_static(); - register_printer_agent(info.id, info.name, - [](std::shared_ptr cloud_agent, - const std::string& log_dir) -> std::shared_ptr { - auto agent = std::make_shared(log_dir); - if (cloud_agent) { - agent->set_cloud_agent(cloud_agent); - } - return agent; - }); - } - - // Register Moonraker printer agent - { - auto info = MoonrakerPrinterAgent::get_agent_info_static(); - register_printer_agent(info.id, info.name, - [](std::shared_ptr cloud_agent, - const std::string& log_dir) -> std::shared_ptr { - auto agent = std::make_shared(log_dir); - if (cloud_agent) { - agent->set_cloud_agent(cloud_agent); - } - return agent; - }); - } - - // Register BBL printer agent (only if bbl network agent is available) + // BBLPrinterAgent takes no constructor args, so register manually { auto info = BBLPrinterAgent::get_agent_info_static(); register_printer_agent(info.id, info.name, [](std::shared_ptr cloud_agent, - const std::string& log_dir) -> std::shared_ptr { + const std::string& /*log_dir*/) -> std::shared_ptr { auto agent = std::make_shared(); - if (cloud_agent) { + if (cloud_agent) agent->set_cloud_agent(cloud_agent); - } return agent; }); } - } std::unique_ptr create_agent_from_config(const std::string& log_dir, AppConfig* app_config) @@ -187,9 +126,8 @@ std::unique_ptr create_agent_from_config(const std::string& log_di } } - // Create NetworkAgent with cloud agent only (printer agent added later) - // We will create the printer agent later when the printer is selected, so we pass nullptr for the printer agent here. - auto agent = NetworkAgentFactory::create_from_agents(std::move(cloud_agent), nullptr); + // Create NetworkAgent with cloud agent only (printer agent added later when printer is selected) + auto agent = std::make_unique(std::move(cloud_agent), nullptr); if (agent && app_config && use_orca_cloud) { auto* orca_cloud = dynamic_cast(agent->get_cloud_agent().get()); diff --git a/src/slic3r/Utils/NetworkAgentFactory.hpp b/src/slic3r/Utils/NetworkAgentFactory.hpp index aa7f54dccb..d4a379365e 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.hpp +++ b/src/slic3r/Utils/NetworkAgentFactory.hpp @@ -12,8 +12,6 @@ #include #include #include -#include -#include namespace Slic3r { @@ -99,11 +97,6 @@ public: */ static bool is_printer_agent_registered(const std::string& id); - /** - * Get info about a registered agent - */ - static const PrinterAgentInfo* get_printer_agent_info(const std::string& id); - /** * Get all registered printer agents (for UI population) */ @@ -121,16 +114,6 @@ public: std::shared_ptr cloud_agent, const std::string& log_dir); - /** - * Get default printer agent ID - */ - static std::string get_default_printer_agent_id(); - - /** - * Set a specific agent as the default - */ - static void set_default_printer_agent_id(const std::string& id); - // ======================================================================== // Cloud Agent Factory // ======================================================================== @@ -164,23 +147,6 @@ public: } } - // ======================================================================== - // NetworkAgent Facade Creation - // ======================================================================== - - /** - * Create a NetworkAgent from pre-created sub-agents - * - * @param cloud_agent Cloud service agent (required, includes auth) - * @param printer_agent Printer agent (optional, can be nullptr) - * @return Unique pointer to NetworkAgent facade - */ - static std::unique_ptr create_from_agents(std::shared_ptr cloud_agent, - std::shared_ptr printer_agent) - { - return std::make_unique(std::move(cloud_agent), std::move(printer_agent)); - } - private: // Factory is not instantiable NetworkAgentFactory() = delete; diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index dc0262e3c2..bf724d98d8 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -49,7 +49,7 @@ bool QidiPrinterAgent::fetch_filament_info(std::string dev_id) } // 4. Build the AMS payload - build_ams_payload(box_count, trays); + build_ams_payload(box_count, box_count * 4 - 1, trays); return true; } From 5935832143d36767ca869e1fcc5a7aa4e69d70d3 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 31 Jan 2026 01:04:27 +0800 Subject: [PATCH 18/25] clean up --- src/slic3r/GUI/GUI_App.cpp | 18 +++++++----------- src/slic3r/GUI/PhysicalPrinterDialog.cpp | 18 +++++------------- src/slic3r/GUI/Tab.cpp | 9 +-------- src/slic3r/GUI/Tab.hpp | 1 - src/slic3r/Utils/NetworkAgentFactory.cpp | 24 ++++++++++++++---------- src/slic3r/Utils/NetworkAgentFactory.hpp | 9 +++++---- 6 files changed, 32 insertions(+), 47 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 50f76fd2c0..9c52d43805 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3476,18 +3476,14 @@ void GUI_App::switch_printer_agent() return; } - std::string agent_id; - + // Read printer_agent from config, falling back to default const DynamicPrintConfig& config = preset_bundle->printers.get_edited_preset().config; + std::string effective_agent_id = ORCA_PRINTER_AGENT_ID; if (config.has("printer_agent")) { - std::string value = config.option("printer_agent")->value; - if (!value.empty()) { - agent_id = value; - } + const std::string& value = config.option("printer_agent")->value; + if (!value.empty()) + effective_agent_id = value; } - // Use registry to validate and create agent - // If empty, use default - std::string effective_agent_id = agent_id.empty() ? ORCA_PRINTER_AGENT_ID : agent_id; // Check if agent is registered if (!NetworkAgentFactory::is_printer_agent_registered(effective_agent_id)) { @@ -3498,10 +3494,10 @@ void GUI_App::switch_printer_agent() } std::string current_agent_id; - if (m_agent && m_agent->get_printer_agent()) + if (m_agent->get_printer_agent()) current_agent_id = m_agent->get_printer_agent()->get_agent_info().id; - if (current_agent_id.empty() || current_agent_id != effective_agent_id) { + if (current_agent_id != effective_agent_id) { std::string log_dir = data_dir(); std::shared_ptr cloud_agent = m_agent->get_cloud_agent(); // Create new printer agent via registry diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 3806215e56..1fc1f94087 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -172,24 +172,16 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr def.enum_labels.push_back(agent.display_name); } - // Set initial selection based on current config value or default - const std::string current_agent = m_config->opt_string("printer_agent"); - std::string selected_agent = current_agent; - - if (selected_agent.empty()) { - selected_agent = ORCA_PRINTER_AGENT_ID; - } - - // Verify selected agent is valid + // Resolve selected agent: use config value if valid, otherwise fall back to default + std::string selected_agent = m_config->opt_string("printer_agent"); auto it = std::find_if(agents.begin(), agents.end(), [&selected_agent](const auto& a) { return a.id == selected_agent; }); if (it == agents.end()) { selected_agent = ORCA_PRINTER_AGENT_ID; + it = std::find_if(agents.begin(), agents.end(), [&selected_agent](const auto& a) { return a.id == selected_agent; }); } - // Set default value for the enum (using the index) - auto def_it = std::find_if(agents.begin(), agents.end(), [&selected_agent](const auto& a) { return a.id == selected_agent; }); - if (def_it != agents.end()) { - size_t default_idx = std::distance(agents.begin(), def_it); + if (it != agents.end()) { + size_t default_idx = std::distance(agents.begin(), it); def.set_default_value(new ConfigOptionInt(static_cast(default_idx))); } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 08656a01ed..2e7ea637d2 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2101,7 +2101,7 @@ void Tab::on_presets_changed() // Check if printer agent needs switching if (m_type == Preset::TYPE_PRINTER) { - update_printer_agent_if_needed(); + wxGetApp().switch_printer_agent(); } bool is_bbl_vendor_preset = m_preset_bundle->is_bbl_vendor(); @@ -2134,13 +2134,6 @@ void Tab::on_presets_changed() wxGetApp().plater()->update_project_dirty_from_presets(); } -void Tab::update_printer_agent_if_needed() -{ - - // Switch agent in GUI_App - wxGetApp().switch_printer_agent(agent_id); -} - void Tab::build_preset_description_line(ConfigOptionsGroup* optgroup) { auto description_line = [this](wxWindow* parent) { diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 9421d88ce8..96b35b26e1 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -437,7 +437,6 @@ protected: // return true if cancelled bool tree_sel_change_delayed(wxCommandEvent& event); void on_presets_changed(); - void update_printer_agent_if_needed(); void build_preset_description_line(ConfigOptionsGroup* optgroup); void update_preset_description_line(); void update_frequently_changed_parameters(); diff --git a/src/slic3r/Utils/NetworkAgentFactory.cpp b/src/slic3r/Utils/NetworkAgentFactory.cpp index 63e30dc0c8..000a6c87f4 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.cpp +++ b/src/slic3r/Utils/NetworkAgentFactory.cpp @@ -54,6 +54,14 @@ bool NetworkAgentFactory::is_printer_agent_registered(const std::string& id) return agents.find(id) != agents.end(); } +const PrinterAgentInfo* NetworkAgentFactory::get_printer_agent_info(const std::string& id) +{ + std::lock_guard lock(s_registry_mutex); + auto& agents = get_printer_agents(); + auto it = agents.find(id); + return (it != agents.end()) ? &it->second : nullptr; +} + std::vector NetworkAgentFactory::get_registered_printer_agents() { std::lock_guard lock(s_registry_mutex); @@ -106,18 +114,14 @@ void NetworkAgentFactory::register_all_agents() std::unique_ptr create_agent_from_config(const std::string& log_dir, AppConfig* app_config) { + if (!app_config) + return std::make_unique(nullptr, nullptr); + // Determine cloud provider from config - bool use_orca_cloud = false; - if (app_config) { - try { - use_orca_cloud = app_config->get("use_orca_cloud") == "true" || app_config->get_bool("use_orca_cloud"); - } catch (...) { - use_orca_cloud = false; - } - } + bool use_orca_cloud = app_config->get_bool("use_orca_cloud"); // Create cloud agent - std::shared_ptr cloud_agent = nullptr; + std::shared_ptr cloud_agent; if (use_orca_cloud || app_config->get_bool("installed_networking")) { CloudAgentProvider provider = use_orca_cloud ? CloudAgentProvider::Orca : CloudAgentProvider::BBL; cloud_agent = NetworkAgentFactory::create_cloud_agent(provider, log_dir); @@ -129,7 +133,7 @@ std::unique_ptr create_agent_from_config(const std::string& log_di // Create NetworkAgent with cloud agent only (printer agent added later when printer is selected) auto agent = std::make_unique(std::move(cloud_agent), nullptr); - if (agent && app_config && use_orca_cloud) { + if (agent && use_orca_cloud) { auto* orca_cloud = dynamic_cast(agent->get_cloud_agent().get()); if (orca_cloud) { orca_cloud->configure_urls(app_config); diff --git a/src/slic3r/Utils/NetworkAgentFactory.hpp b/src/slic3r/Utils/NetworkAgentFactory.hpp index d4a379365e..a4233ebc5f 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.hpp +++ b/src/slic3r/Utils/NetworkAgentFactory.hpp @@ -15,10 +15,6 @@ namespace Slic3r { -// Forward declarations -class ICloudServiceAgent; -class IPrinterAgent; - /** * CloudAgentProvider - Specifies which implementation to use for each agent type. * @@ -97,6 +93,11 @@ public: */ static bool is_printer_agent_registered(const std::string& id); + /** + * Get info about a registered agent + */ + static const PrinterAgentInfo* get_printer_agent_info(const std::string& id); + /** * Get all registered printer agents (for UI population) */ From d782141ca511b2f21dd9ab1e71ea8857a6172373 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 31 Jan 2026 19:32:35 +0800 Subject: [PATCH 19/25] Add handling for unknown filament IDs and improve filament type resolution --- src/libslic3r/Preset.hpp | 1 + src/libslic3r/PresetBundle.cpp | 46 ++++++++++++++++++++-- src/slic3r/GUI/Plater.cpp | 10 +++++ src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 2 +- 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index 65da5d5e26..efb6571090 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -52,6 +52,7 @@ #define BBL_JSON_KEY_BASE_ID "base_id" #define BBL_JSON_KEY_USER_ID "user_id" #define BBL_JSON_KEY_FILAMENT_ID "filament_id" +#define UNKNOWN_FILAMENT_ID "__unknown__" #define ORCA_JSON_KEY_UPDATE_TIME "updated_time" #define ORCA_JSON_KEY_CREATED_TIME "created_time" #define BBL_JSON_KEY_INHERITS "inherits" diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index f45c4e0653..a8ea9a6a20 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -2404,11 +2404,41 @@ unsigned int PresetBundle::sync_ams_list(std::vector(haystack[pos - 1]))); + bool end_ok = (pos + needle.size() >= haystack.size() || + !std::isalnum(static_cast(haystack[pos + needle.size()]))); + if (start_ok && end_ok) + return true; + pos = haystack.find(needle, pos + 1); + } + return false; + }; + // Find the longest-matching preset type to prefer e.g. "PA-CF" over "PA". + size_t best_len = 0; + for (auto it = filaments.begin(); it != filaments.end(); ++it) { + if (!it->is_compatible || !it->is_system || !boost::algorithm::starts_with(it->name, "Generic ")) + continue; + auto preset_type = boost::to_upper_copy(it->config.opt_string("filament_type", 0u)); + if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) { + iter = it; + best_len = preset_type.size(); + filament_type = "Generic " + it->config.opt_string("filament_type", 0u); + } + } + } } if (iter == filaments.end()) { // Prefer old selection @@ -2422,8 +2452,13 @@ unsigned int PresetBundle::sync_ams_list(std::vectorname : filaments.first_visible().name; result_colors.push_back("#CECECE"); result_color_types.push_back("1"); - result_presets.push_back(this->filaments.first_visible().name); + result_presets.push_back(fallback_name); result_multi_colors.push_back({"#CECECE"}); } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 7bfd3da5dc..c1691296dc 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3473,6 +3473,16 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) dlg.ShowModal(); return; } + // Replace unknown filament IDs with the resolved preset's filament_id + auto &filaments = wxGetApp().preset_bundle->filaments; + auto &filament_presets = wxGetApp().preset_bundle->filament_presets; + for (size_t i = 0; i < list2.size() && i < filament_presets.size(); ++i) { + if (list2[i] == UNKNOWN_FILAMENT_ID) { + const Preset *resolved = filaments.find_preset(filament_presets[i]); + if (resolved) + list2[i] = resolved->filament_id; + } + } ams_filament_ids = boost::algorithm::join(list2, ","); wxGetApp().app_config ->set("ams_filament_ids", p->ams_list_device, ams_filament_ids); if (!unknowns.empty()) { diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index b150b7df96..0e0ec418ac 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -775,7 +775,7 @@ std::string MoonrakerPrinterAgent::map_filament_type_to_generic_id(const std::st if (upper == "SBS") return "OFLSBS99"; // Unknown material - return "__unknown__"; + return UNKNOWN_FILAMENT_ID; } int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) From 3da6f3bf77f9b2e9dee42654db04bec312b22f4b Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sun, 1 Feb 2026 21:35:53 +0800 Subject: [PATCH 20/25] Add SnapmakerPrinterAgent with filament sync support Introduce SnapmakerPrinterAgent as a MoonrakerPrinterAgent subclass that handles Snapmaker UI -specific filament detection --- src/slic3r/CMakeLists.txt | 2 + src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 14 +- src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 7 +- src/slic3r/Utils/NetworkAgentFactory.cpp | 2 + src/slic3r/Utils/QidiPrinterAgent.cpp | 12 +- src/slic3r/Utils/SnapmakerPrinterAgent.cpp | 157 +++++++++++++++++++++ src/slic3r/Utils/SnapmakerPrinterAgent.hpp | 25 ++++ 7 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 src/slic3r/Utils/SnapmakerPrinterAgent.cpp create mode 100644 src/slic3r/Utils/SnapmakerPrinterAgent.hpp diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index d016900572..52ba10ab03 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -608,6 +608,8 @@ set(SLIC3R_GUI_SOURCES Utils/OrcaPrinterAgent.hpp Utils/QidiPrinterAgent.cpp Utils/QidiPrinterAgent.hpp + Utils/SnapmakerPrinterAgent.cpp + Utils/SnapmakerPrinterAgent.hpp Utils/MoonrakerPrinterAgent.cpp Utils/MoonrakerPrinterAgent.hpp Utils/BBLCloudServiceAgent.cpp diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 0e0ec418ac..a49c69fc10 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -723,12 +723,18 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) return true; } +std::string MoonrakerPrinterAgent::trim_and_upper(const std::string& input) +{ + std::string result = input; + boost::trim(result); + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return static_cast(std::toupper(c)); }); + return result; +} + std::string MoonrakerPrinterAgent::map_filament_type_to_generic_id(const std::string& filament_type) { - std::string upper = filament_type; - boost::trim(upper); - std::transform(upper.begin(), upper.end(), upper.begin(), - [](unsigned char c) { return static_cast(std::toupper(c)); }); + const std::string upper = trim_and_upper(filament_type); // Map to OrcaFilamentLibrary preset IDs (compatible with all printers) // Source: resources/profiles/OrcaFilamentLibrary/filament/ diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index 34be0dc3e3..0a560388cf 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -70,8 +70,8 @@ public: int set_queue_on_main_fn(QueueOnMainFn fn) override; // Pull-mode agent (on-demand filament sync) - virtual FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } - virtual bool fetch_filament_info(std::string dev_id) override; + FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } + bool fetch_filament_info(std::string dev_id) override; protected: struct MoonrakerDeviceInfo @@ -115,6 +115,9 @@ protected: std::string sanitize_filename(const std::string& filename); std::string join_url(const std::string& base_url, const std::string& path) const; + // Trim whitespace and convert to uppercase + static std::string trim_and_upper(const std::string& input); + // Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility static std::string map_filament_type_to_generic_id(const std::string& filament_type); diff --git a/src/slic3r/Utils/NetworkAgentFactory.cpp b/src/slic3r/Utils/NetworkAgentFactory.cpp index 000a6c87f4..025aec9bf0 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.cpp +++ b/src/slic3r/Utils/NetworkAgentFactory.cpp @@ -4,6 +4,7 @@ #include "BBLPrinterAgent.hpp" #include "OrcaPrinterAgent.hpp" #include "QidiPrinterAgent.hpp" +#include "SnapmakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp" #include #include @@ -96,6 +97,7 @@ void NetworkAgentFactory::register_all_agents() { register_agent(); register_agent(); + register_agent(); register_agent(); // BBLPrinterAgent takes no constructor args, so register manually diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index bf724d98d8..e34dbe855a 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -4,7 +4,6 @@ #include "nlohmann/json.hpp" #include #include -#include #include #include @@ -293,9 +292,7 @@ void QidiPrinterAgent::parse_filament_sections(const std::string& content, std:: std::string QidiPrinterAgent::map_filament_type_to_setting_id(const std::string& filament_type) { - std::string upper = filament_type; - boost::trim(upper); - std::transform(upper.begin(), upper.end(), upper.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); + const std::string upper = trim_and_upper(filament_type); if (upper == "PLA") { return "QD_1_0_1"; @@ -351,10 +348,7 @@ std::string QidiPrinterAgent::infer_series_id(const std::string& model_id, const std::string QidiPrinterAgent::normalize_filament_type(const std::string& filament_type) { - std::string trimmed = filament_type; - boost::trim(trimmed); - std::string upper = trimmed; - std::transform(upper.begin(), upper.end(), upper.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); + const std::string upper = trim_and_upper(filament_type); if (upper.find("PLA") != std::string::npos) return "PLA"; @@ -373,7 +367,7 @@ std::string QidiPrinterAgent::normalize_filament_type(const std::string& filamen if (upper.find("PVA") != std::string::npos) return "PVA"; - return trimmed; + return upper; } } // namespace Slic3r diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp new file mode 100644 index 0000000000..a352ee889a --- /dev/null +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp @@ -0,0 +1,157 @@ +#include "SnapmakerPrinterAgent.hpp" +#include "Http.hpp" + +#include "nlohmann/json.hpp" +#include + +namespace Slic3r { + +namespace { + +constexpr const char* SNAPMAKER_AGENT_VERSION = "0.0.1"; + +// Safely access a parallel array by index, returning a fallback if out of bounds. +template +T safe_at(const std::vector& vec, int index, const T& fallback) +{ + return (index >= 0 && index < static_cast(vec.size())) ? vec[index] : fallback; +} + +} // anonymous namespace + +SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {} + +AgentInfo SnapmakerPrinterAgent::get_agent_info_static() +{ + return AgentInfo{"snapmaker", "Snapmaker Printer Agent", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"}; +} + +std::string SnapmakerPrinterAgent::combine_filament_type(const std::string& type, const std::string& sub_type) +{ + const std::string base = trim_and_upper(type); + const std::string sub = trim_and_upper(sub_type); + + if (base.empty()) + return "PLA"; + + if (sub.empty() || sub == "NONE") + return base; + + if (sub == "CF") + return base + "-CF"; + if (sub == "GF") + return base + "-GF"; + if (sub == "SILK") + return base + " SILK"; + if (sub == "SNAPSPEED" || sub == "HS") + return base + " HIGH SPEED"; + + // Unrecognized sub-type (brand names like Polylite, Basic, etc.) -- use base type only + return base; +} + +bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id) +{ + std::string url = join_url(device_info.base_url, "/printer/objects/query?print_task_config&filament_detect"); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!device_info.api_key.empty()) { + http.header("X-Api-Key", device_info.api_key); + } + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: HTTP request failed: " << http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Invalid JSON response"; + return false; + } + + // Navigate to result.status.print_task_config + if (!json.contains("result") || !json["result"].contains("status") || + !json["result"]["status"].contains("print_task_config")) { + BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Missing print_task_config in response"; + return false; + } + + auto& ptc = json["result"]["status"]["print_task_config"]; + + // Read parallel arrays from print_task_config + auto filament_exist = ptc.value("filament_exist", std::vector{}); + auto filament_type = ptc.value("filament_type", std::vector{}); + auto filament_sub_type = ptc.value("filament_sub_type", std::vector{}); + auto filament_color = ptc.value("filament_color_rgba", std::vector{}); + + const int slot_count = static_cast(filament_exist.size()); + if (slot_count == 0) { + BOOST_LOG_TRIVIAL(info) << "SnapmakerPrinterAgent::fetch_filament_info: No filament slots reported"; + return false; + } + + // Read NFC filament_detect data for temperature info (optional) + nlohmann::json nfc_info; + if (json["result"]["status"].contains("filament_detect") && + json["result"]["status"]["filament_detect"].contains("info")) { + nfc_info = json["result"]["status"]["filament_detect"]["info"]; + } + + static const std::string empty_str; + static const std::string default_color = "FFFFFFFF"; + + std::vector trays; + trays.reserve(slot_count); + + for (int i = 0; i < slot_count; ++i) { + AmsTrayData tray; + tray.slot_index = i; + tray.has_filament = filament_exist[i]; + + if (tray.has_filament) { + tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), + safe_at(filament_sub_type, i, empty_str)); + tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); + tray.tray_color = safe_at(filament_color, i, default_color); + + // Extract NFC temperature data if available + if (nfc_info.is_array() && i < static_cast(nfc_info.size()) && nfc_info[i].is_object()) { + auto& nfc_slot = nfc_info[i]; + std::string vendor = nfc_slot.value("VENDOR", "NONE"); + if (vendor != "NONE" && !vendor.empty()) { + tray.bed_temp = nfc_slot.value("BED_TEMP", 0); + tray.nozzle_temp = nfc_slot.value("FIRST_LAYER_TEMP", 0); + } + } + } + + trays.emplace_back(std::move(tray)); + } + + build_ams_payload(1, slot_count - 1, trays); + return true; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.hpp b/src/slic3r/Utils/SnapmakerPrinterAgent.hpp new file mode 100644 index 0000000000..04c0a66b3f --- /dev/null +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "MoonrakerPrinterAgent.hpp" + +#include + +namespace Slic3r { + +class SnapmakerPrinterAgent final : public MoonrakerPrinterAgent +{ +public: + explicit SnapmakerPrinterAgent(std::string log_dir); + ~SnapmakerPrinterAgent() override = default; + + static AgentInfo get_agent_info_static(); + AgentInfo get_agent_info() override { return get_agent_info_static(); } + + bool fetch_filament_info(std::string dev_id) override; + +private: + // Combine filament_type + filament_sub_type into a unified type string + static std::string combine_filament_type(const std::string& type, const std::string& sub_type); +}; + +} // namespace Slic3r From 86421c637874b7b15b9d03cace067efac676ad16 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sun, 1 Feb 2026 23:53:29 +0800 Subject: [PATCH 21/25] Use visible preset lookup for AMS filament fallback to ensure persistence across restarts --- src/libslic3r/Preset.cpp | 42 +++++++++++++++++++++- src/libslic3r/Preset.hpp | 5 +++ src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 7 ++-- src/slic3r/Utils/QidiPrinterAgent.cpp | 30 +++++++++++++++- src/slic3r/Utils/SnapmakerPrinterAgent.cpp | 7 +++- 5 files changed, 86 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index d711cdd010..d0353e2ed1 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -2764,7 +2764,7 @@ size_t PresetCollection::first_visible_idx() const size_t first_visible = -1; size_t idx = m_default_suppressed ? m_num_default_presets : 0; for (; idx < m_presets.size(); ++ idx) - if (m_presets[idx].is_visible && m_presets[idx].get_printer_id() == "BBL") { + if (m_presets[idx].is_visible && m_presets[idx].get_printer_id() == PresetBundle::ORCA_FILAMENT_LIBRARY) { if (first_visible == -1) first_visible = idx; if (m_type != Preset::TYPE_FILAMENT) @@ -2785,6 +2785,46 @@ size_t PresetCollection::first_visible_idx() const return first_visible; } +size_t PresetCollection::first_visible_idx_by_type(const std::string& filament_type) const +{ + size_t start = m_default_suppressed ? m_num_default_presets : 0; + + // Find the first visible, compatible, system base preset whose filament_type matches target. + auto find_by_type = [&](const std::string& target) -> size_t { + for (size_t i = start; i < m_presets.size(); ++i) { + const auto& p = m_presets[i]; + if (p.is_visible && p.is_compatible && p.is_system + && get_preset_base(p) == &p + && p.config.opt_string("filament_type", 0u) == target) + return i; + } + return size_t(-1); + }; + + // 1. Exact filament_type match + size_t idx = find_by_type(filament_type); + if (idx != size_t(-1)) + return idx; + + // 2. Base type fallback: strip modifier after first space + // e.g. "PLA High Speed" -> "PLA" + // Dash-separated types like "PA-CF", "PET-CF" are distinct materials, not modifiers. + auto sep = filament_type.find(' '); + if (sep != std::string::npos) { + idx = find_by_type(filament_type.substr(0, sep)); + if (idx != size_t(-1)) + return idx; + } + + // 3. Any visible preset + return first_visible_idx(); +} + +std::string PresetCollection::filament_id_by_type(const std::string& filament_type) const +{ + return preset(first_visible_idx_by_type(filament_type)).filament_id; +} + std::vector PresetCollection::diameters_of_selected_printer() { std::set diameters; diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index f2c6b889ad..d76f2b93c2 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -638,6 +638,11 @@ public: return const_cast(this)->find_preset2(name, auto_match); } size_t first_visible_idx() const; + // Return the index of the first visible, compatible, system base preset + // matching the given filament_type. Falls back to base type, then any visible. + size_t first_visible_idx_by_type(const std::string& filament_type) const; + // Return the filament_id of the best-matching visible preset for the given filament type. + std::string filament_id_by_type(const std::string& filament_type) const; // Return index of the first compatible preset. Certainly at least the '- default -' preset shall be compatible. // If one of the prefered_alternates is compatible, select it. template size_t first_compatible_idx(PreferedCondition prefered_condition) const diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index a49c69fc10..6a86224bf9 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -704,7 +704,10 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) tray.bed_temp = safe_int(lane_obj, "bed_temp"); tray.nozzle_temp = safe_int(lane_obj, "nozzle_temp"); tray.has_filament = !tray.tray_type.empty(); - tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); + auto* bundle = GUI::wxGetApp().preset_bundle; + tray.tray_info_idx = bundle + ? bundle->filaments.filament_id_by_type(tray.tray_type) + : map_filament_type_to_generic_id(tray.tray_type); max_lane_index = std::max(max_lane_index, lane_index); trays.push_back(tray); @@ -780,7 +783,7 @@ std::string MoonrakerPrinterAgent::map_filament_type_to_generic_id(const std::st if (upper == "COPE") return "OGFLC99"; if (upper == "SBS") return "OFLSBS99"; - // Unknown material + // Unknown material return UNKNOWN_FILAMENT_ID; } diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index e34dbe855a..8a440b6c29 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -1,5 +1,7 @@ #include "QidiPrinterAgent.hpp" #include "Http.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "slic3r/GUI/GUI_App.hpp" #include "nlohmann/json.hpp" #include @@ -9,6 +11,22 @@ namespace Slic3r { +namespace { + +// Check whether any visible, compatible base preset in the collection has the given filament_id. +bool has_visible_base_preset(const PresetCollection& filaments, const std::string& filament_id) +{ + for (const auto& p : filaments.get_presets()) { + if (p.is_visible && p.is_compatible + && filaments.get_preset_base(p) == &p + && p.filament_id == filament_id) + return true; + } + return false; +} + +} // anonymous namespace + const std::string QidiPrinterAgent_VERSION = "0.0.1"; QidiPrinterAgent::QidiPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) @@ -157,7 +175,17 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, filament_name = filament_it->second; } tray.tray_type = normalize_filament_type(filament_name); - tray.tray_info_idx = build_setting_id(filament_type, vendor_type, tray.tray_type); + + // Try Qidi-specific setting ID first; fall back to visible preset by type + std::string setting_id = build_setting_id(filament_type, vendor_type, tray.tray_type); + auto* bundle = GUI::wxGetApp().preset_bundle; + if (!bundle) { + tray.tray_info_idx = setting_id; + } else if (!setting_id.empty() && has_visible_base_preset(bundle->filaments, setting_id)) { + tray.tray_info_idx = setting_id; + } else { + tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); + } // Look up color from dictionary auto color_it = dict.colors.find(color_index); diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp index a352ee889a..a7dda3fcd6 100644 --- a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp @@ -1,5 +1,7 @@ #include "SnapmakerPrinterAgent.hpp" #include "Http.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "slic3r/GUI/GUI_App.hpp" #include "nlohmann/json.hpp" #include @@ -133,7 +135,10 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id) if (tray.has_filament) { tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), safe_at(filament_sub_type, i, empty_str)); - tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); + auto* bundle = GUI::wxGetApp().preset_bundle; + tray.tray_info_idx = bundle + ? bundle->filaments.filament_id_by_type(tray.tray_type) + : map_filament_type_to_generic_id(tray.tray_type); tray.tray_color = safe_at(filament_color, i, default_color); // Extract NFC temperature data if available From d817a4d5d4eacd008e82d41a03f91f7c261efbb0 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 2 Feb 2026 12:20:09 +0800 Subject: [PATCH 22/25] Fix an issue that sync ams btn is not show/hide after printer agent changes --- src/slic3r/GUI/GUI_App.cpp | 17 ++++++++++------- src/slic3r/GUI/PhysicalPrinterDialog.cpp | 7 ++++++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 9c52d43805..bbd4c26974 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3498,11 +3498,12 @@ void GUI_App::switch_printer_agent() current_agent_id = m_agent->get_printer_agent()->get_agent_info().id; if (current_agent_id != effective_agent_id) { - std::string log_dir = data_dir(); + std::string log_dir = data_dir(); std::shared_ptr cloud_agent = m_agent->get_cloud_agent(); + // Create new printer agent via registry - std::shared_ptr new_printer_agent = NetworkAgentFactory::create_printer_agent_by_id(effective_agent_id, cloud_agent, - log_dir); + std::shared_ptr new_printer_agent = + NetworkAgentFactory::create_printer_agent_by_id(effective_agent_id, cloud_agent, log_dir); if (!new_printer_agent) { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id << "', keeping current agent"; @@ -3511,11 +3512,13 @@ void GUI_App::switch_printer_agent() // Swap the agent m_agent->set_printer_agent(new_printer_agent); - } - // Auto-switch MachineObject - select_machine(effective_agent_id); + sidebar().update_all_preset_comboboxes(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id; + } + + // Auto-switch MachineObject (runs even if agent IDs match) + select_machine(effective_agent_id); } void GUI_App::select_machine(const std::string& agent_id) diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 1fc1f94087..d3c6aa6caf 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -820,8 +820,13 @@ void PhysicalPrinterDialog::check_host_key_valid() void PhysicalPrinterDialog::OnOK(wxEvent& event) { - wxGetApp().get_tab(Preset::TYPE_PRINTER)->save_preset("", false, false, true, m_preset_name ); + wxGetApp().get_tab(Preset::TYPE_PRINTER)->save_preset("", false, false, true, m_preset_name); event.Skip(); + + // Defer printer agent switch to ensure preset save completes first + wxGetApp().CallAfter([] { + wxGetApp().switch_printer_agent(); + }); } }} // namespace Slic3r::GUI From 431943408b2a6a4b4bde14275f5554e861c7d4ea Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 2 Feb 2026 18:12:19 +0800 Subject: [PATCH 23/25] fix UI block issues --- src/slic3r/GUI/DeviceCore/DevManager.cpp | 5 +- src/slic3r/GUI/GUI_App.cpp | 11 ++- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 101 +++++++++++---------- src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 13 +-- src/slic3r/Utils/NetworkAgent.cpp | 8 ++ src/slic3r/Utils/NetworkAgentFactory.cpp | 41 ++++++++- src/slic3r/Utils/NetworkAgentFactory.hpp | 10 ++ 7 files changed, 124 insertions(+), 65 deletions(-) diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index 6f4885f4cc..df161d9dea 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -858,7 +858,10 @@ namespace Slic3r { if (MachineObject* obj_ = get_selected_machine()) { GUI::wxGetApp().sidebar().update_sync_status(obj_); - GUI::wxGetApp().sidebar().load_ams_list(obj_); + if(m_agent->get_filament_sync_mode() == FilamentSyncMode::subscription) + { + GUI::wxGetApp().sidebar().load_ams_list(obj_); + } }; } diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index bbd4c26974..a7a0d49fa0 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2591,6 +2591,11 @@ int GUI_App::OnExit() m_user_manager = nullptr; } + // Clear the printer agent cache before destroying the NetworkAgent. + // This disconnects all cached agents and releases their shared_ptrs, + // ensuring clean thread shutdown before the agent is deleted. + NetworkAgentFactory::clear_printer_agent_cache(); + if (m_agent) { // BBS avoid a crash on mac platform #ifdef __WINDOWS__ @@ -3515,10 +3520,10 @@ void GUI_App::switch_printer_agent() sidebar().update_all_preset_comboboxes(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id; - } - // Auto-switch MachineObject (runs even if agent IDs match) - select_machine(effective_agent_id); + // Auto-switch MachineObject + select_machine(effective_agent_id); + } } void GUI_App::select_machine(const std::string& agent_id) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 6a86224bf9..ee1b2b6235 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -98,7 +98,15 @@ MoonrakerPrinterAgent::MoonrakerPrinterAgent(std::string log_dir) : m_cloud_agen MoonrakerPrinterAgent::~MoonrakerPrinterAgent() { - disconnect_printer(); // This will handle thread cleanup + { + std::lock_guard lock(connect_mutex); + device_info = MoonrakerDeviceInfo{}; + ++connect_generation; + } + if (connect_thread.joinable()) { + connect_thread.join(); + } + stop_status_stream(); } AgentInfo MoonrakerPrinterAgent::get_agent_info_static() @@ -133,31 +141,20 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i return BAMBU_NETWORK_ERR_INVALID_HANDLE; } - // Check if connection already in progress + std::string base_url; + std::string api_key; + uint64_t gen; { std::lock_guard lock(connect_mutex); init_device_info(dev_id, dev_ip, username, password, use_ssl); - if (connect_in_progress.load()) { - // Don't reject - wait for previous connection to complete - // This can happen if MonitorPanel triggers connect while previous connect is still running - } else { - connect_in_progress.store(true); - connect_stop_requested.store(false); + gen = ++connect_generation; + base_url = device_info.base_url; + api_key = device_info.api_key; + if (connect_thread.joinable()) { + connect_thread.detach(); } } - // Wait for previous connection thread to finish - if (connect_thread.joinable()) { - connect_thread.join(); - } - - // Now we can start a new connection - { - std::lock_guard lock(connect_mutex); - connect_in_progress.store(true); - connect_stop_requested.store(false); - } - // Stop existing status stream and clear state stop_status_stream(); { @@ -168,30 +165,26 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i ws_last_dispatch_ms.store(0); last_print_state.clear(); - // Launch connection in background thread - connect_thread = std::thread([this, dev_id]() { perform_connection_async(dev_id, device_info.base_url, device_info.api_key); }); + // Launch connection in background thread (capture by value to avoid data races) + { + std::lock_guard lock(connect_mutex); + connect_thread = std::thread([this, dev_id, base_url, api_key, gen]() { perform_connection_async(dev_id, base_url, api_key, gen); }); + } return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::disconnect_printer() { - // Stop connection thread if running { std::lock_guard lock(connect_mutex); device_info = MoonrakerDeviceInfo{}; - if (connect_in_progress.load()) { - connect_stop_requested.store(true); - // Wake up any sleeping - connect_cv.notify_all(); + ++connect_generation; // Invalidate any in-flight connection + if (connect_thread.joinable()) { + connect_thread.detach(); } } - // Wait for connection thread to finish (with timeout) - if (connect_thread.joinable()) { - connect_thread.join(); - } - stop_status_stream(); return BAMBU_NETWORK_SUCCESS; } @@ -1842,14 +1835,12 @@ int MoonrakerPrinterAgent::pause_print(const std::string& dev_id) int MoonrakerPrinterAgent::resume_print(const std::string& dev_id) { - std::string gcode = "RESUME"; - return send_gcode(dev_id, gcode) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; + return send_gcode(dev_id, "RESUME") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; } int MoonrakerPrinterAgent::cancel_print(const std::string& dev_id) { - std::string gcode = "CANCEL_PRINT"; - return send_gcode(dev_id, gcode) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; + return send_gcode(dev_id, "CANCEL_PRINT") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; } bool MoonrakerPrinterAgent::send_jsonrpc_command(const std::string& base_url, @@ -1889,20 +1880,38 @@ bool MoonrakerPrinterAgent::send_jsonrpc_command(const std::string& base_url, return success; } -void MoonrakerPrinterAgent::perform_connection_async(const std::string& dev_id, const std::string& base_url, const std::string& api_key) +void MoonrakerPrinterAgent::perform_connection_async(const std::string& dev_id, const std::string& base_url, const std::string& api_key, uint64_t generation) { + auto is_stale = [&]() { return generation != connect_generation.load(); }; + int result = BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; std::string error_msg; + // Early exit if a newer connection was started before we begin + if (is_stale()) { + return; + } + try { - if (!fetch_device_info(base_url, api_key, device_info, error_msg)) { + MoonrakerDeviceInfo fetched_info; + if (!fetch_device_info(base_url, api_key, fetched_info, error_msg)) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Failed to fetch server info: " << error_msg; // Orca todo: revist here, for now don't send error, this is set current MachineObject to null // dispatch_local_connect(ConnectStatusFailed, dev_id, "server_info_failed"); - finish_connection(); return; } + // Commit fetched info back to device_info under lock, only if still current + { + std::lock_guard lock(connect_mutex); + if (is_stale()) { + return; + } + device_info.dev_name = fetched_info.dev_name; + device_info.version = fetched_info.version; + device_info.klippy_state = fetched_info.klippy_state; + } + // Orca todo: disable websocket for now, as we don't use MonitorPanel for Moonraker printers yet #if 0 // Query initial status @@ -1929,23 +1938,15 @@ void MoonrakerPrinterAgent::perform_connection_async(const std::string& dev_id, result = BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; } - // Dispatch final result to UI - if (result == BAMBU_NETWORK_SUCCESS) { + // Only dispatch if this connection is still the current one + if (result == BAMBU_NETWORK_SUCCESS && !is_stale()) { dispatch_local_connect(ConnectStatusOk, dev_id, "0"); dispatch_printer_connected(dev_id); BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: connect_printer completed - dev_id=" << dev_id; - } else if (result != BAMBU_NETWORK_ERR_CANCELED) { + } else if (result != BAMBU_NETWORK_SUCCESS && result != BAMBU_NETWORK_ERR_CANCELED) { // Orca todo: revist here, for now don't send error, this is set current MachineObject to null // dispatch_local_connect(ConnectStatusFailed, dev_id, error_msg); } - - finish_connection(); -} - -void MoonrakerPrinterAgent::finish_connection() -{ - std::lock_guard lock(connect_mutex); - connect_in_progress.store(false); } bool MoonrakerPrinterAgent::is_numeric(const std::string& value) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index 0a560388cf..bddf25296a 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -4,7 +4,6 @@ #include "IPrinterAgent.hpp" #include "ICloudServiceAgent.hpp" -#include #include #include #include @@ -158,8 +157,8 @@ private: // Connection thread management void perform_connection_async(const std::string& dev_id, const std::string& base_url, - const std::string& api_key); - void finish_connection(); + const std::string& api_key, + uint64_t generation); std::string ssdp_announced_host; std::string ssdp_announced_id; @@ -194,11 +193,9 @@ private: std::string last_print_state; // Track state for immediate dispatch on change // Connection thread management - std::atomic connect_in_progress{false}; - std::atomic connect_stop_requested{false}; - std::thread connect_thread; - std::recursive_mutex connect_mutex; - std::condition_variable connect_cv; + std::atomic connect_generation{0}; + std::thread connect_thread; + std::recursive_mutex connect_mutex; }; } // namespace Slic3r diff --git a/src/slic3r/Utils/NetworkAgent.cpp b/src/slic3r/Utils/NetworkAgent.cpp index 16d82ba8dc..1ec0ef437d 100644 --- a/src/slic3r/Utils/NetworkAgent.cpp +++ b/src/slic3r/Utils/NetworkAgent.cpp @@ -139,6 +139,7 @@ void NetworkAgent::set_printer_agent(std::shared_ptr printer_agen // Local copies to allow safe access after releasing the lock. // This pattern ensures the objects stay alive (via shared_ptr refcount) even if // another thread modifies m_printer_agent or m_printer_callbacks after we unlock. + std::shared_ptr old_printer_agent; std::shared_ptr new_printer_agent; PrinterCallbacks callbacks; @@ -152,6 +153,8 @@ void NetworkAgent::set_printer_agent(std::shared_ptr printer_agen // Disconnect all callbacks from the old agent apply_printer_callbacks(m_printer_agent, callbacks); + // Capture the old agent before overwriting so we can disconnect it outside the lock + old_printer_agent = m_printer_agent; // Take ownership of the incoming agent and update the agent ID m_printer_agent = std::move(printer_agent); m_printer_agent_id = m_printer_agent->get_agent_info().id; @@ -164,6 +167,11 @@ void NetworkAgent::set_printer_agent(std::shared_ptr printer_agen } // Lock released here - m_agent_mutex is now free for other threads + // Disconnect the old agent's connections/threads. The cache keeps it alive, + // but we release its network resources while it's not the active agent. + if (old_printer_agent && old_printer_agent != new_printer_agent) + old_printer_agent->disconnect_printer(); + // Apply callbacks OUTSIDE the lock to avoid deadlock risk and minimize // critical section duration. The local shared_ptr copy ensures the agent // cannot be destroyed while we're using it. diff --git a/src/slic3r/Utils/NetworkAgentFactory.cpp b/src/slic3r/Utils/NetworkAgentFactory.cpp index 025aec9bf0..4047155881 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.cpp +++ b/src/slic3r/Utils/NetworkAgentFactory.cpp @@ -21,6 +21,12 @@ std::map& get_printer_agents() return agents; } +std::map>& get_printer_agent_cache() +{ + static std::map> cache; + return cache; +} + // Helper to register a printer agent type with the standard factory pattern. // AgentTypes that take a log_dir constructor arg use the default; BBLPrinterAgent // (no log_dir) is registered separately. @@ -82,15 +88,44 @@ std::shared_ptr NetworkAgentFactory::create_printer_agent_by_id(c const std::string& log_dir) { std::lock_guard lock(s_registry_mutex); - auto& agents = get_printer_agents(); - auto it = agents.find(id); + + // Check cache first + auto& cache = get_printer_agent_cache(); + auto cache_it = cache.find(id); + if (cache_it != cache.end()) { + BOOST_LOG_TRIVIAL(info) << "Reusing cached printer agent: " << id; + if (cloud_agent) + cache_it->second->set_cloud_agent(cloud_agent); + return cache_it->second; + } + + // Not cached — create via factory + auto& agents = get_printer_agents(); + auto it = agents.find(id); if (it == agents.end()) { BOOST_LOG_TRIVIAL(warning) << "Unknown printer agent ID: " << id; return nullptr; } - return it->second.factory(cloud_agent, log_dir); + auto agent = it->second.factory(cloud_agent, log_dir); + if (agent) { + BOOST_LOG_TRIVIAL(info) << "Created and cached printer agent: " << id; + cache[id] = agent; + } + return agent; +} + +void NetworkAgentFactory::clear_printer_agent_cache() +{ + std::lock_guard lock(s_registry_mutex); + auto& cache = get_printer_agent_cache(); + for (auto& pair : cache) { + if (pair.second) + pair.second->disconnect_printer(); + } + cache.clear(); + BOOST_LOG_TRIVIAL(info) << "Printer agent cache cleared"; } void NetworkAgentFactory::register_all_agents() diff --git a/src/slic3r/Utils/NetworkAgentFactory.hpp b/src/slic3r/Utils/NetworkAgentFactory.hpp index a4233ebc5f..56dd0efe9c 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.hpp +++ b/src/slic3r/Utils/NetworkAgentFactory.hpp @@ -106,6 +106,9 @@ public: /** * Create a printer agent by ID (using registry) * + * Returns a cached instance if one exists for the given ID, otherwise + * creates a new agent via the registered factory and caches it. + * * @param id Agent ID to create * @param cloud_agent Cloud agent for token access * @param log_dir Directory for log files @@ -115,6 +118,13 @@ public: std::shared_ptr cloud_agent, const std::string& log_dir); + /** + * Clear the printer agent cache. + * Calls disconnect_printer() on each cached agent and releases all shared_ptrs. + * Should be called during application shutdown before destroying the NetworkAgent. + */ + static void clear_printer_agent_cache(); + // ======================================================================== // Cloud Agent Factory // ======================================================================== From 84faac2dd474895ebe1ea276dea1f05c73f5169e Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 2 Feb 2026 23:48:22 +0800 Subject: [PATCH 24/25] fix an issue the bbl printer not working --- src/slic3r/GUI/GUI_App.cpp | 14 +++++++++----- src/slic3r/GUI/Plater.cpp | 2 +- src/slic3r/GUI/WebViewDialog.cpp | 28 ++++++++++++++-------------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index a7a0d49fa0..1a3db613f9 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3482,12 +3482,16 @@ void GUI_App::switch_printer_agent() } // Read printer_agent from config, falling back to default - const DynamicPrintConfig& config = preset_bundle->printers.get_edited_preset().config; std::string effective_agent_id = ORCA_PRINTER_AGENT_ID; - if (config.has("printer_agent")) { - const std::string& value = config.option("printer_agent")->value; - if (!value.empty()) - effective_agent_id = value; + if (preset_bundle->is_bbl_vendor()) { + effective_agent_id = BBL_PRINTER_AGENT_ID; + } else { + const DynamicPrintConfig& config = preset_bundle->printers.get_edited_preset().config; + if (config.has("printer_agent")) { + const std::string& value = config.option("printer_agent")->value; + if (!value.empty()) + effective_agent_id = value; + } } // Check if agent is registered diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index c1691296dc..7b944f414c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9826,7 +9826,7 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) update_sidebar(); int old_sel = e.GetOldSelection(); if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->use_bbl_device_tab() && new_sel == MainFrame::tpMonitor) { - if (!wxGetApp().getAgent()) { + if (!Slic3r::NetworkAgent::is_network_module_loaded()) { e.Veto(); BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins") % old_sel % new_sel; if (q) { diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index 5d9301b7ca..46dddf3812 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -487,24 +487,24 @@ void WebViewPanel::SendLoginInfo() void WebViewPanel::ShowNetpluginTip() { - // Install Network Plugin - const auto bblnetwork_enabled =wxGetApp().app_config->get_bool("installed_networking"); - if(!bblnetwork_enabled) { - return; + const auto bblnetwork_enabled = wxGetApp().app_config->get_bool("installed_networking"); + + // Show tip if: plugin is enabled but incompatible, OR BBL printer selected but plugin not loaded + bool need_show = false; + if (bblnetwork_enabled) { + need_show = !wxGetApp().is_compatibility_version(); + } else if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor()) { + need_show = true; } - bool bValid = wxGetApp().is_compatibility_version(); - int nShow = 0; - if (!bValid) nShow = 1; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": need_show=%1%") % need_show; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": bValid=%1%, nShow=%2%")%bValid %nShow; + json res = json::object(); + res["command"] = "network_plugin_installtip"; + res["sequence_id"] = "10001"; + res["show"] = need_show ? 1 : 0; - json m_Res = json::object(); - m_Res["command"] = "network_plugin_installtip"; - m_Res["sequence_id"] = "10001"; - m_Res["show"] = nShow; - - wxString strJS = wxString::Format("window.postMessage(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore)); + wxString strJS = wxString::Format("window.postMessage(%s)", res.dump(-1, ' ', false, json::error_handler_t::ignore)); RunScript(strJS); } From 86d54863f7712f5b0b3d204c2185db1eaf77604f Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 3 Feb 2026 12:01:19 +0800 Subject: [PATCH 25/25] rename agent --- src/slic3r/Utils/BBLPrinterAgent.cpp | 2 +- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 2 +- src/slic3r/Utils/OrcaPrinterAgent.cpp | 2 +- src/slic3r/Utils/QidiPrinterAgent.cpp | 2 +- src/slic3r/Utils/SnapmakerPrinterAgent.cpp | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/slic3r/Utils/BBLPrinterAgent.cpp b/src/slic3r/Utils/BBLPrinterAgent.cpp index bf13aa84ee..69ea0d32be 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.cpp +++ b/src/slic3r/Utils/BBLPrinterAgent.cpp @@ -205,7 +205,7 @@ int BBLPrinterAgent::set_user_selected_machine(std::string dev_id) // ============================================================================ AgentInfo BBLPrinterAgent::get_agent_info_static() { - return AgentInfo{BBL_PRINTER_AGENT_ID, "Bambu Lab Printer Agent", "", "Bambu Lab printer agent"}; + return AgentInfo{BBL_PRINTER_AGENT_ID, "Bambu Lab", "", "Bambu Lab printer agent"}; } // ============================================================================ diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index ee1b2b6235..cc19fe7db1 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -111,7 +111,7 @@ MoonrakerPrinterAgent::~MoonrakerPrinterAgent() AgentInfo MoonrakerPrinterAgent::get_agent_info_static() { - return AgentInfo{"moonraker", "Moonraker Printer Agent", MoonrakerPrinterAgent_VERSION, "Klipper/Moonraker printer agent"}; + return AgentInfo{"moonraker", "Moonraker", MoonrakerPrinterAgent_VERSION, "Klipper/Moonraker printer agent"}; } void MoonrakerPrinterAgent::set_cloud_agent(std::shared_ptr cloud) diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index 7f14c71696..d16a5c5432 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -124,7 +124,7 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) // ============================================================================ AgentInfo OrcaPrinterAgent::get_agent_info_static() { - return AgentInfo{ORCA_PRINTER_AGENT_ID, "Orca Printer Agent", OrcaPrinterAgent_VERSION, "Orca Printer Communication Protocol Agent"}; + return AgentInfo{ORCA_PRINTER_AGENT_ID, "Orca", OrcaPrinterAgent_VERSION, "Orca Printer Communication Protocol Agent"}; } // ============================================================================ diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index 8a440b6c29..dfd2838513 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -35,7 +35,7 @@ QidiPrinterAgent::QidiPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent( AgentInfo QidiPrinterAgent::get_agent_info_static() { - return AgentInfo{"qidi", "Qidi Printer Agent", QidiPrinterAgent_VERSION, "Qidi printer agent"}; + return AgentInfo{"qidi", "Qidi", QidiPrinterAgent_VERSION, "Qidi printer agent"}; } bool QidiPrinterAgent::fetch_filament_info(std::string dev_id) diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp index a7dda3fcd6..795821df61 100644 --- a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp @@ -25,7 +25,7 @@ SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPri AgentInfo SnapmakerPrinterAgent::get_agent_info_static() { - return AgentInfo{"snapmaker", "Snapmaker Printer Agent", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"}; + return AgentInfo{"snapmaker", "Snapmaker", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"}; } std::string SnapmakerPrinterAgent::combine_filament_type(const std::string& type, const std::string& sub_type)