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 <softfeverever@gmail.com>
This commit is contained in:
anjis
2026-05-16 15:22:31 +08:00
committed by GitHub
parent 5a136a25d1
commit 427d0f7a9f
16 changed files with 1248 additions and 56 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -46,6 +46,7 @@
#include "Widgets/ProgressDialog.hpp"
#include "BindDialog.hpp"
#include "../Utils/MacDarkMode.hpp"
#include "../Utils/PrintHost.hpp"
#include <fstream>
#include <string_view>
@@ -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<ConfigOptionEnum<PrintHostType>>("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);
}
}

View File

@@ -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<ConfigOptionEnum<PrintHostType>>("host_type")->value;
if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint))
apikey = cfg.opt_string("printhost_apikey");

View File

@@ -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;
}

View File

@@ -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 <boost/filesystem/path.hpp>
#include <wx/sizer.h>
#include <wx/string.h>
#include <wx/toolbar.h>
#include <wx/textdlg.h>
#include <slic3r/GUI/Widgets/WebView.hpp>
#include <wx/webview.h>
@@ -19,13 +20,17 @@
#include <webkit2/webkit2.h>
#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<PrinterWebViewHandler>(*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

View File

@@ -25,11 +25,14 @@
#include <wx/tbarbase.h>
#include "wx/textctrl.h"
#include <wx/timer.h>
#include <memory>
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<PrinterWebViewHandler> m_handler;
// DECLARE_EVENT_TABLE()
};

View File

@@ -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 <nlohmann/json.hpp>
#include <atomic>
#include <boost/filesystem/path.hpp>
#include <thread>
#include <wx/filedlg.h>
#include <wx/string.h>
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();
}
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<PrintHost> 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<uint64_t>(progress.ulnow)},
{"totalBytes", static_cast<uint64_t>(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<PrintHost> 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<bool> upload_in_progress { false };
std::atomic<bool> sn_request_in_progress { false };
std::atomic<bool> stop_upload { false };
std::thread upload_thread;
std::thread sn_thread;
};
} // namespace
std::unique_ptr<PrinterWebViewHandler> create_printer_webview_handler(PrinterWebView& owner)
{
auto cfg = get_active_printer_config();
if(cfg == nullptr) return nullptr;
const auto host_type = cfg->option<ConfigOptionEnum<PrintHostType>>("host_type")->value;
switch (host_type)
{
case PrintHostType::htElegooLink:
return std::make_unique<ElegooPrinterWebViewHandler>(owner);
default:
return nullptr;
}
}
} // GUI
} // Slic3r

View File

@@ -0,0 +1,36 @@
#ifndef slic3r_PrinterWebViewHandler_hpp_
#define slic3r_PrinterWebViewHandler_hpp_
#include <memory>
#include <wx/webview.h>
#include <wx/string.h>
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<PrinterWebViewHandler> create_printer_webview_handler(PrinterWebView& owner);
} // GUI
} // Slic3r
#endif /* slic3r_PrinterWebViewHandler_hpp_ */

View File

@@ -11,6 +11,7 @@
#include <boost/asio.hpp>
#include <boost/algorithm/string/split.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
@@ -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<int>("error_code", -1);
if (error_code != 0) {
error_message = root.get<std::string>("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<std::string>("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<std::streamoff>(offset), std::ios::beg);
std::string chunk(length, '\0');
file.read(chunk.data(), static_cast<std::streamsize>(length));
if (!file && static_cast<size_t>(file.gcount()) != length) {
error_fn(_L("Failed to read file chunk for upload."));
return false;
}
chunk.resize(static_cast<size_t>(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<char>(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<int>((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<size_t>(i);
size_t length = MAX_UPLOAD_PACKAGE_LENGTH;
if (i == packageCount - 1 && size % MAX_UPLOAD_PACKAGE_LENGTH > 0)
length = static_cast<size_t>(size % MAX_UPLOAD_PACKAGE_LENGTH);
res = uploadPartCC2(
http, host_header, token, md5, source_path, upload_filename, static_cast<size_t>(size), offset, length,
[size, i, progress_fn](Http::Progress progress, bool& cancel) {
const size_t uploaded = static_cast<size_t>(i) * MAX_UPLOAD_PACKAGE_LENGTH + progress.ulnow;
Http::Progress merged(0, 0, static_cast<size_t>(size), std::min(static_cast<size_t>(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

View File

@@ -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;
};
}

View File

@@ -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<std::mutex> 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) {

View File

@@ -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.

View File

@@ -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<ConfigOptionEnum<PrintHostType>>("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) {

View File

@@ -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; }