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.
This commit is contained in:
SoftFever
2026-07-16 01:44:43 +08:00
parent aa44d57eac
commit d591d50ca0
14 changed files with 244 additions and 17 deletions

View File

@@ -5,6 +5,10 @@
#include "GUI_App.hpp"
#include "MainFrame.hpp"
#include "ReleaseNote.hpp"
#include "wxExtensions.hpp"
#include <wx/mstream.h>
#include <wx/dcmemory.h>
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)));
}

View File

@@ -1,6 +1,8 @@
#pragma once
#include <unordered_set>
#include <atomic>
#include <memory>
#include <wx/statbmp.h>
#include <wx/webrequest.h>
@@ -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<Button*> m_used_button;
wxWebRequest web_request;
wxTimer* m_request_timer{ nullptr };
std::atomic<bool> 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<std::atomic_bool> m_alive{ std::make_shared<std::atomic_bool>(true) };
wxStaticBitmap* m_error_picture;
Label* m_error_msg_label{ nullptr };
Label* m_error_code_label{ nullptr };

View File

@@ -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<int>();
}
// 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<std::string>();
}
}
DevStorage::ParseV1_0(jj, m_storage);

View File

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

View File

@@ -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<void(std::string, int)> 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();

View File

@@ -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<void(std::string, int)> callback) override;
int set_server_callback(OnServerErrFn fn) override;
// Machine Selection

View File

@@ -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<void(std::string, int)> callback) = 0;
/**
* Register callback for fatal HTTP errors.
*/

View File

@@ -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<void(std::string, int)> 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<std::recursive_mutex> lock(state_mutex);

View File

@@ -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<void(std::string, int)> callback) override;
int set_server_callback(OnServerErrFn fn) override;
// Machine Selection

View File

@@ -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<void(std::string, int)> callback)
{
if (m_printer_agent)
return m_printer_agent->get_hms_snapshot(dev_id, file_name, callback);
return -1;
}
} // namespace Slic3r

View File

@@ -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<void(std::string, int)> callback);
private:
struct PrinterCallbacks {

View File

@@ -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<void(std::string, int)> 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<std::mutex> lock(state_mutex);

View File

@@ -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<void(std::string, int)> callback) override;
int set_server_callback(OnServerErrFn fn) override;
// Machine Selection