From d591d50ca05e4f0e40db2c3834ae99ceacd0ef00 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Thu, 16 Jul 2026 01:44:43 +0800 Subject: [PATCH] Show the print-failure snapshot in the device error dialog When a print fails, DeviceErrorDialog now fetches the printer's captured camera frame of the failure and shows it in place of the generic HMS illustration, falling back to the local image and a drawn placeholder on older plugins or errors. - Agent: add get_hms_snapshot through NetworkAgent / IPrinterAgent (BBL calls the bound plugin symbol; other agents no-op, so old plugins degrade gracefully). - DeviceManager: parse and clear m_print_error_img_id from the print-error message. - Dialog: tiered cloud/local/placeholder image reusing the single image widget, with a liveness-guarded async callback decoded on the UI thread. --- resources/images/dev_hms_diag_loading.svg | 18 +++ src/slic3r/GUI/DeviceErrorDialog.cpp | 170 ++++++++++++++++++--- src/slic3r/GUI/DeviceErrorDialog.hpp | 12 ++ src/slic3r/GUI/DeviceManager.cpp | 12 ++ src/slic3r/GUI/DeviceManager.hpp | 1 + src/slic3r/Utils/BBLPrinterAgent.cpp | 13 ++ src/slic3r/Utils/BBLPrinterAgent.hpp | 1 + src/slic3r/Utils/IPrinterAgent.hpp | 6 + src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 9 ++ src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 1 + src/slic3r/Utils/NetworkAgent.cpp | 7 + src/slic3r/Utils/NetworkAgent.hpp | 1 + src/slic3r/Utils/OrcaPrinterAgent.cpp | 9 ++ src/slic3r/Utils/OrcaPrinterAgent.hpp | 1 + 14 files changed, 244 insertions(+), 17 deletions(-) create mode 100644 resources/images/dev_hms_diag_loading.svg diff --git a/resources/images/dev_hms_diag_loading.svg b/resources/images/dev_hms_diag_loading.svg new file mode 100644 index 0000000000..5d95c3e25f --- /dev/null +++ b/resources/images/dev_hms_diag_loading.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/slic3r/GUI/DeviceErrorDialog.cpp b/src/slic3r/GUI/DeviceErrorDialog.cpp index 353250361f..55a0a9231e 100644 --- a/src/slic3r/GUI/DeviceErrorDialog.cpp +++ b/src/slic3r/GUI/DeviceErrorDialog.cpp @@ -5,6 +5,10 @@ #include "GUI_App.hpp" #include "MainFrame.hpp" #include "ReleaseNote.hpp" +#include "wxExtensions.hpp" + +#include +#include namespace Slic3r { namespace GUI @@ -88,10 +92,22 @@ DeviceErrorDialog::DeviceErrorDialog(MachineObject* obj, wxWindow* parent, wxWin if (m_obj) { m_obj->command_clean_print_error_uiop(m_obj->print_error); } e.Skip(); }); + + m_request_timer = new wxTimer(this); + Bind(wxEVT_TIMER, &DeviceErrorDialog::on_request_timeout, this, m_request_timer->GetId()); } DeviceErrorDialog::~DeviceErrorDialog() { + // Orca: invalidate any in-flight cloud snapshot callback (the request has no cancel handle) + *m_alive = false; + + if (m_request_timer) { + m_request_timer->Stop(); + delete m_request_timer; + m_request_timer = nullptr; + } + if (web_request.IsOk() && web_request.GetState() == wxWebRequest::State_Active) { BOOST_LOG_TRIVIAL(info) << "web_request: cancelled"; @@ -103,6 +119,11 @@ DeviceErrorDialog::~DeviceErrorDialog() void DeviceErrorDialog::on_webrequest_state(wxWebRequestEvent& evt) { BOOST_LOG_TRIVIAL(trace) << "monitor: monitor_panel web request state = " << evt.GetState(); + + // A local/HTTP illustration resolved (or the cloud path fell back to it): stop the watchdog. + m_request_cancelled.store(true); + clear_request_timer(); + switch (evt.GetState()) { case wxWebRequest::State_Completed: @@ -120,7 +141,9 @@ void DeviceErrorDialog::on_webrequest_state(wxWebRequestEvent& evt) case wxWebRequest::State_Cancelled: case wxWebRequest::State_Unauthorized: { - m_error_picture->SetBitmap(wxBitmap()); + m_error_picture->SetBitmap(make_placeholder_image(_L("Network unavailable"))); + Layout(); + Fit(); break; } case wxWebRequest::State_Active: @@ -129,6 +152,117 @@ void DeviceErrorDialog::on_webrequest_state(wxWebRequestEvent& evt) } } +void DeviceErrorDialog::on_request_timeout(wxTimerEvent& event) +{ + if (m_request_cancelled.load()) { return; } + m_error_picture->SetBitmap(make_placeholder_image(_L("Network unavailable"))); + Layout(); + Fit(); +} + +void DeviceErrorDialog::clear_request_timer() +{ + if (m_request_timer && m_request_timer->IsRunning()) { + m_request_timer->Stop(); + } +} + +wxBitmap DeviceErrorDialog::make_placeholder_image(const wxString& text) +{ + const int w = FromDIP(320); + const int h = FromDIP(180); + + wxBitmap bmp(wxSize(w, h)); + wxMemoryDC dc(bmp); + dc.SetBackground(wxBrush(wxColour(238, 238, 238))); + dc.Clear(); + + ScalableBitmap icon = ScalableBitmap(this, "dev_hms_diag_loading", 80); + wxBitmap icon_bmp = icon.bmp(); + if (icon_bmp.IsOk()) { + int ix = (w - icon_bmp.GetWidth()) / 2; + int iy = (h - icon_bmp.GetHeight()) / 3; + dc.DrawBitmap(icon_bmp, ix, iy, true); + } + + dc.SetTextForeground(wxColour(158, 158, 158)); + dc.SetFont(Label::Body_14); + wxSize txtSize = dc.GetTextExtent(text); + int tx = (w - txtSize.GetWidth()) / 2; + int ty = h - txtSize.GetHeight() - FromDIP(45); + dc.DrawText(text, tx, ty); + + dc.SelectObject(wxNullBitmap); + return bmp; +} + +bool DeviceErrorDialog::get_fail_snapshot_from_cloud() +{ + if (!m_obj || m_obj->m_print_error_img_id.empty()) { return false; } + + NetworkAgent* agent = wxGetApp().getAgent(); + if (!agent) { return false; } + + // Orca: the cloud request has no cancel handle, so guard the callback against dialog + // destruction with a liveness token (m_alive, cleared in the dtor) and marshal onto the UI + // thread via the always-valid app handler instead of this->CallAfter. The raw bytes are + // decoded on the UI thread so no wxImage (non-atomic refcount) is shared across threads. + // Assumes a single in-flight request per dialog, which all current callers satisfy. + auto alive = m_alive; + int ret = agent->get_hms_snapshot(m_obj->get_dev_id(), m_obj->m_print_error_img_id, + [this, alive](std::string body, int status) { + if (!alive->load()) { return; } + wxGetApp().CallAfter([this, alive, body = std::move(body), status]() { + if (!alive->load()) { return; } + m_request_cancelled.store(true); + clear_request_timer(); + + wxImage image; + bool ok = false; + if (status == 200) { + wxMemoryInputStream stream(body.data(), body.size()); + ok = image.LoadFile(stream, wxBITMAP_TYPE_ANY); + if (!ok) { BOOST_LOG_TRIVIAL(error) << "get_fail_snapshot_from_cloud: failed to resolve stream"; } + } else { + BOOST_LOG_TRIVIAL(error) << "get_fail_snapshot_from_cloud: status = " << status; + } + + if (ok) { + wxImage resize_img = image.Scale(FromDIP(320), FromDIP(180), wxIMAGE_QUALITY_HIGH); + m_error_picture->SetBitmap(wxBitmap(resize_img)); + } else if (!get_fail_snapshot_from_local(m_local_img_url)) { + m_error_picture->SetBitmap(make_placeholder_image(_L("Network unavailable"))); + } + Layout(); + Fit(); + }); + }); + + return ret == 0; // dispatched; the frame arrives later via the callback +} + +bool DeviceErrorDialog::get_fail_snapshot_from_local(const wxString& image_url) +{ + if (image_url.empty()) { return false; } + + const wxImage& img = wxGetApp().get_hms_query()->query_image_from_local(image_url); + if (!img.IsOk() && image_url.Contains("http")) + { + web_request = wxWebSession::GetDefault().CreateRequest(this, image_url); + BOOST_LOG_TRIVIAL(trace) << "monitor: create new webrequest, state = " << web_request.GetState(); + if (web_request.GetState() == wxWebRequest::State_Idle) web_request.Start(); + BOOST_LOG_TRIVIAL(trace) << "monitor: start new webrequest, state = " << web_request.GetState(); + } + else + { + m_request_cancelled.store(true); + clear_request_timer(); + const wxImage& resize_img = img.Scale(FromDIP(320), FromDIP(180), wxIMAGE_QUALITY_HIGH); + m_error_picture->SetBitmap(wxBitmap(resize_img)); + } + return true; +} + void DeviceErrorDialog::init_button(ActionButton style, wxString buton_text) { if (btn_bg_white.count() == 0) @@ -305,22 +439,22 @@ void DeviceErrorDialog::update_contents(const wxString& title, const wxString& t } /* image */ - if (!image_url.empty()) + // Orca: tiered image slot — prefer the printer's captured camera frame of the failure + // (cloud, keyed by m_print_error_img_id), else the local/HTTP HMS illustration, else hide. + // Reuses m_error_picture + on_webrequest_state (single widget, no second request). The + // loading placeholder and 10s watchdog are armed only for the async cloud fetch; the local + // path keeps its existing behavior. + m_local_img_url = image_url; + m_request_cancelled.store(false); + clear_request_timer(); + if (get_fail_snapshot_from_cloud()) + { + m_error_picture->SetBitmap(make_placeholder_image(_L("Loading ..."))); + m_request_timer->StartOnce(10000); + m_error_picture->Show(); + } + else if (get_fail_snapshot_from_local(image_url)) { - const wxImage& img = wxGetApp().get_hms_query()->query_image_from_local(image_url); - if (!img.IsOk() && image_url.Contains("http")) - { - web_request = wxWebSession::GetDefault().CreateRequest(this, image_url); - BOOST_LOG_TRIVIAL(trace) << "monitor: create new webrequest, state = " << web_request.GetState(); - if (web_request.GetState() == wxWebRequest::State_Idle) web_request.Start(); - BOOST_LOG_TRIVIAL(trace) << "monitor: start new webrequest, state = " << web_request.GetState(); - } - else - { - const wxImage& resize_img = img.Scale(FromDIP(320), FromDIP(180), wxIMAGE_QUALITY_HIGH); - m_error_picture->SetBitmap(wxBitmap(resize_img)); - } - m_error_picture->Show(); } else @@ -349,7 +483,9 @@ void DeviceErrorDialog::update_contents(const wxString& title, const wxString& t auto text_size = m_error_msg_label->GetBestSize(); if (text_size.y < FromDIP(360)) { - if (!image_url.empty()) + // Orca: also reserve the image area when only a cloud snapshot (m_print_error_img_id) + // is available, so the scroll area sizes correctly even with an empty local image_url. + if (!image_url.empty() || (m_obj && !m_obj->m_print_error_img_id.empty())) { m_scroll_area->SetMinSize(wxSize(FromDIP(320), text_size.y + FromDIP(220))); } diff --git a/src/slic3r/GUI/DeviceErrorDialog.hpp b/src/slic3r/GUI/DeviceErrorDialog.hpp index af0b850e4b..e941e48ae0 100644 --- a/src/slic3r/GUI/DeviceErrorDialog.hpp +++ b/src/slic3r/GUI/DeviceErrorDialog.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include @@ -84,6 +86,11 @@ protected: void on_button_click(ActionButton btn_id); void on_webrequest_state(wxWebRequestEvent& evt); + void on_request_timeout(wxTimerEvent& event); + void clear_request_timer(); + wxBitmap make_placeholder_image(const wxString& text); + bool get_fail_snapshot_from_cloud(); + bool get_fail_snapshot_from_local(const wxString& image_url); void on_dpi_changed(const wxRect& suggested_rect); private: @@ -93,6 +100,11 @@ private: std::unordered_set m_used_button; wxWebRequest web_request; + wxTimer* m_request_timer{ nullptr }; + std::atomic m_request_cancelled{ false }; + wxString m_local_img_url; + // Orca: liveness token for the async cloud snapshot callback (the request has no cancel handle) + std::shared_ptr m_alive{ std::make_shared(true) }; wxStaticBitmap* m_error_picture; Label* m_error_msg_label{ nullptr }; Label* m_error_code_label{ nullptr }; diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index b23528a372..619477fdf4 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -553,6 +553,7 @@ MachineObject::MachineObject(DeviceManager* manager, NetworkAgent* agent, std::s mc_print_sub_stage = 0; mc_left_time = 0; hw_switch_state = 0; + m_print_error_img_id = ""; has_ipcam = true; // default true @@ -3142,6 +3143,17 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ if (jj["print_error"].is_number()) print_error = jj["print_error"].get(); } + // Orca: keep the failure-snapshot id only while an error is active, so a stale + // id can't leak into a later unrelated error dialog once the failure clears. + if (print_error <= 0) { + m_print_error_img_id.clear(); + } + else if (jj.contains("err2") && jj["err2"].is_object()) { + json err2 = jj["err2"]; + if (err2.contains("img_id") && err2["img_id"].is_string()) { + m_print_error_img_id = err2["img_id"].get(); + } + } DevStorage::ParseV1_0(jj, m_storage); diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 4ce4cad8de..7ea692082e 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -417,6 +417,7 @@ public: bool is_system_printing(); int print_error; + std::string m_print_error_img_id; static std::string get_error_code_str(int error_code); std::string get_print_error_str() const { return MachineObject::get_error_code_str(this->print_error); } diff --git a/src/slic3r/Utils/BBLPrinterAgent.cpp b/src/slic3r/Utils/BBLPrinterAgent.cpp index 9701b04194..57f763487c 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.cpp +++ b/src/slic3r/Utils/BBLPrinterAgent.cpp @@ -171,6 +171,19 @@ int BBLPrinterAgent::request_bind_ticket(std::string* ticket) return -1; } +int BBLPrinterAgent::get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) +{ + auto& plugin = BBLNetworkPlugin::instance(); + auto agent = plugin.get_agent(); + auto func = plugin.get_get_hms_snapshot(); + // dev_id/file_name are passed as lvalues to bind the plugin's std::string& params. + // A null func (older plugin without this symbol) falls through to -1 so callers degrade gracefully. + if (func && agent) { + return func(agent, dev_id, file_name, callback); + } + return -1; +} + int BBLPrinterAgent::set_server_callback(OnServerErrFn fn) { auto& plugin = BBLNetworkPlugin::instance(); diff --git a/src/slic3r/Utils/BBLPrinterAgent.hpp b/src/slic3r/Utils/BBLPrinterAgent.hpp index b4d2726ccf..a8880bf6bf 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.hpp +++ b/src/slic3r/Utils/BBLPrinterAgent.hpp @@ -45,6 +45,7 @@ public: int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override; int unbind(std::string dev_id) override; int request_bind_ticket(std::string* ticket) override; + int get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) override; int set_server_callback(OnServerErrFn fn) override; // Machine Selection diff --git a/src/slic3r/Utils/IPrinterAgent.hpp b/src/slic3r/Utils/IPrinterAgent.hpp index 52da1a069a..f533b34719 100644 --- a/src/slic3r/Utils/IPrinterAgent.hpp +++ b/src/slic3r/Utils/IPrinterAgent.hpp @@ -139,6 +139,12 @@ public: */ virtual int request_bind_ticket(std::string* ticket) = 0; + /** + * Fetch the cloud snapshot image captured at a print failure. + * Returns 0 if the request was dispatched; the image body arrives via callback(body, http_status). + */ + virtual int get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) = 0; + /** * Register callback for fatal HTTP errors. */ diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 17e0573457..d21dce5070 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -254,6 +254,15 @@ int MoonrakerPrinterAgent::request_bind_ticket(std::string* ticket) return BAMBU_NETWORK_SUCCESS; } +int MoonrakerPrinterAgent::get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) +{ + // No BBL cloud snapshot source; report failure so the caller falls back. + (void) dev_id; + (void) file_name; + (void) callback; + return -1; +} + int MoonrakerPrinterAgent::set_server_callback(OnServerErrFn fn) { std::lock_guard lock(state_mutex); diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index bf152bcaac..ae63570063 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -45,6 +45,7 @@ public: int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override; int unbind(std::string dev_id) override; int request_bind_ticket(std::string* ticket) override; + int get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) override; int set_server_callback(OnServerErrFn fn) override; // Machine Selection diff --git a/src/slic3r/Utils/NetworkAgent.cpp b/src/slic3r/Utils/NetworkAgent.cpp index 44489ff445..0929a8524b 100644 --- a/src/slic3r/Utils/NetworkAgent.cpp +++ b/src/slic3r/Utils/NetworkAgent.cpp @@ -937,4 +937,11 @@ int NetworkAgent::request_bind_ticket(std::string* ticket) return -1; } +int NetworkAgent::get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) +{ + if (m_printer_agent) + return m_printer_agent->get_hms_snapshot(dev_id, file_name, callback); + return -1; +} + } // namespace Slic3r diff --git a/src/slic3r/Utils/NetworkAgent.hpp b/src/slic3r/Utils/NetworkAgent.hpp index 5df64d3abf..d7032b7a20 100644 --- a/src/slic3r/Utils/NetworkAgent.hpp +++ b/src/slic3r/Utils/NetworkAgent.hpp @@ -166,6 +166,7 @@ public: FilamentSyncMode get_filament_sync_mode() const; bool fetch_filament_info(std::string dev_id); int request_bind_ticket(std::string* ticket); + int get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback); private: struct PrinterCallbacks { diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index 40ea519aba..cb70dafcca 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -95,6 +95,15 @@ int OrcaPrinterAgent::request_bind_ticket(std::string* ticket) return BAMBU_NETWORK_SUCCESS; } +int OrcaPrinterAgent::get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) +{ + // No BBL cloud snapshot source; report failure so the caller falls back. + (void) dev_id; + (void) file_name; + (void) callback; + return -1; +} + int OrcaPrinterAgent::set_server_callback(OnServerErrFn fn) { std::lock_guard lock(state_mutex); diff --git a/src/slic3r/Utils/OrcaPrinterAgent.hpp b/src/slic3r/Utils/OrcaPrinterAgent.hpp index 5b627c7b81..a1613420a5 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.hpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.hpp @@ -45,6 +45,7 @@ public: int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override; int unbind(std::string dev_id) override; int request_bind_ticket(std::string* ticket) override; + int get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) override; int set_server_callback(OnServerErrFn fn) override; // Machine Selection