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

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