diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index f81c79269d..97c4920be7 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -12,6 +12,7 @@ #include "MsgDialog.hpp" #include "slic3r/Utils/Http.hpp" +#include "slic3r/Utils/MoonrakerPrinterAgent.hpp" #include "libslic3r/Thread.hpp" #include "DeviceErrorDialog.hpp" @@ -2714,7 +2715,8 @@ void StatusPanel::on_subtask_partskip(wxCommandEvent &event) void StatusPanel::on_subtask_pause_resume(wxCommandEvent &event) { if (obj) { - if (obj->can_resume()) { + const bool was_resume = obj->can_resume(); + if (was_resume) { BOOST_LOG_TRIVIAL(info) << "monitor: resume current print task dev_id =" << obj->get_dev_id(); obj->command_task_resume(); } @@ -2722,6 +2724,13 @@ void StatusPanel::on_subtask_pause_resume(wxCommandEvent &event) BOOST_LOG_TRIVIAL(info) << "monitor: pause current print task dev_id =" << obj->get_dev_id(); obj->command_task_pause(); } + if (is_moonraker_agent()) { + m_pause_resume_pending = true; + m_pause_resume_was_resume = was_resume; + m_pause_resume_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(6); + m_pause_resume_machine_id = obj->get_dev_id(); + m_project_task_panel->enable_pause_resume_button(false, was_resume ? "resume_disable" : "pause_disable"); + } } } @@ -2733,6 +2742,12 @@ void StatusPanel::on_subtask_abort(wxCommandEvent &event) if (obj) { BOOST_LOG_TRIVIAL(info) << "monitor: stop current print task dev_id =" << obj->get_dev_id(); obj->command_task_abort(); + if (is_moonraker_agent()) { + m_abort_pending = true; + m_abort_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(6); + m_abort_machine_id = obj->get_dev_id(); + m_project_task_panel->enable_abort_button(false); + } } }); } @@ -3706,6 +3721,25 @@ void StatusPanel::update_model_info() void StatusPanel::update_subtask(MachineObject *obj) { if (!obj) return; + const auto now = std::chrono::steady_clock::now(); + if (m_pause_resume_pending) { + if (!is_moonraker_agent() || m_pause_resume_machine_id != obj->get_dev_id() || + obj->can_resume() != m_pause_resume_was_resume) { + m_pause_resume_pending = false; + } else if (now >= m_pause_resume_deadline) { + BOOST_LOG_TRIVIAL(warning) << "StatusPanel: Moonraker pause/resume command did not change printer state"; + m_pause_resume_pending = false; + } + } + if (m_abort_pending) { + if (!is_moonraker_agent() || m_abort_machine_id != obj->get_dev_id() || obj->print_status == "FAILED" || + obj->print_status == "FINISH" || obj->print_status == "IDLE") { + m_abort_pending = false; + } else if (now >= m_abort_deadline) { + BOOST_LOG_TRIVIAL(warning) << "StatusPanel: Moonraker abort command did not change printer state"; + m_abort_pending = false; + } + } if (m_current_print_mode != PRINGINT) { if (calib_bitmap == nullptr) { m_calib_mode = get_obj_calibration_mode(obj, m_calib_method, cali_stage); @@ -3813,10 +3847,12 @@ void StatusPanel::update_subtask(MachineObject *obj) } update_basic_print_data(false); } else { - if (obj->can_resume()) { - m_project_task_panel->enable_pause_resume_button(true, "resume"); - } else { - m_project_task_panel->enable_pause_resume_button(true, "pause"); + if (!m_pause_resume_pending) { + if (obj->can_resume()) { + m_project_task_panel->enable_pause_resume_button(true, "resume"); + } else { + m_project_task_panel->enable_pause_resume_button(true, "pause"); + } } m_project_task_panel->enable_partskip_button(obj, true); // update printing stage @@ -3873,7 +3909,9 @@ void StatusPanel::update_subtask(MachineObject *obj) m_project_task_panel->market_scoring_hide(); } } else { // model printing is not finished, hide scoring page - m_project_task_panel->enable_abort_button(true); + if (!m_abort_pending) { + m_project_task_panel->enable_abort_button(true); + } m_project_task_panel->market_scoring_hide(); m_project_task_panel->get_request_failed_panel()->Hide(); } @@ -4016,6 +4054,8 @@ void StatusPanel::update_sdcard_subtask(MachineObject *obj) void StatusPanel::reset_printing_values() { + m_pause_resume_pending = false; + m_abort_pending = false; m_project_task_panel->enable_partskip_button(nullptr, false); m_project_task_panel->enable_pause_resume_button(false, "pause_disable"); m_project_task_panel->enable_abort_button(false); @@ -4041,6 +4081,12 @@ void StatusPanel::reset_printing_values() this->Layout(); } +bool StatusPanel::is_moonraker_agent() const +{ + auto* agent = wxGetApp().getAgent(); + return agent && std::dynamic_pointer_cast(agent->get_printer_agent()) != nullptr; +} + void StatusPanel::on_axis_ctrl_xy(wxCommandEvent &event) { if (!obj) return; diff --git a/src/slic3r/GUI/StatusPanel.hpp b/src/slic3r/GUI/StatusPanel.hpp index 04c32e472e..170a9d970d 100644 --- a/src/slic3r/GUI/StatusPanel.hpp +++ b/src/slic3r/GUI/StatusPanel.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include "wxMediaCtrl2.h" #include "MediaPlayCtrl.h" #include "AMSSetting.hpp" @@ -688,6 +689,13 @@ protected: CalibrationMethod m_calib_method; int cali_stage; PrintingTaskType m_current_print_mode = PrintingTaskType::NOT_CLEAR; + bool m_pause_resume_pending = false; + bool m_pause_resume_was_resume = false; + std::chrono::steady_clock::time_point m_pause_resume_deadline; + std::string m_pause_resume_machine_id; + bool m_abort_pending = false; + std::chrono::steady_clock::time_point m_abort_deadline; + std::string m_abort_machine_id; void init_scaled_buttons(); void create_tasklist_info(); @@ -790,6 +798,7 @@ protected: void update_calib_bitmap(); void reset_printing_values(); + bool is_moonraker_agent() const; void on_webrequest_state(wxWebRequestEvent &evt); bool is_task_changed(MachineObject* obj); diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 31e6953bc5..0d02a00eff 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -126,6 +126,53 @@ MoonrakerPrinterAgent::~MoonrakerPrinterAgent() connect_thread.join(); } stop_status_stream(); + { + std::lock_guard lock(cmd_mutex); + cmd_stop = true; + cmd_queue.clear(); + } + cmd_cv.notify_one(); + if (cmd_thread.joinable()) { + cmd_thread.join(); + } +} + +void MoonrakerPrinterAgent::enqueue_command(std::function fn) +{ + { + std::lock_guard lock(cmd_mutex); + if (!cmd_thread.joinable()) { + cmd_thread = std::thread(&MoonrakerPrinterAgent::run_command_worker, this); + } + cmd_queue.emplace_back(std::move(fn)); + } + cmd_cv.notify_one(); +} + +void MoonrakerPrinterAgent::run_command_worker() +{ + for (;;) { + std::function command; + { + std::unique_lock lock(cmd_mutex); + cmd_cv.wait(lock, [this] { return cmd_stop || !cmd_queue.empty(); }); + if (cmd_stop && cmd_queue.empty()) { + return; + } + command = std::move(cmd_queue.front()); + cmd_queue.pop_front(); + } + // why: an exception escaping a worker thread is std::terminate; the old synchronous + // path at least ran under wx's unhandled-exception hook. nlohmann dump() can throw + // on invalid UTF-8 smuggled in via custom g-code. + try { + command(); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: queued command failed: " << e.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: queued command failed: unknown exception"; + } + } } AgentInfo MoonrakerPrinterAgent::get_agent_info_static() @@ -181,6 +228,10 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i // Stop existing status stream and clear state stop_status_stream(); + { + std::lock_guard lock(cmd_mutex); + cmd_queue.clear(); + } { std::lock_guard lock(payload_mutex); status_cache = nlohmann::json::object(); @@ -210,6 +261,10 @@ int MoonrakerPrinterAgent::disconnect_printer() } stop_status_stream(); + { + std::lock_guard lock(cmd_mutex); + cmd_queue.clear(); + } return BAMBU_NETWORK_SUCCESS; } @@ -1016,6 +1071,10 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector& tra int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) { + auto connection_snapshot = [this]() { + std::lock_guard lock(connect_mutex); + return std::make_pair(device_info.base_url, device_info.api_key); + }; auto json = nlohmann::json::parse(json_str, nullptr, false); if (json.is_discarded()) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Invalid JSON request"; @@ -1092,13 +1151,14 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: ledctrl - no light object found, dropping"; return ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE; } - if (!send_gcode(dev_id, gcode)) { - return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; - } - { - std::lock_guard lock(payload_mutex); - assumed_light_on = requested_light_on; - } + auto [base_url, api_key] = connection_snapshot(); + enqueue_command([this, dev_id, gcode = std::move(gcode), base_url = std::move(base_url), + api_key = std::move(api_key), requested_light_on]() { + if (send_gcode(dev_id, gcode, base_url, api_key)) { + std::lock_guard lock(payload_mutex); + assumed_light_on = requested_light_on; + } + }); return BAMBU_NETWORK_SUCCESS; } } @@ -1143,25 +1203,38 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: } response["print"]["param"] = gcode; - if (send_gcode(dev_id, gcode)) { - response["print"]["result"] = "success"; + auto [base_url, api_key] = connection_snapshot(); + enqueue_command([this, dev_id, response = std::move(response), base_url = std::move(base_url), + api_key = std::move(api_key)]() mutable { + response["print"]["result"] = send_gcode(dev_id, response["print"]["param"].get(), base_url, api_key) + ? "success" + : "failed"; dispatch_message(dev_id, response.dump()); - return BAMBU_NETWORK_SUCCESS; - } - response["print"]["result"] = "failed"; - dispatch_message(dev_id, response.dump()); - return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; + }); + return BAMBU_NETWORK_SUCCESS; } // Print control commands if (cmd == "pause") { - return pause_print(dev_id); + auto [base_url, api_key] = connection_snapshot(); + enqueue_command([this, base_url = std::move(base_url), api_key = std::move(api_key)] { + post_print_action("pause", base_url, api_key); + }); + return BAMBU_NETWORK_SUCCESS; } if (cmd == "resume") { - return resume_print(dev_id); + auto [base_url, api_key] = connection_snapshot(); + enqueue_command([this, base_url = std::move(base_url), api_key = std::move(api_key)] { + post_print_action("resume", base_url, api_key); + }); + return BAMBU_NETWORK_SUCCESS; } if (cmd == "stop") { - return cancel_print(dev_id); + auto [base_url, api_key] = connection_snapshot(); + enqueue_command([this, base_url = std::move(base_url), api_key = std::move(api_key)] { + post_print_action("cancel", base_url, api_key); + }); + return BAMBU_NETWORK_SUCCESS; } // Bed temperature - UI sends "temp" field @@ -1169,7 +1242,11 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: if (json["print"].contains("temp") && json["print"]["temp"].is_number()) { int temp = json["print"]["temp"].get(); std::string gcode = "SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=" + std::to_string(temp); - send_gcode(dev_id, gcode); + auto [base_url, api_key] = connection_snapshot(); + enqueue_command([this, dev_id, gcode = std::move(gcode), base_url = std::move(base_url), + api_key = std::move(api_key)] { + send_gcode(dev_id, gcode, base_url, api_key); + }); return BAMBU_NETWORK_SUCCESS; } } @@ -1184,7 +1261,11 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: } std::string heater = (extruder_idx == 0) ? "extruder" : "extruder" + std::to_string(extruder_idx); std::string gcode = "SET_HEATER_TEMPERATURE HEATER=" + heater + " TARGET=" + std::to_string(temp); - send_gcode(dev_id, gcode); + auto [base_url, api_key] = connection_snapshot(); + enqueue_command([this, dev_id, gcode = std::move(gcode), base_url = std::move(base_url), + api_key = std::move(api_key)] { + send_gcode(dev_id, gcode, base_url, api_key); + }); return BAMBU_NETWORK_SUCCESS; } } @@ -1192,7 +1273,11 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: // why: no current OrcaSlicer sender emits the "home" discriminator; // GUI homing uses gcode_line with G28 instead. if (cmd == "home") { - return send_gcode(dev_id, "G28") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; + auto [base_url, api_key] = connection_snapshot(); + enqueue_command([this, dev_id, base_url = std::move(base_url), api_key = std::move(api_key)] { + send_gcode(dev_id, "G28", base_url, api_key); + }); + return BAMBU_NETWORK_SUCCESS; } } @@ -1456,6 +1541,21 @@ bool MoonrakerPrinterAgent::fetch_webcam_info(const std::string& base_url, const } bool MoonrakerPrinterAgent::post_print_action(const std::string& action) const +{ + // why: snapshot then release - holding connect_mutex across the blocking HTTP call + // would stall any UI-thread handle_request waiting to take its own snapshot. + std::string base_url, api_key; + { + std::lock_guard lock(connect_mutex); + base_url = device_info.base_url; + api_key = device_info.api_key; + } + return post_print_action(action, base_url, api_key); +} + +bool MoonrakerPrinterAgent::post_print_action(const std::string& action, + const std::string& base_url, + const std::string& api_key) const { // why: /printer/print/{pause,resume,cancel} map to Klipper's pause_resume // webhook (a direct interrupt). A raw PAUSE/RESUME/CANCEL_PRINT queued through @@ -1463,13 +1563,13 @@ bool MoonrakerPrinterAgent::post_print_action(const std::string& action) const // printer is busy (long move, heating, inside a macro). // note: empty JSON body avoids a body-less POST (curl would treat it as a // streamed upload) - same reason start_print_file sends a body. - const std::string full_url = join_url(device_info.base_url, "/printer/print/" + action); + const std::string full_url = join_url(base_url, "/printer/print/" + action); bool success = false; std::string http_error; auto http = Http::post(full_url); - if (!device_info.api_key.empty()) { - http.header("X-Api-Key", device_info.api_key); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); } http.header("Content-Type", "application/json") .set_post_body(std::string("{}")) @@ -1501,6 +1601,19 @@ bool MoonrakerPrinterAgent::post_print_action(const std::string& action) const } bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::string& gcode) const +{ + // why: snapshot then release - see post_print_action. + std::string base_url, api_key; + { + std::lock_guard lock(connect_mutex); + base_url = device_info.base_url; + api_key = device_info.api_key; + } + return send_gcode(dev_id, gcode, base_url, api_key); +} + +bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::string& gcode, + const std::string& base_url, const std::string& api_key) const { nlohmann::json payload; payload["script"] = gcode; @@ -1510,10 +1623,10 @@ bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::str bool success = false; std::string http_error; - auto full_url = join_url(device_info.base_url, "/printer/gcode/script"); + auto full_url = join_url(base_url, "/printer/gcode/script"); auto http = Http::post(full_url); - if (!device_info.api_key.empty()) { - http.header("X-Api-Key", device_info.api_key); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); } http.header("Content-Type", "application/json") .set_post_body(payload_str) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index 70e734eea6..e35064cda8 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include @@ -125,7 +128,11 @@ protected: // Send a G-code script via Moonraker (/printer/gcode/script) bool send_gcode(const std::string& dev_id, const std::string& gcode) const; + bool send_gcode(const std::string& dev_id, const std::string& gcode, + const std::string& base_url, const std::string& api_key) const; bool post_print_action(const std::string& action) const; + bool post_print_action(const std::string& action, + const std::string& base_url, const std::string& api_key) const; private: int handle_request(const std::string& dev_id, const std::string& json_str); @@ -221,7 +228,15 @@ private: // Connection thread management std::atomic connect_generation{0}; std::thread connect_thread; - std::recursive_mutex connect_mutex; + mutable std::recursive_mutex connect_mutex; + + void enqueue_command(std::function fn); + void run_command_worker(); + std::thread cmd_thread; + std::deque> cmd_queue; + std::mutex cmd_mutex; + std::condition_variable cmd_cv; + bool cmd_stop = false; }; } // namespace Slic3r