From c467990724b4f16bdaec898d5f11bf14a6be5821 Mon Sep 17 00:00:00 2001 From: 3DPrinterOS SDK <74248977+3DPrinterOS-SDK@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:31:57 +0300 Subject: [PATCH] Support for 3DPrinterOS cloud integration (#10403) --- src/libslic3r/PrintConfig.cpp | 5 +- src/libslic3r/PrintConfig.hpp | 2 +- src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/PhysicalPrinterDialog.cpp | 23 +- src/slic3r/Utils/3DPrinterOS.cpp | 667 +++++++++++++++++++++++ src/slic3r/Utils/3DPrinterOS.hpp | 80 +++ src/slic3r/Utils/PrintHost.cpp | 2 + 7 files changed, 776 insertions(+), 5 deletions(-) create mode 100755 src/slic3r/Utils/3DPrinterOS.cpp create mode 100755 src/slic3r/Utils/3DPrinterOS.hpp diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index dafa3bfdb9..8da03fd969 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -148,7 +148,8 @@ static t_config_enum_values s_keys_map_PrintHostType { { "obico", htObico }, { "flashforge", htFlashforge }, { "simplyprint", htSimplyPrint }, - { "elegoolink", htElegooLink } + { "elegoolink", htElegooLink }, + { "3dprinteros", ht3DPrinterOS } }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintHostType) @@ -4860,6 +4861,7 @@ void PrintConfigDef::init_fff_params() def->enum_values.push_back("flashforge"); def->enum_values.push_back("simplyprint"); def->enum_values.push_back("elegoolink"); + def->enum_values.push_back("3dprinteros"); def->enum_labels.push_back("PrusaLink"); def->enum_labels.push_back("PrusaConnect"); def->enum_labels.push_back("Octo/Klipper"); @@ -4874,6 +4876,7 @@ void PrintConfigDef::init_fff_params() def->enum_labels.push_back("Flashforge"); def->enum_labels.push_back("SimplyPrint"); def->enum_labels.push_back("Elegoo Link"); + def->enum_labels.push_back("3DPrinterOS"); def->mode = comAdvanced; def->cli = ConfigOptionDef::nocli; def->set_default_value(new ConfigOptionEnum(htOctoPrint)); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 2dabe8e892..5cc89f4ae5 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -77,7 +77,7 @@ enum class WipeTowerType { }; enum PrintHostType { - htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink + htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink, ht3DPrinterOS }; enum AuthorizationType { diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index c4d6369b72..34286d1411 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -659,6 +659,8 @@ set(SLIC3R_GUI_SOURCES Utils/UndoRedo.cpp Utils/UndoRedo.hpp Utils/WebSocketClient.hpp + Utils/3DPrinterOS.hpp + Utils/3DPrinterOS.cpp Utils/WxFontUtils.cpp Utils/WxFontUtils.hpp Utils/FileTransferUtils.cpp diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 7a85a4c1f2..4a371a46a6 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -40,6 +40,7 @@ #include "MsgDialog.hpp" #include "OAuthDialog.hpp" #include "SimplyPrint.hpp" +#include "3DPrinterOS.hpp" namespace Slic3r { namespace GUI { @@ -273,6 +274,12 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr } else { msg = r.error_message; } + } else if (const auto h = dynamic_cast(host.get()); h) { + GUI::MessageDialog dlg(this, _L("Valid session not detected. Proceed with login to 3DPrinterOS?"), _L("Proceed"), + wxICON_INFORMATION | wxYES | wxNO); + if (dlg.ShowModal() == wxID_YES) { + result = h->login(msg); + } } else { PrinterCloudAuthDialog dlg(this->GetParent(), host.get()); dlg.ShowModal(); @@ -644,7 +651,8 @@ void PhysicalPrinterDialog::update(bool printer_change) const auto current_host = temp->GetValue(); if (current_host == L"https://connect.prusa3d.com" || current_host == L"https://app.obico.io" || - current_host == "https://simplyprint.io" || current_host == "https://simplyprint.io/panel") { + current_host == "https://simplyprint.io" || current_host == "https://simplyprint.io/panel" || + current_host == C3DPrinterOS::default_host()) { temp->SetValue(wxString()); m_config->opt_string("print_host") = ""; } @@ -677,7 +685,7 @@ void PhysicalPrinterDialog::update(bool printer_change) m_config->opt_string("print_host") = "https://app.obico.io"; } } - } else if (opt->value == htSimplyPrint) { + } else if (opt->value == htSimplyPrint) { // Set the host url if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) { printhost_field->disable(); @@ -714,7 +722,16 @@ void PhysicalPrinterDialog::update(bool printer_change) m_optgroup->disable_field("printhost_ssl_ignore_revoke"); if (m_printhost_cafile_browse_btn) m_printhost_cafile_browse_btn->Disable(); - } + } else if (opt->value == ht3DPrinterOS) { + if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) { + if (wxTextCtrl* temp = dynamic_cast(printhost_field)->text_ctrl(); temp && temp->GetValue().IsEmpty()) { + temp->SetValue(C3DPrinterOS::default_host()); + m_config->opt_string("print_host") = C3DPrinterOS::default_host(); + } + } + m_optgroup->hide_field("print_host_webui"); + m_optgroup->hide_field("printhost_apikey"); + } } if (opt->value == htFlashforge) { diff --git a/src/slic3r/Utils/3DPrinterOS.cpp b/src/slic3r/Utils/3DPrinterOS.cpp new file mode 100755 index 0000000000..f135c1ff36 --- /dev/null +++ b/src/slic3r/Utils/3DPrinterOS.cpp @@ -0,0 +1,667 @@ +#include "3DPrinterOS.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/Utils.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/GUI_Utils.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/Widgets/ComboBox.hpp" +#include "slic3r/GUI/Widgets/Button.hpp" +#include "slic3r/GUI/GUI_App.hpp" + +#include "Http.hpp" +#include + + +namespace fs = boost::filesystem; +namespace pt = boost::property_tree; + +namespace { + +class UploadOptionsDialog : public Slic3r::GUI::DPIDialog +{ +public: + UploadOptionsDialog(wxWindow* parent, + const wxArrayString& cloud_projects, + const wxArrayString& cloud_printer_types, + const wxString preset_name) + : Slic3r::GUI::DPIDialog(parent, + wxID_ANY, + "3DPrinterOS Cloud upload options", + wxDefaultPosition, + wxSize(100 * Slic3r::GUI::wxGetApp().em_unit(), -1), + wxDEFAULT_DIALOG_STYLE), + okButton(nullptr) + { + SetFont(Slic3r::GUI::wxGetApp().normal_font()); + SetBackgroundColour(*wxWHITE); + SetForegroundColour(*wxBLACK); + + singleRadio = new wxRadioButton(this, wxID_ANY, "Single file", wxDefaultPosition, wxDefaultSize, wxRB_GROUP); + projectRadio = new wxRadioButton(this, wxID_ANY, "Project File"); + projectsLabel = new wxStaticText(this, wxID_ANY, "Project:"); + wxStaticText* printerLabel = new wxStaticText(this, wxID_ANY, "Printer type:"); + + projectsComboBox = new wxComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, DD_NO_CHECK_ICON); + printerTypeComboBox = new wxComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, DD_NO_CHECK_ICON | wxTE_READONLY); + + printerWarningLabel = new wxStaticText(this, wxID_ANY, "Printer type not found, please select manually."); + printerWarningLabel->SetForegroundColour(*wxRED); + printerWarningLabel->Hide(); + + for (int i = 0; i < cloud_projects.size(); i++) { + projectsComboBox->Append(cloud_projects[i]); + } + if (cloud_printer_types.size() > 0) { + for (int i = 0; i < cloud_printer_types.size(); i++) { + printerTypeComboBox->Append(cloud_printer_types[i]); + if (cloud_printer_types[i].Find(preset_name) != wxNOT_FOUND && printerTypeComboBox->GetSelection() == -1) { + printerTypeComboBox->SetSelection(i); + } + } + if (printerTypeComboBox->GetCount() > 1) { + printerWarningLabel->Show(); + } else { + printerTypeComboBox->SetSelection(0); + } + } + + okButton = new wxButton(this, wxID_OK, "OK"); + wxButton* cancelButton = new wxButton(this, wxID_CANCEL, "Cancel"); + + wxBoxSizer* radioSizer = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer* btnSizer = new wxBoxSizer(wxHORIZONTAL); + radioSizer->Add(singleRadio, 0, wxALL, 5); + radioSizer->Add(projectRadio, 0, wxALL, 5); + btnSizer->Add(okButton, 0, wxALL | wxALIGN_CENTER, 5); + btnSizer->Add(cancelButton, 0, wxALL | wxALIGN_CENTER, 5); + + wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(radioSizer, 0, wxALL, 5); + sizer->Add(projectsLabel, 0, wxALL, 5); + sizer->Add(projectsComboBox, 0, wxALL | wxEXPAND, 5); + sizer->Add(printerLabel, 0, wxALL, 5); + sizer->Add(printerTypeComboBox, 0, wxALL | wxEXPAND, 5); + sizer->Add(printerWarningLabel, 0, wxLEFT | wxRIGHT | wxBOTTOM, 5); + sizer->Add(btnSizer, 0, wxALL | wxALIGN_CENTER, 5); + SetSizer(sizer); + sizer->Fit(this); + projectsComboBox->Hide(); + projectsLabel->Hide(); + projectRadio->Bind(wxEVT_RADIOBUTTON, &UploadOptionsDialog::OnRadioButtonSelected, this); + singleRadio->Bind(wxEVT_RADIOBUTTON, &UploadOptionsDialog::OnRadioButtonSelected, this); + // Bind combo box selection change to validation + printerTypeComboBox->Bind(wxEVT_COMBOBOX, &UploadOptionsDialog::OnPrinterTypeChanged, this); + ValidateOkButton(); // Initial validation + Slic3r::GUI::wxGetApp().UpdateDlgDarkUI(this); + + CenterOnParent(); + } + + void OnRadioButtonSelected(wxCommandEvent& event) + { + wxRadioButton* selectedRadio = dynamic_cast(event.GetEventObject()); + if (selectedRadio) { + wxString label = selectedRadio->GetLabel(); + if (label == wxString("Project File")) { + projectsComboBox->Show(); + projectsLabel->Show(); + } else { + projectsComboBox->Hide(); + projectsLabel->Hide(); + } + Layout(); + } + } + + void on_dpi_changed(const wxRect& suggested_rect) {} + + void OnPrinterTypeChanged(wxCommandEvent& event) + { + ValidateOkButton(); + event.Skip(); + } + + void ValidateOkButton() + { + bool hasSelection = (printerTypeComboBox->GetSelection() != wxNOT_FOUND); + okButton->Enable(hasSelection); + } + + void GetValues(std::string& project, std::string& printer_type) + { + project = projectRadio->GetValue() ? std::string(projectsComboBox->GetValue().c_str()) : ""; + printer_type = std::string(printerTypeComboBox->GetValue().c_str()); + } + +private: + wxComboBox* projectsComboBox; + wxComboBox* printerTypeComboBox; + wxStaticText* projectsLabel; + wxStaticText* printerWarningLabel; + wxRadioButton* singleRadio; + wxRadioButton* projectRadio; + wxButton* okButton; +}; + + +class TokenAuthDialog : public Slic3r::GUI::DPIDialog +{ +public: + TokenAuthDialog(wxWindow* parent, const std::string &url, const std::string& token, const std::string &cafile, pt::ptree& resp) + : Slic3r::GUI::DPIDialog(parent, + wxID_ANY, + "3DPrinterOS", + wxDefaultPosition, + wxSize(45 * Slic3r::GUI::wxGetApp().em_unit(), -1), + wxDEFAULT_DIALOG_STYLE) + , m_url(url) + , m_token(token) + , m_cafile(cafile) + , m_resp(resp) + { + SetFont(Slic3r::GUI::wxGetApp().normal_font()); + SetBackgroundColour(*wxWHITE); + SetForegroundColour(*wxBLACK); + + auto* sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(new wxStaticText(this, wxID_ANY, "Authorizing..."), 1, wxALL | wxCENTER, 10); + auto* cancelBtn = new wxButton(this, wxID_CANCEL, "Cancel"); + sizer->Add(cancelBtn, 0, wxALL | wxALIGN_CENTER, 10); + SetSizerAndFit(sizer); + + Bind(wxEVT_THREAD, [this](wxThreadEvent& e) { EndModal(e.GetId()); }); + Bind(wxEVT_TIMER, &TokenAuthDialog::OnRetry, this); + Bind(wxEVT_SHOW, &TokenAuthDialog::OnShow, this); + Bind(wxEVT_BUTTON, &TokenAuthDialog::OnCancel, this, wxID_CANCEL); + + m_timer.SetOwner(this); + Slic3r::GUI::wxGetApp().UpdateDlgDarkUI(this); + CenterOnParent(); + } + + void on_dpi_changed(const wxRect& suggested_rect) {} + +private: + + void OnShow(wxShowEvent& event) + { + if (event.IsShown() && !m_started) { + m_started = true; + SendRequest(); + } + event.Skip(); + } + + void OnCancel(wxCommandEvent&) + { + m_cancelled = true; + if (m_http_ptr) { + m_http_ptr->cancel(); // abort the background request + } + EndModal(wxID_CANCEL); + } + + void OnRetry(wxTimerEvent&) { SendRequest(); } + + void SendRequest() + { + if (m_cancelled || m_attempt >= m_max_retries) { + if (m_attempt >= m_max_retries) { + m_resp.put("result", false); + m_resp.put("message", "Maximum login retries exceeded"); + } + wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT)); + return; + } + + m_attempt++; + std::string postBody = "token=" + m_token; + auto http = Slic3r::Http::post(m_url); + http.timeout_max(60); + if (!m_cafile.empty()) { + http.ca_file(m_cafile); + } + http.header("Content-Length", std::to_string(postBody.size())); + http.set_post_body(postBody); + http.on_error([this](std::string, std::string error, unsigned status) { + if (!m_cancelled) { + m_resp.put("result", false); + m_resp.put("message", (status != 200) ? "HTTP error: " + std::to_string(status) : error); + wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT)); + } + }) + .on_complete([this](std::string body, unsigned status) { + if (!m_cancelled) { + if (status != 200) { + m_resp.put("result", false); + m_resp.put("message", "HTTP error: " + std::to_string(status)); + wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT)); + return; + } + + try { + std::stringstream ss(body); + pt::read_json(ss, m_resp); + } catch (...) { + m_resp.put("result", false); + m_resp.put("message", "Could not parse server response"); + } + + if (m_resp.get("result", false) && m_resp.get_optional("message.session").has_value()) { + wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_OK)); + } else if (m_resp.get("result", false)) { + if (m_attempt < m_max_retries) + m_timer.StartOnce(m_retry_delay_ms); + else + wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT)); + } else { + wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT)); + } + } + }); + m_http_ptr = http.perform(); + } + +private: + std::string m_token; + std::string m_url; + std::string m_cafile; + pt::ptree& m_resp; + wxTimer m_timer; + std::shared_ptr m_http_ptr; + bool m_cancelled{false}; + bool m_started{false}; + int m_attempt{0}; + const int m_max_retries{10}; + const int m_retry_delay_ms{500}; +}; +} // namespace + +namespace Slic3r { + +static const std::string API_CREDENTIALS_PATH = "3dprinteros_api_cred.json"; + +C3DPrinterOS::C3DPrinterOS(DynamicPrintConfig *config) + : m_host(config->opt_string("print_host")) + , m_apikey(config->opt_string("printhost_apikey")) + , m_preset_name(config->opt_string("printer_model")) +{ + m_api_session_file_path = (boost::filesystem::path(Slic3r::data_dir()) / API_CREDENTIALS_PATH) + .make_preferred() + .string(); + load_api_session(); +} + +const char *C3DPrinterOS::get_name() const { return "3DPrinterOS"; } + +bool C3DPrinterOS::test(wxString &msg) const +{ + return check_session(msg); +} + +bool C3DPrinterOS::login(wxString& msg) const +{ + // Get token for auth + msg.clear(); + std::string token = get_api_auth_token(msg); + if (token.empty()) { + msg = "Error. Can't get api token for authorization"; + return false; + } + + auto login_url = make_url("noauth/apiglobal_login_with_token/" + token); + wxLaunchDefaultBrowser(login_url); + pt::ptree login_resp; + login_with_token(login_resp, token); + std::string session, email; + try { + if (login_resp.get("result")) { + session = login_resp.get("message.session"); + email = login_resp.get("message.email"); + } else { + msg = wxString(login_resp.get("message").c_str()); + return false; + } + } catch (const std::exception&) { + msg = "Could not parse server response"; + return false; + } + bool res = save_api_session(session, email); + if (!res) { + msg = "Error saving session to file"; + } + return res; +} + +wxString C3DPrinterOS::get_test_ok_msg() const +{ + return _("Connection to 3DPrinterOS cloud works correctly.") + (!m_username.empty() ? "" + _(" Logined as user: ") + m_username : ""); +} + +wxString C3DPrinterOS::get_test_failed_msg(wxString &msg) const +{ + return GUI::format_wxstr("%s: %s\n\n", _L("Error session check"), msg); +} + +bool C3DPrinterOS::upload( + PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn +) const +{ + const char *name = get_name(); + const auto upload_filename = upload_data.upload_path.filename(); + const auto upload_parent_path = upload_data.upload_path.parent_path(); + wxString test_msg; + if (!check_session(test_msg)) { + error_fn(std::move(test_msg)); + return false; + } + + pt::ptree cloud_project_resp; + pt::ptree cloud_printer_types_resp; + + get_cloud_projects_list(cloud_project_resp); + get_cloud_printer_types(cloud_printer_types_resp, m_preset_name); + wxArrayString cloud_projects_list; + wxArrayString cloud_printer_types_list; + + try { + if (cloud_project_resp.get("result")) { + for (const auto &messageItem : cloud_project_resp.get_child("message")) { + cloud_projects_list.Add(messageItem.second.get("name")); + } + } + + if (cloud_printer_types_resp.get("result")) { + for (const auto &messageItem : cloud_printer_types_resp.get_child("message")) { + cloud_printer_types_list.Add(messageItem.second.get("description")); + } + } + } catch (const std::exception &) { + error_fn("Could not parse server response"); + return false; + } + + // Show "Confirm cloud printer type and project for 3DPrinterOS upload + + UploadOptionsDialog dlg(GUI::wxGetApp().GetTopWindow(), cloud_projects_list, cloud_printer_types_list, m_preset_name); + + if (dlg.ShowModal() != wxID_OK) { + error_fn("Canceled"); + return false; + } + + std::string selected_project; + std::string selected_printer_type; + dlg.GetValues(selected_project, selected_printer_type); + std::string project_id; + std::string printer_type_id; + + // search for cloud project_id by name + if (!selected_project.empty()) { + for (const auto& messageItem : cloud_project_resp.get_child("message")) { + if (messageItem.second.get("name", "") == selected_project) { + project_id = messageItem.second.get("id", ""); + break; + } + } + } + + // search for cloud printer_type_id by name + for (const auto& messageItem : cloud_printer_types_resp.get_child("message")) { + if (messageItem.second.get("description", "") == selected_printer_type) { + printer_type_id = messageItem.second.get("id", ""); + break; + } + } + + bool res = true; + auto url = make_url("apiglobal/upload"); + std::string file_id; + pt::ptree uploadResponse; + auto http = Http::post(std::move(url)); + if (!m_cafile.empty()) { + http.ca_file(m_cafile); + } + http.form_add("session", m_apikey) + .form_add("upload_type_id", "7") + .form_add("upload_soft_name", "OrcaSlicer") + .form_add("zip", "false") + .form_add_file("file", upload_data.source_path.string(), upload_filename.string()); + + if (!project_id.empty()) { + http.form_add("project_id", project_id); + } else if (!selected_project.empty()) { + http.form_add("project_name", selected_project); + http.form_add("project_color", "grey"); + } + + http.on_complete([&](std::string body, unsigned status) { + std::stringstream ss(body); + try { + pt::read_json(ss, uploadResponse); + } catch (const std::exception &) { + uploadResponse.put("result", false); + uploadResponse.put("message", "Could not parse server response"); + } + }) + .on_error([&](std::string body, std::string error, unsigned status) { + error_fn(format_error(body, error, status)); + res = false; + }) + .on_progress([&](Http::Progress progress, bool &cancel) { + prorgess_fn(std::move(progress), cancel); + if (cancel) { + res = false; + } + }) + .perform_sync(); + + try { + if (uploadResponse.get("result")) { + file_id = uploadResponse.get("message.file_id"); + } else { + res = false; + error_fn(uploadResponse.get("message")); + } + } catch (const std::exception &) { + res = false; + error_fn("Error during file upload"); + } + // set printer type for uploaded gcode + if (res) { + pt::ptree update_file_response; + update_file(update_file_response, file_id, printer_type_id, "OrcaSlicer"); + try { + if (!update_file_response.get("result")) { + const std::string msg = update_file_response.get("message", "Unknown update error"); + BOOST_LOG_TRIVIAL(warning) << "Failed to update uploaded file: " << msg; + } + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(warning) << "Could not parse update response: " << ex.what(); + } + if (upload_data.post_action == PrintHostPostUploadAction::StartPrint && !upload_data.use_3mf) { + auto quick_print_url = make_url("quickprint?file_id=" + file_id); + wxLaunchDefaultBrowser(quick_print_url); + } + } + + return res; +} + +void C3DPrinterOS::log_out() const +{ + boost::filesystem::remove(m_api_session_file_path.c_str()); +} + +bool C3DPrinterOS::validate_version_text(const boost::optional &version_text) const +{ + return version_text ? boost::starts_with(*version_text, "3DPrinterOS") : true; +} + +std::string C3DPrinterOS::make_url(const std::string &path) const +{ + if (m_host.find("http://") == 0 || m_host.find("https://") == 0) { + if (m_host.back() == '/') { + return (boost::format("%1%%2%") % m_host % path).str(); + } else { + return (boost::format("%1%/%2%") % m_host % path).str(); + } + } else { + return (boost::format("https://%1%/%2%") % m_host % path).str(); + } +} + +std::string C3DPrinterOS::get_api_auth_token(wxString &err) const +{ + std::string result; + pt::ptree resp; + std::string postBody = "app_type=plugin&app_name=" + Http::url_encode("OrcaSlicer"); + send_form("apiglobal/generate_login_token", postBody, resp); + try { + if (resp.get("result")) { + result = resp.get("message"); + } else { + err = wxString(resp.get("message").c_str()); + } + } catch (const std::exception &) { + err = "Could not parse server response"; + } + return result; +} + +void C3DPrinterOS::login_with_token(pt::ptree &resp, const std::string &token) const { + auto url = make_url("apiglobal/login_with_token"); + TokenAuthDialog dlg(GUI::wxGetApp().GetTopWindow(), url, token, m_cafile, resp); + dlg.ShowModal(); +} + +bool C3DPrinterOS::check_session(wxString &msg) const { + std::string postBody = "session=" + m_apikey; + pt::ptree resp; + send_form("apiglobal/check_session", postBody, resp); + try { + if (resp.get("result")) { + return true; + } else { + msg = wxString(resp.get("message").c_str()); + return false; + } + + } catch (const std::exception &) { + msg = wxString("Could not parse server response"); + return false; + } + return false; +} + +bool C3DPrinterOS::save_api_session(const std::string &session, const std::string &email) const { + pt::ptree j; + j.put("session", session); + j.put("email", email); + try { + auto temp_path = m_api_session_file_path + ".tmp"; + pt::write_json(temp_path, j); + boost::filesystem::rename(temp_path, m_api_session_file_path); + } catch (const std::exception &err) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to write json to file. Path = " + << m_api_session_file_path + << " Reason = " << err.what(); + return false; + } + return true; +} + +void C3DPrinterOS::load_api_session() +{ + m_apikey.clear(); + if (boost::filesystem::exists(m_api_session_file_path)) { + pt::ptree j; + try { + pt::read_json(m_api_session_file_path, j); + m_apikey = j.get("session"); + m_username = j.get("email"); + } catch (const std::exception &err) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": load_api_session failed, reason = " << err.what(); + // remove corrupted file to avoid repeated failures + try { + boost::filesystem::remove(m_api_session_file_path); + } catch (...) {} + } + }; +} + +void C3DPrinterOS::send_form( + const std::string &endpoint, + const std::string &postBody, + boost::property_tree::ptree &responseTree +) const +{ + responseTree.clear(); + auto url = make_url(endpoint); + auto http = Http::post(std::move(url)); + if (!m_cafile.empty()) { + http.ca_file(m_cafile); + } + http.header("Content-length", std::to_string(postBody.size())); + http.set_post_body(postBody); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("Error sending form: %1%") % error; + responseTree.put("result", false); + responseTree.put("message", error); + }) + .on_complete([&, this](std::string body, unsigned) { + std::stringstream ss(body); + try { + pt::read_json(ss, responseTree); + } catch (const std::exception &) { + responseTree.put("result", false); + responseTree.put("message", "Could not parse server response"); + } + }) + .perform_sync(); +} + +void C3DPrinterOS::get_cloud_projects_list(boost::property_tree::ptree &response) const +{ + std::string postBody = std::string("session=" + m_apikey); + send_form("apiglobal/get_projects", postBody, response); +} + +void C3DPrinterOS::get_cloud_printer_types(boost::property_tree::ptree &response, const std::string &query) const +{ + std::string postBody = std::string("session=" + m_apikey); + if (!query.empty()) { + postBody += "&description=" + Http::url_encode(query) + "&software_version=" + Http::url_encode("OrcaSlicer"); + } + send_form("apiglobal/get_printer_types", postBody, response); +} + +void C3DPrinterOS::update_file(boost::property_tree::ptree &response, const std::string &file_id, const std::string &ptype, const std::string >ype) const +{ + std::string postBody = "session=" + m_apikey + + "&updates[" + file_id + "][ptype]=" + ptype + + "&updates[" + file_id + "][gtype]=" + Http::url_encode(gtype) + + "&updates[" + file_id + "][zip]=false"; + send_form("apiglobal/file_update", postBody, response); +} + +}; + // namespace Slic3r diff --git a/src/slic3r/Utils/3DPrinterOS.hpp b/src/slic3r/Utils/3DPrinterOS.hpp new file mode 100755 index 0000000000..8c8e4ca6dc --- /dev/null +++ b/src/slic3r/Utils/3DPrinterOS.hpp @@ -0,0 +1,80 @@ +#ifndef slic3r_3DPrinterOS_hpp_ +#define slic3r_3DPrinterOS_hpp_ + +#include +#include +#include +#include + +#include "PrintHost.hpp" +#include "slic3r/GUI/GUI.hpp" + + + +namespace Slic3r { + +class DynamicPrintConfig; +class Http; + + +class C3DPrinterOS : public PrintHost +{ +public: + C3DPrinterOS(DynamicPrintConfig *config); + ~C3DPrinterOS() override = default; + + const char* get_name() const override; + bool test(wxString &curl_msg) const override; + bool login(wxString &msg) const; + wxString get_test_ok_msg () const override; + wxString get_test_failed_msg (wxString &msg) const override; + bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; + bool has_auto_discovery() const override { return false; } + bool can_test() const override { return true; } + bool is_cloud() const override { return true; } + void log_out() const override; + bool is_logged_in() const override { return !m_apikey.empty(); } + PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint | PrintHostPostUploadAction::QueuePrint; } + std::string get_host() const override { return m_host; } + static std::string default_host() { return "https://cloud.3dprinteros.com"; } + +protected: + bool validate_version_text(const boost::optional &version_text) const; + +private: + std::string m_host; + std::string m_apikey; + std::string m_cafile; + std::string m_username; + std::string m_host_type; + std::string m_preset_name; + std::string m_api_session_file_path; + + void load_api_session(); + bool save_api_session(const std::string &session, const std::string &email) const; + std::string parse_printer_model(const std::string& input) const; + std::string make_url(const std::string &path) const; + std::string get_api_auth_token(wxString &err) const; + void login_with_token(boost::property_tree::ptree &resp, const std::string &token) const; + bool check_session(wxString &msg) const; + void send_form( + const std::string &endpoint, + const std::string &postBody, + boost::property_tree::ptree &responseTree + ) const; + + + void get_cloud_projects_list(boost::property_tree::ptree &response) const; + void get_cloud_printer_types(boost::property_tree::ptree &response, const std::string &querry) const; + void update_file( + boost::property_tree::ptree &response, + const std::string &file_id, + const std::string &ptype, + const std::string >ype + ) const; + +}; + +} + +#endif diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 3ed4e07382..e64220d30a 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -27,6 +27,7 @@ #include "Flashforge.hpp" #include "SimplyPrint.hpp" #include "ElegooLink.hpp" +#include "3DPrinterOS.hpp" namespace fs = boost::filesystem; using boost::optional; @@ -67,6 +68,7 @@ PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config) case htFlashforge: return new Flashforge(config); case htSimplyPrint: return new SimplyPrint(config); case htElegooLink: return new ElegooLink(config); + case ht3DPrinterOS: return new C3DPrinterOS(config); default: return nullptr; } } else {