diff --git a/plugins/BambuSource.dll b/plugins/BambuSource.dll new file mode 100644 index 00000000000..deef5833854 Binary files /dev/null and b/plugins/BambuSource.dll differ diff --git a/plugins/agora_rtc_sdk.dll b/plugins/agora_rtc_sdk.dll new file mode 100644 index 00000000000..6fb8212e888 Binary files /dev/null and b/plugins/agora_rtc_sdk.dll differ diff --git a/plugins/bambu_networking.dll b/plugins/bambu_networking.dll new file mode 100644 index 00000000000..94ee797e005 Binary files /dev/null and b/plugins/bambu_networking.dll differ diff --git a/plugins/bambu_networking_02.03.00.62.dll b/plugins/bambu_networking_02.03.00.62.dll new file mode 100644 index 00000000000..94ee797e005 Binary files /dev/null and b/plugins/bambu_networking_02.03.00.62.dll differ diff --git a/plugins/libagora-core.dll b/plugins/libagora-core.dll new file mode 100644 index 00000000000..1d8b4a68f49 Binary files /dev/null and b/plugins/libagora-core.dll differ diff --git a/plugins/libagora-ffmpeg.dll b/plugins/libagora-ffmpeg.dll new file mode 100644 index 00000000000..a07f36fde28 Binary files /dev/null and b/plugins/libagora-ffmpeg.dll differ diff --git a/plugins/libagora-soundtouch.dll b/plugins/libagora-soundtouch.dll new file mode 100644 index 00000000000..ab4215abaad Binary files /dev/null and b/plugins/libagora-soundtouch.dll differ diff --git a/plugins/live555.dll b/plugins/live555.dll new file mode 100644 index 00000000000..55241f284ce Binary files /dev/null and b/plugins/live555.dll differ diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 3288c3f55ff..306a8534bfd 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -508,7 +508,7 @@ if (_opts) target_compile_options(libslic3r_cgal PRIVATE "${_opts_bad}") endif() -target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} eigen admesh libigl mcut boost_libs) +target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} eigen admesh libigl mcut boost_libs OpenSSL::Crypto) if (MSVC AND "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # 32 bit MSVC workaround target_compile_definitions(libslic3r_cgal PRIVATE CGAL_DO_NOT_USE_MPZF) @@ -590,11 +590,11 @@ target_link_libraries(libslic3r if(NOT WIN32) # Link freetype for OCCT dependency (CAD operations need font rendering) target_link_libraries(libslic3r PRIVATE ${FREETYPE_LIBRARIES}) - target_link_libraries(libslic3r PRIVATE OpenSSL::Crypto) if (NOT APPLE) target_link_libraries(libslic3r PRIVATE fontconfig) endif() endif() +target_link_libraries(libslic3r PRIVATE OpenSSL::Crypto) if (APPLE) find_library(FOUNDATION Foundation REQUIRED) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index d97af9e9822..da3692125ab 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -3408,9 +3408,9 @@ void PrintConfigDef::init_fff_params() def->label = L("Infill"); def->category = L("Extruders"); def->tooltip = L("Filament to print internal sparse infill."); - def->min = 1; + def->min = 0; def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(1)); + def->set_default_value(new ConfigOptionInt(0)); def = this->add("sparse_infill_line_width", coFloatOrPercent); def->label = L("Sparse infill"); @@ -4147,9 +4147,9 @@ void PrintConfigDef::init_fff_params() def->label = L("Walls"); def->category = L("Extruders"); def->tooltip = L("Filament to print walls."); - def->min = 1; + def->min = 0; def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(1)); + def->set_default_value(new ConfigOptionInt(0)); def = this->add("inner_wall_line_width", coFloatOrPercent); def->label = L("Inner wall"); @@ -4445,7 +4445,7 @@ void PrintConfigDef::init_fff_params() def->category = L("Support"); def->tooltip = L("Expand the first raft or support layer to improve bed plate adhesion."); def->sidetext = "mm"; // milimeters, don't need translation - def->min = 0; + def->min = -1; def->mode = comAdvanced; //BBS: change from 3.0 to 2.0 def->set_default_value(new ConfigOptionFloat(2.0)); @@ -4944,9 +4944,9 @@ void PrintConfigDef::init_fff_params() def->label = L("Solid infill"); def->category = L("Extruders"); def->tooltip = L("Filament to print solid infill."); - def->min = 1; + def->min = 0; def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(1)); + def->set_default_value(new ConfigOptionInt(0)); def = this->add("internal_solid_infill_line_width", coFloatOrPercent); def->label = L("Internal solid infill"); @@ -5677,8 +5677,8 @@ void PrintConfigDef::init_fff_params() def = this->add("tree_support_wall_count", coInt); def->label = L("Support wall loops"); def->category = L("Support"); - def->tooltip = L("This setting specifies the count of support walls in the range of [0,2]. 0 means auto."); - def->min = 0; + def->tooltip = L("This setting specifies the count of support walls in the range of [-1,2]. -1 means default, 0 means auto."); + def->min = -1; def->max = 2; def->mode = comAdvanced; def->set_default_value(new ConfigOptionInt(0)); diff --git a/src/slic3r/GUI/ConnectPrinter.cpp b/src/slic3r/GUI/ConnectPrinter.cpp index 6564c01dfd4..d03ba73d983 100644 --- a/src/slic3r/GUI/ConnectPrinter.cpp +++ b/src/slic3r/GUI/ConnectPrinter.cpp @@ -47,24 +47,7 @@ ConnectPrinterDialog::ConnectPrinterDialog(wxWindow *parent, wxWindowID id, cons sizer_connect->Add(FromDIP(20), 0); m_button_confirm = new Button(this, _L("Confirm")); - m_button_confirm->SetFont(Label::Body_12); - m_button_confirm->SetMinSize(wxSize(-1, FromDIP(24))); - m_button_confirm->SetCornerRadius(FromDIP(12)); - m_button_confirm->SetTextColor(wxColour("#FFFFFE")); - - StateColor btn_bg( - std::pair(wxColour(0, 137, 123), StateColor::Pressed), - std::pair(wxColour(38, 166, 154), StateColor::Hovered), - std::pair(wxColour(0, 150, 136), StateColor::Normal) - ); - - StateColor btn_bd(std::pair(wxColour(0, 150, 136), StateColor::Normal)); - - StateColor btn_text(std::pair(wxColour(255, 255, 255), StateColor::Normal)); - - m_button_confirm->SetBackgroundColor(btn_bg); - m_button_confirm->SetBorderColor(btn_bd); - m_button_confirm->SetTextColor(btn_text); + m_button_confirm->SetStyle(ButtonStyle::Confirm, ButtonType::Choice); sizer_connect->Add(m_button_confirm, 0, wxALL | wxALIGN_CENTER_VERTICAL, 0); @@ -74,7 +57,7 @@ ConnectPrinterDialog::ConnectPrinterDialog(wxWindow *parent, wxWindowID id, cons sizer_top->Add(0, FromDIP(35)); - m_staticText_hints = new wxStaticText(this, wxID_ANY, _L("You can find it in \"Settings > Network > Connection code\"\non the printer, as shown in the figure:"), wxDefaultPosition, wxDefaultSize, 0); + m_staticText_hints = new wxStaticText(this, wxID_ANY, _L("You can find it in \"Settings > Network > Access code\"\non the printer, as shown in the figure:"), wxDefaultPosition, wxDefaultSize, 0); m_staticText_hints->SetFont(Label::Body_15); m_staticText_hints->SetForegroundColour(wxColour(50, 58, 61)); m_staticText_hints->Wrap(-1); @@ -95,9 +78,8 @@ ConnectPrinterDialog::ConnectPrinterDialog(wxWindow *parent, wxWindowID id, cons main_sizer->Add(sizer_top); - this->SetSizer(main_sizer); + this->SetSizerAndFit(main_sizer); this->Layout(); - this->Fit(); CentreOnParent(); m_textCtrl_code->Bind(wxEVT_TEXT, &ConnectPrinterDialog::on_input_enter, this); @@ -118,8 +100,7 @@ void ConnectPrinterDialog::init_bitmap() std::string language = config->get("language"); if (m_obj) { - std::string img_str = DeviceManager::get_printer_diagram_img(m_obj->printer_type); - if(img_str.empty()){img_str = "input_access_code_x1"; } + std::string img_str = "input_access_code_x1"; if (language == "zh_CN") { m_diagram_bmp = create_scaled_bitmap(img_str+"_cn", nullptr, 190); @@ -127,6 +108,19 @@ void ConnectPrinterDialog::init_bitmap() else { m_diagram_bmp = create_scaled_bitmap(img_str+"_en", nullptr, 190); } + + // traverse the guide text + { + // traverse the guide text + if (m_obj->printer_type == "O1D") + { + m_staticText_hints->SetLabel(_L("You can find it in \"Setting > Setting > LAN only > Access Code\"\non the printer, as shown in the figure:")); + } + else + { + m_staticText_hints->SetLabel(_L("You can find it in \"Settings > Network > Access code\"\non the printer, as shown in the figure:")); + } + } } else{ if (language == "zh_CN") { @@ -156,20 +150,19 @@ void ConnectPrinterDialog::on_input_enter(wxCommandEvent& evt) } -void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event) +void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event) { wxString code = m_textCtrl_code->GetTextCtrl()->GetValue(); + if (code.empty()) + code = "88888888"; for (char c : code) { if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) { - show_error(this, _L("Invalid input.")); + show_error(this, _L("Invalid input")); return; } } if (m_obj) { - m_obj->set_user_access_code(code.ToStdString()); - if (m_need_connect) { - wxGetApp().getDeviceManager()->set_selected_machine(m_obj->dev_id); - } + m_obj->set_access_code(code.ToStdString()); } EndModal(wxID_OK); } @@ -181,8 +174,7 @@ void ConnectPrinterDialog::on_dpi_changed(const wxRect &suggested_rect) m_textCtrl_code->GetTextCtrl()->SetSize(wxSize(-1, FromDIP(22))); m_textCtrl_code->GetTextCtrl()->SetMinSize(wxSize(-1, FromDIP(22))); - m_button_confirm->SetCornerRadius(FromDIP(12)); - m_button_confirm->Rescale(); + m_button_confirm->Rescale(); // ORCA No need to set style again Layout(); this->Refresh(); diff --git a/src/slic3r/GUI/ConnectPrinter.hpp b/src/slic3r/GUI/ConnectPrinter.hpp index db45bd11802..95175a1ab03 100644 --- a/src/slic3r/GUI/ConnectPrinter.hpp +++ b/src/slic3r/GUI/ConnectPrinter.hpp @@ -25,7 +25,6 @@ class ConnectPrinterDialog : public DPIDialog { private: protected: - bool m_need_connect{true}; wxStaticText * m_staticText_connection_code; TextInput * m_textCtrl_code; Button * m_button_confirm; @@ -46,12 +45,11 @@ class ConnectPrinterDialog : public DPIDialog ~ConnectPrinterDialog(); - void go_connect_printer(bool need) {m_need_connect = need;}; void end_modal(wxStandardID id); void init_bitmap(); void set_machine_object(MachineObject* obj); void on_input_enter(wxCommandEvent& evt); - void on_button_confirm(wxCommandEvent &event); + void on_button_confirm(wxCommandEvent &event); void on_dpi_changed(const wxRect &suggested_rect) override; }; }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index d4ece5ebbe0..5422eb7b8a9 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -3134,6 +3134,12 @@ int MachineObject::parse_json(std::string payload, bool key_field_only) } if (!key_field_only) { + // Debug: log all support_* keys in MQTT data + for (auto& [key, val] : jj.items()) { + if (key.find("support_") == 0) { + BOOST_LOG_TRIVIAL(warning) << "SUPPORT FIELD: " << key << "=" << val.dump(); + } + } if (!DeviceManager::EnableMultiMachine && !is_support_agora) { if (jj.contains("support_tunnel_mqtt")) { if (jj["support_tunnel_mqtt"].is_boolean()) { @@ -3894,6 +3900,7 @@ int MachineObject::parse_json(std::string payload, bool key_field_only) try { if (jj.contains("ipcam")) { json const & ipcam = jj["ipcam"]; + BOOST_LOG_TRIVIAL(warning) << "CAMERA IPCAM DATA: " << ipcam.dump(); if (ipcam.contains("ipcam_record")) { if (camera_recording_hold_count > 0) camera_recording_hold_count--; @@ -3943,6 +3950,20 @@ int MachineObject::parse_json(std::string payload, bool key_field_only) liveview_local = enum_index_of(ipcam["liveview"].value("local", "none").c_str(), local_protos, 5, LiveviewLocal::LVL_None); char const *remote_protos[] = {"none", "tutk", "agora", "tutk_agaro"}; liveview_remote = enum_index_of(ipcam["liveview"].value("remote", "none").c_str(), remote_protos, 4, LiveviewRemote::LVR_None); + BOOST_LOG_TRIVIAL(warning) << "CAMERA DEBUG: liveview_local=" << liveview_local << " liveview_remote=" << liveview_remote << " is_support_agora=" << is_support_agora; + BOOST_LOG_TRIVIAL(warning) << "CAMERA DEBUG: liveview local_str=" << ipcam["liveview"].value("local", "none") << " remote_str=" << ipcam["liveview"].value("remote", "none"); + } else { + BOOST_LOG_TRIVIAL(warning) << "CAMERA DEBUG: no liveview in ipcam section, trying mode_bits fallback"; + // Fallback for printers that don't send liveview section (e.g. China Bambu) + // mode_bits: bit0=no local, bit1=local, bit2=remote + int mode_bits = ipcam.value("mode_bits", 0); + if (mode_bits & 0x2) liveview_local = LVL_Local; + if (is_support_agora || (mode_bits & 0x4)) liveview_remote = LVR_Agora; + else if (mode_bits & 0x2) liveview_remote = LVR_Agora; + BOOST_LOG_TRIVIAL(warning) << "CAMERA DEBUG: fallback liveview_local=" << liveview_local << " liveview_remote=" << liveview_remote << " mode_bits=" << mode_bits; + // Also set file protocols from mode_bits + if (mode_bits & 0x2) file_local = FL_Local; + if (mode_bits & 0x4 || is_support_agora) file_remote = FR_Agora; } if (ipcam.contains("file")) { char const *local_protos[] = {"none", "local"}; @@ -3950,6 +3971,10 @@ int MachineObject::parse_json(std::string payload, bool key_field_only) char const *remote_protos[] = {"none", "tutk", "agora", "tutk_agaro"}; file_remote = enum_index_of(ipcam["file"].value("remote", "none").c_str(), remote_protos, 4, FileRemote::FR_None); file_model_download = ipcam["file"].value("model_download", "disabled") == "enabled"; + } else { + // Fallback for printers without file section (e.g. China Bambu) + file_local = liveview_local > 0 ? FL_Local : FL_None; + file_remote = liveview_remote > 0 ? FR_Agora : FR_None; } virtual_camera = ipcam.value("virtual_camera", "disabled") == "enabled"; if (ipcam.contains("rtsp_url")) { @@ -5545,7 +5570,7 @@ void MachineObject::parse_new_info(json print) /*fun*/ std::string fun = print["fun"].get(); - BOOST_LOG_TRIVIAL(info) << "new print data fun = " << fun; + BOOST_LOG_TRIVIAL(warning) << "new print data fun = " << fun; if (!fun.empty()) { diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 030eaadfe01..e7e2c2ea617 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1976,12 +1976,7 @@ void GUI_App::init_networking_callbacks() auto sel = this->m_device_manager->get_selected_machine(); - if (sel && sel->dev_id == dev_id) { - obj->parse_json(msg); - } - else { - obj->parse_json(msg, true); - } + obj->parse_json(msg); if (!this->is_enable_multi_machine()) { @@ -2024,20 +2019,12 @@ void GUI_App::init_networking_callbacks() this->process_network_msg(dev_id, msg); MachineObject* obj = m_device_manager->get_my_machine(dev_id); - if (!obj || !obj->is_lan_mode_printer()) { - obj = m_device_manager->get_local_machine(dev_id); - } - if (obj) { - obj->parse_json(msg, DeviceManager::key_field_only); + obj->parse_json(msg); if (this->m_device_manager->get_selected_machine() == obj && obj->is_ams_need_update) { GUI::wxGetApp().sidebar().load_ams_list(obj->dev_id, obj); } } - obj = m_device_manager->get_local_machine(dev_id); - if (obj) { - obj->parse_json(msg, DeviceManager::key_field_only); - } }); }; m_agent->set_on_local_message_fn(lan_message_arrive_fn); diff --git a/src/slic3r/GUI/MediaPlayCtrl.cpp b/src/slic3r/GUI/MediaPlayCtrl.cpp index b1edf8c1f28..2434f03a469 100644 --- a/src/slic3r/GUI/MediaPlayCtrl.cpp +++ b/src/slic3r/GUI/MediaPlayCtrl.cpp @@ -151,6 +151,7 @@ void MediaPlayCtrl::SetMachineObject(MachineObject* obj) m_lan_mode = obj->is_lan_mode_printer(); m_lan_proto = obj->liveview_local; m_remote_proto = obj->get_liveview_remote(); + BOOST_LOG_TRIVIAL(warning) << "CAMERA SetMachine: has_ipcam=" << obj->has_ipcam << " liveview_local=" << m_lan_proto << " liveview_remote=" << m_remote_proto << " is_support_agora=" << obj->is_support_agora << " dev_id=" << machine; m_lan_ip = obj->dev_ip; m_lan_passwd = obj->get_access_code(); m_device_busy = obj->is_camera_busy_off(); @@ -309,6 +310,7 @@ void MediaPlayCtrl::Play() // !m_lan_mode && !m_remote_proto && m_lan_proto == LVL_Disable (*) // !m_lan_mode && !m_remote_proto && m_lan_proto == LVL_None (x) + BOOST_LOG_TRIVIAL(warning) << "CAMERA MEDIA DEBUG: m_lan_proto=" << m_lan_proto << " m_remote_proto=" << m_remote_proto << " m_lan_mode=" << m_lan_mode << " m_camera_exists=" << m_camera_exists << " m_dev_ver=" << m_dev_ver; if (m_lan_proto <= MachineObject::LVL_Disable && (m_lan_mode || !m_remote_proto)) { Stop(m_lan_proto == MachineObject::LVL_None ? _L("A problem occurred. Please update the printer firmware and try again.") diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 63d1f5f92b1..01ae8712535 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9152,7 +9152,16 @@ void Sidebar::show_sync_filament_dialog() { static const std::set white_list_machine_types = { - "Snapmaker U1" + "Snapmaker U1", + "Bambu Lab A1", + "Bambu Lab A1 mini", + "Bambu Lab P1P", + "Bambu Lab P1S", + "Bambu Lab X1", + "Bambu Lab X1 Carbon", + "Bambu Lab X1E", + "N1A", "N1B", "N2S", "N2H", "N2J", "N2K", + "C11", "C12", "C13", "C14" }; std::string machine_type; std::string device_name; @@ -9171,6 +9180,7 @@ void Sidebar::show_sync_filament_dialog() } bool is_white_listed_type = white_list_machine_types.find(machine_type) != white_list_machine_types.end(); + BOOST_LOG_TRIVIAL(warning) << "FILAMENT_SYNC DEBUG: got_machine_info=" << got_machine_info << " machine_type='" << machine_type << "' printer_type='" << (device_machine ? device_machine->printer_type : "null") << "' is_white_listed=" << is_white_listed_type; if (got_machine_info && !machine_type.empty() && !is_white_listed_type) { SyncRichConfirmDialog dlg(this, diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index f33ac56d4a1..d370bc88f20 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -894,19 +894,29 @@ wxWindow *SelectMachineDialog::create_item_checkbox(wxString title, wxWindow *pa void SelectMachineDialog::update_select_layout(MachineObject *obj) { - if (obj && obj->is_support_auto_flow_calibration) { + // Check if this is a Bambu Lab printer by model_id (N2S=A1, N1A=A1 mini, etc.) + static const std::set bambu_model_ids = { + "N1A", "N1B", // A1 mini variants + "N2S", // A1 + "N2H", "N2J", "N2K", // P1P/P1S variants + "C11", "C12", "C13", // X1/X1C/X1E variants + "C14" // X1E + }; + bool is_bambu = obj && bambu_model_ids.count(obj->printer_type) > 0; + + if (obj && (obj->is_support_auto_flow_calibration || is_bambu)) { select_flow->Show(); } else { select_flow->Hide(); } - if (obj && obj->is_support_auto_leveling) { + if (obj && (obj->is_support_auto_leveling || is_bambu)) { select_bed->Show(); } else { select_bed->Hide(); } - if (obj && obj->is_support_timelapse && is_show_timelapse()) { + if (obj && (obj->is_support_timelapse || is_bambu) && is_show_timelapse()) { select_timelapse->Show(); update_timelapse_enable_status(); } else { diff --git a/src/slic3r/Utils/BBLCloudServiceAgent.cpp b/src/slic3r/Utils/BBLCloudServiceAgent.cpp new file mode 100644 index 00000000000..846e4ce5094 --- /dev/null +++ b/src/slic3r/Utils/BBLCloudServiceAgent.cpp @@ -0,0 +1,944 @@ +#include "BBLCloudServiceAgent.hpp" +#include "BBLNetworkPlugin.hpp" + +#include +#include "Http.hpp" +#include "slic3r/GUI/GUI_App.hpp" + +#include +#include +#include +namespace Slic3r { + + +namespace { + +std::string convert_studio_language_to_api(std::string lang_code) +{ + boost::replace_all(lang_code, "_", "-"); + return lang_code; +} + +std::string normalize_homepage_auth_command(std::string payload) +{ + if (payload.empty()) { + return payload; + } + + try { + auto json = nlohmann::json::parse(payload); + if (json.contains("command") && json["command"].is_string()) { + std::string command = json["command"].get(); + if (command == "studio_userlogin") { + json["command"] = "studio_bambu_userlogin"; + } else if (command == "studio_useroffline") { + json["command"] = "studio_bambu_useroffline"; + } + return json.dump(); + } + } catch (...) { + boost::replace_first(payload, "\"command\":\"studio_userlogin\"", "\"command\":\"studio_bambu_userlogin\""); + boost::replace_first(payload, "\"command\":\"studio_useroffline\"", "\"command\":\"studio_bambu_useroffline\""); + } + + return payload; +} + +} // namespace + +std::map BBLCloudServiceAgent::get_extra_header() +{ + std::map extra_headers; + extra_headers.emplace("X-BBL-Client-Type", "slicer"); + + // Unable to get camera live view when the printer is connected to cloud for H2D + extra_headers.emplace("X-BBL-Client-Name", "BambuStudio"); + + extra_headers.emplace("X-BBL-Client-Version", GUI::wxGetApp().get_bbl_client_version()); +#if defined(__WINDOWS__) +#ifdef _M_X64 + extra_headers.emplace("X-BBL-OS-Type", "windows"); +#else + extra_headers.emplace("X-BBL-OS-Type", "windows_arm"); +#endif +#elif defined(__APPLE__) + extra_headers.emplace("X-BBL-OS-Type", "macos"); +#elif defined(__LINUX__) + extra_headers.emplace("X-BBL-OS-Type", "linux"); +#endif + + int major = 0, minor = 0, micro = 0; + wxGetOsVersion(&major, &minor, µ); + + std::ostringstream os_version; + os_version << major << "." << minor << "." << micro; + extra_headers.emplace("X-BBL-OS-Version", os_version.str()); + + auto& app = GUI::wxGetApp(); + if (app.app_config) { + extra_headers.emplace("X-BBL-Device-ID", app.app_config->get("slicer_uuid")); + } + + extra_headers.emplace("X-BBL-Language", + convert_studio_language_to_api(app.current_language_code_safe().ToStdString())); + return extra_headers; +} + +BBLCloudServiceAgent::BBLCloudServiceAgent() = default; + +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() +{ + set_extra_http_header(); + + 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 normalize_homepage_auth_command(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 normalize_homepage_auth_command(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 normalize_homepage_auth_command(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; +} + +// ============================================================================ +// 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, bool force) +{ + 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; +} + +int BBLCloudServiceAgent::get_my_token(std::string ticket, unsigned int* http_code, std::string* http_body) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_my_token(); + if (func && agent) { + return func(agent, ticket, 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() +{ + // Orca: not sure if this required to login into bbl cloud + // Slic3r::Http::set_extra_headers(extra_headers); + + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + + auto func = plugin.get_set_extra_http_header(); + if (func && agent) { + auto extra_headers = get_extra_header(); + return func(agent, extra_headers); + } + return -1; +} + +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(AppOnServerConnectedFn fn) +{ + m_app_on_server_connected_fn = fn; + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_on_server_connected_fn(); + if (func && agent) { + // Register raw callback with DLL, wrap to inject CloudEvent + return func(agent, [this](int return_code, int reason_code) { + if (m_app_on_server_connected_fn) { + m_app_on_server_connected_fn(CloudEvent{BBL_CLOUD_PROVIDER}, return_code, reason_code); + } + }); + } + return -1; +} + +int BBLCloudServiceAgent::set_on_http_error_fn(AppOnHttpErrorFn fn) +{ + m_app_on_http_error_fn = fn; + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_set_on_http_error_fn(); + if (func && agent) { + // Register raw callback with DLL, wrap to inject CloudEvent + return func(agent, [this](unsigned http_code, std::string http_body) { + if (m_app_on_http_error_fn) { + m_app_on_http_error_fn(CloudEvent{BBL_CLOUD_PROVIDER}, http_code, http_body); + } + }); + } + 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 00000000000..9f03f0b8399 --- /dev/null +++ b/src/slic3r/Utils/BBLCloudServiceAgent.hpp @@ -0,0 +1,138 @@ +#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; + + std::string get_id() const override { return BBL_CLOUD_PROVIDER; } + + // ======================================================================== + // 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; + + // ======================================================================== + // 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, bool force = false) 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; + int get_my_token(std::string ticket, 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 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(AppOnServerConnectedFn fn) override; + int set_on_http_error_fn(AppOnHttpErrorFn fn) override; + int set_get_country_code_fn(GetCountryCodeFn fn) override; + int set_queue_on_main_fn(QueueOnMainFn fn) override; + +private: + int set_extra_http_header(); + static std::map get_extra_header(); + bool m_enable_track{false}; + AppOnServerConnectedFn m_app_on_server_connected_fn; + AppOnHttpErrorFn m_app_on_http_error_fn; +}; + +} // 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 00000000000..607e7d16d19 --- /dev/null +++ b/src/slic3r/Utils/BBLNetworkPlugin.cpp @@ -0,0 +1,942 @@ +#include "BBLNetworkPlugin.hpp" +#include "NetworkAgent.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" + +namespace { + +// Named in the load log: the bound generation is what ties a crash report to an ABI choice. +// The label is the whitelist row's series, so it can never drift from the dispatch table. +const char* network_abi_name(NetworkAbi abi) +{ + for (size_t i = 0; i < AVAILABLE_NETWORK_VERSIONS_COUNT; ++i) + if (AVAILABLE_NETWORK_VERSIONS[i].abi == abi) + return AVAILABLE_NETWORK_VERSIONS[i].version; + return "unsupported"; +} + +} // namespace + +// ============================================================================ +// 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() +{ + unload(); // unload() destroys the agent first (see the note there) +} + +// ============================================================================ +// 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, copy it to versioned format + if (is_legacy_version(version)) { + 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)) { + try { + boost::filesystem::copy(legacy_path, versioned_path); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to copy legacy library: " << e.what(); + } + } + } + + // Load versioned library. In the normal plugins folder a bare series (02.08.01) resolves to + // whatever same-series build is actually on disk (see resolve_library_path); the backup + // folder keeps the exact versioned name. +#if defined(_MSC_VER) || defined(_WIN32) + std::string versioned_name = std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".dll"; +#elif defined(__WXMAC__) + std::string versioned_name = std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".dylib"; +#else + std::string versioned_name = std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + "_" + version + ".so"; +#endif + library = using_backup ? (plugin_folder / versioned_name).string() + : resolve_library_path(version); + +#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) { + std::string library_path = get_libpath_in_current_directory(std::string(BAMBU_NETWORK_LIBRARY)); + if (library_path.empty()) { + set_load_error( + "Network library not found", + "Could not locate versioned library: " + library, + library + ); + return -1; + } + 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 + ); + } +#endif + + if (!m_networking_module) { + if (!m_load_error.has_error) { + set_load_error( + "Network library failed to load", + "LoadLibrary/dlopen returned null", + library + ); + } + return -1; + } + + // Load file transfer interface + InitFTModule(m_networking_module); + + // Load all function pointers + load_all_function_pointers(); + + // Key the generation on the library that actually loaded, not the version asked for: + // resolve_library_path() serves any same-series build. m_get_version is read directly, not + // via get_version(), which would substitute the "00.00.00.00" sentinel and pick no generation. + const std::string loaded_version = m_get_version ? m_get_version() : std::string(); + m_network_abi = network_plugin_abi(loaded_version.empty() ? version : loaded_version); + + // A library reporting a series this build has no ABI for stays loaded but uncallable - + // check_networking_version() then reports it as incompatible and offers the update flow. + if (m_network_abi == NetworkAbi::Unsupported) { + BOOST_LOG_TRIVIAL(warning) << "BBLNetworkPlugin::initialize: no ABI for version " + << (loaded_version.empty() ? version : loaded_version) << ", plug-in calls are disabled"; + } + + BOOST_LOG_TRIVIAL(info) << "BBLNetworkPlugin::initialize: abi=" << network_abi_name(m_network_abi) + << ", library=" << library + << ", version=" << (loaded_version.empty() ? "unknown" : loaded_version) + << ", send_message=" << (m_send_message ? "loaded" : "null") + << ", start_print=" << (m_start_print ? "loaded" : "null") + << ", start_local_print=" << (m_start_local_print ? "loaded" : "null") + << ", get_my_token=" << (m_get_my_token ? "loaded" : "null"); + + return 0; +} + +int BBLNetworkPlugin::unload() +{ + // Orca: destroy the plugin agent while its creating DLL is still loaded, so the void* handle + // never dangles into freed memory. A stale m_agent surviving the unload makes create_agent() + // short-circuit on has_agent() after a hot reload, and the next call into the freshly loaded + // DLL dereferences the old-DLL handle -> access violation. + destroy_agent(); + + 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(); + + // Safe to reset only because every pointer was nulled just above and every dispatcher is + // guarded on a non-null pointer, so no stale generation is reachable. + m_network_abi = NetworkAbi::Unsupported; + + 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(info) << __FUNCTION__ << ": reusing existing agent " << m_agent; + return m_agent; + } + + if (m_create_agent) { + m_agent = m_create_agent(log_dir); + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": created agent " << m_agent; + return m_agent; +} + +int BBLNetworkPlugin::destroy_agent() +{ + int ret = 0; + if (m_agent && m_destroy_agent) { + ret = m_destroy_agent(m_agent); + } + 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) { + std::string library_path = get_libpath_in_current_directory(std::string(BAMBU_SOURCE_LIBRARY)); + if (library_path.empty()) { + return m_source_module; + } + 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 + + 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) { + 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 +} + +std::string BBLNetworkPlugin::resolve_library_path(const std::string& version) +{ + std::string exact = get_versioned_library_path(version); + if (boost::filesystem::exists(exact)) + return exact; + + // A bare series (02.08.01) is physically present only as a specific build (02.08.01.53) when + // the startup file-rename was skipped or failed. Resolve to the newest same-series build + // actually on disk. Custom and legacy names are exact and never resolved. + if (is_series_managed_version(version)) { + const std::string series = network_plugin_series(version); + std::string best; + for (const auto& v : scan_plugin_versions()) + if (is_series_managed_version(v) && network_plugin_series(v) == series && (best.empty() || v > best)) + best = v; + if (!best.empty()) + return get_versioned_library_path(best); + } + + return exact; // nonexistent -> caller downloads +} + +bool BBLNetworkPlugin::versioned_library_exists(const std::string& version) +{ + if (version.empty()) return false; + + if (boost::filesystem::exists(resolve_library_path(version))) return true; + + if (is_legacy_version(version)) { + 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::system::error_code ec; + boost::filesystem::remove(legacy_path, ec); + } +} + +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; +} + +// ============================================================================ +// ABI Conversion Helpers +// ============================================================================ + +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; +} + +// Every PrintParams field except the four the 02.08.01 series added +// (task_timelapse_use_internal, extruder_cali_manual_mode, svc_context, slicer_uid). +PrintParams_0203 BBLNetworkPlugin::as_0203(PrintParams& param) +{ + PrintParams_0203 p; + + p.dev_id = std::move(param.dev_id); + p.task_name = std::move(param.task_name); + p.project_name = std::move(param.project_name); + p.preset_name = std::move(param.preset_name); + p.filename = std::move(param.filename); + p.config_filename = std::move(param.config_filename); + p.plate_index = param.plate_index; + p.ftp_folder = std::move(param.ftp_folder); + p.ftp_file = std::move(param.ftp_file); + p.ftp_file_md5 = std::move(param.ftp_file_md5); + p.nozzle_mapping = std::move(param.nozzle_mapping); + p.ams_mapping = std::move(param.ams_mapping); + p.ams_mapping2 = std::move(param.ams_mapping2); + p.ams_mapping_info = std::move(param.ams_mapping_info); + p.nozzles_info = std::move(param.nozzles_info); + p.connection_type = std::move(param.connection_type); + p.comments = std::move(param.comments); + p.origin_profile_id = param.origin_profile_id; + p.stl_design_id = param.stl_design_id; + p.origin_model_id = std::move(param.origin_model_id); + p.print_type = std::move(param.print_type); + p.dst_file = std::move(param.dst_file); + p.dev_name = std::move(param.dev_name); + p.dev_ip = std::move(param.dev_ip); + p.use_ssl_for_ftp = param.use_ssl_for_ftp; + p.use_ssl_for_mqtt = param.use_ssl_for_mqtt; + p.username = std::move(param.username); + p.password = std::move(param.password); + p.task_bed_leveling = param.task_bed_leveling; + p.task_flow_cali = param.task_flow_cali; + p.task_vibration_cali = param.task_vibration_cali; + p.task_layer_inspect = param.task_layer_inspect; + p.task_record_timelapse = param.task_record_timelapse; + p.task_use_ams = param.task_use_ams; + p.task_bed_type = std::move(param.task_bed_type); + p.extra_options = std::move(param.extra_options); + p.auto_bed_leveling = param.auto_bed_leveling; + p.auto_flow_cali = param.auto_flow_cali; + p.auto_offset_cali = param.auto_offset_cali; + p.task_ext_change_assist = param.task_ext_change_assist; + p.try_emmc_print = param.try_emmc_print; + + return p; +} + +// ============================================================================ +// 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_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_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_get_my_token = reinterpret_cast(get_function("bambu_network_get_my_token")); + 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")); + + // Bound late; anything a generation does not export resolves to null so callers no-op. + // See the typedefs for which generation introduced each of these. + m_set_on_user_login_fn = reinterpret_cast(get_function("bambu_network_set_on_user_login_fn")); + m_get_studio_info_url = reinterpret_cast(get_function("bambu_network_get_studio_info_url")); + m_report_consent = reinterpret_cast(get_function("bambu_network_report_consent")); + m_get_camera_url_for_golive = reinterpret_cast(get_function("bambu_network_get_camera_url_for_golive")); + m_get_hms_snapshot = reinterpret_cast(get_function("bambu_network_get_hms_snapshot")); + m_get_filament_spools = reinterpret_cast(get_function("bambu_network_get_filament_spools")); + m_create_filament_spool = reinterpret_cast(get_function("bambu_network_create_filament_spool")); + m_update_filament_spool = reinterpret_cast(get_function("bambu_network_update_filament_spool")); + m_delete_filament_spools = reinterpret_cast(get_function("bambu_network_delete_filament_spools")); + m_get_filament_config = reinterpret_cast(get_function("bambu_network_get_filament_config")); + m_sync_ams_filaments = reinterpret_cast(get_function("bambu_network_sync_ams_filaments")); +} + +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_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_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_get_my_token = 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; + + m_set_on_user_login_fn = nullptr; + m_get_studio_info_url = nullptr; + m_report_consent = nullptr; + m_get_camera_url_for_golive = nullptr; + m_get_hms_snapshot = nullptr; + m_get_filament_spools = nullptr; + m_create_filament_spool = nullptr; + m_update_filament_spool = nullptr; + m_delete_filament_spools = nullptr; + m_get_filament_config = nullptr; + m_sync_ams_filaments = nullptr; +} + +std::vector get_all_available_versions() +{ + // get_version() reports the "00.00.00.00" sentinel when nothing is loaded; resolve + // that here so the list builder only ever sees a real version or an empty string. + const BBLNetworkPlugin& plugin = BBLNetworkPlugin::instance(); + return get_all_available_versions(plugin.is_loaded() ? plugin.get_version() : std::string()); +} + +std::vector get_all_available_versions(const std::string& loaded_version) +{ + std::vector result; + 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])); + all_known_versions.insert(AVAILABLE_NETWORK_VERSIONS[i].version); + } + + std::vector discovered = BBLNetworkPlugin::scan_plugin_versions(); + + // A managed build (pure dotted-numeric AA.BB.CC[.DD]) is represented by its series entry + // above - the OTA-installed 02.08.01.53 and a bare 02.08.01 both collapse into the single + // 02.08.01 row. Only a custom-named build a user dropped in (02.08.01_custom, ..-dev) earns + // its own row, and only when its series is one this build can actually load. The part past + // the series is stored as the "suffix" so the entry sorts and renders nested under it. + for (const auto& version : discovered) { + if (all_known_versions.count(version) > 0) + continue; + if (is_series_managed_version(version)) + continue; + if (!is_supported_network_version(version)) + continue; + const std::string series = network_plugin_series(version); + const std::string sfx = version.size() > series.size() ? version.substr(series.size()) : version; + result.push_back(NetworkLibraryVersionInfo::from_discovered(version, series, sfx)); + all_known_versions.insert(version); + } + + // Newest first. Version components are fixed-width and zero-padded, so a plain + // string compare orders them numerically, and the legacy series sorts last on its + // own. Suffixed dev builds sort directly under the base version they build on. + std::sort(result.begin(), result.end(), + [](const NetworkLibraryVersionInfo& a, const NetworkLibraryVersionInfo& b) { + if (a.base_version != b.base_version) return a.base_version > b.base_version; + return a.suffix < b.suffix; + }); + + const std::string loaded_series = network_plugin_series(loaded_version); + for (auto& info : result) { + // A managed (series) entry matches when a managed build of the same series is loaded - + // the loaded plug-in reports its full build (02.08.01.53) but the row is the series. + // Custom and legacy entries match their exact reported version. + info.is_loaded = !loaded_version.empty() && + (info.version == loaded_version || + (is_series_managed_version(info.version) && is_series_managed_version(loaded_version) && + network_plugin_series(info.version) == loaded_series)); + info.is_latest = false; + } + + // "(Latest)" goes on the highest full version in the list, which after the sort is + // simply the first entry without a dev suffix - an OTA-installed build can be newer + // than the newest whitelisted entry. get_latest_network_version() intentionally + // keeps returning the static whitelist default, which drives the download and + // update-check decisions. + auto latest = std::find_if(result.begin(), result.end(), + [](const NetworkLibraryVersionInfo& info) { return info.suffix.empty(); }); + if (latest != result.end()) + latest->is_latest = true; + + return result; +} + + +} // namespace Slic3r diff --git a/src/slic3r/Utils/BBLNetworkPlugin.hpp b/src/slic3r/Utils/BBLNetworkPlugin.hpp new file mode 100644 index 00000000000..3788568e371 --- /dev/null +++ b/src/slic3r/Utils/BBLNetworkPlugin.hpp @@ -0,0 +1,569 @@ +#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_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 dev_model, 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 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_get_my_token)(void *agent, std::string ticket, 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 the 01.10.01 DLL) +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); + +// 02.03.00 function pointer types. Only PrintParams differs from the current ABI; send_message +// and send_message_to_printer already take the flag argument in this series. +typedef int (*func_start_print_0203)(void *agent, PrintParams_0203 params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); +typedef int (*func_start_local_print_with_record_0203)(void *agent, PrintParams_0203 params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); +typedef int (*func_start_send_gcode_to_sdcard_0203)(void *agent, PrintParams_0203 params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn); +typedef int (*func_start_local_print_0203)(void *agent, PrintParams_0203 params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); +typedef int (*func_start_sdcard_print_0203)(void* agent, PrintParams_0203 params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); + +// bind() gained dev_model in 02.08.01; the legacy and 02.03.00 series share the older form. +typedef int (*func_bind_pre0208)(void *agent, std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn); + +// Exported by every supported generation, but only bound here. +typedef int (*func_set_on_user_login_fn)(void *agent, OnUserLoginFn fn); +typedef std::string (*func_get_studio_info_url)(void *agent); + +// Present since 02.03.00, null on the legacy plugin. +typedef int (*func_report_consent)(void *agent, std::string expand); + +// Added by the 02.08.01.52 plugin ABI (null on older plugins). +typedef int (*func_get_camera_url_for_golive)(void *agent, std::string dev_id, std::string sdev_id, std::function callback); +typedef int (*func_get_hms_snapshot)(void *agent, std::string& dev_id, std::string& file_name, std::function callback); +typedef int (*func_get_filament_spools)(void *agent, FilamentQueryParams params, std::string* http_body); +typedef int (*func_create_filament_spool)(void *agent, std::string request_body, std::string* http_body); +typedef int (*func_update_filament_spool)(void *agent, std::string spool_id, std::string request_body, std::string* http_body); +typedef int (*func_delete_filament_spools)(void *agent, FilamentDeleteParams params, std::string* http_body); +typedef int (*func_get_filament_config)(void *agent, std::string* http_body); +typedef int (*func_sync_ams_filaments)(void *agent, AmsSyncParams params, std::string* http_body); + +/** + * 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); + // Path to load for a stored version: the exact file if present, else - for a bare series + // whose file the startup rename did not produce - the newest same-series build on disk. + static std::string resolve_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); + + // ======================================================================== + // Plug-in ABI Generation + // ======================================================================== + + static bool is_legacy_version(const std::string& version) { return version == BAMBU_NETWORK_AGENT_VERSION_LEGACY; } + // The generation the loaded library speaks - what every call must dispatch on. + NetworkAbi network_abi() const { return m_network_abi; } + bool use_legacy_network() const { return m_network_abi == NetworkAbi::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_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_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_get_my_token get_get_my_token() const { return m_get_my_token; } + 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; } + func_set_on_user_login_fn get_set_on_user_login_fn() const { return m_set_on_user_login_fn; } + func_get_studio_info_url get_get_studio_info_url() const { return m_get_studio_info_url; } + func_report_consent get_report_consent() const { return m_report_consent; } + func_get_camera_url_for_golive get_get_camera_url_for_golive() const { return m_get_camera_url_for_golive; } + func_get_hms_snapshot get_get_hms_snapshot() const { return m_get_hms_snapshot; } + func_get_filament_spools get_get_filament_spools() const { return m_get_filament_spools; } + func_create_filament_spool get_create_filament_spool() const { return m_create_filament_spool; } + func_update_filament_spool get_update_filament_spool() const { return m_update_filament_spool; } + func_delete_filament_spools get_delete_filament_spools() const { return m_delete_filament_spools; } + func_get_filament_config get_get_filament_config() const { return m_get_filament_config; } + func_sync_ams_filaments get_sync_ams_filaments() const { return m_sync_ams_filaments; } + + // ======================================================================== + // ABI Conversion Helpers + // ======================================================================== + + // Both move out of `param`, so convert only inside the branch that will actually run. + static PrintParams_Legacy as_legacy(PrintParams& param); + static PrintParams_0203 as_0203(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; + + // ABI generation of the currently loaded library + NetworkAbi m_network_abi{NetworkAbi::Unsupported}; + + // 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_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_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_get_my_token m_get_my_token{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}; + func_set_on_user_login_fn m_set_on_user_login_fn{nullptr}; + func_get_studio_info_url m_get_studio_info_url{nullptr}; + func_report_consent m_report_consent{nullptr}; + func_get_camera_url_for_golive m_get_camera_url_for_golive{nullptr}; + func_get_hms_snapshot m_get_hms_snapshot{nullptr}; + func_get_filament_spools m_get_filament_spools{nullptr}; + func_create_filament_spool m_create_filament_spool{nullptr}; + func_update_filament_spool m_update_filament_spool{nullptr}; + func_delete_filament_spools m_delete_filament_spools{nullptr}; + func_get_filament_config m_get_filament_config{nullptr}; + func_sync_ams_filaments m_sync_ams_filaments{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 00000000000..ef85e0a1ffe --- /dev/null +++ b/src/slic3r/Utils/BBLPrinterAgent.cpp @@ -0,0 +1,470 @@ +#include "BBLPrinterAgent.hpp" +#include "BBLNetworkPlugin.hpp" +#include "NetworkAgentFactory.hpp" + +#include + +namespace Slic3r { + +BBLPrinterAgent::BBLPrinterAgent() = default; + +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) { + // Only the legacy plug-in lacks `flag`; 02.03.00 already takes it, and routing that + // series through the legacy form would silently drop MessageFlag sign/encrypt. + switch (plugin.network_abi()) { + case NetworkAbi::Legacy: { + auto legacy_func = reinterpret_cast(func); + return legacy_func(agent, std::move(dev_id), std::move(json_str), qos); + } + case NetworkAbi::V0203: + case NetworkAbi::Current: + return func(agent, std::move(dev_id), std::move(json_str), qos, flag); + default: + return -1; + } + } + 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) { + switch (plugin.network_abi()) { + case NetworkAbi::Legacy: { + auto legacy_func = reinterpret_cast(func); + return legacy_func(agent, std::move(dev_id), std::move(json_str), qos); + } + case NetworkAbi::V0203: + case NetworkAbi::Current: + return func(agent, std::move(dev_id), std::move(json_str), qos, flag); + default: + return -1; + } + } + 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 dev_model, 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) { + // dev_model was added in 02.08.01. Passing it to a plug-in that takes the 7-argument + // form shifts every following argument, so the older generations get the older call. + switch (plugin.network_abi()) { + case NetworkAbi::Legacy: + case NetworkAbi::V0203: { + auto older_func = reinterpret_cast(func); + return older_func(agent, dev_ip, dev_id, sec_link, timezone, improved, update_fn); + } + case NetworkAbi::Current: + return func(agent, dev_ip, dev_id, dev_model, sec_link, timezone, improved, update_fn); + default: + return -1; + } + } + 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::get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_hms_snapshot(); + // dev_id/file_name are passed as lvalues to bind the plugin's std::string& params. + // A null func (older plugin without this symbol) falls through to -1 so callers degrade gracefully. + if (func && agent) { + return func(agent, dev_id, file_name, callback); + } + 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; +} + +// ============================================================================ +// Subscriptions +// ============================================================================ + +int BBLPrinterAgent::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 BBLPrinterAgent::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 BBLPrinterAgent::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 BBLPrinterAgent::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; +} + +// ============================================================================ +// Agent Information +// ============================================================================ +AgentInfo BBLPrinterAgent::get_agent_info_static() +{ + return AgentInfo{BBL_PRINTER_AGENT_ID, "Bambu Lab", "", "Bambu Lab printer agent"}; +} + +// ============================================================================ +// Print Job Operations +// ============================================================================ + +namespace { + +// Shared dispatcher for the start_* operations, whose params layout differs per generation. +// The per-generation typedefs are template arguments so a swapped pair fails to compile +// (each arm's converted params must match the casted signature). Each arm converts and calls +// in one step: as_legacy()/as_0203() move out of `params` and their prvalue result lands in +// the by-value ABI argument without another copy; the Current arm moves `params` outright. +template +int dispatch_start(CurrentFn func, PrintParams& params, const CallbackFns&... callbacks) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + if (!func || !agent) + return -1; + switch (plugin.network_abi()) { + case NetworkAbi::Legacy: + return reinterpret_cast(func)(agent, BBLNetworkPlugin::as_legacy(params), callbacks...); + case NetworkAbi::V0203: + return reinterpret_cast(func)(agent, BBLNetworkPlugin::as_0203(params), callbacks...); + case NetworkAbi::Current: + return func(agent, std::move(params), callbacks...); + default: + return -1; + } +} + +} // namespace + +int BBLPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +{ + return dispatch_start( + BBLNetworkPlugin::instance().get_start_print(), params, update_fn, cancel_fn, wait_fn); +} + +int BBLPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +{ + return dispatch_start( + BBLNetworkPlugin::instance().get_start_local_print_with_record(), params, update_fn, cancel_fn, wait_fn); +} + +int BBLPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +{ + return dispatch_start( + BBLNetworkPlugin::instance().get_start_send_gcode_to_sdcard(), params, update_fn, cancel_fn, wait_fn); +} + +int BBLPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) +{ + return dispatch_start( + BBLNetworkPlugin::instance().get_start_local_print(), params, update_fn, cancel_fn); +} + +int BBLPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) +{ + return dispatch_start( + BBLNetworkPlugin::instance().get_start_sdcard_print(), params, update_fn, cancel_fn); +} + +// ============================================================================ +// 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 00000000000..a8880bf6bf9 --- /dev/null +++ b/src/slic3r/Utils/BBLPrinterAgent.hpp @@ -0,0 +1,93 @@ +#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 dev_model, 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 get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) 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; + + // Subscriptions + 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; + + /** + * 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/CrealityPrintAgent.cpp b/src/slic3r/Utils/CrealityPrintAgent.cpp new file mode 100644 index 00000000000..55d99ff07b1 --- /dev/null +++ b/src/slic3r/Utils/CrealityPrintAgent.cpp @@ -0,0 +1,334 @@ +#include "CrealityPrintAgent.hpp" +#include "CrealityPrint.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "slic3r/GUI/GUI_App.hpp" + +#include +#include + +#include +#include + +namespace Slic3r { + +namespace { + +constexpr const char* CrealityPrintAgent_VERSION = "0.1.0"; + +bool has_visible_base_preset(const PresetCollection& filaments, const std::string& filament_id) +{ + for (const auto& p : filaments.get_presets()) { + if (p.is_visible && p.is_compatible + && filaments.get_preset_base(p) == &p + && p.filament_id == filament_id) + return true; + } + return false; +} + +} // namespace + +// Score visible compatible filament presets against the CFS spool metadata and +// return the best-matching filament_id. Scoring: +// +20 preset name contains brand_name as a substring +// (e.g. "Hyper PLA" in "Hyper PLA @Creality K2 0.4 nozzle") +// +10 preset name contains the vendor substring (e.g. "Creality") +// Tiebreak: prefer the SYSTEM (shipped) preset over user copies. Brand- +// specific system presets carry their own filament_id; user copies of +// generic presets inherit a generic filament_id from their parent, so +// preferring the user copy can collapse a brand-specific match back to +// "Generic PLA" via the inherited id. Plus: this code targets upstream +// OrcaSlicer where shipping the user's local tuning would be wrong. +// Requires the preset's declared filament_type to equal the spool's base type +// (PLA/PETG/ABS/...) so we never auto-pick a PETG preset for a PLA spool. +// Falls back to filaments.filament_id_by_type(base_type) when nothing scores. +std::string CrealityPrintAgent::match_filament_preset(const PresetCollection& filaments, + const std::string& vendor, + const std::string& brand_name, + const std::string& base_type) +{ + auto to_lower = [](std::string s) { + for (auto& c : s) c = static_cast(std::tolower(static_cast(c))); + return s; + }; + + const std::string vendor_lower = to_lower(vendor); + const std::string brand_lower = to_lower(brand_name); + const std::string type_lower = to_lower(base_type); + + struct Match { + const Preset* preset; + int score; + bool is_user; + }; + std::vector matches; + + int considered = 0; + for (const auto& p : filaments.get_presets()) { + if (!p.is_visible || !p.is_compatible) continue; + // Note: we deliberately do NOT filter on get_preset_base(p) == &p. + // K2 owners frequently keep tweaked copies of system presets + // (e.g. "Creality Hyper PLA @K2 (Harky)" with their per-spool PA), + // which are derived presets — filtering to bases-only would skip + // exactly the presets users care about most. + ++considered; + + std::string preset_type; + if (const auto* ft = p.config.option("filament_type")) + if (!ft->values.empty()) preset_type = ft->values.front(); + if (to_lower(preset_type) != type_lower) continue; + + const std::string name_lower = to_lower(p.name); + int score = 0; + if (!brand_lower.empty() && name_lower.find(brand_lower) != std::string::npos) + score += 20; + if (!vendor_lower.empty() && name_lower.find(vendor_lower) != std::string::npos) + score += 10; + + if (score > 0) + matches.push_back({&p, score, !p.is_system && !p.is_default}); + } + + if (matches.empty()) { + const std::string fallback = filaments.filament_id_by_type(base_type); + const bool fallback_ok = has_visible_base_preset(filaments, fallback); + BOOST_LOG_TRIVIAL(info) + << "CrealityPrintAgent: no preset scored for spool {" << vendor << " " + << brand_name << " (" << base_type << ")} after considering " << considered + << " presets; falling back to generic preset id \"" << fallback << "\"" + << (fallback_ok ? "" : " (NOT visible — returning empty)"); + return fallback_ok ? fallback : std::string(); + } + + std::sort(matches.begin(), matches.end(), + [](const Match& a, const Match& b) { + if (a.score != b.score) return a.score > b.score; + if (a.is_user != b.is_user) return !a.is_user; // prefer system over user + return false; + }); + + BOOST_LOG_TRIVIAL(info) + << "CrealityPrintAgent: matched spool {" << vendor << " " << brand_name + << " (" << base_type << ")} -> preset \"" << matches.front().preset->name + << "\" (score=" << matches.front().score + << ", " << matches.size() << " candidate(s) of " << considered << " considered)"; + + return matches.front().preset->filament_id; +} + +CrealityPrintAgent::CrealityPrintAgent(std::string log_dir) + : MoonrakerPrinterAgent(std::move(log_dir)) +{ +} + +AgentInfo CrealityPrintAgent::get_agent_info_static() +{ + return AgentInfo{ + "crealityprint", + "CrealityPrint", + CrealityPrintAgent_VERSION, + "Creality K-series printer agent (CFS-aware filament sync)" + }; +} + +std::string CrealityPrintAgent::normalize_filament_type(const std::string& filament_type) +{ + static const std::vector bases = { + "PETG", "PET", "PLA", "ABS", "ASA", "TPU", "PC", "PA", "PVA", "HIPS" + }; + for (const auto& base : bases) { + if (filament_type.rfind(base, 0) == 0) return base; + } + return filament_type; +} + +// Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info(). +// Schema (verified 2026-05-06 against K2 Combo F021 firmware v1.1.260206): +// { "boxsInfo": { "materialBoxs": [ +// { "id": int, "state": int, "type": int, // type 0 = CFS, 1 = single-spool external +// "materials": [ +// { "id": int, "state": int, // state 1 = loaded +// "vendor": str, "type": str, "name": str, +// "color": "#0RRGGBB" }, ... +// ]}, ... +// ]}} +bool CrealityPrintAgent::parse_cfs_response(const std::string& response, + std::vector& slots, + int& box_count, + std::string& error) +{ + using nlohmann::json; + + slots.clear(); + box_count = 0; + + if (response.empty()) { + error = "empty response"; + return false; + } + + json resp; + try { + resp = json::parse(response); + } catch (const std::exception& e) { + error = std::string("JSON parse error: ") + e.what(); + return false; + } + + if (!resp.contains("boxsInfo") || !resp["boxsInfo"].contains("materialBoxs")) { + error = "invalid schema (missing boxsInfo.materialBoxs)"; + return false; + } + + // Sequential AMS-style index for accepted CFS boxes. The K2's raw box.id has + // gaps (id 0 is the external spool holder, type=1, skipped) — using the raw id + // would publish phantom slots for the gap. Renumber accepted boxes 0,1,2,... + int cfs_count = 0; + for (const auto& box : resp["boxsInfo"]["materialBoxs"]) { + const int box_st = box.value("state", 0); + const int box_type = box.value("type", 0); + if (box_st != 1) continue; // inactive boxes + if (box_type != 0) continue; // non-CFS (external spool holder, handled separately by upload dialog) + + const int cfs_index = cfs_count++; + + if (!box.contains("materials") || !box["materials"].is_array()) + continue; + + for (const auto& mat : box["materials"]) { + // CFS slot state encoding observed across K2 family firmwares: + // * K2 (base) / K2 Pro : 0 = empty, 1 = loaded. + // * K2 Plus (1.1.5.5/CFS 1.4.2 onwards): 0 = empty, + // 1 = loaded AND currently + // selected as the active + // spool for printing, + // 2 = loaded but not selected. + // We treat anything non-zero as loaded. Belt-and-braces: also skip + // entries that look blank (no vendor and no type) regardless of state. + const int s_state = mat.value("state", 0); + const std::string s_vendor = mat.value("vendor", std::string()); + const std::string s_type = mat.value("type", std::string()); + if (s_state == 0) continue; // explicitly empty + if (s_vendor.empty() && s_type.empty()) continue; // blank entry — likely empty under a different state encoding + + CFSSlot s; + s.box_id = cfs_index; + s.slot_id = mat.value("id", 0); + s.vendor = s_vendor; + s.brand_name = mat.value("name", ""); + s.filament_type = s_type; + s.color_hex = mat.value("color", "#FFFFFF"); + + // Creality reports colour as "#0RRGGBB" (8 chars with a leading zero + // after '#'). Normalise to standard "#RRGGBB". + if (s.color_hex.size() == 8 && s.color_hex[0] == '#') + s.color_hex = "#" + s.color_hex.substr(2); + + slots.push_back(std::move(s)); + } + } + + box_count = cfs_count; + return true; +} + +bool CrealityPrintAgent::fetch_filament_info(std::string dev_id) +{ + if (device_info.dev_ip.empty()) { + BOOST_LOG_TRIVIAL(warning) + << "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent"; + return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id)); + } + + // Build a CrealityPrint helper so we can use its model detection + WS helpers + // (added in upstream PR #13291). + DynamicPrintConfig cfg; + cfg.set_key_value("print_host", new ConfigOptionString("http://" + device_info.dev_ip)); + cfg.set_key_value("print_host_webui", new ConfigOptionString("")); + cfg.set_key_value("printhost_cafile", new ConfigOptionString("")); + cfg.set_key_value("printhost_port", new ConfigOptionString("")); + cfg.set_key_value("printhost_apikey", new ConfigOptionString(device_info.api_key)); + cfg.set_key_value("printhost_ssl_ignore_revoke", new ConfigOptionBool(false)); + + CrealityPrint host(&cfg); + + // Defer to base if this isn't a K-series board with CFS firmware support. + if (!host.supports_multi_color_print()) { + BOOST_LOG_TRIVIAL(info) + << "CrealityPrintAgent: " << host.model_name() + << " is not CFS-capable, deferring to base Moonraker agent"; + return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id)); + } + + BOOST_LOG_TRIVIAL(info) + << "CrealityPrintAgent: querying CFS slots on " << host.model_name(); + + const std::string response = host.query_boxes_info(); + + std::vector slots; + int box_count = 0; + std::string parse_err; + if (!parse_cfs_response(response, slots, box_count, parse_err)) { + BOOST_LOG_TRIVIAL(warning) + << "CrealityPrintAgent: CFS query failed (" << parse_err << "), " + << "falling back to base agent"; + return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id)); + } + + if (box_count == 0) { + // No active CFS boxes attached — printer is in direct-spool mode. Let the + // base agent take over so the user still gets whatever filament info + // Moonraker exposes. + BOOST_LOG_TRIVIAL(info) + << "CrealityPrintAgent: no active CFS boxes, deferring to base agent"; + return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id)); + } + + BOOST_LOG_TRIVIAL(info) + << "CrealityPrintAgent: " << box_count << " CFS box(es), " + << slots.size() << " loaded slot(s)"; + + // Index loaded slots by (box, slot) for O(1) lookup as we walk the full + // box_count * 4 grid, emitting an AmsTrayData entry for each physical slot. + std::map, const CFSSlot*> by_position; + for (const auto& s : slots) + by_position[{s.box_id, s.slot_id}] = &s; + + auto* bundle = GUI::wxGetApp().preset_bundle; + + const int max_slots = box_count * 4; + std::vector trays; + trays.reserve(max_slots); + + for (int box = 0; box < box_count; ++box) { + for (int idx = 0; idx < 4; ++idx) { + AmsTrayData tray; + tray.slot_index = box * 4 + idx; + + auto it = by_position.find({box, idx}); + if (it == by_position.end()) { + tray.has_filament = false; + trays.push_back(std::move(tray)); + continue; + } + + const CFSSlot& s = *it->second; + tray.has_filament = true; + tray.tray_type = normalize_filament_type(s.filament_type); + tray.tray_color = s.color_hex; + + if (bundle) { + tray.tray_info_idx = match_filament_preset( + bundle->filaments, s.vendor, s.brand_name, tray.tray_type); + } + + trays.push_back(std::move(tray)); + } + } + + build_ams_payload(box_count, max_slots - 1, trays); + return true; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/CrealityPrintAgent.hpp b/src/slic3r/Utils/CrealityPrintAgent.hpp new file mode 100644 index 00000000000..e1096db5362 --- /dev/null +++ b/src/slic3r/Utils/CrealityPrintAgent.hpp @@ -0,0 +1,67 @@ +#ifndef __CREALITY_PRINT_AGENT_HPP__ +#define __CREALITY_PRINT_AGENT_HPP__ + +#include "MoonrakerPrinterAgent.hpp" + +#include +#include + +namespace Slic3r { + +class PresetCollection; + +// Filament sync for Creality K-series printers with CFS. +// +// Inherits MoonrakerPrinterAgent for all communication / certificates / discovery / +// binding / print-job operations. Overrides fetch_filament_info() to query the +// K-series CFS over its port-9999 WebSocket, convert each loaded slot to an +// AmsTrayData entry, and publish via the base-class build_ams_payload() — the +// same shape used by QidiPrinterAgent and SnapmakerPrinterAgent. +// +// Model detection delegated to CrealityPrint::supports_multi_color_print() (PR #13291). +// For non-CFS K-series boards or when the WS query fails, falls back to the base +// MoonrakerPrinterAgent behaviour. + +class CrealityPrintAgent final : public MoonrakerPrinterAgent +{ +public: + struct CFSSlot + { + int box_id = 0; // CFS unit index (0 for first box, 1 for chained second box) + int slot_id = 0; // Slot index within the box (0..3) + std::string color_hex; // "#RRGGBB" + std::string filament_type; // "PLA", "ABS", "PETG", ... + std::string brand_name; // "Hyper PLA", ... + std::string vendor; // "Creality", "eSUN", or "" if unknown + }; + + explicit CrealityPrintAgent(std::string log_dir); + ~CrealityPrintAgent() override = default; + + static AgentInfo get_agent_info_static(); + AgentInfo get_agent_info() override { return get_agent_info_static(); } + + bool fetch_filament_info(std::string dev_id) override; + + // Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info() into + // a flat list of loaded slots, plus the count of CFS boxes the printer reports. + static bool parse_cfs_response(const std::string& response, + std::vector& slots, + int& box_count, + std::string& error); + + // Strip PLA/PETG/... subtype suffixes ("PLA Silk", "PLA+", "ABS Pro") to base + // type so the preset_bundle->filaments.filament_id_by_type() lookup succeeds. + static std::string normalize_filament_type(const std::string& filament_type); + + // Score visible compatible filament presets against the CFS spool metadata and + // return the best-matching filament_id. See implementation for scoring details. + static std::string match_filament_preset(const PresetCollection& filaments, + const std::string& vendor, + const std::string& brand_name, + const std::string& base_type); +}; + +} // namespace Slic3r + +#endif diff --git a/src/slic3r/Utils/ICloudServiceAgent.hpp b/src/slic3r/Utils/ICloudServiceAgent.hpp new file mode 100644 index 00000000000..556c2536410 --- /dev/null +++ b/src/slic3r/Utils/ICloudServiceAgent.hpp @@ -0,0 +1,481 @@ +#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 +#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. + */ + +static const std::string ORCA_CLOUD_PROVIDER("orca"); +static const std::string BBL_CLOUD_PROVIDER("bbl"); + +struct CloudEvent { + std::string provider; // ORCA_CLOUD_PROVIDER or BBL_CLOUD_PROVIDER +}; + +using AppOnServerConnectedFn = std::function; +using AppOnHttpErrorFn = std::function; + +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; + + virtual std::string get_id() const = 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": {...}}} + * + */ + 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, bool force = false) = 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; + + /** + * Exchange a one-time login ticket (from a third-party OAuth callback) + * for token + profile JSON. Used by the localhost callback handler when + * the auth server redirects with ?ticket=...&redirect_url=... instead of + * passing tokens directly in the URL. + */ + virtual int get_my_token(std::string ticket, 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 + // ======================================================================== + /** + * 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 server connection status callback. + */ + virtual int set_on_server_connected_fn(AppOnServerConnectedFn fn) = 0; + + /** + * Register HTTP error callback. + */ + virtual int set_on_http_error_fn(AppOnHttpErrorFn 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 00000000000..0fa36163443 --- /dev/null +++ b/src/slic3r/Utils/IPrinterAgent.hpp @@ -0,0 +1,297 @@ +#ifndef __I_PRINTER_AGENT_HPP__ +#define __I_PRINTER_AGENT_HPP__ + +#include "bambu_networking.hpp" +// why: these extend the BAMBU_NETWORK_* return space rather than opening a new one - the value +// flows through the same int domain callers already compare against BAMBU_NETWORK_SUCCESS. +// They live here and not in bambu_networking.hpp because that file is a vendor header replaced +// wholesale by header-sync commits (see c09252ce11), which would silently clobber them. +// -70xx is free: the vendor occupies -1..-25 and -10xx through -60xx. +#define ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED -7010 // no translation exists for this command +#define ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE -7020 // a translation exists; this printer lacks the capability +#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) + * - PrinterAgentPluginCapability: Python printer-agent plugin capability that + * implements IPrinterAgent directly and is handed out as the live agent + * - 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 dev_model, 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; + + /** + * Fetch the cloud snapshot image captured at a print failure. + * Returns 0 if the request was dispatched; the image body arrives via callback(body, http_status). + */ + virtual int get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) = 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; + + // ======================================================================== + // Subscriptions + // ======================================================================== + /** + * Subscribe to a logical module (for example app- or tunnel-scoped streams). + */ + virtual int start_subscribe(std::string module) { (void) module; return BAMBU_NETWORK_SUCCESS; } + + /** + * Stop listening to a formerly subscribed module. + */ + virtual int stop_subscribe(std::string module) { (void) module; return BAMBU_NETWORK_SUCCESS; } + + /** + * Subscribe to push streams for specific device identifiers. + */ + virtual int add_subscribe(std::vector dev_list) { (void) dev_list; return BAMBU_NETWORK_SUCCESS; } + + /** + * Remove device-level subscriptions. + */ + virtual int del_subscribe(std::vector dev_list) { (void) dev_list; return BAMBU_NETWORK_SUCCESS; } + + // ======================================================================== + // 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 bool fetch_filament_info(std::string dev_id) { return false; } +}; + +} // 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 00000000000..cd3ef82b62b --- /dev/null +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -0,0 +1,2191 @@ +#include "MoonrakerPrinterAgent.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 "../GUI/DeviceCore/DevStorage.h" +#include "../GUI/DeviceCore/DevFirmware.h" +#include "nlohmann/json.hpp" +#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; + +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) { (void) log_dir; } + +MoonrakerPrinterAgent::~MoonrakerPrinterAgent() +{ + { + std::lock_guard lock(connect_mutex); + device_info = MoonrakerDeviceInfo{}; + ++connect_generation; + } + if (connect_thread.joinable()) { + connect_thread.join(); + } + stop_status_stream(); +} + +AgentInfo MoonrakerPrinterAgent::get_agent_info_static() +{ + return AgentInfo{"moonraker", "Moonraker", MoonrakerPrinterAgent_VERSION, "Klipper/Moonraker printer agent"}; +} + +void MoonrakerPrinterAgent::set_cloud_agent(std::shared_ptr cloud) +{ + std::lock_guard lock(state_mutex); + m_cloud_agent = cloud; +} + +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) +{ + if (dev_id.empty() || dev_ip.empty()) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: connect_printer missing dev_id or dev_ip"; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + std::string base_url; + std::string api_key; + uint64_t gen; + { + std::lock_guard lock(connect_mutex); + init_device_info(dev_id, dev_ip, username, password, use_ssl); + gen = ++connect_generation; + base_url = device_info.base_url; + api_key = device_info.api_key; + if (connect_thread.joinable()) { + connect_thread.detach(); + } + } + + // 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); + ws_last_dispatch_ms.store(0); + last_print_state.clear(); + + // Launch connection in background thread (capture by value to avoid data races) + { + std::lock_guard lock(connect_mutex); + connect_thread = std::thread([this, dev_id, base_url, api_key, gen]() { perform_connection_async(dev_id, base_url, api_key, gen); }); + } + + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::disconnect_printer() +{ + { + std::lock_guard lock(connect_mutex); + device_info = MoonrakerDeviceInfo{}; + ++connect_generation; // Invalidate any in-flight connection + if (connect_thread.joinable()) { + connect_thread.detach(); + } + } + + stop_status_stream(); + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::check_cert() { return BAMBU_NETWORK_SUCCESS; } + +void MoonrakerPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) +{ + (void) dev_id; + (void) 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) +{ + (void) ping_code; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) +{ + (void) sec_link; + + detect.dev_id = device_info.dev_id.empty() ? dev_ip : device_info.dev_id; + detect.model_id = device_info.model_id.empty() ? device_info.model_name : device_info.model_id; + // Prefer fetched hostname, then preset model name, then generic fallback + detect.dev_name = device_info.dev_name; + detect.model_id = device_info.model_id; + detect.version = device_info.version; + detect.connect_type = "lan"; + detect.bind_state = "free"; + + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::bind( + std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) +{ + (void) dev_ip; + (void) dev_id; + (void) dev_model; + (void) sec_link; + (void) timezone; + (void) improved; + (void) update_fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::unbind(std::string dev_id) +{ + (void) dev_id; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::request_bind_ticket(std::string* ticket) +{ + if (ticket) + *ticket = ""; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) +{ + // No BBL cloud snapshot source; report failure so the caller falls back. + (void) dev_id; + (void) file_name; + (void) callback; + return -1; +} + +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) +{ + (void) params; + (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) +{ + (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..."); + + 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, device_info.base_url, device_info.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; + } + // 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, device_info.base_url, device_info.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(device_info.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) +{ + (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; + } + 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::build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays) +{ + + // Look up MachineObject via DeviceManager + auto* dev_manager = GUI::wxGetApp().getDeviceManager(); + if (!dev_manager) { + return; + } + MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id); + if (!obj) { + return; + } + + // Build BBL-format JSON for DevFilaSystemParser::ParseV1_0 + nlohmann::json ams_json = nlohmann::json::object(); + nlohmann::json ams_array = nlohmann::json::array(); + + // Calculate ams_exist_bits and tray_exist_bits + unsigned long ams_exist_bits = 0; + unsigned long tray_exist_bits = 0; + + for (int ams_id = 0; ams_id < ams_count; ++ams_id) { + ams_exist_bits |= (1 << ams_id); + + nlohmann::json ams_unit = nlohmann::json::object(); + ams_unit["id"] = std::to_string(ams_id); + ams_unit["info"] = "0002"; // treat as AMS_LITE + + nlohmann::json tray_array = nlohmann::json::array(); + int max_slot_in_this_ams = std::min(3, max_lane_index - ams_id * 4); + for (int slot_id = 0; slot_id <= max_slot_in_this_ams; ++slot_id) { + int slot_index = ams_id * 4 + slot_id; + + // Find tray with matching slot_index + const AmsTrayData* tray = nullptr; + for (const auto& t : trays) { + if (t.slot_index == slot_index) { + tray = &t; + break; + } + } + + nlohmann::json tray_json = nlohmann::json::object(); + tray_json["id"] = std::to_string(slot_id); + tray_json["tag_uid"] = "0000000000000000"; + + if (tray && tray->has_filament) { + tray_exist_bits |= (1 << slot_index); + + tray_json["tray_info_idx"] = tray->tray_info_idx; + tray_json["tray_type"] = tray->tray_type; + tray_json["tray_color"] = normalize_color_value(tray->tray_color); + + // Add temperature data if provided + if (tray->bed_temp > 0) { + tray_json["bed_temp"] = std::to_string(tray->bed_temp); + } + if (tray->nozzle_temp > 0) { + tray_json["nozzle_temp_max"] = std::to_string(tray->nozzle_temp); + } + } else { + tray_json["tray_info_idx"] = ""; + tray_json["tray_type"] = ""; + tray_json["tray_color"] = "00000000"; + tray_json["tray_slot_placeholder"] = "1"; + } + + 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().get(), false); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays"; + + // Set printer_type so update_sync_status() can match it against the preset's printer type. + // Without this, the comparison fails and all sync badges are cleared. + obj->printer_type = device_info.model_id; + + // Set push counters so is_info_ready() returns true for pull-mode agents. + if (obj->m_push_count == 0) { + obj->m_push_count = 1; + } + if (obj->m_full_msg_count == 0) { + obj->m_full_msg_count = 1; + } + obj->last_push_time = std::chrono::system_clock::now(); + + // Set storage state - Moonraker printers use virtual_sdcard, storage is always available. + // This is required for SelectMachineDialog to allow printing (otherwise it blocks with "No SD card"). + obj->GetStorage()->set_sdcard_state(DevStorage::HAS_SDCARD_NORMAL); + + // Populate module_vers so is_info_ready() passes the version check. + // Moonraker printers don't have BBL-style version info, but we need a non-empty map. + if (obj->module_vers.empty()) { + DevFirmwareVersionInfo ota_info; + ota_info.name = "ota"; + ota_info.sw_ver = "1.0.0"; // Placeholder version for Moonraker printers + obj->module_vers.emplace("ota", ota_info); + } +} + +bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) +{ + std::vector trays; + int max_lane_index = 0; + + // Try Moonraker filament data (more generic, supports any filament changer + // software that reports lane data to Moonraker like AFC and recent Happy + // Hare as of Feb 15, 2026) + if (fetch_moonraker_filament_data(trays, max_lane_index)) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Moonraker filament system with " + << (max_lane_index + 1) << " lanes"; + int ams_count = (max_lane_index + 4) / 4; + build_ams_payload(ams_count, max_lane_index, trays); + return true; + } + + // Attempt Happy Hare first (more widely adopted, supports more filament changers) + if (fetch_hh_filament_info(trays, max_lane_index)) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Happy Hare MMU with " + << (max_lane_index + 1) << " gates"; + int ams_count = (max_lane_index + 4) / 4; + build_ams_payload(ams_count, max_lane_index, trays); + return true; + } + + // No MMU detected - this is normal for printers without MMU, not an error + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: No MMU system detected (neither HH nor Moonraker)"; + return false; +} + +std::string MoonrakerPrinterAgent::trim_and_upper(const std::string& input) +{ + std::string result = input; + boost::trim(result); + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return static_cast(std::toupper(c)); }); + return result; +} + +std::string MoonrakerPrinterAgent::map_filament_type_to_generic_id(const std::string& filament_type) +{ + const std::string upper = trim_and_upper(filament_type); + + // Map to OrcaFilamentLibrary preset IDs (compatible with all printers) + // Source: resources/profiles/OrcaFilamentLibrary/filament/ + + // PLA variants + if (upper == "PLA") return "OGFL99"; + if (upper == "PLA-CF") return "OGFL98"; + if (upper == "PLA SILK" || upper == "PLA-SILK") return "OGFL96"; + if (upper == "PLA HIGH SPEED" || upper == "PLA-HS" || upper == "PLA HS") return "OGFL95"; + + // ABS/ASA variants + if (upper == "ABS") return "OGFB99"; + if (upper == "ASA") return "OGFB98"; + + // PETG/PET variants + if (upper == "PETG" || upper == "PET") return "OGFG99"; + if (upper == "PCTG") return "OGFG97"; + + // PA/Nylon variants + if (upper == "PA" || upper == "NYLON") return "OGFN99"; + if (upper == "PA-CF") return "OGFN98"; + if (upper == "PPA" || upper == "PPA-CF") return "OGFN97"; + if (upper == "PPA-GF") return "OGFN96"; + + // PC variants + if (upper == "PC") return "OGFC99"; + + // PP/PE variants + if (upper == "PE") return "OGFP99"; + if (upper == "PP") return "OGFP97"; + + // Support materials + if (upper == "PVA") return "OGFS99"; + if (upper == "HIPS") return "OGFS98"; + if (upper == "BVOH") return "OGFS97"; + + // TPU variants + if (upper == "TPU") return "OGFU99"; + + // Other materials + if (upper == "EVA") return "OGFR99"; + if (upper == "PHA") return "OGFR98"; + if (upper == "COPE") return "OGFLC99"; + if (upper == "SBS") return "OFLSBS99"; + + // Unknown material + return UNKNOWN_FILAMENT_ID; +} + +// JSON helper methods - null-safe accessors +std::string MoonrakerPrinterAgent::safe_json_string(const nlohmann::json& obj, const char* key) +{ + auto it = obj.find(key); + if (it != obj.end() && it->is_string()) + return it->get(); + return ""; +} + +int MoonrakerPrinterAgent::safe_json_int(const nlohmann::json& obj, const char* key) +{ + auto it = obj.find(key); + if (it != obj.end() && it->is_number()) + return it->get(); + return 0; +} + +std::string MoonrakerPrinterAgent::safe_array_string(const nlohmann::json& arr, int idx) +{ + if (arr.is_array() && idx >= 0 && idx < static_cast(arr.size()) && arr[idx].is_string()) + return arr[idx].get(); + return ""; +} + +int MoonrakerPrinterAgent::safe_array_int(const nlohmann::json& arr, int idx) +{ + if (arr.is_array() && idx >= 0 && idx < static_cast(arr.size()) && arr[idx].is_number()) + return arr[idx].get(); + return 0; +} + +std::string MoonrakerPrinterAgent::normalize_color_value(const std::string& color) +{ + std::string value = color; + boost::trim(value); + + // Remove 0x or 0X prefix if present + if (value.size() >= 2 && (value.rfind("0x", 0) == 0 || value.rfind("0X", 0) == 0)) { + value = value.substr(2); + } + // Remove # prefix if present + if (!value.empty() && value[0] == '#') { + value = value.substr(1); + } + + // Extract only hex digits + std::string normalized; + for (char c : value) { + if (std::isxdigit(static_cast(c))) { + normalized.push_back(static_cast(std::toupper(static_cast(c)))); + } + } + + // If 6 hex digits, add FF alpha + if (normalized.size() == 6) { + normalized += "FF"; + } + + // Validate length - return default if invalid + if (normalized.size() != 8) { + return "00000000"; + } + + return normalized; +} + +// Fetch filament info from moonraker database +bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector& trays, int& max_lane_index) +{ + // Fetch lane data from Moonraker database + std::string url = join_url(device_info.base_url, "/server/database/item?namespace=lane_data"); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!device_info.api_key.empty()) { + http.header("X-Api-Key", device_info.api_key); + } + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: Failed to fetch lane data: " << http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: Invalid JSON response"; + return false; + } + + // Expected structure: { "result": { "namespace": "lane_data", "value": { "lane1": {...}, ... } } } + if (!json.contains("result") || !json["result"].contains("value") || !json["result"]["value"].is_object()) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: Unexpected JSON structure or no lane_data found"; + return false; + } + + // Parse response into AmsTrayData + const auto& value = json["result"]["value"]; + trays.clear(); + max_lane_index = 0; + + for (const auto& [lane_key, lane_obj] : value.items()) { + if (!lane_obj.is_object()) { + continue; + } + + // Extract lane index from the "lane" field (tool number, 0-based) + std::string lane_str = safe_json_string(lane_obj, "lane"); + int lane_index = -1; + if (!lane_str.empty()) { + try { + lane_index = std::stoi(lane_str); + } catch (...) { + lane_index = -1; + } + } + + if (lane_index < 0) { + continue; + } + + AmsTrayData tray; + tray.slot_index = lane_index; + tray.tray_color = safe_json_string(lane_obj, "color"); + tray.tray_type = safe_json_string(lane_obj, "material"); + tray.bed_temp = safe_json_int(lane_obj, "bed_temp"); + tray.nozzle_temp = safe_json_int(lane_obj, "nozzle_temp"); + tray.has_filament = !tray.tray_type.empty(); + auto* bundle = GUI::wxGetApp().preset_bundle; + tray.tray_info_idx = bundle + ? bundle->filaments.filament_id_by_type(tray.tray_type) + : map_filament_type_to_generic_id(tray.tray_type); + + max_lane_index = std::max(max_lane_index, lane_index); + trays.push_back(tray); + } + + if (trays.empty()) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: No lanes found"; + return false; + } + + return true; +} + +// Fetch filament info from Happy Hare MMU +bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector& trays, int& max_lane_index) +{ + // Query Happy Hare MMU status + std::string url = join_url(device_info.base_url, "/printer/objects/query?mmu"); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!device_info.api_key.empty()) { + http.header("X-Api-Key", device_info.api_key); + } + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent::fetch_hh_filament_info: Failed to fetch HH data: " << http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent::fetch_hh_filament_info: Invalid JSON response"; + return false; + } + + // Expected structure: { "result": { "status": { "mmu": { ... } } } } + if (!json.contains("result") || !json["result"].contains("status") || + !json["result"]["status"].contains("mmu") || !json["result"]["status"]["mmu"].is_object()) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent::fetch_hh_filament_info: No mmu object in response"; + return false; + } + + const auto& mmu = json["result"]["status"]["mmu"]; + + // Check if HH is installed (empty mmu object means HH not installed) + if (mmu.empty()) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent::fetch_hh_filament_info: Empty mmu object (HH not installed)"; + return false; + } + + // Get num_gates + if (!mmu.contains("num_gates") || !mmu["num_gates"].is_number()) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent::fetch_hh_filament_info: No num_gates field"; + return false; + } + + int num_gates = mmu["num_gates"].get(); + if (num_gates <= 0) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_hh_filament_info: Invalid num_gates: " << num_gates; + return false; + } + + // Get arrays + const auto& gate_status = mmu.contains("gate_status") ? mmu["gate_status"] : nlohmann::json::array(); + const auto& gate_material = mmu.contains("gate_material") ? mmu["gate_material"] : nlohmann::json::array(); + const auto& gate_color = mmu.contains("gate_color") ? mmu["gate_color"] : nlohmann::json::array(); + const auto& gate_temperature = mmu.contains("gate_temperature") ? mmu["gate_temperature"] : nlohmann::json::array(); + + if (!gate_status.is_array() || !gate_material.is_array() || + !gate_color.is_array() || !gate_temperature.is_array()) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_hh_filament_info: HH arrays not found or invalid type"; + return false; + } + + // Parse gate data + trays.clear(); + max_lane_index = 0; + + for (int gate_idx = 0; gate_idx < num_gates; ++gate_idx) { + // Check gate_status: -1 = unknown, 0 = empty, 1 or 2 = available + int status = safe_array_int(gate_status, gate_idx); + if (status <= 0) { + continue; // Skip unknown or empty gates + } + + // Extract gate data + std::string material = safe_array_string(gate_material, gate_idx); + std::string color = safe_array_string(gate_color, gate_idx); + int nozzle_temp = safe_array_int(gate_temperature, gate_idx); + + // Skip if no material type (empty gate) + if (material.empty()) { + continue; + } + + AmsTrayData tray; + tray.slot_index = gate_idx; + tray.tray_type = material; + tray.tray_color = color; + tray.nozzle_temp = nozzle_temp; + tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays + tray.has_filament = true; + + auto* bundle = GUI::wxGetApp().preset_bundle; + tray.tray_info_idx = bundle + ? bundle->filaments.filament_id_by_type(tray.tray_type) + : map_filament_type_to_generic_id(tray.tray_type); + + max_lane_index = std::max(max_lane_index, gate_idx); + trays.push_back(tray); + } + + if (trays.empty()) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_hh_filament_info: No valid HH gates found"; + return false; + } + + return true; +} + +int MoonrakerPrinterAgent::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) << "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(); + + // 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; + } + + // 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::init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) +{ + device_info = MoonrakerDeviceInfo{}; + auto* preset_bundle = GUI::wxGetApp().preset_bundle; + if (!preset_bundle) { + return false; + } + + auto& preset = preset_bundle->printers.get_edited_preset(); + const auto& printer_cfg = preset.config; + device_info.dev_ip = dev_ip; + + device_info.api_key = password; + device_info.model_name = printer_cfg.opt_string("printer_model"); + device_info.model_id = preset.get_printer_type(preset_bundle); + device_info.base_url = use_ssl ? "https://" + dev_ip : "http://" + dev_ip; + device_info.dev_id = dev_id; + device_info.version = ""; + device_info.dev_name = device_info.dev_id; + + return true; +} + +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(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + 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("machine_name", result.value("hostname", "")); + info.version = result.value("moonraker_version", ""); + info.klippy_state = result.value("klippy_state", ""); + + return true; +} + +bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url, + const std::string& api_key, + nlohmann::json& status, + std::string& error) const +{ + 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(5) + .timeout_max(10) + .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 +{ + nlohmann::json payload; + payload["script"] = gcode; + std::string payload_str = payload.dump(); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::post(join_url(device_info.base_url, "/printer/gcode/script")); + if (!device_info.api_key.empty()) { + http.header("X-Api-Key", device_info.api_key); + } + http.header("Content-Type", "application/json") + .set_post_body(payload_str) + .timeout_connect(5) + .timeout_max(10) + .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) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: send_gcode failed: " << http_error; + return false; + } + + 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(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + + nlohmann::json result = json.contains("result") ? json["result"] : json; + if (!result.contains("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) +{ + 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"] = device_info.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"] = device_info.api_key; + dispatch_message(dev_id, payload.dump()); + return BAMBU_NETWORK_SUCCESS; +} + +void MoonrakerPrinterAgent::announce_printhost_device() +{ + OnMsgArrivedFn ssdp_fn; + { + std::lock_guard lock(state_mutex); + ssdp_fn = on_ssdp_msg_fn; + if (!ssdp_fn) { + return; + } + if (ssdp_announced_host == device_info.base_url && !ssdp_announced_id.empty()) { + return; + } + } + + // 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(device_info.base_url, device_info.api_key, info, fetch_error) && !info.dev_name.empty()) { + dev_name = info.dev_name; + } else { + dev_name = device_info.model_name.empty() ? "Moonraker Printer" : device_info.model_name; + } + + const std::string model_id = device_info.model_id; + + if (auto* app_config = GUI::wxGetApp().app_config) { + const std::string access_code = device_info.api_key.empty() ? "88888888" : device_info.api_key; + app_config->set_str("access_code", device_info.dev_id, access_code); + } + + nlohmann::json payload; + payload["dev_name"] = dev_name; + payload["dev_id"] = device_info.dev_id; + payload["dev_ip"] = device_info.dev_ip; + payload["dev_type"] = model_id.empty() ? dev_name : model_id; + payload["dev_signal"] = "0"; + payload["connect_type"] = "lan"; + 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 = device_info.base_url; + ssdp_announced_id = device_info.dev_id; + + // Set this as the selected machine if nothing is currently selected + if (selected_machine.empty()) { + selected_machine = device_info.dev_id; + } + } +} + +void MoonrakerPrinterAgent::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 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)) { + { + std::lock_guard lock(payload_mutex); + this->available_objects = std::move(available_objects); + } + + if (this->available_objects.count("heater_bed") != 0) { + subscribe_objects.insert("heater_bed"); + } + if (this->available_objects.count("fan") != 0) { + subscribe_objects.insert("fan"); + } + + // 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; + bool is_critical = false; // Track if this is a critical update that bypasses throttle + + // Check for subscription response (has "result.status") - initial subscription is critical + if (json.contains("result") && json["result"].contains("status") && json["result"]["status"].is_object()) { + update_status_cache(json["result"]["status"]); + updated = true; + is_critical = true; // Initial subscription response - dispatch immediately + } + + // Check for status update notifications + if (json.contains("method") && json["method"].is_string()) { + const std::string method = json["method"].get(); + if (method == "notify_status_update" && json.contains("params") && json["params"].is_array() && !json["params"].empty() && + json["params"][0].is_object()) { + update_status_cache(json["params"][0]); + updated = true; + // Note: is_critical stays false for regular status updates (telemetry) + } else if (method == "notify_klippy_ready") { + nlohmann::json updates; + updates["print_stats"]["state"] = "standby"; + update_status_cache(updates); + updated = true; + is_critical = true; // Klippy events are critical + } else if (method == "notify_klippy_shutdown") { + nlohmann::json updates; + updates["print_stats"]["state"] = "error"; + update_status_cache(updates); + updated = true; + is_critical = true; // Klippy events are critical + } + // Handle Klippy disconnect - update status and trigger reconnection + else if (method == "notify_klippy_disconnected") { + // Klippy disconnected - update status to reflect disconnect state + nlohmann::json updates; + updates["print_stats"]["state"] = "error"; + update_status_cache(updates); + updated = true; + is_critical = true; // Klippy events are critical + // Set flag to trigger reconnection after dispatching the status update + ws_reconnect_requested.store(true); + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Klippy disconnected, triggering reconnection"; + } + } + + // Check for print state changes (critical - always dispatch immediately) + if (updated && !is_critical) { + std::string current_state; + { + std::lock_guard lock(payload_mutex); + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("state") && + status_cache["print_stats"]["state"].is_string()) { + current_state = status_cache["print_stats"]["state"].get(); + } + } + + if (!current_state.empty() && current_state != last_print_state) { + is_critical = true; + last_print_state = current_state; + } + } + + if (updated) { + const auto now_ms = static_cast( + std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); + const auto last_dispatch_ms = ws_last_dispatch_ms.load(); + + // Dispatch if: critical change OR throttle interval elapsed + const bool should_dispatch = is_critical || last_dispatch_ms == 0 || now_ms - last_dispatch_ms >= STATUS_UPDATE_INTERVAL_MS; + + if (should_dispatch) { + nlohmann::json message; + { + std::lock_guard lock(payload_mutex); + message = build_print_payload_locked(); + } + + dispatch_message(dev_id, message.dump()); + ws_last_dispatch_ms.store(now_ms); + ws_last_emit_ms.store(now_ms); // Also update heartbeat timer + } + // else: skip dispatch, cache is updated for next dispatch cycle + } +} + +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; + + // 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; + + // 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; + + // 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; + + // Moonraker doesn't provide temperature ranges via API - use hardcoded defaults + payload["print"]["nozzle_temp_range"] = {100, 370}; // Typical Klipper range + payload["print"]["bed_temp_range"] = {0, 120}; // Typical bed range + + payload["print"]["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(); + } + + 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; + + return payload; +} + +void MoonrakerPrinterAgent::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; + } + + 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(5) + .timeout_max(300) // 5 minutes for large files + .on_complete([&](std::string body, unsigned status) { + (void) body; + (void) status; + }) + .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) +{ + return send_gcode(dev_id, "PAUSE") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; +} + +int MoonrakerPrinterAgent::resume_print(const std::string& dev_id) +{ + return send_gcode(dev_id, "RESUME") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; +} + +int MoonrakerPrinterAgent::cancel_print(const std::string& dev_id) +{ + return send_gcode(dev_id, "CANCEL_PRINT") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; +} + +bool MoonrakerPrinterAgent::send_jsonrpc_command(const std::string& base_url, + 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(5) + .timeout_max(10) + .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, uint64_t generation) +{ + auto is_stale = [&]() { return generation != connect_generation.load(); }; + + int result = BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; + std::string error_msg; + + // Early exit if a newer connection was started before we begin + if (is_stale()) { + return; + } + + try { + MoonrakerDeviceInfo fetched_info; + if (!fetch_device_info(base_url, api_key, fetched_info, error_msg)) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Failed to fetch server info: " << error_msg; + // Orca todo: revist here, for now don't send error, this is set current MachineObject to null + // dispatch_local_connect(ConnectStatusFailed, dev_id, "server_info_failed"); + return; + } + + // Commit fetched info back to device_info under lock, only if still current + { + std::lock_guard lock(connect_mutex); + if (is_stale()) { + return; + } + device_info.dev_name = fetched_info.dev_name; + device_info.version = fetched_info.version; + device_info.klippy_state = fetched_info.klippy_state; + } + +// Orca todo: disable websocket for now, as we don't use MonitorPanel for Moonraker printers yet +#if 0 + // Query initial status + 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); +#endif + + // 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; + } + + // Only dispatch if this connection is still the current one + if (result == BAMBU_NETWORK_SUCCESS && !is_stale()) { + dispatch_local_connect(ConnectStatusOk, dev_id, "0"); + dispatch_printer_connected(dev_id); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: connect_printer completed - dev_id=" << dev_id; + } else if (result != BAMBU_NETWORK_SUCCESS && result != BAMBU_NETWORK_ERR_CANCELED) { + // Orca todo: revist here, for now don't send error, this is set current MachineObject to null + // dispatch_local_connect(ConnectStatusFailed, dev_id, error_msg); + } +} + +bool MoonrakerPrinterAgent::is_numeric(const std::string& value) +{ + return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c) != 0; }); +} + +std::string MoonrakerPrinterAgent::normalize_base_url(std::string host, const std::string& port) +{ + boost::trim(host); + if (host.empty()) { + return ""; + } + + std::string value = host; + if (is_numeric(port) && value.find("://") == std::string::npos && value.find(':') == std::string::npos) { + value += ":" + port; + } + + if (!boost::istarts_with(value, "http://") && !boost::istarts_with(value, "https://")) { + value = "http://" + value; + } + + if (value.size() > 1 && value.back() == '/') { + value.pop_back(); + } + + return value; +} + +std::string MoonrakerPrinterAgent::join_url(const std::string& base_url, const std::string& path) const +{ + if (base_url.empty()) { + return ""; + } + if (path.empty()) { + return base_url; + } + if (base_url.back() == '/' && path.front() == '/') { + return base_url.substr(0, base_url.size() - 1) + path; + } + if (base_url.back() != '/' && path.front() != '/') { + return base_url + "/" + path; + } + return base_url + path; +} + +// Sanitize filename to prevent path traversal attacks +// Extracts only the basename, removing any path components +std::string MoonrakerPrinterAgent::sanitize_filename(const std::string& filename) +{ + if (filename.empty()) { + return "print.gcode"; + } + namespace fs = boost::filesystem; + fs::path p(filename); + std::string basename = p.filename().string(); + if (basename.empty() || basename == "." || basename == "..") { + return "print.gcode"; + } + return basename; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp new file mode 100644 index 00000000000..ae635700634 --- /dev/null +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -0,0 +1,215 @@ +#ifndef __MOONRAKER_PRINTER_AGENT_HPP__ +#define __MOONRAKER_PRINTER_AGENT_HPP__ + +#include "IPrinterAgent.hpp" +#include "ICloudServiceAgent.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace Slic3r { + +class MoonrakerPrinterAgent : 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 dev_model, 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 get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) 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; } + bool fetch_filament_info(std::string dev_id) override; + +protected: + struct MoonrakerDeviceInfo + { + std::string dev_id; + std::string dev_ip; + std::string api_key; + std::string base_url; + std::string model_id; + std::string model_name; + std::string dev_name; + std::string version; + std::string klippy_state; + bool use_ssl = false; + } device_info; + + // Tray data for AMS payload building + struct AmsTrayData { + int slot_index = 0; // 0-based slot index + bool has_filament = false; + std::string tray_type; // Material type (e.g., "PLA", "ASA") + std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA) + std::string tray_info_idx; // Setting ID (optional) + int bed_temp = 0; // Optional + int nozzle_temp = 0; // Optional + }; + + // Build ams JSON and call parser + void build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays); + + // Methods that derived classes may need to override or access + virtual bool init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); + virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; + + // State access for derived classes + mutable std::recursive_mutex state_mutex; + + // Helpers + bool is_numeric(const std::string& value); + std::string normalize_base_url(std::string host, const std::string& port); + std::string sanitize_filename(const std::string& filename); + std::string join_url(const std::string& base_url, const std::string& path) const; + + // Trim whitespace and convert to uppercase + static std::string trim_and_upper(const std::string& input); + + // Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility + static std::string map_filament_type_to_generic_id(const std::string& filament_type); + +private: + int handle_request(const std::string& dev_id, const std::string& json_str); + int send_version_info(const std::string& dev_id); + int send_access_code(const std::string& dev_id); + + bool 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; + + 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; + + // Connection thread management + void perform_connection_async(const std::string& dev_id, + const std::string& base_url, + const std::string& api_key, + uint64_t generation); + + // System-specific filament fetch methods + bool fetch_hh_filament_info(std::vector& trays, int& max_lane_index); + bool fetch_moonraker_filament_data(std::vector& trays, int& max_lane_index); + + // JSON helper methods + static std::string safe_json_string(const nlohmann::json& obj, const char* key); + static int safe_json_int(const nlohmann::json& obj, const char* key); + static std::string safe_array_string(const nlohmann::json& arr, int idx); + static int safe_array_int(const nlohmann::json& arr, int idx); + static std::string normalize_color_value(const std::string& color); + + 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; + + // Throttling configuration for WebSocket updates + // Critical changes (state transitions) dispatch immediately; telemetry is throttled + static constexpr uint64_t STATUS_UPDATE_INTERVAL_MS = 1000; // 1 update/sec for telemetry + std::atomic ws_last_dispatch_ms{0}; + std::string last_print_state; // Track state for immediate dispatch on change + + // Connection thread management + std::atomic connect_generation{0}; + std::thread connect_thread; + std::recursive_mutex connect_mutex; +}; + +} // namespace Slic3r + +#endif diff --git a/src/slic3r/Utils/NetworkAgentFactory.cpp b/src/slic3r/Utils/NetworkAgentFactory.cpp new file mode 100644 index 00000000000..4aaa2483c36 --- /dev/null +++ b/src/slic3r/Utils/NetworkAgentFactory.cpp @@ -0,0 +1,190 @@ +#include "NetworkAgentFactory.hpp" +#include "IPrinterAgent.hpp" +#include "ICloudServiceAgent.hpp" +#include "BBLPrinterAgent.hpp" +#include "OrcaPrinterAgent.hpp" +#include "QidiPrinterAgent.hpp" +#include "SnapmakerPrinterAgent.hpp" +#include "MoonrakerPrinterAgent.hpp" +#include "CrealityPrintAgent.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { +namespace { + +static std::mutex s_registry_mutex; + +std::map& get_printer_agents() +{ + static std::map agents; + return agents; +} + +std::map>& get_printer_agent_cache() +{ + static std::map> cache; + return cache; +} + +// Helper to register a printer agent type with the standard factory pattern. +// AgentTypes that take a log_dir constructor arg use the default; BBLPrinterAgent +// (no log_dir) is registered separately. +template void register_agent() +{ + auto info = T::get_agent_info_static(); + NetworkAgentFactory::register_printer_agent(info.id, info.name, + [](std::shared_ptr cloud_agent, + const std::string& log_dir) -> std::shared_ptr { + auto agent = std::make_shared(log_dir); + if (cloud_agent) + agent->set_cloud_agent(cloud_agent); + return agent; + }); +} + +} // anonymous namespace + +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(); + return agents.emplace(id, PrinterAgentInfo(id, display_name, std::move(factory))).second; +} + +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::string NetworkAgentFactory::get_printer_agent_plugin_identifier(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.is_plugin()) ? it->second.plugin_identifier : std::string(); +} + +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) { + if (!pair.second.is_plugin()) + result.push_back(pair.second); + } + + for (const auto& pair : agents) { + if (pair.second.is_plugin()) + 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); + + // Check cache first + auto& cache = get_printer_agent_cache(); + auto cache_it = cache.find(id); + if (cache_it != cache.end()) { + BOOST_LOG_TRIVIAL(info) << "Reusing cached printer agent: " << id; + if (cloud_agent) + cache_it->second->set_cloud_agent(cloud_agent); + return cache_it->second; + } + + // Not cached — create via factory + auto& agents = get_printer_agents(); + auto it = agents.find(id); + + if (it == agents.end()) { + BOOST_LOG_TRIVIAL(warning) << "Unknown printer agent ID: " << id; + return nullptr; + } + + auto agent = it->second.factory(cloud_agent, log_dir); + if (agent) { + BOOST_LOG_TRIVIAL(info) << "Created and cached printer agent: " << id; + cache[id] = agent; + } + return agent; +} + +void NetworkAgentFactory::clear_printer_agent_cache() +{ + std::lock_guard lock(s_registry_mutex); + auto& cache = get_printer_agent_cache(); + for (auto& pair : cache) { + if (pair.second) + pair.second->disconnect_printer(); + } + cache.clear(); + BOOST_LOG_TRIVIAL(info) << "Printer agent cache cleared"; +} + +void NetworkAgentFactory::register_all_agents() +{ + register_agent(); + register_agent(); + register_agent(); + register_agent(); // Must come BEFORE MoonrakerPrinterAgent — + // CrealityPrintAgent extends Moonraker behaviour + // for K-series boards with CFS support. + register_agent(); + + // Keep BBL as a built-in option. Python printer-agent plugins with the + // same AgentInfo ID are listed separately under the plugin registry key. + { + 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; + }); + } +} + +std::unique_ptr create_agent_from_config(const std::string& log_dir, AppConfig* app_config) +{ + if (!app_config) + return std::make_unique(nullptr, nullptr); + + // Create BBL cloud agent for Bambu printers + auto cloud_agent = NetworkAgentFactory::create_cloud_agent("bbl", log_dir); + if (!cloud_agent) { + BOOST_LOG_TRIVIAL(error) << "Failed to create BBL cloud agent"; + } + + auto agent = std::make_unique(std::move(cloud_agent), nullptr); + + return agent; +} + + +} // namespace Slic3r diff --git a/src/slic3r/Utils/NetworkAgentFactory.hpp b/src/slic3r/Utils/NetworkAgentFactory.hpp new file mode 100644 index 00000000000..7bbb83d08a6 --- /dev/null +++ b/src/slic3r/Utils/NetworkAgentFactory.hpp @@ -0,0 +1,188 @@ +#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 + +namespace Slic3r { +static constexpr char ORCA_PRINTER_AGENT_ID[] = "orca"; +static constexpr char BBL_PRINTER_AGENT_ID[] = "bbl"; + +// Factory function type for creating printer agents +using PrinterAgentFactory = + std::function(std::shared_ptr cloud_agent, const std::string& log_dir)>; + +// Information about a registered printer agent +struct PrinterAgentInfo +{ + std::string id; // Registry/config key, e.g. "orca" or a plugin AgentInfo::id + std::string display_name; // e.g., "Orca Native", "Bambu Lab" + std::string plugin_identifier; // Empty for built-ins, otherwise ;; + PrinterAgentFactory factory; // Function to create the agent + + bool is_plugin() const { return !plugin_identifier.empty(); } + PrinterAgentInfo(const std::string& id_, const std::string& display_name_, PrinterAgentFactory factory_) + : id(id_), display_name(display_name_), factory(std::move(factory_)) + {} + + PrinterAgentInfo(const std::string& id_, const std::string& display_name_, const std::string& plugin_identifier, PrinterAgentFactory factory_) + : id(id_), display_name(display_name_), plugin_identifier(plugin_identifier), 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 registry/config key is registered + */ + static bool is_printer_agent_registered(const std::string& id); + + /** + * Get info about a registered agent by registry/config key + */ + static const PrinterAgentInfo* get_printer_agent_info(const std::string& id); + + /** + * Return the full plugin reference for a plugin-backed printer agent ID, or empty for built-ins. + */ + static std::string get_printer_agent_plugin_identifier(const std::string& id); + + /** + * Get all registered printer agents (for UI population) + */ + static std::vector get_registered_printer_agents(); + + /** + * Create a printer agent by registry/config key + * + * Returns a cached instance if one exists for the given key, otherwise + * creates a new agent via the registered factory and caches it. + * + * @param id Agent registry/config key 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); + + /** + * Clear the printer agent cache. + * Calls disconnect_printer() on each cached agent and releases all shared_ptrs. + * Should be called during application shutdown before destroying the NetworkAgent. + */ + static void clear_printer_agent_cache(); + + // ======================================================================== + // Cloud Agent Factory + // ======================================================================== + + /** + * 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(const std::string& provider, const std::string& log_dir) + { + if (provider == ORCA_CLOUD_PROVIDER) { + return std::make_shared(log_dir); + } else if (provider == BBL_CLOUD_PROVIDER) { + 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(); + } + return nullptr; + } + +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: Always creates OrcaCloudServiceAgent as the primary provider. + * Third-party cloud agents (e.g., Bambu) are created from the cloud_providers + * AppConfig setting and added via NetworkAgent::add_cloud_agent(). + * + * @param log_dir Directory for log files + * @param app_config Application configuration object + * @return NetworkAgent with cloud agent, or nullptr on failure + */ +std::unique_ptr create_agent_from_config(const std::string& log_dir, AppConfig* app_config); + +} // namespace Slic3r + +#endif // __NETWORK_AGENT_FACTORY_HPP__ diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp new file mode 100644 index 00000000000..cb70dafccae --- /dev/null +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -0,0 +1,231 @@ +#include "OrcaPrinterAgent.hpp" +#include "NetworkAgentFactory.hpp" + +namespace Slic3r { + +const std::string OrcaPrinterAgent_VERSION = "0.0.1"; + +OrcaPrinterAgent::OrcaPrinterAgent(std::string log_dir) : log_dir(std::move(log_dir)) +{ +} + +OrcaPrinterAgent::~OrcaPrinterAgent() = default; + +void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr cloud) +{ + std::lock_guard lock(state_mutex); + m_cloud_agent = cloud; +} + +// ============================================================================ +// Communication - All Stubs +// ============================================================================ + +int OrcaPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) +{ + 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) +{ + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::disconnect_printer() +{ + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) +{ + return BAMBU_NETWORK_SUCCESS; +} + +// ============================================================================ +// Certificates - All Stubs +// ============================================================================ + +int OrcaPrinterAgent::check_cert() +{ + return BAMBU_NETWORK_SUCCESS; +} + +void OrcaPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) +{ +} + +// ============================================================================ +// Discovery - Stub +// ============================================================================ + +bool OrcaPrinterAgent::start_discovery(bool start, bool sending) +{ + return true; +} + +// ============================================================================ +// Binding - All Stubs +// ============================================================================ + +int OrcaPrinterAgent::ping_bind(std::string ping_code) +{ + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) +{ + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::bind( + std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) +{ + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::unbind(std::string dev_id) +{ + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::request_bind_ticket(std::string* ticket) +{ + if (ticket) + *ticket = ""; + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) +{ + // No BBL cloud snapshot source; report failure so the caller falls back. + (void) dev_id; + (void) file_name; + (void) callback; + return -1; +} + +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{ORCA_PRINTER_AGENT_ID, "Orca", OrcaPrinterAgent_VERSION, "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) +{ + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::start_local_print_with_record(PrintParams params, + OnUpdateStatusFn update_fn, + WasCancelledFn cancel_fn, + OnWaitFn wait_fn) +{ + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +{ + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) +{ + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) +{ + 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 00000000000..a1613420a57 --- /dev/null +++ b/src/slic3r/Utils/OrcaPrinterAgent.hpp @@ -0,0 +1,101 @@ +#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 dev_model, 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 get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) 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/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp new file mode 100644 index 00000000000..6b054801942 --- /dev/null +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -0,0 +1,405 @@ +#include "QidiPrinterAgent.hpp" +#include "Http.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "slic3r/GUI/GUI_App.hpp" + +#include "nlohmann/json.hpp" +#include +#include +#include +#include + +namespace Slic3r { + +namespace { + +// Check whether any visible, compatible base preset in the collection has the given filament_id. +bool has_visible_base_preset(const PresetCollection& filaments, const std::string& filament_id) +{ + for (const auto& p : filaments.get_presets()) { + if (p.is_visible && p.is_compatible + && filaments.get_preset_base(p) == &p + && p.filament_id == filament_id) + return true; + } + return false; +} + +} // anonymous namespace + +const std::string QidiPrinterAgent_VERSION = "0.0.1"; + +QidiPrinterAgent::QidiPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) +{ +} + +AgentInfo QidiPrinterAgent::get_agent_info_static() +{ + return AgentInfo{"qidi", "Qidi", QidiPrinterAgent_VERSION, "Qidi printer agent"}; +} + +bool QidiPrinterAgent::fetch_filament_info(std::string dev_id) +{ + std::string error; + + // 1. Fetch device info and infer series_id + std::string series_id; + { + MoonrakerDeviceInfo info; + if (fetch_device_info(device_info.base_url, device_info.api_key, info, error)) { + series_id = infer_series_id(info.model_id, info.dev_name); + } + } + if (series_id.empty()) { + // Fall back to the configured Orca model if Moonraker doesn't expose a usable identifier. + series_id = infer_series_id(device_info.model_id, device_info.model_name); + } + + // 2. Fetch filament dictionary + QidiFilamentDict dict; + if (!fetch_filament_dict(device_info.base_url, device_info.api_key, dict, error)) { + BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error; + } + + // 3. Fetch slot info and build AmsTrayData directly + std::vector trays; + int box_count = 0; + if (!fetch_slot_info(device_info.base_url, device_info.api_key, dict, series_id, trays, box_count, error)) { + BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error; + return false; + } + + // 4. Build the AMS payload + build_ams_payload(box_count, box_count * 4 - 1, trays); + return true; +} + +bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, + const std::string& api_key, + const QidiFilamentDict& dict, + const std::string& series_id, + std::vector& trays, + int& box_count, + std::string& error) +{ + std::string url = join_url(base_url, "/printer/objects/query?save_variables=variables"); + for (int i = 0; i < 16; ++i) { + 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(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + + 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; + trays.clear(); + trays.reserve(max_slots); + + // Lambda to build setting_id from slot data + auto build_setting_id = [&](int filament_type_idx, int vendor_type, const std::string& tray_type) { + const int vendor = (vendor_type == 1) ? 1 : 0; + if (is_numeric(series_id) && filament_type_idx > 0) { + return "QD_" + series_id + "_" + std::to_string(vendor) + "_" + std::to_string(filament_type_idx); + } + return map_filament_type_to_setting_id(tray_type); + }; + + for (int i = 0; i < max_slots; ++i) { + AmsTrayData tray; + tray.slot_index = i; + + // Read slot variables + const int color_index = variables.value("color_slot" + std::to_string(i), 1); + const int filament_type = variables.value("filament_slot" + std::to_string(i), 1); + const int vendor_type = variables.value("vendor_slot" + std::to_string(i), 0); + + // Check filament presence via runout sensor + std::string box_stepper_key = "box_stepper slot" + std::to_string(i); + tray.has_filament = false; + if (status.contains(box_stepper_key)) { + auto& box_stepper = status[box_stepper_key]; + if (box_stepper.contains("runout_button") && !box_stepper["runout_button"].is_null()) { + int runout_button = box_stepper["runout_button"].template get(); + tray.has_filament = (runout_button == 0); + } + } + + if (tray.has_filament) { + // Look up filament type name from dictionary + std::string filament_name = "PLA"; + auto filament_it = dict.filaments.find(filament_type); + if (filament_it != dict.filaments.end()) { + filament_name = filament_it->second; + } + tray.tray_type = normalize_filament_type(filament_name); + + // Try Qidi-specific setting ID first; fall back to visible preset by type + std::string setting_id = build_setting_id(filament_type, vendor_type, tray.tray_type); + auto* bundle = GUI::wxGetApp().preset_bundle; + if (!bundle) { + tray.tray_info_idx = setting_id; + } else if (!setting_id.empty() && has_visible_base_preset(bundle->filaments, setting_id)) { + tray.tray_info_idx = setting_id; + } else { + tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); + } + + // Look up color from dictionary + auto color_it = dict.colors.find(color_index); + if (color_it != dict.colors.end()) { + tray.tray_color = color_it->second; + } else { + tray.tray_color = "FFFFFFFF"; + } + } + + trays.push_back(tray); + } + + return true; +} + +bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url, + const std::string& api_key, + QidiFilamentDict& dict, + std::string& error) const +{ + std::string url = join_url(base_url, "/server/files/config/officiall_filas_list.cfg"); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + dict.colors.clear(); + dict.filaments.clear(); + parse_ini_section(response_body, "colordict", dict.colors); + parse_filament_sections(response_body, dict.filaments); + + return !dict.colors.empty(); +} + +void QidiPrinterAgent::parse_ini_section(const std::string& content, const std::string& section_name, std::map& result) +{ + std::istringstream stream(content); + 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; + } + } + } + } +} + +std::string QidiPrinterAgent::map_filament_type_to_setting_id(const std::string& filament_type) +{ + const std::string upper = trim_and_upper(filament_type); + + 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 ""; +} + +std::string QidiPrinterAgent::normalize_model_key(std::string value) +{ + boost::algorithm::to_lower(value); + std::string normalized; + normalized.reserve(value.size()); + for (unsigned char c : value) { + if (std::isalnum(c)) { + normalized.push_back(static_cast(c)); + } + } + return normalized; +} + +std::string QidiPrinterAgent::infer_series_id(const std::string& model_id, const std::string& dev_name) +{ + std::string source = model_id.empty() ? dev_name : model_id; + boost::trim(source); + if (source.empty()) { + return ""; + } + if (is_numeric(source)) { + return source; + } + + const std::string key = normalize_model_key(source); + if (key.find("q2") != std::string::npos) { + return "1"; + } + if (key.find("xmax") != std::string::npos && key.find("4") != std::string::npos) { + return "3"; + } + if ((key.find("xplus") != std::string::npos || key.find("plus") != std::string::npos) && key.find("4") != std::string::npos) { + return "0"; + } + return ""; +} + +std::string QidiPrinterAgent::normalize_filament_type(const std::string& filament_type) +{ + const std::string upper = trim_and_upper(filament_type); + + 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 upper; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/QidiPrinterAgent.hpp b/src/slic3r/Utils/QidiPrinterAgent.hpp new file mode 100644 index 00000000000..a8326edb535 --- /dev/null +++ b/src/slic3r/Utils/QidiPrinterAgent.hpp @@ -0,0 +1,52 @@ +#ifndef __QIDI_PRINTER_AGENT_HPP__ +#define __QIDI_PRINTER_AGENT_HPP__ + +#include "MoonrakerPrinterAgent.hpp" + +#include +#include +#include + +namespace Slic3r { + +class QidiPrinterAgent final : public MoonrakerPrinterAgent +{ +public: + explicit QidiPrinterAgent(std::string log_dir); + ~QidiPrinterAgent() override = default; + + static AgentInfo get_agent_info_static(); + AgentInfo get_agent_info() override { return get_agent_info_static(); } + + // Override filament sync (Qidi-specific implementation) + bool fetch_filament_info(std::string dev_id) override; + +private: + struct QidiFilamentDict + { + std::map colors; + std::map filaments; + }; + + // Qidi-specific methods + bool fetch_slot_info(const std::string& base_url, + const std::string& api_key, + const QidiFilamentDict& dict, + const std::string& series_id, + std::vector& trays, + int& box_count, + std::string& error); + bool fetch_filament_dict(const std::string& base_url, const std::string& api_key, QidiFilamentDict& dict, std::string& error) const; + std::string normalize_filament_type(const std::string& filament_type); + std::string infer_series_id(const std::string& model_id, const std::string& dev_name); + std::string normalize_model_key(std::string value); + + // Static helpers + static void parse_ini_section(const std::string& content, const std::string& section_name, std::map& result); + static void parse_filament_sections(const std::string& content, std::map& result); + static std::string map_filament_type_to_setting_id(const std::string& filament_type); +}; + +} // namespace Slic3r + +#endif diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp new file mode 100644 index 00000000000..9b9a6809fd1 --- /dev/null +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp @@ -0,0 +1,228 @@ +#include "SnapmakerPrinterAgent.hpp" +#include "Http.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "slic3r/GUI/GUI_App.hpp" + +#include "nlohmann/json.hpp" +#include + +namespace Slic3r { + +namespace { + +constexpr const char* SNAPMAKER_AGENT_VERSION = "0.0.1"; + +// Safely access a parallel array by index, returning a fallback if out of bounds. +template +T safe_at(const std::vector& vec, int index, const T& fallback) +{ + return (index >= 0 && index < static_cast(vec.size())) ? vec[index] : fallback; +} + +std::string find_closest_color_preset_by_vendor_and_type(const PresetCollection& filaments, + const std::string& vendor_name, + const std::string& filament_type, + const std::string& color_rgba) +{ + std::string best_match_id = ""; + int best_color_distance = 0xffffffff; + + for (const auto& p : filaments.get_presets()) { + if (p.is_visible && p.is_compatible && + // Filament profile must be detached from parent to be considered for matching + filaments.get_preset_base(p) == &p && p.config.opt_string("filament_vendor", 0u) == vendor_name && + p.config.opt_string("filament_type", 0u) == filament_type) { + // The printer returns RGBA in the format RRGGBBAA, but profiles store color as #RRGGBB, + // so we must remove # and ignore alpha channel for distance calculation + unsigned int target_color_value = std::stoul(color_rgba.substr(0, color_rgba.length() - 2), nullptr, 16); + + std::string p_color = p.config.opt_string("default_filament_colour", 0u); + unsigned int p_color_value; + if (!p_color.empty()) { + unsigned int hash_pos = p_color.find("#"); + p_color_value = std::stoul(p_color.substr(hash_pos != std::string::npos ? hash_pos + 1 : 0), nullptr, 16); + } else { + // Default to black if no color specified in profile. Assume other profiles might be a closer color match. + // Could be a problem if the target color is also black and there exist a specific profile for that type, vendor and color + // combination. + p_color_value = 0; + } + + // Calculate Euclidean color distance in RGB space + int dr = ((target_color_value & 0xff) - (p_color_value & 0xff)); + int dg = (((target_color_value >> 8) & 0xff) - ((p_color_value >> 8) & 0xff)); + int db = (((target_color_value >> 16) & 0xff) - ((p_color_value >> 16) & 0xff)); + unsigned int distance = dr * dr + dg * dg + db * db; + + if (distance < best_color_distance) { + best_color_distance = distance; + best_match_id = p.filament_id; + } + } + } + return best_match_id; +} + +} // anonymous namespace + +SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {} + +AgentInfo SnapmakerPrinterAgent::get_agent_info_static() +{ + return AgentInfo{"snapmaker", "Snapmaker", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"}; +} + +std::string SnapmakerPrinterAgent::combine_filament_type(const std::string& type, const std::string& sub_type) +{ + const std::string base = trim_and_upper(type); + const std::string sub = trim_and_upper(sub_type); + + if (base.empty()) + return "PLA"; + + if (sub.empty() || sub == "NONE") + return base; + + if (sub == "CF") + return base + "-CF"; + if (sub == "GF") + return base + "-GF"; + if (sub == "SNAPSPEED" || sub == "HS") + return base + " HIGH SPEED"; + if (sub == "SILK") + return base + " SILK"; + if (sub == "WOOD") + return base + " WOOD"; + if (sub == "MATTE") + return base + " MATTE"; + if (sub == "MARBLE") + return base + " MARBLE"; + + // Unrecognized sub-type (brand names like Polylite, Basic, etc.) -- use base type only + return base; +} + +bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id) +{ + std::string url = join_url(device_info.base_url, "/printer/objects/query?print_task_config&filament_detect"); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!device_info.api_key.empty()) { + http.header("X-Api-Key", device_info.api_key); + } + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: HTTP request failed: " << http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Invalid JSON response"; + return false; + } + + // Navigate to result.status.print_task_config + if (!json.contains("result") || !json["result"].contains("status") || + !json["result"]["status"].contains("print_task_config")) { + BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Missing print_task_config in response"; + return false; + } + + auto& ptc = json["result"]["status"]["print_task_config"]; + + // Read parallel arrays from print_task_config + auto filament_exist = ptc.value("filament_exist", std::vector{}); + auto filament_type = ptc.value("filament_type", std::vector{}); + auto filament_sub_type = ptc.value("filament_sub_type", std::vector{}); + auto filament_color = ptc.value("filament_color_rgba", std::vector{}); + auto filament_vendor = ptc.value("filament_vendor", std::vector{}); + + const int slot_count = static_cast(filament_exist.size()); + if (slot_count == 0) { + BOOST_LOG_TRIVIAL(info) << "SnapmakerPrinterAgent::fetch_filament_info: No filament slots reported"; + return false; + } + + // Read NFC filament_detect data for temperature info (optional) + nlohmann::json nfc_info; + if (json["result"]["status"].contains("filament_detect") && + json["result"]["status"]["filament_detect"].contains("info")) { + nfc_info = json["result"]["status"]["filament_detect"]["info"]; + } + + static const std::string empty_str; + static const std::string default_color = "FFFFFFFF"; + + std::vector trays; + trays.reserve(slot_count); + + for (int i = 0; i < slot_count; ++i) { + AmsTrayData tray; + tray.slot_index = i; + tray.has_filament = filament_exist[i]; + + if (tray.has_filament) { + tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), + safe_at(filament_sub_type, i, empty_str)); + tray.tray_color = safe_at(filament_color, i, default_color); + + auto* bundle = GUI::wxGetApp().preset_bundle; + // Try to find a matching preset for this filament based on vendor, type and color. + // If not found, default to traditional search by type only or generic type mapping. + if (bundle) { + std::string vendor = safe_at(filament_vendor, i, empty_str); + std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendor, tray.tray_type, + tray.tray_color); + + if (!filament_id.empty()) { + tray.tray_info_idx = filament_id; + BOOST_LOG_TRIVIAL(warning) << "Filament sync: Found manufacturer-specific profile for slot " << i << ": " + << filament_id; + } else { + tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); + } + } else { + tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); + } + + // Extract NFC temperature data if available + if (nfc_info.is_array() && i < static_cast(nfc_info.size()) && nfc_info[i].is_object()) { + auto& nfc_slot = nfc_info[i]; + std::string vendor = nfc_slot.value("VENDOR", "NONE"); + if (vendor != "NONE" && !vendor.empty()) { + tray.bed_temp = nfc_slot.value("BED_TEMP", 0); + tray.nozzle_temp = nfc_slot.value("FIRST_LAYER_TEMP", 0); + } + } + } + + trays.emplace_back(std::move(tray)); + } + + build_ams_payload(1, slot_count - 1, trays); + return true; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.hpp b/src/slic3r/Utils/SnapmakerPrinterAgent.hpp new file mode 100644 index 00000000000..04c0a66b3fc --- /dev/null +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "MoonrakerPrinterAgent.hpp" + +#include + +namespace Slic3r { + +class SnapmakerPrinterAgent final : public MoonrakerPrinterAgent +{ +public: + explicit SnapmakerPrinterAgent(std::string log_dir); + ~SnapmakerPrinterAgent() override = default; + + static AgentInfo get_agent_info_static(); + AgentInfo get_agent_info() override { return get_agent_info_static(); } + + bool fetch_filament_info(std::string dev_id) override; + +private: + // Combine filament_type + filament_sub_type into a unified type string + static std::string combine_filament_type(const std::string& type, const std::string& sub_type); +}; + +} // namespace Slic3r