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