From 427d0f7a9f5a81911e775fd6557f93078a421d92 Mon Sep 17 00:00:00 2001 From: anjis Date: Sat, 16 May 2026 15:22:31 +0800 Subject: [PATCH] Support file uploads and the device details page for Elegoo Centauri Carbon 2 (#13212) * Support file uploads and the device details page for CC2 printers. * Resolved build issues for Linux and macOS. * 1. Added `ElegooPrinterWebViewHandler` to handle WebUI messages for Elegoo printers. Other printers will keep the current behavior. 2. Added a static `get_print_host_webui` method in `PrintHost` to retrieve the printer WebUI URL. * Improved timeout handling for CC2 file upload and SN info APIs. --------- Co-authored-by: SoftFever --- .../web/lan_service_web/favicon.ico | Bin 0 -> 353 bytes .../elegoolink/web/lan_service_web/index.html | 265 +++++++++ src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/MainFrame.cpp | 6 +- src/slic3r/GUI/Plater.cpp | 5 +- src/slic3r/GUI/PrintHostDialogs.cpp | 2 +- src/slic3r/GUI/PrinterWebView.cpp | 48 +- src/slic3r/GUI/PrinterWebView.hpp | 9 +- src/slic3r/GUI/PrinterWebViewHandler.cpp | 339 ++++++++++++ src/slic3r/GUI/PrinterWebViewHandler.hpp | 36 ++ src/slic3r/Utils/ElegooLink.cpp | 502 ++++++++++++++++-- src/slic3r/Utils/ElegooLink.hpp | 32 +- src/slic3r/Utils/Http.cpp | 15 + src/slic3r/Utils/Http.hpp | 2 + src/slic3r/Utils/PrintHost.cpp | 34 ++ src/slic3r/Utils/PrintHost.hpp | 7 + 16 files changed, 1248 insertions(+), 56 deletions(-) create mode 100644 resources/plugins/elegoolink/web/lan_service_web/favicon.ico create mode 100644 resources/plugins/elegoolink/web/lan_service_web/index.html create mode 100644 src/slic3r/GUI/PrinterWebViewHandler.cpp create mode 100644 src/slic3r/GUI/PrinterWebViewHandler.hpp diff --git a/resources/plugins/elegoolink/web/lan_service_web/favicon.ico b/resources/plugins/elegoolink/web/lan_service_web/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0673a251e9c119bff6c2292499c3cd2d41b04db2 GIT binary patch literal 353 zcmV-n0iON<0096202USi0096X080S?02TlM0EtjeM-2)Z3IG5A4M|8uQUCw|761SM z76=9a006|aY&!q|0TM|>K~#90?UPLrgD?<K>ht)i8ii~=rIBD+OkYaR`bDGzf8{hfw=0JKL4moxj zRI_(68I`88EK8gpR6&K`fR>$_TGOrZV7XQ7#^CA>*N`FHsvM6a3~qi^H^er_I)gp@ z01X&IZ*mnNNipXh_?&YM`!k00g{DRUZmm)s{c1TUqkG^5@@!oAp^1ZKG{hR0-BdG_ z46^F@z*2Rw+qOO=vymd8TP1fna&W+%^|Sf{0RL`qI(*cI00000NkvXXu0mjf-UEwE literal 0 HcmV?d00001 diff --git a/resources/plugins/elegoolink/web/lan_service_web/index.html b/resources/plugins/elegoolink/web/lan_service_web/index.html new file mode 100644 index 0000000000..e552aa1e8e --- /dev/null +++ b/resources/plugins/elegoolink/web/lan_service_web/index.html @@ -0,0 +1,265 @@ +ELEGOO-Create The Future
\ No newline at end of file diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 7f5b1e05ba..c22b9b64fd 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -390,6 +390,8 @@ set(SLIC3R_GUI_SOURCES GUI/Printer/PrinterFileSystem.cpp GUI/Printer/PrinterFileSystem.h GUI/PrinterWebView.cpp + GUI/PrinterWebViewHandler.cpp + GUI/PrinterWebViewHandler.hpp GUI/PrinterWebView.hpp GUI/PrintHostDialogs.cpp GUI/PrintHostDialogs.hpp diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 7f598c3c62..f4aeae01f6 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -46,6 +46,7 @@ #include "Widgets/ProgressDialog.hpp" #include "BindDialog.hpp" #include "../Utils/MacDarkMode.hpp" +#include "../Utils/PrintHost.hpp" #include #include @@ -4162,15 +4163,12 @@ void MainFrame::load_printer_url() return; auto cfg = preset_bundle.printers.get_edited_preset().config; - wxString url = cfg.opt_string("print_host_webui").empty() ? cfg.opt_string("print_host") : cfg.opt_string("print_host_webui"); + wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); wxString apikey; const auto host_type = cfg.option>("host_type")->value; if (cfg.has("printhost_apikey") && (host_type == htPrusaLink || host_type == htPrusaConnect)) apikey = cfg.opt_string("printhost_apikey"); if (!url.empty()) { - if (!url.Lower().starts_with("http")) - url = wxString::Format("http://%s", url); - load_printer_url(url, apikey); } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index be952e7200..6c2254d302 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -81,6 +81,7 @@ #include "GUI_Utils.hpp" #include "GUI_Factories.hpp" #include "wxExtensions.hpp" +#include "../Utils/PrintHost.hpp" #include "MainFrame.hpp" #include "format.hpp" #include "3DScene.hpp" @@ -2483,13 +2484,11 @@ void Sidebar::update_all_preset_comboboxes() p->m_bpButton_ams_filament->Hide(); auto print_btn_type = MainFrame::PrintSelectType::eExportGcode; - wxString url = cfg.opt_string("print_host_webui").empty() ? cfg.opt_string("print_host") : cfg.opt_string("print_host_webui"); + wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); wxString apikey; if(url.empty()) url = wxString::Format("file://%s/web/orca/missing_connection.html", from_u8(resources_dir())); else { - if (!url.Lower().starts_with("http")) - url = wxString::Format("http://%s", url); const auto host_type = cfg.option>("host_type")->value; if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint)) apikey = cfg.opt_string("printhost_apikey"); diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp index 81da98366b..f59ff23e9d 100644 --- a/src/slic3r/GUI/PrintHostDialogs.cpp +++ b/src/slic3r/GUI/PrintHostDialogs.cpp @@ -643,7 +643,7 @@ void ElegooPrintHostSendDialog::init() { auto preset_bundle = wxGetApp().preset_bundle; auto model_id = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); - if (!boost::starts_with(model_id, "Elegoo-C")) { + if (model_id != "Elegoo-CC" && model_id != "Elegoo-C") { PrintHostSendDialog::init(); return; } diff --git a/src/slic3r/GUI/PrinterWebView.cpp b/src/slic3r/GUI/PrinterWebView.cpp index 9fd1dc5c46..1dccfab66f 100644 --- a/src/slic3r/GUI/PrinterWebView.cpp +++ b/src/slic3r/GUI/PrinterWebView.cpp @@ -1,16 +1,17 @@ #include "PrinterWebView.hpp" #include "I18N.hpp" +#include "PrinterWebViewHandler.hpp" #include "slic3r/GUI/PrinterWebView.hpp" #include "slic3r/GUI/wxExtensions.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/MainFrame.hpp" #include "libslic3r_version.h" +#include #include #include #include -#include #include #include @@ -19,13 +20,17 @@ #include #endif -namespace pt = boost::property_tree; - namespace Slic3r { namespace GUI { PrinterWebView::PrinterWebView(wxWindow *parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) + , m_browser(nullptr) + , m_zoomFactor(100) + , m_apikey() + , m_apikey_sent(false) + , m_url_deferred() + , m_handler(std::make_unique(*this)) { wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); @@ -47,6 +52,8 @@ PrinterWebView::PrinterWebView(wxWindow *parent) m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PrinterWebView::OnError, this); m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PrinterWebView::OnLoaded, this); + m_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PrinterWebView::OnNewWindow, this); + m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PrinterWebView::OnScriptMessage, this); SetSizer(topsizer); @@ -64,9 +71,6 @@ PrinterWebView::PrinterWebView(wxWindow *parent) } */ - //Zoom - m_zoomFactor = 100; - //Connect the idle events Bind(wxEVT_CLOSE_WINDOW, &PrinterWebView::OnClose, this); @@ -76,11 +80,18 @@ PrinterWebView::~PrinterWebView() { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Start"; SetEvtHandlerEnabled(false); + m_handler.reset(); + + // Destroy the webview + if(m_browser){ + m_browser->Destroy(); + m_browser = nullptr; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " End"; } - void PrinterWebView::load_url(wxString& url, wxString apikey) { // this->Show(); @@ -89,6 +100,7 @@ void PrinterWebView::load_url(wxString& url, wxString apikey) return; m_apikey = apikey; m_apikey_sent = false; + m_handler = create_printer_webview_handler(*this); if (this->IsShown()) { //ORCA: m_url_deferred will be cleared on load success @@ -190,14 +202,34 @@ void PrinterWebView::OnError(wxWebViewEvent &evt) BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": error loading page %1% %2% %3% %4%") %evt.GetURL() %evt.GetTarget() %e %evt.GetString(); } -void PrinterWebView::OnLoaded(wxWebViewEvent &evt) +void PrinterWebView::OnLoaded(wxWebViewEvent& evt) { if (evt.GetURL().IsEmpty()) return; //ORCA: url loaded successfully, safe to clear m_url_deferred.clear(); SendAPIKey(); + + if (m_handler != nullptr) { + m_handler->on_loaded(evt); + return; + } } +void PrinterWebView::OnNewWindow(wxWebViewEvent& evt) +{ + const wxString url = evt.GetURL(); + if (!url.empty()) + wxLaunchDefaultBrowser(url); + evt.Veto(); +} + +void PrinterWebView::OnScriptMessage(wxWebViewEvent& evt) +{ + if (m_handler != nullptr) + m_handler->on_script_message(evt); +} + + } // GUI } // Slic3r diff --git a/src/slic3r/GUI/PrinterWebView.hpp b/src/slic3r/GUI/PrinterWebView.hpp index 887091dde3..8a62ce70a2 100644 --- a/src/slic3r/GUI/PrinterWebView.hpp +++ b/src/slic3r/GUI/PrinterWebView.hpp @@ -25,11 +25,14 @@ #include #include "wx/textctrl.h" #include +#include namespace Slic3r { namespace GUI { +class PrinterWebViewHandler; + class PrinterWebView : public wxPanel { public: @@ -41,20 +44,24 @@ public: void OnClose(wxCloseEvent& evt); void OnError(wxWebViewEvent& evt); void OnLoaded(wxWebViewEvent& evt); + void OnNewWindow(wxWebViewEvent& evt); + void OnScriptMessage(wxWebViewEvent& evt); void reload(); void update_mode(); bool Show(bool show = true) override; private: + friend class PrinterWebViewHandler; + void SendAPIKey(); wxWebView* m_browser; long m_zoomFactor; wxString m_apikey; bool m_apikey_sent; - wxString m_url_deferred; + std::unique_ptr m_handler; // DECLARE_EVENT_TABLE() }; diff --git a/src/slic3r/GUI/PrinterWebViewHandler.cpp b/src/slic3r/GUI/PrinterWebViewHandler.cpp new file mode 100644 index 0000000000..16d63795db --- /dev/null +++ b/src/slic3r/GUI/PrinterWebViewHandler.cpp @@ -0,0 +1,339 @@ +#include "PrinterWebViewHandler.hpp" + +#include "I18N.hpp" +#include "PrinterWebView.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Widgets/WebView.hpp" +#include "slic3r/Utils/PrintHost.hpp" +#include "libslic3r/Preset.hpp" + +#include +#include +#include +#include +#include +#include + +using json = nlohmann::json; + +namespace Slic3r { +namespace GUI { + +PrinterWebViewHandler::PrinterWebViewHandler(PrinterWebView& owner) + : m_owner(owner) +{ +} + +PrinterWebViewHandler::~PrinterWebViewHandler() = default; + +void PrinterWebViewHandler::on_loaded(wxWebViewEvent &evt) +{ +} + +void PrinterWebViewHandler::on_script_message(wxWebViewEvent &evt) +{ +} + +PrinterWebView& PrinterWebViewHandler::owner() const +{ + return m_owner; +} + +wxWebView* PrinterWebViewHandler::browser() const +{ + return m_owner.m_browser; +} + +namespace { + +DynamicPrintConfig* get_active_printer_config() +{ + if (wxGetApp().preset_bundle == nullptr) + return nullptr; + + return &wxGetApp().preset_bundle->printers.get_edited_preset().config; +} + +std::string json_string(const json& node, const char* key) +{ + auto it = node.find(key); + return (it != node.end() && it->is_string()) ? it->get() : std::string(); +} + +std::string dump_json(const json& node) +{ + return node.dump(-1, ' ', false, json::error_handler_t::replace); +} + +boost::filesystem::path path_from_utf8(const std::string& utf8_path) +{ +#ifdef _WIN32 + const wxString wide_path = wxString::FromUTF8(utf8_path.c_str()); + return boost::filesystem::path(wide_path.ToStdWstring()); +#else + return boost::filesystem::path(utf8_path); +#endif +} + +std::string filename_to_utf8(const boost::filesystem::path& path) +{ +#ifdef _WIN32 + const wxString wx_filename(path.filename().c_str()); + const wxScopedCharBuffer utf8 = wx_filename.ToUTF8(); + return utf8.data() != nullptr ? std::string(utf8.data()) : std::string(); +#else + return path.filename().string(); +#endif +} + +class ElegooPrinterWebViewHandler final : public PrinterWebViewHandler { +public: + explicit ElegooPrinterWebViewHandler(PrinterWebView& owner) + : PrinterWebViewHandler(owner) + { + } + + ~ElegooPrinterWebViewHandler() override + { + stop_upload = true; + if (upload_thread.joinable()) + upload_thread.join(); + if (sn_thread.joinable()) + sn_thread.join(); + } + + void on_script_message(wxWebViewEvent &evt) override + { + const wxString message = evt.GetString(); + if (message.empty()) + return; + + json root = json::parse(message.ToUTF8().data(), nullptr, false); + if (root.is_discarded() || !root.is_object()) + return; + + std::string request_id = json_string(root, "id"); + std::string method = json_string(root, "method"); + json params = root.contains("params") && root["params"].is_object() ? root["params"] : json::object(); + + if (method.empty()) { + method = json_string(root, "command"); + if (params.empty() && root.contains("data") && root["data"].is_object()) + params = root["data"]; + } + + if (method == "open" || method == "common_openurl") { + const std::string url = json_string(params, "url").empty() ? json_string(root, "url") : json_string(params, "url"); + if (!url.empty()) + wxLaunchDefaultBrowser(url); + if (!request_id.empty()) + send_ipc_message("response", request_id, method, 0, "success"); + return; + } + + if (method == "upload_file") { + handle_upload_request(request_id, method, dump_json(params)); + return; + } + + if (method == "open_file_dialog") { + handle_open_file_dialog_request(request_id, method, dump_json(params)); + return; + } + + if (method == "get_sn") { + handle_get_sn_request(request_id, method); + return; + } + } + +private: + void send_ipc_message(const char* type, const std::string& request_id, const std::string& method, int code, + const std::string& message, const std::string& data_json = "{}") + { + if (browser() == nullptr) + return; + + json body = json::object(); + body["type"] = type; + if (!request_id.empty()) + body["id"] = request_id; + if (!method.empty()) + body["method"] = method; + + json data = json::parse(data_json, nullptr, false); + if (data.is_discarded()) + data = json::object(); + body["data"] = std::move(data); + + if (std::string(type) == "response") { + body["code"] = code; + body["message"] = message; + } + + const wxString payload = wxString::FromUTF8(dump_json(body)); + const wxString script = "if (typeof HandleStudio === 'function') { HandleStudio(" + payload + "); } else { window.postMessage(" + payload + ", '*'); }"; + wxGetApp().CallAfter([this, script]() { + if (browser() != nullptr) + WebView::RunScript(browser(), script); + }); + } + + void handle_upload_request(const std::string& request_id, const std::string& method, const std::string& params_json) + { + if (upload_in_progress.exchange(true)) { + send_ipc_message("response", request_id, method, 1, "Upload already in progress"); + return; + } + + if (upload_thread.joinable()) + upload_thread.join(); + + json params = json::parse(params_json, nullptr, false); + if (params.is_discarded()) + params = json::object(); + + std::string file_path = json_string(params, "filePath"); + std::string file_name = json_string(params, "fileName"); + + if (file_path.empty()) { + upload_in_progress = false; + send_ipc_message("response", request_id, method, 1, "Missing filePath"); + return; + } + + // HTML IPC passes UTF-8 strings; decode explicitly to avoid Windows codepage issues. + boost::filesystem::path source_path = path_from_utf8(file_path); + if (file_name.empty()) + file_name = filename_to_utf8(source_path); + + DynamicPrintConfig* config = get_active_printer_config(); + std::unique_ptr print_host(config == nullptr ? nullptr : PrintHost::get_print_host(config)); + if (print_host == nullptr) { + upload_in_progress = false; + send_ipc_message("response", request_id, method, 1, "Could not get a valid Printer Host reference"); + return; + } + + stop_upload = false; + upload_thread = std::thread([this, request_id, method, file_path, file_name, source_path, print_host = std::move(print_host)]() mutable { + std::string error_message; + + PrintHostUpload upload_data; + upload_data.use_3mf = false; + upload_data.post_action = PrintHostPostUploadAction::None; + upload_data.source_path = source_path; + upload_data.upload_path = path_from_utf8(file_name); + + const bool success = print_host->upload( + std::move(upload_data), + [this, request_id](Http::Progress progress, bool& cancel) { + cancel = stop_upload.load(); + json data = { + {"uploadedBytes", static_cast(progress.ulnow)}, + {"totalBytes", static_cast(progress.ultotal)} + }; + send_ipc_message("event", request_id, "upload_progress", 0, "", dump_json(data)); + }, + [&error_message](wxString error) { + error_message = error.ToUTF8().data(); + }, + [this, request_id](wxString tag, wxString status) { + json data = { + {"tag", tag.ToUTF8().data()}, + {"status", status.ToUTF8().data()} + }; + send_ipc_message("event", request_id, "upload_info", 0, "", dump_json(data)); + }); + + upload_in_progress = false; + + if (success) { + json data = { + {"success", true}, + {"filePath", file_path}, + {"fileName", file_name} + }; + send_ipc_message("response", request_id, method, 0, "success", dump_json(data)); + } else { + if (error_message.empty()) + error_message = "Upload failed"; + send_ipc_message("response", request_id, method, 1, error_message); + } + }); + } + + void handle_open_file_dialog_request(const std::string& request_id, const std::string& method, const std::string& params_json) + { + json params = json::parse(params_json, nullptr, false); + if (params.is_discarded()) + params = json::object(); + + const std::string filter = json_string(params, "filter").empty() ? "All files (*.*)|*.*" : json_string(params, "filter"); + + wxWindow* parent = owner().GetParent(); + if (parent == nullptr) + parent = wxGetApp().GetTopWindow(); + + wxFileDialog open_file_dialog(parent, _L("Open File"), "", "", wxString::FromUTF8(filter), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + json data = json::object(); + data["files"] = json::array(); + if (open_file_dialog.ShowModal() != wxID_CANCEL) + data["files"].push_back(open_file_dialog.GetPath().ToUTF8().data()); + + send_ipc_message("response", request_id, method, 0, "success", dump_json(data)); + } + + void handle_get_sn_request(const std::string& request_id, const std::string& method) + { + if (sn_request_in_progress.exchange(true)) { + send_ipc_message("response", request_id, method, 1, "SN request already in progress"); + return; + } + + if (sn_thread.joinable()) + sn_thread.join(); + + sn_thread = std::thread([this, request_id, method]() { + std::string sn; + + DynamicPrintConfig* config = get_active_printer_config(); + std::unique_ptr print_host(config == nullptr ? nullptr : PrintHost::get_print_host(config)); + if (print_host != nullptr) + sn = print_host->get_sn(); + + sn_request_in_progress = false; + json data = { + {"sn", sn} + }; + send_ipc_message("response", request_id, method, 0, "success", dump_json(data)); + }); + } + + std::atomic upload_in_progress { false }; + std::atomic sn_request_in_progress { false }; + std::atomic stop_upload { false }; + std::thread upload_thread; + std::thread sn_thread; +}; + +} // namespace + +std::unique_ptr create_printer_webview_handler(PrinterWebView& owner) +{ + auto cfg = get_active_printer_config(); + if(cfg == nullptr) return nullptr; + + const auto host_type = cfg->option>("host_type")->value; + switch (host_type) + { + case PrintHostType::htElegooLink: + return std::make_unique(owner); + default: + return nullptr; + } +} + +} // GUI +} // Slic3r \ No newline at end of file diff --git a/src/slic3r/GUI/PrinterWebViewHandler.hpp b/src/slic3r/GUI/PrinterWebViewHandler.hpp new file mode 100644 index 0000000000..02da72c75f --- /dev/null +++ b/src/slic3r/GUI/PrinterWebViewHandler.hpp @@ -0,0 +1,36 @@ +#ifndef slic3r_PrinterWebViewHandler_hpp_ +#define slic3r_PrinterWebViewHandler_hpp_ + +#include +#include +#include + +class wxWebView; + +namespace Slic3r { +namespace GUI { + +class PrinterWebView; + +class PrinterWebViewHandler { +public: + explicit PrinterWebViewHandler(PrinterWebView& owner); + virtual ~PrinterWebViewHandler(); + + virtual void on_loaded(wxWebViewEvent &evt); + virtual void on_script_message(wxWebViewEvent &evt); + +protected: + PrinterWebView& owner() const; + wxWebView* browser() const; + +private: + PrinterWebView& m_owner; +}; + +std::unique_ptr create_printer_webview_handler(PrinterWebView& owner); + +} // GUI +} // Slic3r + +#endif /* slic3r_PrinterWebViewHandler_hpp_ */ \ No newline at end of file diff --git a/src/slic3r/Utils/ElegooLink.cpp b/src/slic3r/Utils/ElegooLink.cpp index 7347641e74..90bbe65226 100644 --- a/src/slic3r/Utils/ElegooLink.cpp +++ b/src/slic3r/Utils/ElegooLink.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -58,6 +59,69 @@ namespace Slic3r { namespace { + constexpr const char* ELEGOO_CC2_DEFAULT_TOKEN = "123456"; + + enum class ElegooPrinterType { + Other, + CC, + CC2, + }; + + ElegooPrinterType classify_printer_model(const std::string& printer_model) + { + if (!boost::algorithm::starts_with(printer_model, "Elegoo Centauri")) + return ElegooPrinterType::Other; + + const auto last_char = printer_model.find_last_not_of(" \t\r\n"); + if (last_char != std::string::npos && printer_model[last_char] == '2') + return ElegooPrinterType::CC2; + + return ElegooPrinterType::CC; + } + + std::string get_cc2_token(const std::string& apikey) + { + return apikey.empty() ? ELEGOO_CC2_DEFAULT_TOKEN : apikey; + } + + bool parse_cc2_response(const std::string& body, std::string& error_message, std::string* serial_number = nullptr) + { + try { + pt::ptree root; + std::istringstream is(body); + pt::read_json(is, root); + + const int error_code = root.get("error_code", -1); + if (error_code != 0) { + error_message = root.get("message", "Printer returned an error"); + if (error_message.empty()) + error_message = "Printer returned an error"; + error_message += " (" + std::to_string(error_code) + ")"; + return false; + } + + if (serial_number != nullptr) { + const auto system_info = root.get_child_optional("system_info"); + if (!system_info) { + error_message = "Missing system_info in response"; + return false; + } + + const auto sn = system_info->get_optional("sn"); + if (!sn || sn->empty()) { + error_message = "Missing printer serial number in response"; + return false; + } + *serial_number = *sn; + } + + return true; + } catch (const std::exception&) { + error_message = "Error parsing response"; + return false; + } + } + std::string get_host_from_url(const std::string& url_in) { std::string url = url_in; @@ -220,15 +284,126 @@ namespace Slic3r { return ret_val; } + std::string path_to_utf8(const boost::filesystem::path& path) + { + #ifdef WIN32 + return boost::nowide::narrow(path.wstring()); + #else + return path.string(); + #endif + } + + std::string filename_to_utf8(const boost::filesystem::path& path) + { + #ifdef WIN32 + return boost::nowide::narrow(path.filename().wstring()); + #else + return path.filename().string(); + #endif + } + } //namespace ElegooLink::ElegooLink(DynamicPrintConfig *config): - OctoPrint(config) { + OctoPrint(config), m_printerModel(config->opt_string("printer_model")) { } + std::string ElegooLink::get_print_host_webui(DynamicPrintConfig* config) + { + if (config == nullptr) + return {}; + + std::string fallback_webui = config->opt_string("print_host_webui"); + if (fallback_webui.empty()) + fallback_webui = config->opt_string("print_host"); + if (!fallback_webui.empty()) { + const bool has_http_scheme = boost::algorithm::istarts_with(fallback_webui, "http"); + const bool has_file_scheme = boost::algorithm::istarts_with(fallback_webui, "file:"); + + if (!has_http_scheme && !has_file_scheme) + fallback_webui = "http://" + fallback_webui; + } + + const std::string host = config->opt_string("print_host"); + if (host.empty()) + return fallback_webui; + + if (classify_printer_model(config->opt_string("printer_model")) != ElegooPrinterType::CC2) + return fallback_webui; + + std::string web_path = resources_dir() + "/plugins/elegoolink/web/lan_service_web/index.html"; + std::replace(web_path.begin(), web_path.end(), '\\', '/'); + web_path = "file://" + web_path; + web_path += "?access_code=" + get_cc2_token(config->opt_string("printhost_apikey")); + web_path += "&ip=" + get_host_from_url(host) + "&id=elegoo_123456"; + + const std::string lang = GUI::wxGetApp().current_language_code_safe().utf8_string(); + if (!lang.empty()) + web_path += "&lang=" + lang; + + if (GUI::get_app_config()->get_bool("developer_mode")) + web_path += "&dev=true"; + + return web_path; + } + + std::string ElegooLink::cc2_token() const + { + return get_cc2_token(m_apikey); + } + + std::string ElegooLink::make_cc2_info_url() const + { + return make_url("system/info?X-Token=" + escape_string(cc2_token())); + } + + std::string ElegooLink::make_cc2_upload_url() const + { + return make_url("upload"); + } + const char* ElegooLink::get_name() const { return "ElegooLink"; } + PrintHostPostUploadActions ElegooLink::get_post_upload_actions() const + { + if (classify_printer_model(m_printerModel) == ElegooPrinterType::CC2) { + return PrintHostPostUploadAction::None; + } else { + return PrintHostPostUploadAction::StartPrint; + } + } + + std::string ElegooLink::get_sn() const + { + if (classify_printer_model(m_printerModel) != ElegooPrinterType::CC2) + return ""; + + const char* name = get_name(); + std::string sn; + const auto token = cc2_token(); + auto http = Http::get(make_cc2_info_url()); + http.timeout_connect(10) + .timeout_max(15); + http.header("X-Token", token); + http.header("Accept", "application/json"); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting CC2 device info for SN: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + }) + .on_complete([&](std::string body, unsigned status) { + std::string error_message; + if (!parse_cc2_response(body, error_message, &sn)) { + BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: Failed to parse CC2 SN response, HTTP %2%, reason: %3%") % name % status % error_message; + sn.clear(); + } + }) +#ifdef WIN32 + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) +#endif // WIN32 + .perform_sync(); + + return sn; + } bool ElegooLink::elegoo_test(wxString& msg) const{ @@ -268,11 +443,54 @@ namespace Slic3r { return res; } bool ElegooLink::test(wxString &curl_msg) const{ - if(OctoPrint::test(curl_msg)){ - return true; + switch (classify_printer_model(m_printerModel)) { + case ElegooPrinterType::Other: + return OctoPrint::test(curl_msg); + case ElegooPrinterType::CC2: + return elegoo_cc2_test(curl_msg); + case ElegooPrinterType::CC: + return elegoo_test(curl_msg); } - curl_msg=""; - return elegoo_test(curl_msg); + return false; + } + + bool ElegooLink::elegoo_cc2_test(wxString& msg) const + { + const char* name = get_name(); + bool res = true; + const auto token = cc2_token(); + auto url = make_cc2_info_url(); + auto http = Http::get(std::move(url)); + + http.header("X-Token", token); + http.header("Accept", "application/json"); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting CC2 device info: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + res = false; + if (status == 401 || status == 403) + msg = format_error(body, "Invalid access code", status); + else + msg = format_error(body, error.empty() ? "CC2 device not detected" : error, status); + }) + .on_complete([&](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got CC2 device info: %2%") % name % body; + std::string error_message; + std::string serial_number; + if (!parse_cc2_response(body, error_message, &serial_number)) { + res = false; + msg = format_error(body, error_message.empty() ? "CC2 device not detected" : error_message, status); + return; + } + res = true; + }) +#ifdef WIN32 + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) + .on_ip_resolve([&](std::string address) { + msg = GUI::from_u8(address); + }) +#endif // WIN32 + .perform_sync(); + return res; } #ifdef WIN32 @@ -320,12 +538,53 @@ namespace Slic3r { } bool ElegooLink::test_with_resolved_ip(wxString& msg) const { - // Elegoo supports both otcoprint and Elegoo link - if (OctoPrint::test_with_resolved_ip(msg)) { - return true; + switch (classify_printer_model(m_printerModel)) { + case ElegooPrinterType::Other: + return OctoPrint::test_with_resolved_ip(msg); + case ElegooPrinterType::CC2: + return elegoo_cc2_test_with_resolved_ip(msg); + case ElegooPrinterType::CC: + return elegoo_test_with_resolved_ip(msg); } - msg = ""; - return elegoo_test_with_resolved_ip(msg); + return false; + } + + bool ElegooLink::elegoo_cc2_test_with_resolved_ip(wxString& msg) const + { + const char* name = get_name(); + bool res = true; + const auto token = cc2_token(); + auto url = substitute_host(make_cc2_info_url(), GUI::into_u8(msg)); + std::string host_header = get_host_from_url(m_host); + auto http = Http::get(url); + msg.Clear(); + + http.header("Host", host_header); + http.header("X-Token", token); + http.header("Accept", "application/json"); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting CC2 device info at %2% : %3%, HTTP %4%, body: `%5%`") % name % url % + error % status % body; + res = false; + if (status == 401 || status == 403) + msg = format_error(body, "Invalid access code", status); + else + msg = format_error(body, error.empty() ? "CC2 device not detected" : error, status); + }) + .on_complete([&](std::string body, unsigned status) { + std::string error_message; + std::string serial_number; + if (!parse_cc2_response(body, error_message, &serial_number)) { + res = false; + msg = format_error(body, error_message.empty() ? "CC2 device not detected" : error_message, status); + return; + } + res = true; + }) + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) + .perform_sync(); + + return res; } #endif // WIN32 @@ -343,25 +602,31 @@ namespace Slic3r { #ifdef WIN32 bool ElegooLink::upload_inner_with_resolved_ip(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn, const boost::asio::ip::address& resolved_addr) const { - wxString test_msg_or_host_ip = ""; + const auto printer_type = classify_printer_model(m_printerModel); + + if (printer_type == ElegooPrinterType::Other) + return OctoPrint::upload_inner_with_resolved_ip(std::move(upload_data), prorgess_fn, error_fn, info_fn, resolved_addr); info_fn(L"resolve", boost::nowide::widen(resolved_addr.to_string())); - // If test fails, test_msg_or_host_ip contains the error message. - // Otherwise on Windows it contains the resolved IP address of the host. - // Test_msg already contains resolved ip and will be cleared on start of test(). - test_msg_or_host_ip = GUI::from_u8(resolved_addr.to_string()); - //Elegoo supports both otcoprint and Elegoo link - if(OctoPrint::test_with_resolved_ip(test_msg_or_host_ip)){ - return OctoPrint::upload_inner_with_host(upload_data, prorgess_fn, error_fn, info_fn); + + if (printer_type == ElegooPrinterType::CC2) { + wxString cc2_msg = GUI::from_u8(resolved_addr.to_string()); + if (!elegoo_cc2_test_with_resolved_ip(cc2_msg)) { + error_fn(std::move(cc2_msg)); + return false; + } + + std::string url = substitute_host(make_cc2_upload_url(), resolved_addr.to_string()); + info_fn(L"resolve", boost::nowide::widen(url)); + return loopUploadCC2(url, get_host_from_url(m_host), std::move(upload_data), prorgess_fn, error_fn, info_fn); } - test_msg_or_host_ip = GUI::from_u8(resolved_addr.to_string()); - if(!elegoo_test_with_resolved_ip(test_msg_or_host_ip)){ - error_fn(std::move(test_msg_or_host_ip)); + wxString legacy_msg = GUI::from_u8(resolved_addr.to_string()); + if (!elegoo_test_with_resolved_ip(legacy_msg)) { + error_fn(std::move(legacy_msg)); return false; } - std::string url = substitute_host(make_url("uploadFile/upload"), resolved_addr.to_string()); info_fn(L"resolve", boost::nowide::widen(url)); @@ -372,23 +637,46 @@ namespace Slic3r { bool ElegooLink::upload_inner_with_host(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const { - // If test fails, test_msg_or_host_ip contains the error message. - // Otherwise on Windows it contains the resolved IP address of the host. - wxString test_msg_or_host_ip; - //Elegoo supports both otcoprint and Elegoo link - if(OctoPrint::test(test_msg_or_host_ip)){ - return OctoPrint::upload_inner_with_host(upload_data, prorgess_fn, error_fn, info_fn); + const auto printer_type = classify_printer_model(m_printerModel); + + if (printer_type == ElegooPrinterType::Other) + return OctoPrint::upload_inner_with_host(std::move(upload_data), prorgess_fn, error_fn, info_fn); + + if (printer_type == ElegooPrinterType::CC2) { + wxString cc2_msg; + if (!elegoo_cc2_test(cc2_msg)) { + error_fn(std::move(cc2_msg)); + return false; + } + + std::string url; +#ifdef WIN32 + if (m_host.find("https://") == 0 || cc2_msg.empty() || !GUI::get_app_config()->get_bool("allow_ip_resolve")) +#endif // _WIN32 + { + url = make_cc2_upload_url(); + } +#ifdef WIN32 + else { + info_fn(L"resolve", cc2_msg); + url = substitute_host(make_cc2_upload_url(), GUI::into_u8(cc2_msg)); + BOOST_LOG_TRIVIAL(info) << "CC2 upload address after ip resolve: " << url; + } +#endif // _WIN32 + + return loopUploadCC2(url, get_host_from_url(m_host), std::move(upload_data), prorgess_fn, error_fn, info_fn); } - test_msg_or_host_ip=""; - if(!elegoo_test(test_msg_or_host_ip)){ - error_fn(std::move(test_msg_or_host_ip)); + + wxString legacy_msg; + if(!elegoo_test(legacy_msg)){ + error_fn(std::move(legacy_msg)); return false; } std::string url; #ifdef WIN32 // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. - if (m_host.find("https://") == 0 || test_msg_or_host_ip.empty() || !GUI::get_app_config()->get_bool("allow_ip_resolve")) + if (m_host.find("https://") == 0 || legacy_msg.empty() || !GUI::get_app_config()->get_bool("allow_ip_resolve")) #endif // _WIN32 { // If https is entered we assume signed ceritificate is being used @@ -403,8 +691,8 @@ namespace Slic3r { // This new address returns in "test_msg_or_host_ip" variable. // Solves troubles of uploades failing with name address. // in original address (m_host) replace host for resolved ip - info_fn(L"resolve", test_msg_or_host_ip); - url = substitute_host(make_url("uploadFile/upload"), GUI::into_u8(test_msg_or_host_ip)); + info_fn(L"resolve", legacy_msg); + url = substitute_host(make_url("uploadFile/upload"), GUI::into_u8(legacy_msg)); BOOST_LOG_TRIVIAL(info) << "Upload address after ip resolve: " << url; } #endif // _WIN32 @@ -490,11 +778,11 @@ namespace Slic3r { bool ElegooLink::loopUpload(std::string url, PrintHostUpload upload_data, ProgressFn progress_fn, ErrorFn error_fn, InfoFn info_fn) const { const char* name = get_name(); - const auto upload_filename = upload_data.upload_path.filename().string(); - std::string source_path = upload_data.source_path.string(); + const auto upload_filename = filename_to_utf8(upload_data.upload_path); + std::string source_path = path_to_utf8(upload_data.source_path); // calc file size - std::ifstream file(source_path, std::ios::binary | std::ios::ate); + boost::nowide::ifstream file(source_path, std::ios::binary | std::ios::ate); std::streamsize size = file.tellg(); file.close(); const std::string fileSize = std::to_string(size); @@ -583,6 +871,146 @@ namespace Slic3r { return res; } + bool ElegooLink::uploadPartCC2(Http& http, + const std::string& host_header, + const std::string& token, + const std::string& md5, + const boost::filesystem::path& path, + const std::string& filename, + size_t filesize, + size_t offset, + size_t length, + ProgressFn prorgess_fn, + ErrorFn error_fn) const + { + const char* name = get_name(); + + boost::nowide::ifstream file(path, std::ios::binary); + if (!file.is_open()) { + error_fn(_L("Failed to open file for upload.")); + return false; + } + + file.seekg(static_cast(offset), std::ios::beg); + std::string chunk(length, '\0'); + file.read(chunk.data(), static_cast(length)); + if (!file && static_cast(file.gcount()) != length) { + error_fn(_L("Failed to read file chunk for upload.")); + return false; + } + chunk.resize(static_cast(file.gcount())); + + const size_t end_offset = offset + chunk.size() - 1; + const auto range = std::string("bytes ") + std::to_string(offset) + "-" + std::to_string(end_offset) + "/" + std::to_string(filesize); + + bool result = false; + http.headers_reset(); + if (!host_header.empty()) + http.header("Host", host_header); + http.header("Accept", "application/json") + .header("Content-Type", "application/octet-stream") + .header("Content-Length", std::to_string(chunk.size())) + .header("Content-Range", range) + .header("X-File-Name", filename) + .header("X-File-MD5", md5) + .header("X-Token", token) + .set_post_body(chunk) + .on_complete([&](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: CC2 chunk uploaded: HTTP %2%: %3%") % name % status % body; + std::string error_message; + if (!parse_cc2_response(body, error_message)) { + error_fn(format_error(body, error_message.empty() ? "CC2 upload failed" : error_message, status)); + return; + } + result = true; + }) + .on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error uploading CC2 chunk: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + if (status == 401 || status == 403) + error_fn(format_error(body, "Invalid access code", status)); + else + error_fn(format_error(body, error.empty() ? "CC2 upload failed" : error, status)); + }) + .on_progress([&](Http::Progress progress, bool& cancel) { + if (progress.ultotal == progress.ulnow) + return; + prorgess_fn(std::move(progress), cancel); + if (cancel) + BOOST_LOG_TRIVIAL(info) << name << ": CC2 upload canceled"; + }) +#ifdef WIN32 + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) +#endif + .perform_sync(); + + return result; + } + + bool ElegooLink::loopUploadCC2(std::string url, + const std::string& host_header, + PrintHostUpload upload_data, + ProgressFn progress_fn, + ErrorFn error_fn, + InfoFn info_fn) const + { + BOOST_LOG_TRIVIAL(info) << get_name() << ": Uploading file to Elegoo CC2"; + const auto upload_filename = filename_to_utf8(upload_data.upload_path); + std::string source_path = path_to_utf8(upload_data.source_path); + std::string md5; + bbl_calc_md5(source_path, md5); + std::transform(md5.begin(), md5.end(), md5.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); + + boost::nowide::ifstream file(source_path, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + error_fn(_L("Failed to open file for upload.")); + return false; + } + + const std::streamsize size = file.tellg(); + file.close(); + if (size <= 0) { + error_fn(_L("The file is empty or could not be read.")); + return false; + } + + if (md5.empty()) { + error_fn(_L("Failed to calculate file checksum.")); + return false; + } + + const std::string token = cc2_token(); + const int packageCount = static_cast((size + MAX_UPLOAD_PACKAGE_LENGTH - 1) / MAX_UPLOAD_PACKAGE_LENGTH); + auto http = Http::put2(url); + http.timeout_connect(30) + .timeout_max(180); + + bool res = false; + for (int i = 0; i < packageCount; ++i) { + const size_t offset = MAX_UPLOAD_PACKAGE_LENGTH * static_cast(i); + size_t length = MAX_UPLOAD_PACKAGE_LENGTH; + if (i == packageCount - 1 && size % MAX_UPLOAD_PACKAGE_LENGTH > 0) + length = static_cast(size % MAX_UPLOAD_PACKAGE_LENGTH); + + res = uploadPartCC2( + http, host_header, token, md5, source_path, upload_filename, static_cast(size), offset, length, + [size, i, progress_fn](Http::Progress progress, bool& cancel) { + const size_t uploaded = static_cast(i) * MAX_UPLOAD_PACKAGE_LENGTH + progress.ulnow; + Http::Progress merged(0, 0, static_cast(size), std::min(static_cast(size), uploaded), progress.buffer, + progress.upload_spd); + progress_fn(merged, cancel); + }, + error_fn); + if (!res) + break; + } + + if (res && upload_data.post_action == PrintHostPostUploadAction::StartPrint) + BOOST_LOG_TRIVIAL(info) << get_name() << ": CC2 upload completed; start print is not supported."; + + (void) info_fn; + return res; + } + bool ElegooLink::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const { #ifndef WIN32 diff --git a/src/slic3r/Utils/ElegooLink.hpp b/src/slic3r/Utils/ElegooLink.hpp index b0c379c29f..eb1ca7ba26 100644 --- a/src/slic3r/Utils/ElegooLink.hpp +++ b/src/slic3r/Utils/ElegooLink.hpp @@ -20,14 +20,16 @@ class ElegooLink : public OctoPrint public: ElegooLink(DynamicPrintConfig *config); ~ElegooLink() override = default; + static std::string get_print_host_webui(DynamicPrintConfig *config); const char* get_name() const override; virtual bool test(wxString &curl_msg) const override; 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; + std::string get_sn() const override; bool has_auto_discovery() const override { return false; } bool can_test() const override { return true; } - PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } + PrintHostPostUploadActions get_post_upload_actions() const override; protected: #ifdef WIN32 virtual bool upload_inner_with_resolved_ip(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn, const boost::asio::ip::address& resolved_addr) const; @@ -39,9 +41,9 @@ protected: virtual bool test_with_resolved_ip(wxString& curl_msg) const override; bool elegoo_test_with_resolved_ip(wxString& curl_msg) const; #endif - private: bool elegoo_test(wxString& curl_msg) const; + bool elegoo_cc2_test(wxString& curl_msg) const; bool print(WebSocketClient& client, std::string timeLapse, std::string heatedBedLeveling, @@ -54,6 +56,12 @@ private: ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const; + bool loopUploadCC2(std::string url, + const std::string& host_header, + PrintHostUpload upload_data, + ProgressFn prorgess_fn, + ErrorFn error_fn, + InfoFn info_fn) const; bool uploadPart(Http &http, std::string md5, @@ -66,6 +74,26 @@ private: ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const; + bool uploadPartCC2(Http& http, + const std::string& host_header, + const std::string& token, + const std::string& md5, + const boost::filesystem::path& path, + const std::string& filename, + size_t filesize, + size_t offset, + size_t length, + ProgressFn prorgess_fn, + ErrorFn error_fn) const; + + std::string cc2_token() const; + std::string make_cc2_info_url() const; + std::string make_cc2_upload_url() const; +#ifdef WIN32 + bool elegoo_cc2_test_with_resolved_ip(wxString& curl_msg) const; +#endif + + std::string m_printerModel; }; } diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index f75da63fb5..4f5227167c 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -570,6 +570,21 @@ Http& Http::header(std::string name, const std::string &value) return *this; } +Http& Http::headers_reset() +{ + if (!p) { return *this; } + + ::curl_slist_free_all(p->headerlist); + p->headerlist = nullptr; + p->headerlist = curl_slist_append(p->headerlist, "Expect:"); + + std::lock_guard l(g_mutex); + for (auto it = extra_headers.begin(); it != extra_headers.end(); ++it) + this->header(it->first, it->second); + + return *this; +} + Http& Http::remove_header(std::string name) { if (p) { diff --git a/src/slic3r/Utils/Http.hpp b/src/slic3r/Utils/Http.hpp index b08894ea62..4b6967a454 100644 --- a/src/slic3r/Utils/Http.hpp +++ b/src/slic3r/Utils/Http.hpp @@ -109,6 +109,8 @@ public: Http& set_range(const std::string& range); // Sets a HTTP header field. Http& header(std::string name, const std::string &value); + // Clears all custom headers and restores default implicit headers. + Http& headers_reset(); // Removes a header field. Http& remove_header(std::string name); // Authorization by HTTP digest, based on RFC2617. diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 097d26d4c0..3ed4e07382 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -74,6 +74,40 @@ PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config) } } +std::string PrintHost::get_print_host_webui(DynamicPrintConfig* config) +{ + if (config == nullptr) + return {}; + + std::string webui_url; + const auto* host_type_opt = config->option>("host_type"); + const auto host_type = host_type_opt != nullptr ? host_type_opt->value : htOctoPrint; + + switch (host_type) { + case htElegooLink: { + webui_url = ElegooLink::get_print_host_webui(config); + break; + } + default: break; + } + + if (webui_url.empty()) { + webui_url = config->opt_string("print_host_webui"); + if (webui_url.empty()) + webui_url = config->opt_string("print_host"); + if (webui_url.empty()) + return webui_url; + } + + const bool has_http_scheme = boost::algorithm::istarts_with(webui_url, "http"); + const bool has_file_scheme = boost::algorithm::istarts_with(webui_url, "file:"); + + if (!has_http_scheme && !has_file_scheme) + webui_url = "http://" + webui_url; + + return webui_url; +} + wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const { if (status != 0) { diff --git a/src/slic3r/Utils/PrintHost.hpp b/src/slic3r/Utils/PrintHost.hpp index 2b5e300008..2b0611849d 100644 --- a/src/slic3r/Utils/PrintHost.hpp +++ b/src/slic3r/Utils/PrintHost.hpp @@ -63,6 +63,12 @@ public: // A print host usually does not support multiple printers, with the exception of Repetier server. virtual bool supports_multiple_printers() const { return false; } virtual std::string get_host() const = 0; + /** + * Get the serial number for connecting to the printer. + * For Elegoo CC2, the device details connection to the printer requires the serial number. + * Other print hosts do not need to implement this interface, and it returns an empty string by default. + */ + virtual std::string get_sn() const { return ""; } // Support for Repetier server multiple groups & printers. Not supported by other print hosts. // Returns false if not supported. May throw HostNetworkError. @@ -73,6 +79,7 @@ public: virtual bool get_storage(wxArrayString& /*storage_path*/, wxArrayString& /*storage_name*/) const { return false; } static PrintHost* get_print_host(DynamicPrintConfig *config); + static std::string get_print_host_webui(DynamicPrintConfig *config); //Support for cloud webui login virtual bool is_cloud() const { return false; }