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") {