From 3adcb3e9539606cb89bd6777557da3df06897ac2 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 15 Sep 2026 13:27:17 +0800 Subject: [PATCH] fix: split infra from impl --- src/slic3r/CMakeLists.txt | 4 - src/slic3r/Utils/BBLPrinterAgent.cpp | 175 +- src/slic3r/Utils/BBLPrinterAgent.hpp | 18 +- src/slic3r/Utils/CrealityPrintAgent.cpp | 5 +- src/slic3r/Utils/CrealityPrintAgent.hpp | 1 - src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 1010 +--------- src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 80 +- src/slic3r/Utils/OrcaCloudServiceAgent.cpp | 297 +-- src/slic3r/Utils/OrcaCloudServiceAgent.hpp | 79 +- .../Utils/OrcaCloudSignalingChannel.cpp | 320 ---- .../Utils/OrcaCloudSignalingChannel.hpp | 63 - src/slic3r/Utils/OrcaMqttConnection.cpp | 839 --------- src/slic3r/Utils/OrcaMqttConnection.hpp | 152 -- src/slic3r/Utils/OrcaPrinterAgent.cpp | 1618 +---------------- src/slic3r/Utils/OrcaPrinterAgent.hpp | 146 +- src/slic3r/Utils/QidiPrinterAgent.cpp | 209 +-- src/slic3r/Utils/QidiPrinterAgent.hpp | 18 - src/slic3r/Utils/SnapmakerPrinterAgent.cpp | 268 ++- src/slic3r/Utils/SnapmakerPrinterAgent.hpp | 12 - tests/slic3rutils/CMakeLists.txt | 3 - tests/slic3rutils/orca_mqtt_mock_broker.hpp | 317 ---- .../slic3rutils/test_orca_mqtt_connection.cpp | 229 --- tests/slic3rutils/test_orca_printer_agent.cpp | 204 --- tests/slic3rutils/test_printer_agent.cpp | 155 -- tests/slic3rutils/test_qidi_printer_agent.cpp | 131 -- 25 files changed, 300 insertions(+), 6053 deletions(-) delete mode 100644 src/slic3r/Utils/OrcaCloudSignalingChannel.cpp delete mode 100644 src/slic3r/Utils/OrcaCloudSignalingChannel.hpp delete mode 100644 src/slic3r/Utils/OrcaMqttConnection.cpp delete mode 100644 src/slic3r/Utils/OrcaMqttConnection.hpp delete mode 100644 tests/slic3rutils/orca_mqtt_mock_broker.hpp delete mode 100644 tests/slic3rutils/test_orca_mqtt_connection.cpp delete mode 100644 tests/slic3rutils/test_orca_printer_agent.cpp delete mode 100644 tests/slic3rutils/test_qidi_printer_agent.cpp diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 5023b42bcf..67151edeba 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -734,12 +734,8 @@ set(SLIC3R_GUI_SOURCES Utils/ICameraSignalingChannel.hpp Utils/OrcaCloudServiceAgent.cpp Utils/OrcaCloudServiceAgent.hpp - Utils/OrcaMqttConnection.cpp - Utils/OrcaMqttConnection.hpp Utils/OrcaPrinterAgent.cpp Utils/OrcaPrinterAgent.hpp - Utils/OrcaCloudSignalingChannel.cpp - Utils/OrcaCloudSignalingChannel.hpp Utils/QidiPrinterAgent.cpp Utils/QidiPrinterAgent.hpp Utils/SnapmakerPrinterAgent.cpp diff --git a/src/slic3r/Utils/BBLPrinterAgent.cpp b/src/slic3r/Utils/BBLPrinterAgent.cpp index 9f4f8cbe06..0c6225cc55 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.cpp +++ b/src/slic3r/Utils/BBLPrinterAgent.cpp @@ -1,11 +1,8 @@ #include "BBLPrinterAgent.hpp" #include "BBLNetworkPlugin.hpp" -#include "IPrinterAgent.hpp" #include "NetworkAgentFactory.hpp" #include "libslic3r/Utils.hpp" -#include "NetworkAgent.hpp" -#include #include #include #include @@ -13,10 +10,6 @@ using json = nlohmann::json; #include #include -#include -#include -#include -#include namespace Slic3r { @@ -140,7 +133,7 @@ BBLPrinterAgent::~BBLPrinterAgent() = default; void BBLPrinterAgent::set_cloud_agent(std::shared_ptr cloud) { - (void) cloud; + m_cloud_agent = cloud; // BBL DLL manages tokens internally, so this is just for interface compliance } @@ -148,163 +141,6 @@ void BBLPrinterAgent::set_cloud_agent(std::shared_ptr cloud) // Communication // ============================================================================ -std::string BBLPrinterAgent::ams_refresh_rfid_gcode(const std::string& tray_id) -{ - return (boost::format("M620 R%1% \n") % tray_id).str(); -} - -std::string BBLPrinterAgent::ams_calibrate_gcode(int ams_id) -{ - return (boost::format("M620 C%1% \n") % ams_id).str(); -} - -std::string BBLPrinterAgent::ams_select_tray_gcode(const std::string& tray_id) -{ - return (boost::format("M620 P%1% \n") % tray_id).str(); -} - -int BBLPrinterAgent::command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) -{ - const std::string gcode = ams_refresh_rfid_gcode(tray_id); - BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode; - nlohmann::json j; - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = gcode; - j["print"]["sequence_id"] = std::to_string(sequence_id); - return publish(dev_id, j, lan_mode); -} - -int BBLPrinterAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) -{ - const std::string gcode = ams_calibrate_gcode(ams_id); - BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode; - nlohmann::json j; - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = gcode; - j["print"]["sequence_id"] = std::to_string(sequence_id); - return publish(dev_id, j, lan_mode); -} - -int BBLPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) -{ - const std::string gcode = ams_select_tray_gcode(tray_id); - BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode; - nlohmann::json j; - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = gcode; - j["print"]["sequence_id"] = std::to_string(sequence_id); - return publish(dev_id, j, lan_mode); -} - -int BBLPrinterAgent::command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) -{ - nlohmann::json j; - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = "G90 \n"; - j["print"]["sequence_id"] = std::to_string(sequence_id); - return publish(dev_id, j, lan_mode); -} - -int BBLPrinterAgent::command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) -{ - nlohmann::json j; - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = "G29 \n"; - j["print"]["sequence_id"] = std::to_string(sequence_id); - return publish(dev_id, j, lan_mode); -} - -int BBLPrinterAgent::command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) -{ - nlohmann::json j; - j["print"]["sequence_id"] = std::to_string(sequence_id); - if (supports_mqtt_homing) { - j["print"]["command"] = "back_to_center"; - return publish(dev_id, j, lan_mode); - } - - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = is_printing ? "G28 X\n" : "G28 \n"; - return publish(dev_id, j, lan_mode); -} - -int BBLPrinterAgent::command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode) -{ - nlohmann::json j; - j["print"]["sequence_id"] = std::to_string(sequence_id); - if (supports_mqtt_bed_ctrl) { - j["print"]["command"] = "set_bed_temp"; - j["print"]["temp"] = temp; - return publish(dev_id, j, lan_mode); - } - - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = (boost::format("M140 S%1%\n") % temp).str(); - return publish(dev_id, j, lan_mode); -} - -int BBLPrinterAgent::command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) -{ - nlohmann::json j; - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = (boost::format("M104 S%1%\n") % temp).str(); - j["print"]["sequence_id"] = std::to_string(sequence_id); - return publish(dev_id, j, lan_mode); -} - -int BBLPrinterAgent::command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, int speed, - bool is_core_xy, bool supports_mqtt_axis_control, int sequence_id, bool lan_mode) -{ - nlohmann::json j; - j["print"]["sequence_id"] = std::to_string(sequence_id); - - if (supports_mqtt_axis_control) { - int dir = input_val > 0 ? 1 : -1; - // i3-arch printers move the bed for Y/Z, so the on-screen direction is - // reversed -- same negation the g-code fallback below applies. - if (!is_core_xy && (axis == "Y" || axis == "Z")) { - dir = -dir; - } - - j["print"]["command"] = "xyz_ctrl"; - j["print"]["axis"] = axis; - j["print"]["dir"] = dir; - j["print"]["mode"] = (std::abs(input_val) >= 10) ? 1 : 0; - return publish(dev_id, j, lan_mode); - } - - double value = input_val; - if (!is_core_xy && (axis == "Y" || axis == "Z")) { - value = -1.0 * input_val; - } - - std::string value_str = (boost::format("%.1f") % (value * unit)).str(); - std::string gcode; - if (axis == "X" || axis == "Y" || axis == "Z") { - gcode = (boost::format("M211 S \nM211 X1 Y1 Z1\nM1002 push_ref_mode\nG91 \nG1 %1%%2% F%3%\nM1002 pop_ref_mode\nM211 R\n") - % axis % value_str % speed).str(); - } else if (axis == "E") { - gcode = (boost::format("M83 \nG0 %1%%2% F%3%\n") % axis % value_str % speed).str(); - } else { - return -1; - } - - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = gcode; - return publish(dev_id, j, lan_mode); -} - -int BBLPrinterAgent::publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode) -{ - const int rtn = lan_mode ? send_message_to_printer(dev_id, j.dump(), 0, 0) : send_message(dev_id, j.dump(), 0, 0); - if (rtn == 0) { - BOOST_LOG_TRIVIAL(info) << "publish_json: " << j.dump() << " code: " << rtn; - } else { - BOOST_LOG_TRIVIAL(error) << "publish_json: " << j.dump() << " code: " << rtn; - } - return rtn; -} - int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) { json_str = from_orca_payload(std::move(json_str)); @@ -637,15 +473,8 @@ int BBLPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateS int BBLPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { - int result = dispatch_start( + return dispatch_start( BBLNetworkPlugin::instance().get_start_send_gcode_to_sdcard(), params, update_fn, cancel_fn, wait_fn); - if (result != 0) { - BOOST_LOG_TRIVIAL(error) << "start_send_gcode_to_sdcard failed: result=" << result - << ", try_emmc_print=" << params.try_emmc_print - << ", legacy_mode=" << BBLNetworkPlugin::instance().use_legacy_network() - << ", dev_ip=" << params.dev_ip << ", dev_id=" << params.dev_id; - } - return result; } int BBLPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) diff --git a/src/slic3r/Utils/BBLPrinterAgent.hpp b/src/slic3r/Utils/BBLPrinterAgent.hpp index 8ffb201d48..83e21bb6d8 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.hpp +++ b/src/slic3r/Utils/BBLPrinterAgent.hpp @@ -5,7 +5,6 @@ #include "ICloudServiceAgent.hpp" #include #include -#include namespace Slic3r { @@ -29,23 +28,9 @@ public: // Communication int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; - static std::string ams_refresh_rfid_gcode(const std::string& tray_id); - static std::string ams_calibrate_gcode(int ams_id); - static std::string ams_select_tray_gcode(const std::string& tray_id); - int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override; - int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override; - int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override; - int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override; - int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override; - int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) override; - int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode) override; - int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override; - int command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, int speed, - bool is_core_xy, bool supports_mqtt_axis_control, int sequence_id, bool lan_mode) override; int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; int disconnect_printer() override; int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; - std::string default_lan_username() const override { return "bblp"; } // Certificates int check_cert() override; @@ -114,8 +99,7 @@ public: static std::string from_orca_payload(std::string json_text); private: - // why: the lan/cloud DECISION stays machine-side; keep this mechanical branch in sync with publish_json. - int publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode); + std::shared_ptr m_cloud_agent; }; } // namespace Slic3r diff --git a/src/slic3r/Utils/CrealityPrintAgent.cpp b/src/slic3r/Utils/CrealityPrintAgent.cpp index 9eb3a2e87a..4730903c59 100644 --- a/src/slic3r/Utils/CrealityPrintAgent.cpp +++ b/src/slic3r/Utils/CrealityPrintAgent.cpp @@ -282,11 +282,8 @@ bool CrealityPrintAgent::parse_cfs_response(const std::string& response, return true; } -bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode) +bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/) { - if (sync_mode != get_filament_sync_mode()) - return false; - if (device_info.dev_ip.empty()) { BOOST_LOG_TRIVIAL(warning) << "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent"; diff --git a/src/slic3r/Utils/CrealityPrintAgent.hpp b/src/slic3r/Utils/CrealityPrintAgent.hpp index 3b925c5d21..cf22d104ec 100644 --- a/src/slic3r/Utils/CrealityPrintAgent.hpp +++ b/src/slic3r/Utils/CrealityPrintAgent.hpp @@ -1,7 +1,6 @@ #ifndef __CREALITY_PRINT_AGENT_HPP__ #define __CREALITY_PRINT_AGENT_HPP__ -#include "IPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp" #include diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 2d27e5cb44..60e68c6ecd 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -1,9 +1,7 @@ #include "MoonrakerPrinterAgent.hpp" #include "Http.hpp" -#include "IPrinterAgent.hpp" #include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" -#include "libslic3r/Utils.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/DeviceManager.hpp" #include "slic3r/GUI/DeviceCore/DevFilaSystem.h" @@ -98,36 +96,10 @@ namespace Slic3r { const std::string MoonrakerPrinterAgent_VERSION = "1.0.0"; -bool moonraker_is_light_name(const std::string& name) -{ - std::string lower_name = name; - std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - - size_t pos = lower_name.find("led"); - while (pos != std::string::npos) { - const bool at_start = pos == 0 || lower_name[pos - 1] == '_' || lower_name[pos - 1] == '-'; - const size_t end = pos + 3; - const bool at_end = end == lower_name.size() || lower_name[end] == '_' || lower_name[end] == '-'; - if (at_start && at_end) { - return true; - } - pos = lower_name.find("led", pos + 1); - } - return lower_name.find("light") != std::string::npos; -} - MoonrakerPrinterAgent::MoonrakerPrinterAgent(std::string log_dir) : m_cloud_agent(nullptr) { (void) log_dir; } MoonrakerPrinterAgent::~MoonrakerPrinterAgent() { - // Detached fetch_filament_info() threads (see QidiPrinterAgent::fetch_filament_info) - // hold a raw `this` with no other lifetime protection — wait for them to finish before - // any part of this object is torn down, so they never touch freed memory. - while (filament_fetch_in_flight.load() > 0) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - { std::lock_guard lock(connect_mutex); device_info = MoonrakerDeviceInfo{}; @@ -137,53 +109,6 @@ 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() @@ -218,11 +143,6 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i return BAMBU_NETWORK_ERR_INVALID_HANDLE; } - // why: Moonraker/print-host serves plain HTTP (nginx :80 or Moonraker :7125), never - // https:443; MachineObject::connect defaults use_ssl=true -> forced https -> refused. - // Pin http. (matches feature/printer-agent-port-pristine) - use_ssl = false; - std::string base_url; std::string api_key; uint64_t gen; @@ -239,17 +159,12 @@ 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(); } ws_last_emit_ms.store(0); ws_last_dispatch_ms.store(0); - ams_last_fetch_ms.store(0); last_print_state.clear(); // Launch connection in background thread (capture by value to avoid data races) @@ -273,10 +188,6 @@ int MoonrakerPrinterAgent::disconnect_printer() } stop_status_stream(); - { - std::lock_guard lock(cmd_mutex); - cmd_queue.clear(); - } return BAMBU_NETWORK_SUCCESS; } @@ -290,15 +201,11 @@ void MoonrakerPrinterAgent::install_device_cert(std::string dev_id, bool lan_onl bool MoonrakerPrinterAgent::start_discovery(bool start, bool sending) { - // Discovery is not properly implemented, avoid populating machine list - // with stale device - // (void) sending; - // if (start) { - // announce_printhost_device(); - // } - // return true; - - return BAMBU_NETWORK_SUCCESS; + (void) sending; + if (start) { + announce_printhost_device(); + } + return true; } int MoonrakerPrinterAgent::ping_bind(std::string ping_code) @@ -311,20 +218,11 @@ int MoonrakerPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, { (void) sec_link; - // why: bind_detect runs BEFORE any connect (from the "Bind with Access Code" IP - // dialog that creates the new MachineObject), so device_info is unpopulated. Hydrate - // it from the edited preset first - init_device_info sets dev_name = dev_id = dev_ip, - // so the name falls back to the IP instead of blank. (matches - // feature/printer-agent-port-pristine; the IP is what shipped before the port) - // note: dummy id/creds; use_ssl false because Moonraker/print-host is http. - init_device_info(dev_ip, dev_ip, "", "", false); - detect.dev_id = device_info.dev_id.empty() ? dev_ip : device_info.dev_id; detect.model_id = device_info.model_id.empty() ? device_info.model_name : device_info.model_id; - // Name priority: known device name, then preset model name, then the address (never empty). - detect.dev_name = !device_info.dev_name.empty() ? device_info.dev_name - : !device_info.model_name.empty() ? device_info.model_name - : dev_ip; + // Prefer fetched hostname, then preset model name, then generic fallback + detect.dev_name = device_info.dev_name; + detect.model_id = device_info.model_id; detect.version = device_info.version; detect.connect_type = "lan"; detect.bind_state = "free"; @@ -448,17 +346,11 @@ int MoonrakerPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusF if (cancel_fn && cancel_fn()) { return BAMBU_NETWORK_ERR_CANCELED; } - // Determine the G-code file to upload. - // - params.dst_file, when set, points directly at the sliced G-code. - // - Otherwise params.filename is the exported .3mf archive; the sliced - // G-code sits next to it with the same stem (".12345.0.3mf" -> - // ".12345.0.gcode"), so swap the extension to upload the actual G-code - // rather than the archive (which Klipper cannot print). + // Determine the G-code file to upload + // params.filename may be .3mf, params.dst_file contains actual G-code std::string gcode_path = params.filename; if (!params.dst_file.empty()) { gcode_path = params.dst_file; - } else if (boost::iends_with(gcode_path, ".3mf")) { - gcode_path.replace(gcode_path.size() - 4, 4, ".gcode"); } // Check if file exists and has .gcode extension @@ -469,19 +361,8 @@ int MoonrakerPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusF return BAMBU_NETWORK_ERR_FILE_NOT_EXIST; } - // Use the human-readable project name for the uploaded file rather than the - // internal temp G-code name (e.g. ".12345.0.gcode"). Fall back to the source - // file's own name when no project name is available. - std::string upload_filename = params.project_name.empty() - ? source_path.filename().string() - : params.project_name; - - // SDCARD_PRINT_FILE parses its parameters by whitespace, so the printed - // filename must not contain spaces; collapse any whitespace to underscores. - std::replace_if( - upload_filename.begin(), upload_filename.end(), - [](unsigned char c) { return std::isspace(c) != 0; }, '_'); - + // Extract filename for upload (relative to gcodes root) + std::string upload_filename = source_path.filename().string(); if (!boost::iends_with(upload_filename, ".gcode")) { upload_filename += ".gcode"; } @@ -500,12 +381,11 @@ int MoonrakerPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusF return BAMBU_NETWORK_ERR_CANCELED; } - // Start print via Moonraker's print API, referencing the file we just uploaded. + // Start print via gcode script (simpler than JSON-RPC) if (update_fn) update_fn(PrintingStageSending, 0, "Starting print..."); - - std::string start_error; - if (!start_print_file(device_info.base_url, device_info.api_key, upload_filename, start_error)) { + std::string gcode = "SDCARD_PRINT_FILE FILENAME=" + upload_filename; + if (!send_gcode(device_info.dev_id, gcode)) { return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED; } @@ -582,28 +462,13 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays) { - // This may be called from a background thread (e.g. run_status_stream's read loop, - // for subscription-mode agents) as well as from the GUI thread (Sidebar's pull-mode - // path). Everything below touches MachineObject/DevFilaSystem, which the GUI thread - // reads without locking — so the actual mutation must run on the main thread. Snapshot - // queue_on_main_fn and the two device_info fields we need up front, then defer the rest, - // mirroring dispatch_message's existing queue_fn ? queue_fn(x) : x() idiom. - QueueOnMainFn queue_fn; - { - std::lock_guard lock(state_mutex); - queue_fn = queue_on_main_fn; - } - std::string dev_id = device_info.dev_id; - std::string model_id = device_info.model_id; - - auto apply = [dev_id, model_id, ams_count, max_lane_index, trays]() { // Look up MachineObject via DeviceManager auto* dev_manager = GUI::wxGetApp().getDeviceManager(); if (!dev_manager) { return; } - MachineObject* obj = dev_manager->get_my_machine(dev_id); + MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id); if (!obj) { return; } @@ -688,7 +553,7 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, // Set printer_type so update_sync_status() can match it against the preset's printer type. // Without this, the comparison fails and all sync badges are cleared. - obj->printer_type = model_id; + obj->printer_type = device_info.model_id; // Set push counters so is_info_ready() returns true for pull-mode agents. if (obj->m_push_count == 0) { @@ -711,20 +576,10 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, ota_info.sw_ver = "1.0.0"; // Placeholder version for Moonraker printers obj->module_vers.emplace("ota", ota_info); } - }; - - if (queue_fn) { - queue_fn(apply); - } else { - apply(); - } } -bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode) +bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/) { - if (sync_mode != get_filament_sync_mode()) - return false; - std::vector trays; int max_lane_index = 0; @@ -753,57 +608,6 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync return false; } -CameraStreamMode MoonrakerPrinterAgent::get_camera_stream_mode() const -{ - refresh_webcam_info(); - std::lock_guard lock(payload_mutex); - return webcam_stream_mode; -} - -std::string MoonrakerPrinterAgent::get_camera_url() const -{ - refresh_webcam_info(); - std::lock_guard lock(payload_mutex); - return webcam_stream_url; -} - -void MoonrakerPrinterAgent::refresh_webcam_info() const -{ - std::string base_url; - std::string api_key; - uint64_t generation; - { - std::lock_guard lock(connect_mutex); - base_url = device_info.base_url; - api_key = device_info.api_key; - generation = connect_generation.load(); - } - - const uint64_t now_ms = static_cast( - std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); - - if (base_url.empty()) { - std::lock_guard lock(payload_mutex); - webcam_stream_url.clear(); - webcam_stream_mode = CameraStreamMode::none; - webcam_info_last_lookup_ms = 0; - webcam_info_generation = generation; - return; - } - - { - std::lock_guard lock(payload_mutex); - if (webcam_info_generation == generation && - now_ms - webcam_info_last_lookup_ms < WEBCAM_INFO_REFRESH_INTERVAL_MS) { - return; - } - webcam_info_generation = generation; - webcam_info_last_lookup_ms = now_ms; - } - - fetch_webcam_info(base_url, api_key, generation); -} - std::string MoonrakerPrinterAgent::trim_and_upper(const std::string& input) { std::string result = input; @@ -1178,10 +982,6 @@ 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"; @@ -1202,81 +1002,6 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: if (command.is_string() && command.get() == "get_access_code") { return send_access_code(dev_id); } - if (command.is_string() && command.get() == "ledctrl") { - const auto& system = json["system"]; - if (!system.contains("led_node") || !system["led_node"].is_string() || !system.contains("led_mode") || - !system["led_mode"].is_string()) { - return BAMBU_NETWORK_ERR_INVALID_RESULT; - } - const std::string led_node = system["led_node"].get(); - // why: DevLamp::CtrlSetChamberLight publishes chamber_light and chamber_light2 per click. - // That is idempotent for absolute writes, but toggle macros fired twice are a net no-op. - if (led_node != "chamber_light") { - return BAMBU_NETWORK_SUCCESS; - } - const std::string led_mode = system["led_mode"].get(); - // why: Klipper has no flash primitive, so flashing collapses to full brightness. - const std::string value = (led_mode == "on" || led_mode == "flashing") ? "1" : "0"; - const bool requested_light_on = value == "1"; - std::string gcode; - { - std::lock_guard lock(payload_mutex); - if (requested_light_on == assumed_light_on) { - return BAMBU_NETWORK_SUCCESS; - } - // why: available_objects is a std::set, so first-match-and-break means alphabetical priority. - // That deliberately prefers FLASHLIGHT_SWITCH over MODLELIGHT_SWITCH on Elegoo Neptune 4. - for (const auto& object : available_objects) { - // why: Klipper wants the bare pin name, not the "output_pin " section prefix. - const size_t prefix = object.rfind("output_pin ", 0) == 0 ? 11 : 0; - if (prefix != 0 && object.size() > prefix && moonraker_is_light_name(object.substr(prefix))) { - gcode = "SET_PIN PIN=" + object.substr(prefix) + " VALUE=" + value; - break; - } - } - if (gcode.empty()) { - for (const auto& object : available_objects) { - const size_t prefix = object.rfind("led ", 0) == 0 ? 4 : object.rfind("neopixel ", 0) == 0 ? 9 : 0; - if (prefix != 0 && object.size() > prefix && moonraker_is_light_name(object.substr(prefix))) { - gcode = "SET_LED LED=" + object.substr(prefix) + " RED=" + value + " GREEN=" + value + - " BLUE=" + value + " WHITE=" + value; - break; - } - } - } - if (gcode.empty()) { - for (const auto& object : available_objects) { - const size_t prefix = object.rfind("gcode_macro ", 0) == 0 ? 12 : 0; - if (prefix != 0 && object.size() > prefix && moonraker_is_light_name(object.substr(prefix))) { - gcode = object.substr(prefix); - break; - } - } - } - } - if (gcode.empty()) { - BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: ledctrl - no light object found, dropping"; - return ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE; - } - 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; - } - } - - // why: the whole "pushing" namespace asks the printer to (re)send status - pushall, start, stop. - // The ws status stream already pushes unsolicited, so every member of it is genuinely satisfied - // rather than dropped. Accepting the namespace instead of naming its members keeps this a - // handled case rather than a suppression list; it also fires from the DevManager keepalive - // timer roughly once a second, which the not-supported default below would otherwise warn on. - if (json.contains("pushing") && json["pushing"].contains("command")) { - return BAMBU_NETWORK_SUCCESS; } // Handle print commands @@ -1310,38 +1035,25 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: } response["print"]["param"] = gcode; - 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"; + if (send_gcode(dev_id, gcode)) { + response["print"]["result"] = "success"; dispatch_message(dev_id, response.dump()); - }); - return BAMBU_NETWORK_SUCCESS; + return BAMBU_NETWORK_SUCCESS; + } + response["print"]["result"] = "failed"; + dispatch_message(dev_id, response.dump()); + return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; } // Print control commands if (cmd == "pause") { - 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; + return pause_print(dev_id); } if (cmd == "resume") { - 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; + return resume_print(dev_id); } if (cmd == "stop") { - 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; + return cancel_print(dev_id); } // Bed temperature - UI sends "temp" field @@ -1349,11 +1061,7 @@ 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); - 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); - }); + send_gcode(dev_id, gcode); return BAMBU_NETWORK_SUCCESS; } } @@ -1368,49 +1076,17 @@ 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); - 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); - }); + send_gcode(dev_id, gcode); return BAMBU_NETWORK_SUCCESS; } } - // why: no current OrcaSlicer sender emits the "home" discriminator; - // GUI homing uses gcode_line with G28 instead. if (cmd == "home") { - 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; + return send_gcode(dev_id, "G28") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; } } - std::string command_namespace = "unknown"; - std::string command_name = "unknown"; - if (json.is_object()) { - for (const char* namespace_name : {"info", "system", "print", "camera", "xcam", "upgrade", "pushing"}) { - if (!json.contains(namespace_name) || !json[namespace_name].is_object()) { - continue; - } - const auto& namespace_object = json[namespace_name]; - if (!namespace_object.contains("command") || !namespace_object["command"].is_string()) { - continue; - } - command_namespace = namespace_name; - command_name = namespace_object["command"].get(); - break; - } - } - - // why: reaching here means no case claimed the command, which is the honest verdict for - // every control Klipper has no equivalent for. Returning SUCCESS instead made all of them - // look like they worked. Nothing surfaces this code to the user yet. - BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: no translation for " << command_namespace << "." << command_name - << ", dropping"; - return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; + return BAMBU_NETWORK_SUCCESS; } bool MoonrakerPrinterAgent::init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) @@ -1426,7 +1102,6 @@ bool MoonrakerPrinterAgent::init_device_info(std::string dev_id, std::string dev device_info.dev_ip = dev_ip; device_info.api_key = password; - device_info.use_ssl = use_ssl; device_info.model_name = printer_cfg.opt_string("printer_model"); device_info.model_id = preset.get_printer_type(preset_bundle); device_info.base_url = use_ssl ? "https://" + dev_ip : "http://" + dev_ip; @@ -1437,57 +1112,12 @@ bool MoonrakerPrinterAgent::init_device_info(std::string dev_id, std::string dev return true; } -float MoonrakerPrinterAgent::parse_nozzle_diameter(const nlohmann::json& response) -{ - const nlohmann::json* status = nullptr; - if (response.contains("result") && response["result"].is_object()) { - status = response["result"].contains("status") ? &response["result"]["status"] : &response["result"]; - } else if (response.contains("status")) { - status = &response["status"]; - } - - if (status == nullptr || !status->is_object() || !status->contains("configfile") || !(*status)["configfile"].is_object()) { - return 0.0f; - } - - const auto& configfile = (*status)["configfile"]; - for (const char* key : {"settings", "config"}) { - if (!configfile.contains(key) || !configfile[key].is_object()) { - continue; - } - - for (const auto& item : configfile[key].items()) { - if (item.key() != "extruder" && item.key().rfind("extruder", 0) != 0) { - continue; - } - const auto& section = item.value(); - if (!section.is_object() || !section.contains("nozzle_diameter")) { - continue; - } - - const auto& value = section["nozzle_diameter"]; - try { - if (value.is_number()) { - return value.get(); - } - if (value.is_string()) { - return std::stof(value.get()); - } - } catch (...) { - return 0.0f; - } - } - } - - return 0.0f; -} - bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const { - auto fetch_json = [&](const std::string& url, nlohmann::json& out, std::string& fetch_error) { + auto fetch_json = [&](const std::string& url, nlohmann::json& out) { std::string response_body; bool success = false; std::string http_error; @@ -1515,13 +1145,13 @@ bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, .perform_sync(); if (!success) { - fetch_error = http_error.empty() ? "Connection failed" : http_error; + error = http_error.empty() ? "Connection failed" : http_error; return false; } out = nlohmann::json::parse(response_body, nullptr, false, true); if (out.is_discarded()) { - fetch_error = "Invalid JSON response"; + error = "Invalid JSON response"; return false; } return true; @@ -1529,7 +1159,7 @@ bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, nlohmann::json json; std::string url = join_url(base_url, "/server/info"); - if (!fetch_json(url, json, error)) { + if (!fetch_json(url, json)) { return false; } @@ -1538,17 +1168,6 @@ bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, info.version = result.value("moonraker_version", ""); info.klippy_state = result.value("klippy_state", ""); - // nozzle_diameter is part of Klipper's configfile object rather than the live - // extruder status object. Keep this optional so older/custom Moonraker builds - // remain connectable when they do not expose configfile through the API. - nlohmann::json config_response; - std::string config_error; - if (fetch_json(join_url(base_url, "/printer/objects/query?configfile"), config_response, config_error)) { - info.nozzle_diameter = parse_nozzle_diameter(config_response); - } else { - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: nozzle configuration unavailable: " << config_error; - } - return true; } @@ -1605,261 +1224,7 @@ bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url, return true; } -bool MoonrakerPrinterAgent::send_ws_rpc(const std::string& method, const nlohmann::json& params) -{ - std::string base_url; - std::string api_key; - { - std::lock_guard lock(connect_mutex); - base_url = device_info.base_url; - api_key = device_info.api_key; - } - - WsEndpoint endpoint; - if (!parse_ws_endpoint(base_url, endpoint) || endpoint.secure) { - BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: send_ws_rpc has no usable websocket for base_url=" - << base_url; - return false; - } - - nlohmann::json request; - request["jsonrpc"] = "2.0"; - request["method"] = method; - if (!params.is_null()) { - request["params"] = params; - } - request["id"] = next_jsonrpc_id++; - const std::string body = request.dump(); - - std::vector ports{endpoint.port}; - if (endpoint.port != "7125") { - ports.emplace_back("7125"); - } - - for (const auto& port : ports) { - try { - net::io_context ioc; - tcp::resolver resolver{ioc}; - beast::tcp_stream stream{ioc}; - stream.expires_after(std::chrono::seconds(5)); - stream.connect(resolver.resolve(endpoint.host, port)); - - websocket::stream ws{std::move(stream)}; - ws.set_option(websocket::stream_base::decorator([&](websocket::request_type& req) { - req.set(http::field::user_agent, "OrcaSlicer"); - if (!api_key.empty()) { - req.set("X-Api-Key", api_key); - } - })); - - std::string host_header = endpoint.host; - if (!port.empty() && port != "80") { - host_header += ":" + port; - } - ws.handshake(host_header, endpoint.target); - ws.text(true); - ws.write(net::buffer(body)); - - ws.next_layer().expires_after(std::chrono::seconds(2)); - beast::flat_buffer buffer; - beast::error_code read_ec; - ws.read(buffer, read_ec); - - beast::error_code close_ec; - ws.close(websocket::close_code::normal, close_ec); - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: sent " << method << " over ws to " - << endpoint.host << ":" << port; - return true; - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: " << method << " over ws to " << endpoint.host - << ":" << port << " failed: " << e.what(); - } - } - return false; -} - -bool MoonrakerPrinterAgent::fetch_webcam_info(const std::string& base_url, const std::string& api_key, uint64_t generation) const -{ - std::string camera_url; - std::string webcam_name; - CameraStreamMode stream_mode = CameraStreamMode::none; - std::string error; - try { - std::string response_body; - bool success = false; - std::string http_error; - - auto http = Http::get(join_url(base_url, "/server/webcams/list")); - if (!api_key.empty()) { - http.header("X-Api-Key", api_key); - } - http.timeout_connect(5) - .timeout_max(10) - .on_complete([&](std::string body, unsigned status_code) { - if (status_code == 200) { - response_body = body; - success = true; - } else { - http_error = "HTTP error: " + std::to_string(status_code); - } - }) - .on_error([&](std::string body, std::string err, unsigned status_code) { - http_error = err; - if (status_code > 0) { - http_error += " (HTTP " + std::to_string(status_code) + ")"; - } - }) - .perform_sync(); - - if (!success) { - error = http_error.empty() ? "Connection failed" : http_error; - } else { - auto json = nlohmann::json::parse(response_body, nullptr, false, true); - if (json.is_discarded()) { - error = "Invalid JSON response"; - } else { - const auto result = json.contains("result") ? json["result"] : json; - if (!result.contains("webcams") || !result["webcams"].is_array()) { - error = "Unexpected JSON structure"; - } else { - for (const auto& webcam : result["webcams"]) { - if (webcam.is_object() && webcam.value("enabled", false)) { - if (webcam.contains("stream_url") && webcam["stream_url"].is_string() && - !webcam["stream_url"].get().empty()) { - camera_url = webcam["stream_url"].get(); - stream_mode = CameraStreamMode::http; - } else if (webcam.contains("snapshot_url") && webcam["snapshot_url"].is_string() && - !webcam["snapshot_url"].get().empty()) { - camera_url = webcam["snapshot_url"].get(); - stream_mode = CameraStreamMode::http_snapshot; - } - if (webcam.contains("name") && webcam["name"].is_string()) { - webcam_name = webcam["name"].get(); - } - if (!camera_url.empty()) - break; - } - } - if (camera_url.empty()) { - error = "No enabled webcam"; - } - } - } - } - - if (error.empty()) { - if (camera_url.rfind("http", 0) != 0 && !camera_url.empty() && camera_url.front() == '/') { - // why: Moonraker's API port serves a JSON 404 for /webcam; relative camera URLs use the printer web root. - const size_t scheme_end = base_url.find("://"); - const size_t authority_start = scheme_end == std::string::npos ? 0 : scheme_end + 3; - const size_t authority_end = base_url.find('/', authority_start); - const std::string scheme = scheme_end == std::string::npos ? "" : base_url.substr(0, scheme_end + 3); - std::string authority = base_url.substr(authority_start, authority_end - authority_start); - const size_t port_start = authority.rfind(':'); - if (port_start != std::string::npos && port_start + 1 < authority.size() && - std::all_of(authority.begin() + port_start + 1, authority.end(), [](char c) { return c >= '0' && c <= '9'; })) { - authority.erase(port_start); - } - camera_url = scheme + authority + camera_url; - } else if (camera_url.rfind("http", 0) != 0) { - error = "Unsupported webcam URL"; - } - } - } catch (const std::exception& e) { - error = e.what(); - } catch (...) { - error = "Unknown webcam discovery error"; - } - - { - std::lock_guard lock(payload_mutex); - if (generation == connect_generation.load()) { - webcam_stream_url = error.empty() ? camera_url : ""; - webcam_stream_mode = error.empty() ? stream_mode : CameraStreamMode::none; - } - } - if (!error.empty()) { - BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: webcam discovery failed: " << error; - return false; - } - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: selected webcam '" << webcam_name << "' with camera URL " << camera_url; - return true; -} - -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 - // /printer/gcode/script waits behind the gcode queue and can no-op while the - // 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(base_url, "/printer/print/" + action); - bool success = false; - std::string http_error; - - auto http = Http::post(full_url); - if (!api_key.empty()) { - http.header("X-Api-Key", api_key); - } - http.header("Content-Type", "application/json") - .set_post_body(std::string("{}")) - .timeout_connect(5) - .timeout_max(10) - .on_complete([&](std::string body, unsigned status_code) { - (void) body; - if (status_code == 200) { - success = true; - } else { - http_error = "HTTP error: " + std::to_string(status_code); - } - }) - .on_error([&](std::string body, std::string err, unsigned status_code) { - (void) body; - http_error = err; - if (status_code > 0) { - http_error += " (HTTP " + std::to_string(status_code) + ")"; - } - }) - .perform_sync(); - - if (!success) { - BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: print/" << action << " failed: " << http_error; - return false; - } - - return true; -} - 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; @@ -1869,10 +1234,9 @@ bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::str bool success = false; std::string http_error; - auto full_url = join_url(base_url, "/printer/gcode/script"); - auto http = Http::post(full_url); - if (!api_key.empty()) { - http.header("X-Api-Key", api_key); + auto http = Http::post(join_url(device_info.base_url, "/printer/gcode/script")); + if (!device_info.api_key.empty()) { + http.header("X-Api-Key", device_info.api_key); } http.header("Content-Type", "application/json") .set_post_body(payload_str) @@ -2099,14 +1463,6 @@ void MoonrakerPrinterAgent::start_status_stream(const std::string& dev_id, const void MoonrakerPrinterAgent::stop_status_stream() { ws_stop.store(true); - { - // Wake a blocked synchronous ws.read()/ws.write() in run_status_stream(); - // ws_stop by itself is only observed between reads. - std::lock_guard lock(ws_abort_mutex); - if (ws_abort_io) { - ws_abort_io(); - } - } if (ws_thread.joinable()) { ws_thread.join(); } @@ -2143,25 +1499,6 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba stream.connect(results); websocket::stream ws{std::move(stream)}; - - // Allow stop_status_stream() to force this socket shut so a blocked - // synchronous ws.read()/ws.write() returns with an error (Beast's - // expires_after() does not bound synchronous operations). Declared - // after `ws` so the hook is cleared before `ws` is destroyed on every - // exit path (fallthrough, break, exception); ws_abort_mutex keeps the - // hook from running against a half-destroyed `ws`. - ScopeGuard ws_abort_guard([this] { - std::lock_guard lock(ws_abort_mutex); - ws_abort_io = nullptr; - }); - { - std::lock_guard lock(ws_abort_mutex); - ws_abort_io = [&ws] { - beast::error_code ec; - ws.next_layer().socket().shutdown(tcp::socket::shutdown_both, ec); - }; - } - ws.set_option(websocket::stream_base::decorator([&](websocket::request_type& req) { req.set(http::field::user_agent, "OrcaSlicer"); if (!api_key.empty()) { @@ -2239,16 +1576,8 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba subscribe["id"] = 1; ws.write(net::buffer(subscribe.dump())); - // Eager fetch so AMS data is available immediately after connecting, - // without waiting on the loop's own refresh clock below. - fetch_filament_info(dev_id, FilamentSyncMode::subscription); - ams_last_fetch_ms.store(static_cast( - std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count())); - // Read loop while (!ws_stop.load()) { - on_status_loop_tick(dev_id); - ws.next_layer().expires_after(std::chrono::seconds(2)); beast::flat_buffer buffer; beast::error_code ec; @@ -2277,19 +1606,8 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba connection_lost = true; break; } - // AMS/filament refresh on its own clock: gated independently of - // ws_last_emit_ms so a steady telemetry stream can't starve it. - { - const auto now_ms = static_cast( - std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); - const auto last_ams_ms = ams_last_fetch_ms.load(); - if (last_ams_ms == 0 || now_ms - last_ams_ms >= AMS_REFRESH_INTERVAL_MS) { - fetch_filament_info(dev_id, FilamentSyncMode::subscription); - ams_last_fetch_ms.store(now_ms); - } - } - handle_ws_message(dev_id, beast::buffers_to_string(buffer.data()), base_url, api_key); - // Check if handle_ws_message triggered reconnection request` + handle_ws_message(dev_id, beast::buffers_to_string(buffer.data())); + // Check if handle_ws_message triggered reconnection request if (ws_reconnect_requested.exchange(false)) { connection_lost = true; break; @@ -2332,7 +1650,7 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba } } -void MoonrakerPrinterAgent::handle_ws_message(std::string dev_id, std::string payload, std::string base_url, std::string api_key) +void MoonrakerPrinterAgent::handle_ws_message(const std::string& dev_id, const std::string& payload) { auto json = nlohmann::json::parse(payload, nullptr, false); if (json.is_discarded()) { @@ -2403,8 +1721,6 @@ void MoonrakerPrinterAgent::handle_ws_message(std::string dev_id, std::string pa } if (updated) { - refresh_thumbnail_url(base_url, api_key); - const auto now_ms = static_cast( std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); const auto last_dispatch_ms = ws_last_dispatch_ms.load(); @@ -2453,105 +1769,6 @@ void MoonrakerPrinterAgent::update_status_cache(const nlohmann::json& updates) } } -// why: resolving a thumbnail is a blocking HTTP round-trip, so it must stay OUT of -// build_print_payload_locked() - that runs on the ws thread holding payload_mutex, which the UI -// thread also takes. Cached per filename, and a miss caches empty so a printer whose gcode has no -// embedded thumbnail is not re-queried on every push. -void MoonrakerPrinterAgent::refresh_thumbnail_url(std::string base_url, std::string api_key) -{ - std::string filename; - { - std::lock_guard lock(payload_mutex); - if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("filename") && - status_cache["print_stats"]["filename"].is_string()) { - filename = status_cache["print_stats"]["filename"].get(); - } - if (filename.empty() || (filename == thumbnail_filename && (!thumbnail_url.empty() || thumbnail_lookup_attempts >= 3))) { - return; - } - if (filename != thumbnail_filename) { - // why: claim the filename on the FIRST attempt, not on give-up. Leaving it unclaimed made - // every transient failure look like a new file and reset the counter, so an unreachable - // printer re-issued this blocking lookup on every push instead of stopping after 3. - thumbnail_filename = filename; - thumbnail_url.clear(); - thumbnail_lookup_attempts = 0; - } - ++thumbnail_lookup_attempts; - } - - std::string response_body; - bool request_succeeded = false; - auto http = Http::get(join_url(base_url, "/server/files/thumbnails?filename=" + Http::url_encode(filename))); - if (!api_key.empty()) { - http.header("X-Api-Key", api_key); - } - http.timeout_connect(2) - .timeout_max(4) - .on_complete([&](std::string body, unsigned status) { - if (status == 200) { - response_body = std::move(body); - request_succeeded = true; - } - }) - .on_error([&](std::string, std::string, unsigned) { request_succeeded = false; }) - .on_progress([this](Http::Progress, bool& cancel) { cancel = ws_stop.load(); }) - .perform_sync(); - - // why: transient (connection refused, non-2xx, timeout, cancel) - leave the counter standing so the - // next push retries, and the >= 3 guard above stops it. Only a clean 200 caches a verdict. - if (!request_succeeded) { - return; - } - - std::string path; - int best_width = -1; - const auto thumbnails = nlohmann::json::parse(response_body, nullptr, false, true); - if (!thumbnails.is_discarded() && thumbnails.contains("result") && thumbnails["result"].is_array()) { - for (const auto& item : thumbnails["result"]) { - if (!item.is_object()) { - continue; - } - // note: Moonraker renamed this key across versions; accept both spellings. - const char* key = item.contains("thumbnail_path") ? "thumbnail_path" : "relative_path"; - if (!item.contains(key) || !item[key].is_string()) { - continue; - } - // why: the list is ordered smallest-first, so taking [0] would pick the 32x32 icon. - const int width = (item.contains("width") && item["width"].is_number()) ? item["width"].get() : 0; - if (width > best_width) { - best_width = width; - path = item[key].get(); - } - } - } - - std::string url; - if (!path.empty()) { - // why: the returned path is relative to the gcodes root, which is served at /server/files/gcodes. - const std::string root = path.rfind("gcodes/", 0) == 0 ? "/server/files/" : "/server/files/gcodes/"; - std::string encoded_path; - size_t segment_start = 0; - while (segment_start <= path.size()) { - const size_t segment_end = path.find('/', segment_start); - if (!encoded_path.empty() || segment_start > 0) { - encoded_path += '/'; - } - encoded_path += Http::url_encode(path.substr(segment_start, segment_end - segment_start)); - if (segment_end == std::string::npos) { - break; - } - segment_start = segment_end + 1; - } - url = join_url(base_url, root + encoded_path); - } - - std::lock_guard lock(payload_mutex); - thumbnail_filename = filename; - thumbnail_url = url; - thumbnail_lookup_attempts = url.empty() ? 3 : 0; -} - nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const { nlohmann::json payload; @@ -2588,12 +1805,9 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const payload["print"]["print_error"] = 0; // Map homed axes to bit field: X=bit0, Y=bit1, Z=bit2 - // NOTE: home_flag is a packed BBL status field. MachineObject::parse_home_flag() - // decodes the storage state from bits 8-9 (get_flag_bits(flag, 8, 2)) and writes it - // to DevStorage. We encode HAS_SDCARD_NORMAL there (bits 8-9 = 01) so the printer - // reports its virtual_sdcard as present and LAN printing is allowed; otherwise the - // bits stay 0 (NO_SDCARD) and printing is blocked. Other support bits (3=220V, - // 4=auto-recovery, etc.) are intentionally left 0 for Moonraker. + // WARNING: This only sets bits 0-2, clearing support flags (bit 3+) + // Bit 3 = 220V voltage, bit 4 = auto recovery, etc. + // This is acceptable for Moonraker (no AMS, different feature set) int home_flag = 0; if (status_cache.contains("toolhead") && status_cache["toolhead"].contains("homed_axes")) { std::string homed = status_cache["toolhead"]["homed_axes"].get(); @@ -2604,33 +1818,17 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const if (homed.find('Z') != std::string::npos) home_flag |= 4; // bit 2 } - home_flag |= (1 << 8); // bits 8-9 = 01 -> HAS_SDCARD_NORMAL (virtual_sdcard always present) payload["print"]["home_flag"] = home_flag; // Moonraker doesn't provide temperature ranges via API - use hardcoded defaults payload["print"]["nozzle_temp_range"] = {100, 370}; // Typical Klipper range payload["print"]["bed_temp_range"] = {0, 120}; // Typical bed range - // MachineObject::parse_json routes nozzle_diameter through the legacy nozzle - // parser only when nozzle_type is present as well. Moonraker/Klipper exposes - // the diameter but not Bambu's nozzle type, so use the parser's neutral value. - if (device_info.nozzle_diameter > 0.0f) { - payload["print"]["nozzle_diameter"] = device_info.nozzle_diameter; - payload["print"]["nozzle_type"] = "N/A"; - } - payload["print"]["support_send_to_sd"] = true; - if (!webcam_stream_url.empty()) { - payload["print"]["ipcam"]["ipcam_dev"] = "1"; - payload["print"]["ipcam"]["stream_url"] = webcam_stream_url; - } else { - payload["print"]["ipcam"]["ipcam_dev"] = "0"; - } // Detect bed_leveling support from available objects (bed_mesh or probe) // Default to 0 (not supported) if neither object exists bool has_bed_leveling = (available_objects.count("bed_mesh") != 0 || available_objects.count("probe") != 0); payload["print"]["support_bed_leveling"] = has_bed_leveling ? 1 : 0; - payload["print"]["lights_report"] = {{{"node", "chamber_light"}, {"mode", assumed_light_on ? "on" : "off"}}}; const nlohmann::json* extruder = nullptr; if (status_cache.contains("extruder") && status_cache["extruder"].is_object()) { @@ -2692,23 +1890,6 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("filename")) { payload["print"]["gcode_file"] = status_cache["print_stats"]["filename"]; - if (status_cache["print_stats"]["filename"].is_string()) { - const std::string filename = status_cache["print_stats"]["filename"].get(); - payload["print"]["task_id"] = filename; - // why: url is resolved by refresh_thumbnail_url() off this lock - the lookup is a blocking - // HTTP round-trip and this builder runs on the ws thread holding payload_mutex. - if (thumbnail_filename == filename && !thumbnail_url.empty()) - payload["print"]["thumbnail_url"] = thumbnail_url; - } - } - - if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("info") && - status_cache["print_stats"]["info"].contains("current_layer") && status_cache["print_stats"]["info"]["current_layer"].is_number()) { - payload["print"]["layer_num"] = status_cache["print_stats"]["info"]["current_layer"]; - } - if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("info") && - status_cache["print_stats"]["info"].contains("total_layer") && status_cache["print_stats"]["info"]["total_layer"].is_number()) { - payload["print"]["total_layer_num"] = status_cache["print_stats"]["info"]["total_layer"]; } int mc_percent = -1; @@ -2723,15 +1904,13 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const payload["print"]["mc_percent"] = mc_percent; } - // why: total_duration and print_duration are both elapsed counters; their difference is overhead, not ETA. - if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("print_duration") && - status_cache["print_stats"]["print_duration"].is_number() && status_cache.contains("virtual_sdcard") && - status_cache["virtual_sdcard"].contains("progress") && status_cache["virtual_sdcard"]["progress"].is_number()) { - const double elapsed = status_cache["print_stats"]["print_duration"].get(); - const double progress = status_cache["virtual_sdcard"]["progress"].get(); - // why: progress is file position and starts before the print, so ETA is garbage below 2%. - if (elapsed >= 0.0 && progress >= 0.02) { - const auto remaining_minutes = std::max(0, static_cast((elapsed * (1.0 - progress) / progress) / 60.0)); + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("total_duration") && + status_cache["print_stats"].contains("print_duration") && status_cache["print_stats"]["total_duration"].is_number() && + status_cache["print_stats"]["print_duration"].is_number()) { + const double total = status_cache["print_stats"]["total_duration"].get(); + const double elapsed = status_cache["print_stats"]["print_duration"].get(); + if (total > 0.0 && elapsed >= 0.0) { + const auto remaining_minutes = std::max(0, static_cast((total - elapsed) / 60.0)); payload["print"]["mc_remaining_time"] = remaining_minutes; } } @@ -2850,98 +2029,54 @@ bool MoonrakerPrinterAgent::upload_gcode(const std::string& local_path, int MoonrakerPrinterAgent::pause_print(const std::string& dev_id) { - (void) dev_id; - return post_print_action("pause") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; + return send_gcode(dev_id, "PAUSE") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; } int MoonrakerPrinterAgent::resume_print(const std::string& dev_id) { - (void) dev_id; - return post_print_action("resume") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; + return send_gcode(dev_id, "RESUME") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; } int MoonrakerPrinterAgent::cancel_print(const std::string& dev_id) { - (void) dev_id; - return post_print_action("cancel") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; + return send_gcode(dev_id, "CANCEL_PRINT") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; } -bool MoonrakerPrinterAgent::start_print_file(const std::string& base_url, - const std::string& api_key, - const std::string& filename, - std::string& error_msg) const +bool MoonrakerPrinterAgent::send_jsonrpc_command(const std::string& base_url, + const std::string& api_key, + const nlohmann::json& request, + std::string& response) const { - // Start the given file (path relative to the gcodes root). The filename is - // sent both as a query parameter and in the JSON body: Moonraker accepts - // either, and sending a body avoids a body-less POST (which curl would treat - // as a streamed upload and try to read via the file-read callback). - std::string url = join_url(base_url, "/printer/print/start") + - "?filename=" + Http::url_encode(filename); + std::string request_str = request.dump(); + std::string url = join_url(base_url, "/printer/print/start"); - nlohmann::json payload; - payload["filename"] = filename; - - bool success = false; + bool success = false; + std::string http_error; auto http = Http::post(url); if (!api_key.empty()) { http.header("X-Api-Key", api_key); } http.header("Content-Type", "application/json") - .set_post_body(payload.dump()) + .set_post_body(request_str) .timeout_connect(5) .timeout_max(10) .on_complete([&](std::string body, unsigned status) { - (void) body; if (status == 200) { - success = true; + response = body; + success = true; } else { - error_msg = "HTTP " + std::to_string(status); - } - }) - .on_error([&](std::string body, std::string err, unsigned status) { - (void) body; - error_msg = err; - if (status > 0) { - error_msg += " (HTTP " + std::to_string(status) + ")"; + http_error = "HTTP " + std::to_string(status); } }) + .on_error([&](std::string body, std::string err, unsigned status) { http_error = err; }) .perform_sync(); - if (success) { - return true; + if (!success) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: JSON-RPC command failed: " << http_error; } - // Moonraker holds the /printer/print/start response until the print actually - // begins, so a slow PRINT_START (heating, homing, bed mesh) can exceed our HTTP - // timeout even though the command was accepted and the print is starting. Don't - // report failure on the HTTP result alone: poll print_stats and treat a - // printing/paused state as success. print_stats.state flips to "printing" as soon - // as the file starts streaming, which is earlier than the held HTTP response. - BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: start print not confirmed over HTTP (" << error_msg - << "); verifying print_stats state"; - for (int attempt = 0; attempt < 10; ++attempt) { - std::this_thread::sleep_for(std::chrono::milliseconds(1500)); - - nlohmann::json status; - std::string query_err; - if (!query_printer_status(base_url, api_key, status, query_err)) { - continue; - } - - std::string state; - if (status.contains("print_stats") && status["print_stats"].contains("state") && - status["print_stats"]["state"].is_string()) { - state = status["print_stats"]["state"].get(); - } - if (state == "printing" || state == "paused") { - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: print confirmed started (print_stats.state=" << state << ")"; - return true; - } - } - - BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: start print failed: " << error_msg << ", url: " << url; - return false; + return success; } void MoonrakerPrinterAgent::perform_connection_async(const std::string& dev_id, const std::string& base_url, const std::string& api_key, uint64_t generation) @@ -2974,11 +2109,10 @@ void MoonrakerPrinterAgent::perform_connection_async(const std::string& dev_id, device_info.dev_name = fetched_info.dev_name; device_info.version = fetched_info.version; device_info.klippy_state = fetched_info.klippy_state; - device_info.nozzle_diameter = fetched_info.nozzle_diameter; } // Orca todo: disable websocket for now, as we don't use MonitorPanel for Moonraker printers yet -#if 1 +#if 0 // Query initial status nlohmann::json initial_status; if (query_printer_status(base_url, api_key, initial_status, error_msg)) { diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index a662c096df..48e5815663 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -9,16 +9,11 @@ #include #include #include -#include -#include -#include #include namespace Slic3r { -bool moonraker_is_light_name(const std::string& name); - class MoonrakerPrinterAgent : public IPrinterAgent { public: @@ -73,9 +68,10 @@ public: int set_on_local_connect_fn(OnLocalConnectedFn fn) override; int set_on_local_message_fn(OnMessageFn fn) override; int set_queue_on_main_fn(QueueOnMainFn fn) override; + + // Pull-mode agent (on-demand filament sync) + FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override; - CameraStreamMode get_camera_stream_mode() const override; - std::string get_camera_url() const override; protected: struct MoonrakerDeviceInfo @@ -89,7 +85,6 @@ protected: std::string dev_name; std::string version; std::string klippy_state; - float nozzle_diameter = 0.0f; bool use_ssl = false; } device_info; @@ -110,17 +105,10 @@ protected: // Methods that derived classes may need to override or access virtual bool init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; - static float parse_nozzle_diameter(const nlohmann::json& response); // State access for derived classes mutable std::recursive_mutex state_mutex; - // Counts detached fetch_filament_info() background threads currently touching `this` - // (see QidiPrinterAgent::fetch_filament_info). Those threads hold a raw `this` with no - // other lifetime protection, so the destructor waits for this to reach 0 before any part - // of the object is torn down — see ~MoonrakerPrinterAgent(). - std::atomic filament_fetch_in_flight{0}; - // Helpers bool is_numeric(const std::string& value); std::string normalize_base_url(std::string host, const std::string& port); @@ -133,18 +121,6 @@ protected: // Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility static std::string map_filament_type_to_generic_id(const std::string& filament_type); - // 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; - - bool send_ws_rpc(const std::string& method, const nlohmann::json& params); - - virtual void on_status_loop_tick(const std::string& dev_id) {} - private: int handle_request(const std::string& dev_id, const std::string& json_str); int send_version_info(const std::string& dev_id); @@ -152,6 +128,7 @@ private: bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set& objects, std::string& error) const; bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const; + bool send_gcode(const std::string& dev_id, const std::string& gcode) const; void announce_printhost_device(); void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg); @@ -160,8 +137,7 @@ private: void start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key); void stop_status_stream(); void run_status_stream(std::string dev_id, std::string base_url, std::string api_key); - void handle_ws_message(std::string dev_id, std::string payload, std::string base_url, std::string api_key); - void refresh_thumbnail_url(std::string base_url, std::string api_key); + void handle_ws_message(const std::string& dev_id, const std::string& payload); void update_status_cache(const nlohmann::json& updates); nlohmann::json build_print_payload_locked() const; @@ -175,10 +151,9 @@ private: const std::string& base_url, const std::string& api_key, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); - // Start a print of a previously uploaded G-code file (path relative to the - // Moonraker gcodes root). - bool start_print_file(const std::string& base_url, const std::string& api_key, - const std::string& filename, std::string& error_msg) const; + // JSON-RPC helper + bool send_jsonrpc_command(const std::string& base_url, const std::string& api_key, + const nlohmann::json& request, std::string& response) const; // Connection thread management void perform_connection_async(const std::string& dev_id, @@ -186,12 +161,6 @@ private: const std::string& api_key, uint64_t generation); - // why: a printer with no /server/webcams/list entry can still name its stream directly; - // subclasses (e.g. printers with a fixed webcam path) can override this instead. - virtual std::string webcam_stream_override(const std::string& base_url) const { return {}; } - void refresh_webcam_info() const; - bool fetch_webcam_info(const std::string& base_url, const std::string& api_key, uint64_t generation) const; - // System-specific filament fetch methods bool fetch_hh_filament_info(std::vector& trays, int& max_lane_index); bool fetch_moonraker_filament_data(std::vector& trays, int& max_lane_index); @@ -220,38 +189,15 @@ private: mutable std::recursive_mutex payload_mutex; nlohmann::json status_cache; - // note: guarded by payload_mutex; filled by refresh_thumbnail_url(), empty url = looked up, none found - std::string thumbnail_filename; - std::string thumbnail_url; - mutable std::string webcam_stream_url; - mutable CameraStreamMode webcam_stream_mode = CameraStreamMode::none; - mutable uint64_t webcam_info_last_lookup_ms = 0; - mutable uint64_t webcam_info_generation = 0; - unsigned thumbnail_lookup_attempts = 0; - - static constexpr uint64_t WEBCAM_INFO_REFRESH_INTERVAL_MS = 1000; std::atomic next_jsonrpc_id{1}; std::set available_objects; // Track for feature detection - bool assumed_light_on = false; std::atomic ws_stop{false}; std::atomic ws_reconnect_requested{false}; // Flag to trigger reconnection std::atomic ws_last_emit_ms{0}; std::thread ws_thread; - // stop_status_stream() invokes ws_abort_io to wake a blocked synchronous - // ws.read()/ws.write()/handshake in run_status_stream(): ws_stop is only - // observed between reads, and Beast's expires_after() does not bound - // synchronous operations. - std::mutex ws_abort_mutex; - std::function ws_abort_io; // guarded by ws_abort_mutex - - // AMS/filament refresh cadence, independent of telemetry dispatch so a steady - // stream of status updates can't starve it (ws_last_emit_ms is reset by those). - static constexpr uint64_t AMS_REFRESH_INTERVAL_MS = 10000; - std::atomic ams_last_fetch_ms{0}; - // Throttling configuration for WebSocket updates // Critical changes (state transitions) dispatch immediately; telemetry is throttled static constexpr uint64_t STATUS_UPDATE_INTERVAL_MS = 1000; // 1 update/sec for telemetry @@ -261,15 +207,7 @@ private: // Connection thread management std::atomic connect_generation{0}; std::thread connect_thread; - 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; + std::recursive_mutex connect_mutex; }; } // namespace Slic3r diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp index ba6f845564..f84d250c0d 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp @@ -6,8 +6,6 @@ #include #include -#include -#include #include #include #include @@ -23,18 +21,14 @@ #include #include #include -#include #include #include -#include -#include #include #include #include #include #include -#include #include #include @@ -498,7 +492,6 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir) , api_base_url(ORCA_DEFAULT_API_URL) , auth_base_url(ORCA_DEFAULT_AUTH_URL) , cloud_base_url(ORCA_DEFAULT_CLOUD_URL) - , mqtt_connection(std::make_unique()) { auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY; pkce_bundle.loopback_port = choose_loopback_port(); @@ -509,8 +502,6 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir) OrcaCloudServiceAgent::~OrcaCloudServiceAgent() { - if (mqtt_connection) - mqtt_connection->stop(); if (refresh_thread.joinable()) { refresh_thread.join(); } @@ -951,45 +942,22 @@ bool OrcaCloudServiceAgent::ensure_token_fresh(const std::string& reason) { retu int OrcaCloudServiceAgent::connect_server() { - const bool logged_in = is_user_login(); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::connect_server logged_in=" << logged_in - << " api_base_url=" << api_base_url; - if (!logged_in) { - if (mqtt_connection) - mqtt_connection->stop(); - { - std::lock_guard lock(state_mutex); - is_connected = false; - } - BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: connect_server requires a logged-in user"; - invoke_server_connected_callback(-1, 401); - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - std::string response; unsigned int http_code = 0; int result = http_get(ORCA_HEALTH_PATH, &response, &http_code); bool connected = (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: cloud health result=" << result << " http_code=" << http_code - << " connected=" << connected << " response_bytes=" << response.size(); - - // connect_server() remains a REST health probe. The long-lived fleet MQTT socket - // is started lazily by set_user_selected_machine -> configure_selected_printer_mqtt; - // subscriptions queued before that point are replayed when it starts. { std::lock_guard lock(state_mutex); is_connected = connected; } + invoke_server_connected_callback(connected ? 0 : -1, http_code); return connected ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; } bool OrcaCloudServiceAgent::is_server_connected() { - // The REST health probe is the signal; the per-printer MQTT socket does not gate - // whole-cloud connectivity (one printer reconnecting must not report the whole - // cloud as lost). std::lock_guard lock(state_mutex); return is_connected; } @@ -1010,232 +978,13 @@ int OrcaCloudServiceAgent::stop_subscribe(std::string module) int OrcaCloudServiceAgent::add_subscribe(std::vector dev_list) { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::add_subscribe count=" << dev_list.size() - << " logged_in=" << is_user_login() << " mqtt_connection=" << (mqtt_connection ? "set" : "null"); - if (!is_user_login() || !mqtt_connection) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: add_subscribe rejected because cloud is not ready"; - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - bool queued = true; - for (const std::string& dev_id : dev_list) - queued = mqtt_connection->subscribe(dev_id) && queued; - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: add_subscribe queued=" << queued; - return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; + (void) dev_list; + return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::del_subscribe(std::vector dev_list) { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::del_subscribe count=" << dev_list.size() - << " logged_in=" << is_user_login() << " mqtt_connection=" << (mqtt_connection ? "set" : "null"); - if (!is_user_login() || !mqtt_connection) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: del_subscribe rejected because cloud is not ready"; - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - bool queued = true; - for (const std::string& dev_id : dev_list) - queued = mqtt_connection->unsubscribe(dev_id) && queued; - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: del_subscribe queued=" << queued; - return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; -} - -int OrcaCloudServiceAgent::configure_selected_printer_mqtt(const std::string& dev_id, - OrcaMqttConnection::StateHandler state_handler) -{ - (void) dev_id; - OrcaMqttConnection::Config cfg; - cfg.url = "wss://" + api_base_url + "/api/v1/printers/mqtt"; - cfg.use_tls = true; - cfg.bearer_provider = [this] { return get_access_token(); }; - cfg.client_id = "OrcaSlicer"; - cfg.keepalive_seconds = 300; - - { - std::lock_guard lock(m_selected_url_mutex); - m_selected_printer_mqtt_url = cfg.url; - } - - if (mqtt_connection->is_running()) { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: fleet MQTT connection already running"; - return BAMBU_NETWORK_SUCCESS; - } - - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: configuring fleet MQTT endpoint=" << cfg.url; - - // NOTE: no lock is held across start() — it blocks for the whole initial connect - // attempt (up to ~10s), and the message handler below re-enters callback_mutex on - // the MQTT worker thread. - const bool ok = mqtt_connection->start( - cfg, - [this](const std::string& id, const std::string& payload) { deliver_cloud_message(id, payload); }, - std::move(state_handler)); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: fleet MQTT start returned=" << ok; - return ok ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; -} - -void OrcaCloudServiceAgent::teardown_selected_printer_mqtt() -{ - if (mqtt_connection) { - mqtt_connection->stop(); - // The connection object is reused for the next printer; drop this printer's - // report topic so its 1:1 socket does not re-subscribe the previous device. - mqtt_connection->clear_subscriptions(); - } - std::lock_guard lock(m_selected_url_mutex); - m_selected_printer_mqtt_url.clear(); -} - -std::string OrcaCloudServiceAgent::selected_printer_mqtt_url() const -{ - std::lock_guard lock(m_selected_url_mutex); - return m_selected_printer_mqtt_url; -} - -void OrcaCloudServiceAgent::deliver_cloud_message(const std::string& dev_id, const std::string& payload) -{ - OnMessageFn callback; - { - std::lock_guard lock(callback_mutex); - callback = printer_status_callback; - } - if (callback) - callback(dev_id, payload); -} - -int OrcaCloudServiceAgent::set_printer_status_callback(OnMessageFn fn) -{ - std::lock_guard lock(callback_mutex); - printer_status_callback = std::move(fn); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: printer status callback=" << (printer_status_callback ? "set" : "clear"); - return BAMBU_NETWORK_SUCCESS; -} - -int OrcaCloudServiceAgent::send_printer_command(const std::string& dev_id, const std::string& body) -{ - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: send_printer_command dev_id=" << dev_id - << " body_bytes=" << body.size() << " logged_in=" << is_user_login(); - if (dev_id.empty() || !is_user_login()) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: send_printer_command rejected"; - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - - const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + dev_id + "/commands"; - std::string response; - unsigned int http_code = 0; - int result = http_post(path, body, &response, &http_code); - BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: command dev=" << dev_id - << " http=" << http_code << " result=" << result - << " response_bytes=" << response.size(); - return (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300) - ? BAMBU_NETWORK_SUCCESS - : BAMBU_NETWORK_ERR_CONNECT_FAILED; -} - -int OrcaCloudServiceAgent::upload_gcode_via_cloud(const std::string& dev_id, - const std::string& local_gcode_path, - std::string* job_id, - OnUpdateStatusFn update_fn, - WasCancelledFn cancel_fn) -{ - if (dev_id.empty() || local_gcode_path.empty() || !is_user_login()) - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - if (cancel_fn && cancel_fn()) - return BAMBU_NETWORK_ERR_CANCELED; - - // Step 1: POST print-jobs/uploads -> a short-lived presigned R2 PUT URL. No - // metadata rides this request; filename/start are only relevant to the HTTP - // .../start finalize route, which this MQTT-driven flow does not call. - const std::string uploads_path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs/uploads"; - std::string response; - unsigned int http_code = 0; - int result = http_post(uploads_path, "{}", &response, &http_code); - if (result != BAMBU_NETWORK_SUCCESS || http_code < 200 || http_code >= 300) { - BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: print-jobs/uploads failed http_code=" << http_code; - return BAMBU_NETWORK_ERR_CONNECT_FAILED; - } - - std::string upload_job_id; - std::string upload_url; - try { - const nlohmann::json j = nlohmann::json::parse(response); - upload_job_id = j.value("job_id", ""); - upload_url = j.value("upload_url", ""); - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: failed to parse print-jobs/uploads response: " << e.what(); - return BAMBU_NETWORK_ERR_CONNECT_FAILED; - } - if (upload_job_id.empty() || upload_url.empty()) { - BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: print-jobs/uploads response missing job_id/upload_url"; - return BAMBU_NETWORK_ERR_CONNECT_FAILED; - } - - if (cancel_fn && cancel_fn()) - return BAMBU_NETWORK_ERR_CANCELED; - - // Step 2: PUT the G-code straight to R2 with the one-time URL from step 1. This - // is a scoped, PUT-only, short-TTL capability with no bearer token of its own, - // so it bypasses http_put (which always prefixes api_base_url and attaches the - // cloud session's Authorization header - neither belongs on an R2 PUT). - bool canceled = false; - unsigned put_status = 0; - std::string put_error; - Http::put(upload_url) - .tls_verify(true) - .header("Content-Type", "text/x.gcode") - .set_put_body(boost::filesystem::path(local_gcode_path)) - .timeout_connect(5) - .timeout_max(300) // large G-code over a slow link - .on_progress([&](Http::Progress progress, bool& cancel) { - if (cancel_fn && cancel_fn()) { - cancel = true; - canceled = true; - return; - } - if (update_fn && progress.ultotal > 0) { - const int percent = static_cast((progress.ulnow * 100) / progress.ultotal); - update_fn(PrintingStageUpload, percent, "Uploading..."); - } - }) - .on_complete([&](std::string, unsigned status) { put_status = status; }) - .on_error([&](std::string, std::string err, unsigned status) { - put_status = status; - put_error = std::move(err); - }) - .perform_sync(); - - if (canceled) - return BAMBU_NETWORK_ERR_CANCELED; - if (put_status < 200 || put_status >= 300) { - BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: R2 upload failed status=" << put_status << " error=" << put_error; - return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; - } - - if (job_id) - *job_id = std::move(upload_job_id); - return BAMBU_NETWORK_SUCCESS; -} - -int OrcaCloudServiceAgent::start_cloud_print_job(const std::string& dev_id, - const std::string& job_id, - const std::string& filename, - bool start) -{ - if (dev_id.empty() || job_id.empty() || !is_user_login()) - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - - nlohmann::json body; - if (!filename.empty()) - body["filename"] = filename; - body["start"] = start; - - const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs/" + - Http::url_encode(job_id) + "/start"; - std::string response; - unsigned int http_code = 0; - const int result = http_post(path, body.dump(), &response, &http_code); - if (result != BAMBU_NETWORK_SUCCESS || http_code < 200 || http_code >= 300) { - BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: print-jobs/" << job_id << "/start failed http_code=" << http_code; - return BAMBU_NETWORK_ERR_CONNECT_FAILED; - } + (void) dev_list; return BAMBU_NETWORK_SUCCESS; } @@ -2276,10 +2025,6 @@ bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool noti void OrcaCloudServiceAgent::clear_session() { - if (mqtt_connection) { - mqtt_connection->stop(); - mqtt_connection->clear_subscriptions(); - } { std::lock_guard lock(session_mutex); session = SessionInfo{}; @@ -2395,11 +2140,7 @@ int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* respon return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; } -int OrcaCloudServiceAgent::http_post(const std::string& path, - const std::string& body, - std::string* response_body, - unsigned int* http_code, - const std::string& content_type) +int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code) { std::string url = api_base_url + path; BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << url; @@ -2423,7 +2164,7 @@ int OrcaCloudServiceAgent::http_post(const std::string& path, http.header("Authorization", "Bearer " + token); } - http.header("Content-Type", content_type); + http.header("Content-Type", "application/json"); http.set_post_body(body); http.on_complete([&](std::string resp_body, unsigned resp_status) { @@ -2890,28 +2631,19 @@ int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::str if (http_code) *http_code = code; - if (result != 0 || code != 200) { - BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: get_user_print_info failed - http_code=" << code << ", response=" << response; + if (result != 0 || code != 200) return result != 0 ? result : BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED; - } - - BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: get_user_print_info fetched - http_code=" << code << ", response=" << response; try { auto resp_json = nlohmann::json::parse(response); nlohmann::json devices = nlohmann::json::array(); for (const auto& printer : resp_json.value("data", nlohmann::json::array())) { - nlohmann::json device; - std::string role = printer.value("access_role", ""); - - // A printer with the role "view" only has monitoring access for orca cloud. - // The printer is owned by a different person and was shared to the current user without - // any permission to control the printer so we discard this printer. Comment this out if - // OrcaSlicer wants to support view only printers. + const std::string role = printer.value("access_role", ""); if (role.empty() || role == "viewer") continue; + nlohmann::json device; device["dev_id"] = printer.value("id", ""); device["dev_name"] = printer.value("name", ""); if (printer.contains("model") && printer["model"].is_string()) @@ -2925,20 +2657,15 @@ int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::str device["task_status"] = status["job"].value("state", ""); } device["dev_online"] = online; - - devices.push_back(device); + devices.push_back(std::move(device)); } if (http_body) { nlohmann::json out; - out["devices"] = devices; + out["devices"] = std::move(devices); *http_body = out.dump(); } - - BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_print_info parsed - device_count=" << devices.size() - << ", devices=" << devices.dump(); - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: get_user_print_info parse exception - " << e.what(); + } catch (const std::exception&) { return BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED; } diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp index ef282dc132..3ae86ec27c 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp @@ -2,12 +2,6 @@ #define __ORCA_CLOUD_SERVICE_AGENT_HPP__ #include "ICloudServiceAgent.hpp" - -#include -#include -#include -#include -#include #include #include #include @@ -15,17 +9,12 @@ #include #include #include -#include -#include -#include #include #include #include #include #include -#include "OrcaMqttConnection.hpp" - class wxSecretStore; namespace Slic3r { @@ -218,42 +207,6 @@ public: int del_subscribe(std::vector dev_list) override; void enable_multi_machine(bool enable) override; - // The fleet MQTT socket carries both directions: inbound reports from - // device//report and commands PUBLISHed to device//request on this - // socket. OrcaPrinterAgent registers its message callback here to receive the - // inbound half; pass an empty fn to clear it before the agent is destroyed. - int set_printer_status_callback(OnMessageFn fn); - - // Send a Bambu-dialect command to one printer via the cloud relay's REST - // endpoint (POST /api/v1/printers//commands). Synchronous - wraps http_post, - // so it carries the standard apikey + bearer headers and token refresh. Callers - // that need non-blocking behaviour run it on their own thread. - int send_printer_command(const std::string& dev_id, const std::string& body); - - // Upload sliced G-code to the cloud printer's print job storage: requests a - // short-lived presigned URL (POST print-jobs/uploads), then PUTs the file - // straight to R2 with that URL. Does not start the print or wait for - // OrcaSonar to download it - see OrcaPrinterAgent::start_print/start_sdcard_print - // for the MQTT hand-off that follows. *job_id receives the id to correlate - // with that hand-off; update_fn receives upload progress via Http's on_progress. - int upload_gcode_via_cloud(const std::string& dev_id, - const std::string& local_gcode_path, - std::string* job_id, - OnUpdateStatusFn update_fn, - WasCancelledFn cancel_fn); - - // Finalize a presigned print-job upload (step 3 of 3): POST - // print-jobs//start. The gateway HEAD-verifies the object landed in - // R2, then relays a print.project_file command (download URL + filename + - // start) to the printer over the gateway's OWN cloud relay connection to - // OrcaSonar - NOT this agent's MQTT session. See - // CLOUD_PRINT_JOB_MQTT_DESIGN.md for the MQTT-native alternative this - // stands in for until the gap documented there is closed. - int start_cloud_print_job(const std::string& dev_id, - const std::string& job_id, - const std::string& filename, - bool start = true); - // ======================================================================== // ICloudServiceAgent Interface Implementation - Settings Synchronization // ======================================================================== @@ -393,29 +346,7 @@ public: static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = ""); - OrcaMqttConnection* get_mqtt_connection() noexcept { - return mqtt_connection.get(); - } - - const OrcaMqttConnection* get_mqtt_connection() const noexcept { - return mqtt_connection.get(); - } - - // Account-scoped cloud socket: wss:///api/v1/printers/mqtt. - // configure_ blocks for the duration of the initial connect attempt, so callers - // drive it off the UI thread; teardown_ is synchronous. The dev_id argument is - // retained for source compatibility with the printer-agent lifecycle; it does - // not participate in endpoint construction. - int configure_selected_printer_mqtt(const std::string& dev_id, - OrcaMqttConnection::StateHandler state_handler = {}); - void teardown_selected_printer_mqtt(); - // Test hook: the wss:// URL of the current fleet socket ("" when none). - std::string selected_printer_mqtt_url() const; - private: - // Fans one inbound fleet MQTT message out to printer_status_callback. - void deliver_cloud_message(const std::string& dev_id, const std::string& payload); - // Sync protocol helpers int sync_pull( std::function on_success, @@ -439,11 +370,7 @@ private: // HTTP request helpers int http_get(const std::string& path, std::string* response_body, unsigned int* http_code); - int http_post(const std::string& path, - const std::string& body, - std::string* response_body, - unsigned int* http_code, - const std::string& content_type = "application/json"); + int http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code); int http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code); int http_delete(const std::string& path, std::string* response_body, unsigned int* http_code); std::map data_headers(); @@ -496,9 +423,6 @@ private: std::chrono::system_clock::now().time_since_epoch()).count()}; // Member variables - connection state - std::unique_ptr mqtt_connection; - std::string m_selected_printer_mqtt_url; // guarded by m_selected_url_mutex - mutable std::mutex m_selected_url_mutex; bool is_connected{false}; bool enable_track{false}; bool multi_machine_enabled{false}; @@ -512,7 +436,6 @@ private: AppOnHttpErrorFn on_http_error_fn; GetCountryCodeFn get_country_code_fn; QueueOnMainFn queue_on_main_fn; - OnMessageFn printer_status_callback; mutable std::mutex callback_mutex; // Thread safety diff --git a/src/slic3r/Utils/OrcaCloudSignalingChannel.cpp b/src/slic3r/Utils/OrcaCloudSignalingChannel.cpp deleted file mode 100644 index 9c3a029db3..0000000000 --- a/src/slic3r/Utils/OrcaCloudSignalingChannel.cpp +++ /dev/null @@ -1,320 +0,0 @@ -#include "OrcaCloudSignalingChannel.hpp" - -#include "Http.hpp" - -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include - -namespace Slic3r { - -OrcaCloudSignalingChannel::OrcaCloudSignalingChannel(std::shared_ptr cloud, std::string dev_id) - : m_cloud(std::move(cloud)) - , m_dev_id(std::move(dev_id)) -{ -} - -OrcaCloudSignalingChannel::~OrcaCloudSignalingChannel() -{ - close(); -} - -void OrcaCloudSignalingChannel::open() -{ - bool expected = false; - if (!m_open.compare_exchange_strong(expected, true)) - return; - m_stop.store(false); - m_thread = std::thread([this] { run(); }); -} - -void OrcaCloudSignalingChannel::close() -{ - m_stop.store(true); - std::shared_ptr conn; - { - std::lock_guard lock(m_mutex); - conn = m_conn; - } - if (conn) { - // Established session: close the socket on the io_context's own thread so - // the pending async_read completes and io_context.run() unwinds. - boost::asio::post(conn->io_context, [conn] { - boost::system::error_code ec; - boost::beast::get_lowest_layer(conn->websocket).cancel(ec); - boost::beast::get_lowest_layer(conn->websocket).close(ec); - }); - // Pre-run() phase (still in the synchronous connect/handshake): best-effort - // direct interruption. - boost::system::error_code ec; - boost::beast::get_lowest_layer(conn->websocket).cancel(ec); - boost::beast::get_lowest_layer(conn->websocket).close(ec); - } - if (m_thread.joinable()) - m_thread.join(); - m_open.store(false); -} - -void OrcaCloudSignalingChannel::send_offer(std::string sdp) -{ - send_json(nlohmann::json{{"type", "webrtc.offer"}, {"sdp", std::move(sdp)}}.dump()); -} - -void OrcaCloudSignalingChannel::send_ice(std::string candidate, std::string mid) -{ - send_json(nlohmann::json{{"type", "webrtc.ice"}, - {"candidate", std::move(candidate)}, - {"sdpMid", std::move(mid)}} - .dump()); -} - -std::string OrcaCloudSignalingChannel::encode_path_component(const std::string& value) -{ - std::ostringstream encoded; - encoded << std::uppercase << std::hex; - for (unsigned char c : value) { - if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') - encoded << c; - else - encoded << '%' << std::setw(2) << std::setfill('0') << static_cast(c); - } - return encoded.str(); -} - -std::string OrcaCloudSignalingChannel::host_without_scheme(std::string value) -{ - const auto scheme = value.find("://"); - if (scheme != std::string::npos) - value.erase(0, scheme + 3); - const auto slash = value.find('/'); - if (slash != std::string::npos) - value.erase(slash); - return value; -} - -void OrcaCloudSignalingChannel::unavailable(CameraUnavailableReason reason, std::string detail) -{ - if (on_unavailable) - on_unavailable(reason, std::move(detail)); -} - -void OrcaCloudSignalingChannel::run() -{ - try { - if (!m_cloud || !m_cloud->ensure_token_fresh("camera")) { - unavailable(CameraUnavailableReason::Error, "Unable to refresh OrcaCloud credentials"); - m_open.store(false); - return; - } - const std::string token = m_cloud->get_access_token(); - const std::string host = host_without_scheme(m_cloud->get_cloud_service_host()); - if (token.empty() || host.empty()) { - unavailable(CameraUnavailableReason::Error, "OrcaCloud session is unavailable"); - m_open.store(false); - return; - } - - const std::string live_token_url = - "https://" + host + "/api/v1/printers/" + encode_path_component(m_dev_id) + "/live-token"; - BOOST_LOG_TRIVIAL(info) << "signaling: POST " << live_token_url << " (dev_id=" << m_dev_id << ")"; - - nlohmann::json token_response; - std::string token_body; - std::string token_error; - unsigned int http_code = 0; - auto request = Http::post(live_token_url); - request.set_post_body(std::string("{}")) - .header("Authorization", "Bearer " + token) - .header("Content-Type", "application/json") - .tls_verify(true) - .timeout_max(30) - .on_complete([&token_body, &http_code](std::string body, unsigned status) { - http_code = status; - token_body = std::move(body); - }) - .on_error([&token_body, &token_error, &http_code](std::string body, std::string error, unsigned status) { - http_code = status; - token_body = std::move(body); - token_error = std::move(error); - }) - .perform_sync(); - BOOST_LOG_TRIVIAL(info) << "signaling: live-token HTTP " << http_code - << (token_error.empty() ? "" : " error=" + token_error) - << " body=" << token_body.substr(0, 512); - try { - token_response = nlohmann::json::parse(token_body); - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(warning) << "signaling: live-token body is not JSON: " << e.what(); - } - if (http_code < 200 || http_code >= 300 || !token_response.contains("token")) { - unavailable(CameraUnavailableReason::Error, "Unable to mint camera live token"); - m_open.store(false); - return; - } - - std::vector ice_servers; - if (token_response.contains("ice_servers") && token_response["ice_servers"].is_array()) { - for (const auto& entry : token_response["ice_servers"]) { - if (entry.is_string()) { - ice_servers.push_back({entry.get(), {}, {}}); - } else if (entry.is_object()) { - // RTCIceServer.urls is "string | string[]" (Cloudflare - // Realtime returns an array). Emit one CameraIceServer per - // URL, sharing the credentials. - const std::string username = entry.value("username", std::string{}); - const std::string credential = entry.value("credential", std::string{}); - const auto add_url = [&](const nlohmann::json& url) { - if (url.is_string() && !url.get().empty()) - ice_servers.push_back({url.get(), username, credential}); - }; - const auto urls = entry.find("urls"); - if (urls != entry.end()) { - if (urls->is_array()) { - for (const auto& url : *urls) - add_url(url); - } else { - add_url(*urls); - } - } - } - } - } - - auto conn = std::make_shared(); - conn->ssl_context.set_default_verify_paths(); - { - std::lock_guard lock(m_mutex); - m_conn = conn; - } - auto& websocket = conn->websocket; - boost::asio::ip::tcp::resolver resolver(conn->io_context); - const auto endpoints = resolver.resolve(host, "443"); - boost::asio::connect(boost::beast::get_lowest_layer(websocket), endpoints); - if (!SSL_set_tlsext_host_name(websocket.next_layer().native_handle(), host.c_str())) - throw std::runtime_error("Unable to configure TLS server name"); - websocket.next_layer().set_verify_mode(boost::asio::ssl::verify_peer); - websocket.next_layer().handshake(boost::asio::ssl::stream_base::client); - const std::string ws_target = "/api/v1/printers/" + encode_path_component(m_dev_id) + - "/camera/live?token=" + - encode_path_component(token_response["token"].get()); - websocket.handshake(host, ws_target); - BOOST_LOG_TRIVIAL(info) << "signaling: websocket handshake ok (" << ice_servers.size() - << " ice servers)"; - - if (on_ready) - on_ready(std::move(ice_servers)); - send_json(nlohmann::json{{"type", "camera.mode"}, {"mode", "webrtc"}}.dump()); - // Async read loop, driven by the connection's own io_context. run() - // returns once close() has shut the socket down, giving a bounded, - // deadlock-free teardown from any thread. - do_read(conn); - conn->io_context.run(); - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(warning) << "signaling: run() exception: " << e.what(); - if (!m_stop.load()) - unavailable(CameraUnavailableReason::Closed, e.what()); - } - { - std::lock_guard lock(m_mutex); - m_conn.reset(); - } - m_open.store(false); -} - -void OrcaCloudSignalingChannel::do_read(std::shared_ptr conn) -{ - auto buffer = std::make_shared(); - conn->websocket.async_read( - *buffer, [this, conn, buffer](boost::system::error_code ec, std::size_t) { - if (ec) { - if (!m_stop.load()) - unavailable(CameraUnavailableReason::Closed, ec.message()); - return; // do not re-arm; io_context.run() unwinds - } - const std::string raw = boost::beast::buffers_to_string(buffer->data()); - // A malformed or unexpectedly-shaped message must not tear down the - // session: parse/dispatch is guarded. - try { - dispatch_message(nlohmann::json::parse(raw), raw); - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(warning) << "signaling: ignoring malformed message: " << e.what() - << " raw=" << raw.substr(0, 256); - } - if (!m_stop.load()) - do_read(conn); - }); -} - -// Returns the string at key, or "" if absent or not a string (JSON null included). -static std::string json_string(const nlohmann::json& object, const char* key) -{ - const auto it = object.find(key); - return (it != object.end() && it->is_string()) ? it->get() : std::string{}; -} - -void OrcaCloudSignalingChannel::dispatch_message(const nlohmann::json& message, const std::string& raw) -{ - const std::string type = json_string(message, "type"); - BOOST_LOG_TRIVIAL(info) << "signaling: recv type=" << type << " raw=" << raw.substr(0, 256); - if (type == "webrtc.answer") { - const std::string sdp = json_string(message, "sdp"); - if (on_answer && !sdp.empty()) - on_answer(sdp); - } else if (type == "webrtc.ice" && message.contains("candidate")) { - // The peer may send "candidate" as a flat string or as a nested - // RTCIceCandidateInit object { candidate, sdpMid, sdpMLineIndex }. - const nlohmann::json& candidate = message["candidate"]; - std::string sdp_candidate; - std::string mid = json_string(message, "sdpMid"); - if (candidate.is_string()) { - sdp_candidate = candidate.get(); - } else if (candidate.is_object()) { - sdp_candidate = json_string(candidate, "candidate"); - std::string nested_mid = json_string(candidate, "sdpMid"); - if (!nested_mid.empty()) - mid = std::move(nested_mid); - } - if (on_ice && !sdp_candidate.empty()) - on_ice(sdp_candidate, mid); - } else if (type == "webrtc.unavailable") { - const std::string reason = json_string(message, "reason"); - unavailable(reason == "busy" ? CameraUnavailableReason::Busy - : reason == "disabled" ? CameraUnavailableReason::Disabled - : CameraUnavailableReason::Error, - reason.empty() ? "error" : reason); - } -} - -void OrcaCloudSignalingChannel::send_json(const std::string& message) -{ - std::shared_ptr conn; - { - std::lock_guard lock(m_mutex); - conn = m_conn; - } - if (!conn || m_stop.load()) - return; - // Serialize the write onto the io_context thread (same thread that runs - // async_read), so reads and writes never touch the stream concurrently. - auto payload = std::make_shared(message); - boost::asio::post(conn->io_context, [this, conn, payload] { - if (m_stop.load()) - return; - boost::system::error_code ec; - conn->websocket.write(boost::asio::buffer(*payload), ec); - if (ec && !m_stop.load()) - unavailable(CameraUnavailableReason::Closed, ec.message()); - }); -} - -} // namespace Slic3r diff --git a/src/slic3r/Utils/OrcaCloudSignalingChannel.hpp b/src/slic3r/Utils/OrcaCloudSignalingChannel.hpp deleted file mode 100644 index b87b7400e2..0000000000 --- a/src/slic3r/Utils/OrcaCloudSignalingChannel.hpp +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include "ICameraSignalingChannel.hpp" -#include "ICloudServiceAgent.hpp" - -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include - -namespace Slic3r { - -class OrcaCloudSignalingChannel : public ICameraSignalingChannel { -public: - OrcaCloudSignalingChannel(std::shared_ptr cloud, std::string dev_id); - ~OrcaCloudSignalingChannel() override; - - void open() override; - void close() override; - void send_offer(std::string sdp) override; - void send_ice(std::string candidate, std::string mid) override; - -private: - using WebSocket = boost::beast::websocket::stream< - boost::beast::ssl_stream>; - - // The io_context and ssl_context must outlive the websocket stream that - // references them. Bundling them here with the stream declared last makes - // the destruction order correct (stream first, then contexts), and lets a - // single shared_ptr own the whole set. - struct Connection { - boost::asio::io_context io_context; - boost::asio::ssl::context ssl_context{boost::asio::ssl::context::tls_client}; - WebSocket websocket{io_context, ssl_context}; - }; - - void run(); - void do_read(std::shared_ptr conn); - void dispatch_message(const nlohmann::json& message, const std::string& raw); - void send_json(const std::string& message); - void unavailable(CameraUnavailableReason reason, std::string detail); - static std::string encode_path_component(const std::string& value); - static std::string host_without_scheme(std::string value); - - std::shared_ptr m_cloud; - std::string m_dev_id; - std::atomic m_stop{false}; - std::atomic m_open{false}; - std::thread m_thread; - mutable std::mutex m_mutex; - std::shared_ptr m_conn; -}; - -} // namespace Slic3r diff --git a/src/slic3r/Utils/OrcaMqttConnection.cpp b/src/slic3r/Utils/OrcaMqttConnection.cpp deleted file mode 100644 index 2173231310..0000000000 --- a/src/slic3r/Utils/OrcaMqttConnection.cpp +++ /dev/null @@ -1,839 +0,0 @@ -#include "OrcaMqttConnection.hpp" - -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace Slic3r { - -struct OrcaMqttConnection::Connection { - boost::asio::io_context io_context; - boost::asio::ssl::context ssl_context; - boost::asio::ip::tcp::resolver resolver; - // Exactly one of these is engaged once ws_handshake() has run: wss for - // wss:// endpoints, ws for plaintext ws://. - std::optional wss; - std::optional ws; - - Connection() - : ssl_context(boost::asio::ssl::context::tls_client) - , resolver(io_context) - {} -}; - -namespace { -// Apply / clear a tcp_stream timeout on whichever websocket is engaged. -// Templated on the connection type only because Connection is a private nested -// type: a deduced parameter needs no (inaccessible) name for it. -template void expires_after(Conn& conn, std::chrono::seconds timeout) { - if (conn.wss) boost::beast::get_lowest_layer(*conn.wss).expires_after(timeout); - else if (conn.ws) boost::beast::get_lowest_layer(*conn.ws).expires_after(timeout); -} -template void expires_never(Conn& conn) { - if (conn.wss) boost::beast::get_lowest_layer(*conn.wss).expires_never(); - else if (conn.ws) boost::beast::get_lowest_layer(*conn.ws).expires_never(); -} -} // namespace - -OrcaMqttConnection::~OrcaMqttConnection() { stop(); } - -bool OrcaMqttConnection::start(const Config& config, MessageHandler on_message, StateHandler on_state) { - std::lock_guard lifecycle_lock(lifecycle_mutex); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT start url=" << config.url - << " use_tls=" << config.use_tls - << " bearer_provider=" << (config.bearer_provider ? "set" : "null") - << " username_present=" << (!config.username.empty()) - << " password_present=" << (!config.password.empty()) - << " client_id=" << config.client_id - << " keepalive_seconds=" << config.keepalive_seconds - << " message_callback=" << (on_message ? "set" : "null") - << " state_callback=" << (on_state ? "set" : "null"); - stop(); - { - std::lock_guard lock(mutex); - current_config = config; - this->on_message = std::move(on_message); - this->on_state = std::move(on_state); - initial_result = false; - initial_completed = false; - connected = false; - m_last_connack_rc.store(-1); - } - stopping.store(false); - worker = std::thread(&OrcaMqttConnection::run, this); - - std::unique_lock lock(mutex); - if (!initial_cv.wait_for(lock, std::chrono::seconds(10), [this] { return initial_completed; })) { - initial_completed = true; - initial_result = false; - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT initial connection timed out after 10 seconds" - << " url=" << current_config.url - << " last_connack_rc=" << m_last_connack_rc.load() - << " connected=" << connected.load() - << "; worker will retry"; - } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT start initial_result=" << initial_result - << " initial_completed=" << initial_completed - << " last_connack_rc=" << m_last_connack_rc.load() - << " worker_running=" << (worker.joinable() && !stopping.load()); - return initial_result; -} - -void OrcaMqttConnection::stop() { - std::lock_guard lifecycle_lock(lifecycle_mutex); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT stop requested" - << " url=" << current_config.url - << " connected=" << connected.load() - << " worker_joinable=" << worker.joinable() - << " last_connack_rc=" << m_last_connack_rc.load(); - stopping.store(true); - state_cv.notify_all(); - { - std::lock_guard lock(connection_mutex); - if (active_connection) { - // Generic so it accepts either the TLS or the plaintext websocket. - auto shutdown_socket = [](auto& websocket) { - auto& socket = boost::beast::get_lowest_layer(websocket).socket(); - boost::system::error_code socket_error; - socket.cancel(socket_error); - socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, socket_error); - socket.close(socket_error); - }; - if (active_connection->wss) - shutdown_socket(*active_connection->wss); - else if (active_connection->ws) - shutdown_socket(*active_connection->ws); - active_connection->resolver.cancel(); - } - } - if (worker.joinable()) - worker.join(); - - { - std::lock_guard lock(mutex); - connected = false; - acknowledged_subscriptions.clear(); - pending_subscribe_packets.clear(); - pending_requests.clear(); - if (!initial_completed) { - initial_completed = true; - initial_result = false; - } - } - initial_cv.notify_all(); -} - -bool OrcaMqttConnection::is_running() const { - return worker.joinable() && !stopping.load(); -} - -void OrcaMqttConnection::flush_subscription_change() { - std::shared_ptr conn; - { - std::lock_guard lock(connection_mutex); - conn = active_connection; - } - bool connacked; - { - std::lock_guard lock(mutex); - connacked = connected; - } - if (!conn || !connacked) - return; // no live MQTT session yet — the worker sends the set on CONNACK - - // beast permits a concurrent writer while the worker is blocked in - // websocket.read(); every write is serialised by write_mutex inside ws_write(). - try { - send_pending_subscriptions(*conn); - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: direct subscription write failed (" << e.what() - << "); worker will resend the full set on reconnect"; - } -} - -bool OrcaMqttConnection::subscribe(const std::string& dev_id) { - if (dev_id.empty()) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT subscribe rejected empty dev_id"; - return false; - } - const std::string topic = report_topic(dev_id); - if (topic.size() > 96) { // MQTT topic filter cap enforced by the service - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT subscribe rejected oversized topic=" << topic; - return false; - } - { - std::lock_guard lock(mutex); - if (subscriptions.count(topic) != 0 && pending_unsubscriptions.count(topic) == 0) { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT subscribe already queued or active topic=" << topic - << " acknowledged=" << (acknowledged_subscriptions.count(topic) != 0); - return true; - } - subscriptions.insert(topic); - pending_unsubscriptions.erase(topic); - pending_subscriptions.insert(topic); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT subscribe queued topic=" << topic - << " total_subscriptions=" << subscriptions.size() - << " connected=" << connected.load(); - } - state_cv.notify_all(); - flush_subscription_change(); // emit SUBSCRIBE now on the live socket (no reconnect) - return true; -} - -bool OrcaMqttConnection::unsubscribe(const std::string& dev_id) { - const std::string topic = report_topic(dev_id); - { - std::lock_guard lock(mutex); - subscriptions.erase(topic); - acknowledged_subscriptions.erase(topic); - pending_subscriptions.erase(topic); - pending_unsubscriptions.insert(topic); - for (auto it = pending_subscribe_packets.begin(); it != pending_subscribe_packets.end();) { - if (it->second == topic) - it = pending_subscribe_packets.erase(it); - else - ++it; - } - for (auto it = pending_requests.begin(); it != pending_requests.end();) { - if (it->first == dev_id) - it = pending_requests.erase(it); - else - ++it; - } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT unsubscribe queued topic=" << topic - << " total_subscriptions=" << subscriptions.size() - << " connected=" << connected.load(); - } - state_cv.notify_all(); - flush_subscription_change(); // emit UNSUBSCRIBE now on the live socket (no reconnect) - return true; -} - -void OrcaMqttConnection::clear_subscriptions() { - std::lock_guard lock(mutex); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT clear subscriptions count=" << subscriptions.size(); - subscriptions.clear(); - pending_subscriptions.clear(); - pending_unsubscriptions.clear(); - acknowledged_subscriptions.clear(); - pending_subscribe_packets.clear(); - pending_requests.clear(); -} - -bool OrcaMqttConnection::parse_endpoint(const std::string& url, Endpoint& endpoint) { - std::string rest; - std::string default_port; - if (url.rfind("wss://", 0) == 0) { rest = url.substr(6); default_port = "443"; } - else if (url.rfind("ws://", 0) == 0) { rest = url.substr(5); default_port = "80"; } - else return false; - - const auto slash = rest.find('/'); - const std::string authority = rest.substr(0, slash); - endpoint.target = (slash == std::string::npos) ? "/" : rest.substr(slash); - - // host[:port] — leave an unbracketed IPv6 literal alone - const auto colon = authority.rfind(':'); - if (colon != std::string::npos && authority.find(']') == std::string::npos) { - endpoint.host = authority.substr(0, colon); - endpoint.port = authority.substr(colon + 1); - } else { - endpoint.host = authority; - endpoint.port = default_port; - } - return !endpoint.host.empty() && !endpoint.port.empty() && !endpoint.target.empty(); -} - -void OrcaMqttConnection::append_string(std::vector& packet, const std::string& value) { - if (value.size() > 0xffff) - throw std::runtime_error("MQTT string is too long"); - packet.push_back(static_cast(value.size() >> 8)); - packet.push_back(static_cast(value.size() & 0xff)); - packet.insert(packet.end(), value.begin(), value.end()); -} - -void OrcaMqttConnection::prepend_remaining_length(std::vector& packet, size_t length) { - std::vector encoded; - do { - uint8_t byte = static_cast(length % 128); - length /= 128; - if (length != 0) - byte |= 0x80; - encoded.push_back(byte); - } while (length != 0); - packet.insert(packet.begin() + 1, encoded.begin(), encoded.end()); -} - -std::vector OrcaMqttConnection::make_connect_packet( - const std::string& client_id, const std::string& username, - const std::string& password, int keepalive_seconds) { - std::vector packet{0x10}; - append_string(packet, "MQTT"); - packet.push_back(4); // protocol level 3.1.1 - - uint8_t flags = 0x02; // clean session - if (!username.empty()) { flags |= 0x80; if (!password.empty()) flags |= 0x40; } - packet.push_back(flags); - - packet.push_back(static_cast(keepalive_seconds >> 8)); - packet.push_back(static_cast(keepalive_seconds & 0xff)); - - append_string(packet, client_id.empty() ? "OrcaSlicer" : client_id); - if (!username.empty()) { - append_string(packet, username); - if (!password.empty()) append_string(packet, password); - } - prepend_remaining_length(packet, packet.size() - 1); - return packet; -} - -std::string OrcaMqttConnection::report_topic(const std::string& device_id) { return "device/" + device_id + "/report"; } - -std::string OrcaMqttConnection::request_topic(const std::string& id) { return "device/" + id + "/request"; } - -std::vector OrcaMqttConnection::make_publish_packet(const std::string& topic, const std::string& payload) { - std::vector packet{0x30}; // PUBLISH, QoS 0, no retain - append_string(packet, topic); // no packet id at QoS 0 - packet.insert(packet.end(), payload.begin(), payload.end()); - prepend_remaining_length(packet, packet.size() - 1); - return packet; -} - -std::vector OrcaMqttConnection::make_subscribe_packet(uint16_t id, const std::string& topic, uint8_t qos) { - std::vector packet{0x82}; - packet.push_back(id >> 8); packet.push_back(id & 0xff); - append_string(packet, topic); - packet.push_back(qos); - prepend_remaining_length(packet, packet.size() - 1); - return packet; -} - -std::vector OrcaMqttConnection::make_unsubscribe_packet(uint16_t id, const std::string& topic) { - std::vector packet{0xA2}; - packet.push_back(id >> 8); packet.push_back(id & 0xff); - append_string(packet, topic); - prepend_remaining_length(packet, packet.size() - 1); - return packet; -} - -std::vector OrcaMqttConnection::make_ping_packet() { return {0xc0, 0}; } - -void OrcaMqttConnection::ws_write(Connection& conn, const std::vector& packet) { - if (packet.empty()) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: attempted to send empty MQTT packet"; - return; - } - // Writes come from the worker thread AND, for dynamic (un)subscribes, the - // caller thread. Serialise them; the worker's concurrent read is fine (beast - // allows one reader + one writer). - std::lock_guard lock(write_mutex); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending MQTT packet type=0x" << std::hex - << static_cast(packet[0] >> 4) << std::dec - << " bytes=" << packet.size(); - if (conn.wss) { - conn.wss->binary(true); - conn.wss->write(boost::asio::buffer(packet)); - } else if (conn.ws) { - conn.ws->binary(true); - conn.ws->write(boost::asio::buffer(packet)); - } -} - -std::size_t OrcaMqttConnection::ws_read(Connection& conn, boost::beast::flat_buffer& buffer, - boost::system::error_code& ec) { - if (conn.wss) - return conn.wss->read(buffer, ec); - if (conn.ws) - return conn.ws->read(buffer, ec); - ec = boost::asio::error::not_connected; - return 0; -} - -void OrcaMqttConnection::ws_close(Connection& conn) { - boost::system::error_code close_error; - if (conn.wss) - conn.wss->close(boost::beast::websocket::close_code::normal, close_error); - else if (conn.ws) - conn.ws->close(boost::beast::websocket::close_code::normal, close_error); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection closed code=" << close_error.value() - << " message=" << close_error.message(); -} - -void OrcaMqttConnection::ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint) { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: WebSocket resolve starting host=" << endpoint.host - << " port=" << endpoint.port << " target=" << endpoint.target - << " tls=" << config.use_tls; - const auto results = conn.resolver.resolve(endpoint.host, endpoint.port); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT DNS resolution succeeded host=" << endpoint.host; - - std::string token; - if (config.bearer_provider) { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: requesting bearer token for WebSocket upgrade"; - token = config.bearer_provider(); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: bearer token callback completed token_present=" << !token.empty(); - } - auto decorator = [token](boost::beast::websocket::request_type& request) { - request.set(boost::beast::http::field::user_agent, "OrcaSlicer"); - if (!token.empty()) - request.set(boost::beast::http::field::authorization, "Bearer " + token); - request.set("Sec-WebSocket-Protocol", "mqtt"); - }; - boost::beast::http::response response; - boost::system::error_code handshake_error; - - if (config.use_tls) { - // stop() inspects the engaged optional under connection_mutex; publish it - // under the same lock, then release before the blocking connect. - { - std::lock_guard lock(connection_mutex); - conn.wss.emplace(conn.io_context, conn.ssl_context); - } - auto& websocket = *conn.wss; - auto& stream = boost::beast::get_lowest_layer(websocket); - stream.expires_after(std::chrono::seconds(10)); - stream.connect(results); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TCP connection established host=" << endpoint.host - << " port=" << endpoint.port; - // Set SNI before the TLS handshake so the cloud edge selects the correct - // certificate. - auto& tls_stream = websocket.next_layer(); - if (!SSL_set_tlsext_host_name(tls_stream.native_handle(), endpoint.host.c_str())) - throw std::runtime_error("failed to set Orca Cloud TLS server name"); - conn.ssl_context.set_default_verify_paths(); - tls_stream.set_verify_mode(boost::asio::ssl::verify_peer); - tls_stream.set_verify_callback(boost::asio::ssl::host_name_verification(endpoint.host)); - tls_stream.handshake(boost::asio::ssl::stream_base::client); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TLS handshake completed host=" << endpoint.host; - websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator)); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending TLS WebSocket upgrade target=" << endpoint.target - << " bearer_header=" << (!token.empty()); - websocket.handshake(response, endpoint.host, endpoint.target, handshake_error); - } else { - { - std::lock_guard lock(connection_mutex); - conn.ws.emplace(conn.io_context); - } - auto& websocket = *conn.ws; - auto& stream = boost::beast::get_lowest_layer(websocket); - stream.expires_after(std::chrono::seconds(10)); - stream.connect(results); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TCP connection established host=" << endpoint.host - << " port=" << endpoint.port << " (plaintext)"; - websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator)); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending plaintext WebSocket upgrade target=" << endpoint.target - << " bearer_header=" << (!token.empty()); - websocket.handshake(response, endpoint.host, endpoint.target, handshake_error); - } - - if (handshake_error) { - // Surface the server's HTTP status so a persistent rejection (stale token, - // missing api key, wrong route) is diagnosable from the log rather than an - // opaque "handshake declined". - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: WS handshake rejected http=" - << response.result_int() << " (" << response.reason() << "), " - << handshake_error.message(); - throw boost::system::system_error(handshake_error, "Orca WebSocket handshake"); - } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: WebSocket handshake completed http=" << response.result_int() - << " negotiated_protocol=" << response["Sec-WebSocket-Protocol"]; - if (response["Sec-WebSocket-Protocol"] != "mqtt") { - BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: WebSocket handshake did not negotiate MQTT"; - throw std::runtime_error("Orca WebSocket did not negotiate MQTT"); - } -} - -bool OrcaMqttConnection::send_request(const std::string& dev_id, const std::string& payload) { - if (dev_id.empty()) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT send_request rejected empty dev_id"; - return false; - } - if (!connected.load()) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT send_request rejected because connection is not ready" - << " dev_id=" << dev_id << " last_connack_rc=" << m_last_connack_rc.load(); - return false; - } - std::shared_ptr conn; - { - std::lock_guard lock(connection_mutex); - conn = active_connection; - } - if (!conn) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT send_request rejected because active connection is null" - << " dev_id=" << dev_id; - return false; - } - const std::string report = report_topic(dev_id); - { - std::lock_guard lock(mutex); - if (subscriptions.count(report) != 0 && acknowledged_subscriptions.count(report) == 0) { - pending_requests.emplace_back(dev_id, payload); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT request queued until SUBACK" - << " dev_id=" << dev_id << " payload_bytes=" << payload.size() - << " pending_requests=" << pending_requests.size(); - return true; - } - } - try { - // ws_write() serialises the write via write_mutex; do not lock it here. - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT PUBLISH request dev_id=" << dev_id - << " topic=" << request_topic(dev_id) - << " payload_bytes=" << payload.size(); - ws_write(*conn, make_publish_packet(request_topic(dev_id), payload)); - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: send_request failed dev_id=" << dev_id - << " (" << e.what() << ")"; - return false; - } - return true; -} - -void OrcaMqttConnection::connect_and_read() { - m_connection_stage = "creating connection"; - auto connection = std::make_shared(); - { - std::lock_guard lock(connection_mutex); - active_connection = connection; - if (stopping.load()) - return; - } - - m_connection_stage = "parsing endpoint"; - Endpoint endpoint; - if (!parse_endpoint(current_config.url, endpoint)) { - BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: invalid MQTT endpoint=" << current_config.url; - throw std::runtime_error("invalid Orca Cloud WebSocket endpoint"); - } - - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connecting host=" << endpoint.host - << " port=" << endpoint.port << " target=" << endpoint.target; - - m_connection_stage = "WebSocket handshake"; - ws_handshake(*connection, current_config, endpoint); - - m_connection_stage = "sending MQTT CONNECT"; - expires_never(*connection); - // Auth precedence: a bearer_provider authenticates the WebSocket upgrade, so the - // CONNECT username/password fields are omitted entirely (the cloud form). - const bool use_bearer = static_cast(current_config.bearer_provider); - ws_write(*connection, make_connect_packet(current_config.client_id, - use_bearer ? std::string() : current_config.username, - use_bearer ? std::string() : current_config.password, - current_config.keepalive_seconds)); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT CONNECT packet sent"; - - m_connection_stage = "waiting for MQTT CONNACK"; - boost::beast::flat_buffer buffer; - expires_after(*connection, std::chrono::seconds(10)); - boost::system::error_code connack_error; - ws_read(*connection, buffer, connack_error); - if (connack_error) - throw boost::system::system_error(connack_error, "read Orca MQTT CONNACK"); - const std::string connack = boost::beast::buffers_to_string(buffer.data()); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT CONNACK received bytes=" << connack.size() - << " header=" << (connack.empty() ? -1 : static_cast(static_cast(connack[0]))) - << " return_code=" << (connack.size() > 3 ? static_cast(static_cast(connack[3])) : -1); - // rc: 0 accepted, 1..5 refusal, -1 malformed/not a CONNACK. - const int rc = (connack.size() == 4 && static_cast(connack[0]) == 0x20) - ? static_cast(static_cast(connack[3])) - : -1; - m_last_connack_rc.store(rc); - if (rc != 0) { - BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: MQTT CONNECT refused rc=" << rc; - if (rc == 4 || rc == 5) { - // Bad credentials / not authorized — retrying cannot help. Make run()'s - // loop exit and unblock any waiting start(). - stopping.store(true); - { - std::lock_guard lock(mutex); - initial_completed = true; - initial_result = false; - } - initial_cv.notify_all(); - } - throw std::runtime_error("Orca MQTT CONNECT refused rc=" + std::to_string(rc)); - } - - m_connection_stage = "reading MQTT messages"; - // The subscription acknowledgement belongs to this MQTT session. Clear - // the previous session's state before notifying the owner, because the - // reconnect callback immediately queues the printer's initial requests. - { - std::lock_guard lock(mutex); - acknowledged_subscriptions.clear(); - pending_subscribe_packets.clear(); - } - notify_state(true); - reconnect_delay_seconds.store(1); // a fresh CONNACK resets the backoff - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection is ready; sending current subscriptions"; - send_current_subscriptions(*connection); - std::chrono::steady_clock::time_point next_ping = std::chrono::steady_clock::now() + std::chrono::seconds(30); - - while (!stopping.load()) { - send_pending_subscriptions(*connection); - // Keepalive is driven every iteration, not only from the read-timeout branch: - // a printer pushing faster than the 1s read deadline would otherwise keep the - // read hot and the broker would drop us at 1.5 x keepalive. - if (std::chrono::steady_clock::now() >= next_ping) { - ws_write(*connection, make_ping_packet()); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT PINGREQ sent"; - next_ping = std::chrono::steady_clock::now() + std::chrono::seconds(30); - } - buffer.consume(buffer.size()); - m_connection_stage = "reading MQTT frame"; - expires_after(*connection, std::chrono::seconds(1)); - boost::system::error_code error; - ws_read(*connection, buffer, error); - if (error == boost::beast::error::timeout) - continue; - if (error) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT WebSocket read failed code=" << error.value() - << " message=" << error.message(); - throw boost::system::system_error(error, "read Orca MQTT message"); - } - handle_packet(boost::beast::buffers_to_string(buffer.data())); - } - - ws_close(*connection); - if (!stopping.load()) - notify_state(false); -} - -void OrcaMqttConnection::send_current_subscriptions(Connection& conn) { - std::vector topics; - { - std::lock_guard lock(mutex); - topics.assign(subscriptions.begin(), subscriptions.end()); - acknowledged_subscriptions.clear(); - pending_subscribe_packets.clear(); - for (const std::string& topic : topics) - pending_subscriptions.erase(topic); - } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending current MQTT subscriptions count=" << topics.size(); - for (const std::string& topic : topics) { - const uint16_t packet_id = next_packet_id++; - { - std::lock_guard lock(mutex); - pending_subscribe_packets[packet_id] = topic; - } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending SUBSCRIBE topic=" << topic << " packet_id=" << packet_id; - ws_write(conn, make_subscribe_packet(packet_id, topic, 1)); - } -} - -void OrcaMqttConnection::send_pending_subscriptions(Connection& conn) { - std::vector subscribe_topics; - std::vector unsubscribe_topics; - { - std::lock_guard lock(mutex); - subscribe_topics.assign(pending_subscriptions.begin(), pending_subscriptions.end()); - unsubscribe_topics.assign(pending_unsubscriptions.begin(), pending_unsubscriptions.end()); - pending_subscriptions.clear(); - pending_unsubscriptions.clear(); - } - for (const std::string& topic : subscribe_topics) { - const uint16_t packet_id = next_packet_id++; - { - std::lock_guard lock(mutex); - pending_subscribe_packets[packet_id] = topic; - } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending pending SUBSCRIBE topic=" << topic - << " packet_id=" << packet_id; - ws_write(conn, make_subscribe_packet(packet_id, topic, 1)); - } - for (const std::string& topic : unsubscribe_topics) { - const uint16_t packet_id = next_packet_id++; - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending pending UNSUBSCRIBE topic=" << topic - << " packet_id=" << packet_id; - ws_write(conn, make_unsubscribe_packet(packet_id, topic)); - } -} - -void OrcaMqttConnection::handle_packet(const std::string& packet) { - if (packet.size() < 2) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: received undersized MQTT packet bytes=" << packet.size(); - return; - } - const uint8_t header = static_cast(packet[0]); - const uint8_t packet_type = header >> 4; - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received MQTT packet type=" << static_cast(packet_type) - << " header=0x" << std::hex << static_cast(header) << std::dec - << " bytes=" << packet.size(); - if (packet_type != 3) { // Only QoS 0 PUBLISH carries printer status. - if (packet_type == 9 && packet.size() >= 5) { - const uint16_t packet_id = (static_cast(static_cast(packet[2])) << 8) | - static_cast(static_cast(packet[3])); - std::ostringstream result_codes; - for (size_t index = 4; index < packet.size(); ++index) { - if (index != 4) - result_codes << ','; - result_codes << "0x" << std::hex << static_cast(static_cast(packet[index])); - } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received SUBACK packet_id=" - << packet_id - << " result_codes=" << result_codes.str(); - - // Each production SUBSCRIBE packet currently contains one topic. - // MQTT grants QoS 0 or 1 for a requested QoS 1 subscription; 0x80 - // means the subscription was rejected. - const uint8_t result = static_cast(packet[4]); - std::string topic; - std::deque> requests; - { - std::lock_guard lock(mutex); - auto pending = pending_subscribe_packets.find(packet_id); - if (pending != pending_subscribe_packets.end()) { - topic = pending->second; - pending_subscribe_packets.erase(pending); - for (auto it = pending_requests.begin(); it != pending_requests.end();) { - if (report_topic(it->first) == topic) { - requests.push_back(std::move(*it)); - it = pending_requests.erase(it); - } else { - ++it; - } - } - if (result == 0 || result == 1) { - acknowledged_subscriptions.insert(topic); - } - } - } - if (topic.empty()) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: SUBACK has no pending topic packet_id=" << packet_id; - } else if (result == 0 || result == 1) { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: report subscription active topic=" << topic - << " granted_qos=" << static_cast(result) - << " releasing_requests=" << requests.size(); - for (const auto& request : requests) { - if (!send_request(request.first, request.second)) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: queued MQTT request could not be sent" - << " after SUBACK dev_id=" << request.first; - } - } - } else { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: report subscription rejected topic=" << topic - << " result_code=0x" << std::hex << static_cast(result) << std::dec - << " dropped_requests=" << requests.size(); - } - } - return; - } - size_t index = 1; - size_t multiplier = 1; - size_t remaining = 0; - uint8_t encoded = 0; - do { - if (index >= packet.size() || multiplier > 128 * 128 * 128) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH remaining length"; - return; - } - encoded = static_cast(packet[index++]); - remaining += (encoded & 0x7f) * multiplier; - multiplier *= 128; - } while ((encoded & 0x80) != 0); - const size_t remaining_end = index + remaining; - if (remaining_end > packet.size() || remaining < 2 || index + 2 > remaining_end) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH body remaining=" << remaining - << " packet_bytes=" << packet.size(); - return; - } - const uint16_t topic_length = (static_cast(packet[index]) << 8) | - static_cast(packet[index + 1]); - index += 2; - if (topic_length > packet.size() - index) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH topic length=" << topic_length; - return; - } - const std::string topic(packet.data() + index, topic_length); - index += topic_length; - if (((header >> 1) & 0x03) != 0) { - if (index + 2 > remaining_end) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH packet identifier"; - return; - } - index += 2; // QoS 1/2 packet identifier; the service currently sends QoS 0. - } - const size_t payload_size = remaining_end - index; - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received PUBLISH topic=" << topic - << " payload_bytes=" << payload_size - << " message_callback=" << (on_message ? "set" : "null"); - // topic is "device//report" (or "/request"); hand the id up, drop anything else. - std::string dev_id; - if (topic.rfind("device/", 0) == 0) { - const size_t id_start = 7; - const size_t id_end = topic.rfind('/'); - if (id_end != std::string::npos && id_end > id_start) - dev_id = topic.substr(id_start, id_end - id_start); - } - if (dev_id.empty()) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping PUBLISH on unrecognized topic=" << topic; - } else if (on_message) { - on_message(dev_id, packet.substr(index, remaining_end - index)); - } else { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping PUBLISH because message callback is not set"; - } -} - -void OrcaMqttConnection::notify_state(bool is_now_connected) { - StateHandler callback; - bool initial = false; - { - std::lock_guard lock(mutex); - connected = is_now_connected; - initial = !initial_completed; - if (initial) { - initial_result = is_now_connected; - initial_completed = true; - } - callback = on_state; - } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT state changed connected=" << is_now_connected - << " initial=" << initial << " state_callback=" << (callback ? "set" : "null"); - if (initial) - initial_cv.notify_all(); - else if (callback) - callback(is_now_connected, false); -} - -void OrcaMqttConnection::run() { - while (!stopping.load()) { - const int retry_seconds = reconnect_delay_seconds.load(); - const uint64_t attempt = ++m_attempt_number; - m_connection_stage = "starting attempt"; - try { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection attempt=" << attempt - << " retry_delay=" << retry_seconds - << " url=" << current_config.url; - connect_and_read(); - } catch (const std::exception& error) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT connection attempt=" << attempt - << " failed stage=" << m_connection_stage - << " error=" << error.what() - << " last_connack_rc=" << m_last_connack_rc.load() - << " stopping=" << stopping.load(); - if (!stopping.load()) - notify_state(false); - } - if (stopping.load()) - break; - // Grow the backoff only across attempts that never reached CONNACK; a - // successful connection resets reconnect_delay_seconds to 1 (connect_and_read). - reconnect_delay_seconds.store(std::min(retry_seconds * 2, 30)); - std::unique_lock lock(mutex); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT waiting before reconnect seconds=" << retry_seconds; - state_cv.wait_for(lock, std::chrono::seconds(retry_seconds), [this] { return stopping.load(); }); - } -} - -} // namespace Slic3r diff --git a/src/slic3r/Utils/OrcaMqttConnection.hpp b/src/slic3r/Utils/OrcaMqttConnection.hpp deleted file mode 100644 index e103e90167..0000000000 --- a/src/slic3r/Utils/OrcaMqttConnection.hpp +++ /dev/null @@ -1,152 +0,0 @@ -#ifndef slic3r_OrcaMqttConnection_hpp_ -#define slic3r_OrcaMqttConnection_hpp_ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace Slic3r { - -// Minimal MQTT 3.1.1 codec + WebSocket transport (ws:// and wss://), shared by the -// LAN (OrcaSonar) and cloud (fleet) printer connections. Both PUBLISH -// commands to device//request and SUBSCRIBE device//report; Config is the -// only per-transport difference. -class OrcaMqttConnection -{ -public: - using TokenProvider = std::function; - using MessageHandler = std::function; - using StateHandler = std::function; - - struct Endpoint { std::string host; std::string port; std::string target; }; - - struct Config { - std::string url; - bool use_tls = false; - TokenProvider bearer_provider; // set => bearer on WS upgrade, CONNECT creds omitted - std::string username; - std::string password; - std::string client_id = "OrcaSlicer"; - int keepalive_seconds = 60; - }; - - static bool parse_endpoint(const std::string& url, Endpoint& endpoint); - - // Build an MQTT 3.1.1 CONNECT packet. Clean-session is always set; the - // username/password connect flags and payload fields are added only when - // username is non-empty (the cloud form authenticates via a bearer on the - // WebSocket upgrade and omits CONNECT credentials). Public for unit tests. - static std::vector make_connect_packet(const std::string& client_id, - const std::string& username, - const std::string& password, - int keepalive_seconds); - - // Topic-string helpers for the per-device request/report channels and the - // MQTT 3.1.1 PUBLISH / SUBSCRIBE / UNSUBSCRIBE packet builders. All public - // for unit tests. make_publish_packet emits QoS 0 (no packet identifier). - static std::string request_topic(const std::string& dev_id); // "device//request" - static std::string report_topic(const std::string& dev_id); // "device//report" - static std::vector make_publish_packet(const std::string& topic, const std::string& payload); - static std::vector make_subscribe_packet(uint16_t packet_id, const std::string& topic, uint8_t qos); - static std::vector make_unsubscribe_packet(uint16_t packet_id, const std::string& topic); - - ~OrcaMqttConnection(); - - bool start(const Config& config, MessageHandler on_message, StateHandler on_state); - void stop(); - // True while the worker thread is alive (connected OR retrying). Lets callers - // avoid restarting a healthy connection. - bool is_running() const; - // True once CONNACK has been received and the socket has not since dropped. - bool is_connected() const { return connected.load(); } - bool subscribe(const std::string& dev_id); - bool unsubscribe(const std::string& dev_id); - // Last MQTT CONNACK return code: 0 ok, 1..5 refusal, -1 none seen this attempt. - int last_connack_rc() const { return m_last_connack_rc.load(); } - void clear_subscriptions(); - bool send_request(const std::string& dev_id, const std::string& payload); - -private: - // The endpoint may be either a TLS (wss://) or a plaintext (ws://) WebSocket; - // Connection holds whichever one is engaged and the ws_* helpers below - // dispatch on it. - using TlsWebSocket = boost::beast::websocket::stream< - boost::asio::ssl::stream>; - using PlainWebSocket = boost::beast::websocket::stream; - struct Connection; - - static void append_string(std::vector& packet, const std::string& value); - static void prepend_remaining_length(std::vector& packet, size_t length); - static std::vector make_ping_packet(); - - // Transport dispatch: each forwards to conn.wss (TLS) or conn.ws (plaintext). - void ws_write(Connection& conn, const std::vector& packet); // locks write_mutex - std::size_t ws_read(Connection& conn, boost::beast::flat_buffer& buffer, boost::system::error_code& ec); - void ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint); - void ws_close(Connection& conn); - // Emit a queued SUBSCRIBE/UNSUBSCRIBE on the live socket right now (from the - // caller thread), so a selection change is applied without waiting for the - // blocking read loop to next return. No-op if no CONNACKed socket exists yet - // (the worker sends the set on connect). The WebSocket is never dropped for a - // subscription change. - void flush_subscription_change(); - void connect_and_read(); - void send_current_subscriptions(Connection& conn); - void send_pending_subscriptions(Connection& conn); - void handle_packet(const std::string& packet); - void notify_state(bool is_now_connected); - void run(); - - std::atomic_bool stopping{true}; - std::atomic_int reconnect_delay_seconds{1}; - // Serialises the whole of start() and stop() against each other, so the UI - // thread's stop() (disconnect / dtor) cannot race the connect thread's start() - // into a concurrent worker.join(). Recursive because start() calls stop(). - std::recursive_mutex lifecycle_mutex; - std::thread worker; - std::mutex mutex; - std::mutex connection_mutex; - std::mutex write_mutex; // serialises every websocket write (worker + caller threads) - std::shared_ptr active_connection; - std::condition_variable initial_cv; - std::condition_variable state_cv; - Config current_config; - MessageHandler on_message; - StateHandler on_state; - // Full report-topic strings ("device//report"), not bare device ids. - std::set subscriptions; - std::set pending_subscriptions; - std::set pending_unsubscriptions; - // Requests for a subscribed device wait until the corresponding SUBACK is - // received. Otherwise an immediate pushall response can be published by - // the broker before this client is actually subscribed to the report topic. - std::set acknowledged_subscriptions; - std::map pending_subscribe_packets; - std::deque> pending_requests; - std::atomic next_packet_id{1}; - std::atomic m_last_connack_rc{-1}; - uint64_t m_attempt_number{0}; // worker-thread diagnostic sequence - std::string m_connection_stage; // worker-thread diagnostic stage - bool initial_result{false}; - bool initial_completed{false}; - std::atomic_bool connected{false}; -}; - -} // namespace Slic3r - -#endif // slic3r_OrcaMqttConnection_hpp_ diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index fc6d1320ee..cb70dafcca 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -1,1279 +1,92 @@ #include "OrcaPrinterAgent.hpp" -#include "OrcaCloudSignalingChannel.hpp" -#include "Http.hpp" -#include "IPrinterAgent.hpp" #include "NetworkAgentFactory.hpp" -#include "OrcaCloudServiceAgent.hpp" -#include "bambu_networking.hpp" -#include "json_diff.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include namespace Slic3r { const std::string OrcaPrinterAgent_VERSION = "0.0.1"; -namespace { - -namespace fs = boost::filesystem; - -// params.filename is normally the exported .3mf archive; the sliced G-code sits -// beside it with the same stem (".12345.0.3mf" -> ".12345.0.gcode"). params.dst_file, -// when set, already points straight at a file (the "print a file already on the -// card" flow), so it wins. -std::string resolve_local_gcode_path(const PrintParams& params) +OrcaPrinterAgent::OrcaPrinterAgent(std::string log_dir) : log_dir(std::move(log_dir)) { - if (!params.dst_file.empty()) - return params.dst_file; - - std::string path = params.filename; - if (boost::iends_with(path, ".3mf")) - path.replace(path.size() - 4, 4, ".gcode"); - return path; } -// The name the file is stored as under the printer's `gcodes` root, and the value -// passed to OrcaSonar's print.gcode_file `param`. Must be a pure function of params -// so start_local_print's upload and start_sdcard_print's start agree on it. -// OrcaSonar rejects newlines, ';', '#', '*' and NUL in the path, and Klipper's -// SDCARD_PRINT_FILE splits its argument on whitespace, so collapse anything unsafe. -std::string remote_gcode_name(const PrintParams& params) -{ - std::string name = params.project_name.empty() ? fs::path(resolve_local_gcode_path(params)).filename().string() : - fs::path(params.project_name).filename().string(); - - if (boost::iends_with(name, ".3mf")) // "model.gcode.3mf" -> "model.gcode" - name.erase(name.size() - 4); - - std::replace_if( - name.begin(), name.end(), - [](unsigned char c) { return std::isspace(c) != 0 || c == ';' || c == '#' || c == '*' || c == '/' || c == '\\'; }, '_'); - - if (name.empty()) - name = "orca_print"; - if (!boost::iends_with(name, ".gcode")) - name += ".gcode"; - return name; -} - -// http(s) origin of the Moonraker-compatible upload facade, derived from the live -// LAN MQTT session URL ("ws://host:port/mqtt" -> "http://host:port"). Used only as -// a fallback when the print job carries no dev_ip of its own. -std::string http_origin_from_lan_ws(const std::string& ws_url) -{ - if (ws_url.empty()) - return {}; - std::string s = ws_url; - if (boost::istarts_with(s, "wss://")) - s = "https://" + s.substr(6); - else if (boost::istarts_with(s, "ws://")) - s = "http://" + s.substr(5); - const auto scheme = s.find("://"); - if (scheme != std::string::npos) { - if (const auto slash = s.find('/', scheme + 3); slash != std::string::npos) - s.erase(slash); - } - return s; -} - -// print.gcode_file is non-idempotent and OrcaSonar replays a cached response for a -// reused (namespace, command, sequence_id). Seed from the wall clock so ids do not -// collide across slicer restarts, then bump once per call within a run. -std::string next_gcode_file_sequence_id() -{ - static std::atomic counter{[] { - const auto now = std::chrono::system_clock::now().time_since_epoch(); - return static_cast(std::chrono::duration_cast(now).count()); - }()}; - return std::to_string(counter.fetch_add(1, std::memory_order_relaxed)); -} - -static constexpr const char* ORCASONAR_FALLBACK = "orcasonar"; - -bool fetch_orcasonar_body(const std::string& url, std::string& body) -{ - bool ok = false; - Http::get(url) - .timeout_connect(4) - .timeout_max(6) - .on_complete([&](std::string b, unsigned status) { - if (status == 200) { - body = std::move(b); - ok = true; - } - }) - .on_error([&](std::string, std::string err, unsigned status) { - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: identity probe " << url << " failed status=" << status << " err=" << err; - }) - .perform_sync(); - return ok; -} - -std::string extract_line_value(const std::string& body, const std::string& key) -{ - const auto pos = body.find(key); - if (pos == std::string::npos) - return {}; - const auto value_start = pos + key.size(); - const auto value_end = body.find('\n', value_start); - std::string value = body.substr(value_start, value_end == std::string::npos ? std::string::npos : value_end - value_start); - boost::trim(value); - return value; -} - -// Manual binding uses the OrcaSonar landing page as its sole identity source. -// The page returns device_id and may return device_name/model_id. -bool probe_orcasonar_landing_page( - const std::string& host, const std::string& port, std::string& device_id, std::string& device_name, std::string& model_id) -{ - device_name = ORCASONAR_FALLBACK; - model_id = ORCASONAR_FALLBACK; - - std::string body; - if (fetch_orcasonar_body("http://" + host + ":" + port + "/", body)) { - device_id = extract_line_value(body, "device_id="); - const std::string name = extract_line_value(body, "device_name="); - const std::string model = extract_line_value(body, "model_id="); - if (!name.empty()) - device_name = name; - if (!model.empty()) - model_id = model; - if (!device_id.empty()) - return true; - } - - return false; -} - -// SSDP discovery uses the LOCATION URL's UPnP device description as its sole -// identity source. OrcaSonar maps device_id/device_name/model_id to UDN, -// friendlyName, and modelNumber respectively. -bool parse_orcasonar_device_xml(const std::string& body, std::string& device_id, std::string& device_name, std::string& model_id) -{ - device_name = ORCASONAR_FALLBACK; - model_id = ORCASONAR_FALLBACK; - - try { - boost::property_tree::ptree tree; - std::istringstream stream(body); - boost::property_tree::read_xml(stream, tree, boost::property_tree::xml_parser::trim_whitespace); - const auto device = tree.get_child_optional("root.device"); - if (!device) - return false; - - std::string udn = device->get("UDN", ""); - boost::trim(udn); - if (boost::istarts_with(udn, "uuid:")) - device_id = udn.substr(5); - else - device_id = device->get("device_id", ""); - boost::trim(device_id); - if (device_id.empty()) - return false; - - device_name = device->get("friendlyName", ""); - if (device_name.empty()) - device_name = device->get("device_name", ORCASONAR_FALLBACK); - model_id = device->get("modelNumber", ""); - if (model_id.empty()) - model_id = device->get("model_id", ORCASONAR_FALLBACK); - boost::trim(device_name); - boost::trim(model_id); - if (device_name.empty()) - device_name = ORCASONAR_FALLBACK; - if (model_id.empty()) - model_id = ORCASONAR_FALLBACK; - return true; - } catch (const std::exception& error) { - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: failed to parse OrcaSonar device.xml: " << error.what(); - return false; - } -} - -bool probe_orcasonar_device_xml(const std::string& location, std::string& device_id, std::string& device_name, std::string& model_id) -{ - std::string body; - return fetch_orcasonar_body(location, body) && parse_orcasonar_device_xml(body, device_id, device_name, model_id); -} -} // namespace - -class OrcaPrinterAgent::OrcaSonarDiscovery -{ -public: - using EmitFn = std::function; - - explicit OrcaSonarDiscovery(EmitFn emit) : m_emit(std::move(emit)) {} - ~OrcaSonarDiscovery() { stop(); } - - OrcaSonarDiscovery(const OrcaSonarDiscovery&) = delete; - OrcaSonarDiscovery& operator=(const OrcaSonarDiscovery&) = delete; - - void start() - { - std::lock_guard lock(m_lifecycle_mutex); - if (m_running.exchange(true)) - return; - m_thread = std::thread(&OrcaSonarDiscovery::browse_loop, this); - } - - void stop() - { - std::lock_guard lock(m_lifecycle_mutex); - if (!m_running.exchange(false)) - return; - m_wait_cv.notify_all(); - if (m_thread.joinable()) - m_thread.join(); - } - -private: - static std::string lower_ascii(std::string value) - { - std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - return value; - } - - static std::string trim_ascii(const std::string& value) - { - const auto first = value.find_first_not_of(" \t\r\n"); - if (first == std::string::npos) - return {}; - const auto last = value.find_last_not_of(" \t\r\n"); - return value.substr(first, last - first + 1); - } - - static bool get_header(const std::string& datagram, const std::string& header_name, std::string& value) - { - std::size_t line_start = 0; - while (line_start < datagram.size()) { - const std::size_t line_end = datagram.find('\n', line_start); - const std::string line = datagram.substr(line_start, line_end == std::string::npos ? std::string::npos : line_end - line_start); - line_start = line_end == std::string::npos ? datagram.size() : line_end + 1; - - const std::size_t colon = line.find(':'); - if (colon == std::string::npos) - continue; - if (lower_ascii(trim_ascii(line.substr(0, colon))) != lower_ascii(header_name)) - continue; - value = trim_ascii(line.substr(colon + 1)); - return !value.empty(); - } - return false; - } - - static bool make_machine_alive_json(const std::string& usn, const std::string& host, const std::string& location, std::string& json) - { - const std::string lower_usn = lower_ascii(usn); - static const std::string uuid_prefix = "uuid:"; - static const std::string device_type = "::urn:schemas-upnp-org:device:basic:1"; - if (lower_usn.rfind(uuid_prefix, 0) != 0) - return false; - - const std::size_t type_start = lower_usn.find(device_type, uuid_prefix.size()); - if (type_start == std::string::npos) - return false; - if (host.empty()) - return false; - - const std::string lower_location = lower_ascii(location); - const std::size_t scheme_end = lower_location.find("://"); - if (scheme_end == std::string::npos) - return false; - const std::size_t authority_start = scheme_end + 3; - const std::size_t authority_end = location.find_first_of("/ ?#", authority_start); - const std::string authority = location.substr(authority_start, authority_end == std::string::npos ? - std::string::npos : - authority_end - authority_start); - std::string port = "8280"; - if (!authority.empty()) { - if (authority.front() == '[') { - const std::size_t bracket = authority.find(']'); - if (bracket != std::string::npos && bracket + 1 < authority.size() && authority[bracket + 1] == ':') - port = authority.substr(bracket + 2); - } else { - const std::size_t colon = authority.rfind(':'); - if (colon != std::string::npos && colon + 1 < authority.size()) - port = authority.substr(colon + 1); - } - } - if (port.empty() || port.find_first_not_of("0123456789") != std::string::npos) - return false; - - // SSDP LOCATION commonly advertises the device's mDNS name (for - // example, http://orcasonar-123.local:8280/upnp/device.xml). The - // discovery response already gives us the sender's reachable address, - // so use that address for the HTTP probe instead of requiring the - // platform HTTP client to resolve .local. Keep the advertised path so - // this remains compatible with non-default device-description URLs. - const std::string location_path = authority_end == std::string::npos ? "/" : location.substr(authority_end); - const std::string probe_host = host.find(':') == std::string::npos ? host : "[" + host + "]"; - const std::string probe_url = location.substr(0, scheme_end + 3) + probe_host + ":" + port + location_path; - - std::string device_id; - std::string device_name; - std::string model_id; - if (!probe_orcasonar_device_xml(probe_url, device_id, device_name, model_id)) - return false; - - nlohmann::json machine; - machine["dev_name"] = device_name; - machine["dev_id"] = device_id; - machine["dev_type"] = model_id; - machine["connection_name"] = device_id; - machine["dev_ip"] = host + ":" + port; - - machine["dev_signal"] = "0"; - machine["connect_type"] = "lan"; - machine["bind_state"] = "free"; - machine["sec_link"] = "secure"; - machine["ssdp_version"] = "v1"; - json = machine.dump(); - return true; - } - - void ssdp_round() - { - namespace asio = boost::asio; - using asio::ip::udp; - try { - asio::io_context io_context; - udp::socket socket(io_context); - socket.open(udp::v4()); - socket.set_option(udp::socket::reuse_address(true)); - socket.bind(udp::endpoint(udp::v4(), 0)); - socket.non_blocking(true); - - static constexpr char search_request[] = "M-SEARCH * HTTP/1.1\r\n" - "HOST: 239.255.255.250:1900\r\n" - "MAN: \"ssdp:discover\"\r\n" - "MX: 2\r\n" - "ST: urn:schemas-upnp-org:device:Basic:1\r\n" - "\r\n"; - const auto multicast = asio::ip::make_address_v4("239.255.255.250"); - socket.send_to(asio::buffer(search_request, sizeof(search_request) - 1), udp::endpoint(multicast, 1900)); - - std::array buffer{}; - udp::endpoint sender; - std::set seen_ids; - const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); - while (m_running.load() && std::chrono::steady_clock::now() < deadline) { - boost::system::error_code error; - const std::size_t received = socket.receive_from(asio::buffer(buffer), sender, 0, error); - if (!error) { - const std::string datagram(buffer.data(), received); - std::string usn; - std::string location; - std::string server; - if (get_header(datagram, "USN", usn) && get_header(datagram, "LOCATION", location) && - (!get_header(datagram, "SERVER", server) || lower_ascii(server).find("orcasonar") != std::string::npos)) { - std::string machine_alive; - if (make_machine_alive_json(usn, sender.address().to_string(), location, machine_alive)) { - nlohmann::json machine = nlohmann::json::parse(machine_alive); - const std::string device_id = machine["dev_id"].get(); - if (seen_ids.insert(device_id).second && m_emit) - m_emit(machine_alive); - } - } - } else if (error != asio::error::would_block && error != asio::error::try_again) { - BOOST_LOG_TRIVIAL(warning) << "OrcaSonarDiscovery: SSDP receive failed: " << error.message(); - break; - } else { - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } - } - } catch (const std::exception& error) { - BOOST_LOG_TRIVIAL(warning) << "OrcaSonarDiscovery: SSDP round failed: " << error.what(); - } - } - - void browse_loop() - { - while (m_running.load()) { - ssdp_round(); - std::unique_lock lock(m_wait_mutex); - m_wait_cv.wait_for(lock, std::chrono::seconds(5), [this] { return !m_running.load(); }); - } - } - - EmitFn m_emit; - std::atomic m_running{false}; - std::thread m_thread; - std::mutex m_lifecycle_mutex; - std::mutex m_wait_mutex; - std::condition_variable m_wait_cv; -}; - -OrcaPrinterAgent::OrcaPrinterAgent(std::string log_dir) : log_dir(std::move(log_dir)) {} - -const char* OrcaPrinterAgent::connection_type_name(CurrentConn connection) -{ - switch (connection) { - case LAN: return "LAN"; - case CLOUD: return "cloud"; - case NONE: return "none"; - } - return "unknown"; -} - -OrcaPrinterAgent::~OrcaPrinterAgent() -{ - start_discovery(false, false); - ++m_lan_generation; // fence any late worker callback - ++m_cloud_generation; - - // Drop the cloud status callback before anything else: it holds `this`, and the - // cloud agent outlives the printer agent (NetworkAgent::set_printer_agent swaps - // the printer agent while m_cloud_agents persist). - if (auto* cloud = get_orca_cloud_agent()) - cloud->set_printer_status_callback(nullptr); - - // Stop the LAN connection so the connect thread's start() returns, but keep the - // object alive until that thread is joined (the thread holds a raw conn pointer). - OrcaMqttConnection* live_lan = nullptr; - { - std::lock_guard l(state_mutex); - live_lan = lan_mqtt_connection.get(); - } - if (live_lan) - live_lan->stop(); - - // Same for the cloud per-printer connection the cloud connect thread may hold. - if (auto* cloud = get_orca_cloud_agent()) - cloud->teardown_selected_printer_mqtt(); - - if (m_lan_connect_thread.joinable()) - m_lan_connect_thread.join(); - if (m_cloud_connect_thread.joinable()) - m_cloud_connect_thread.join(); - - // stop() is not sticky: a connect thread that had not yet reached start() when the - // teardown above ran could have raised a fresh socket in between. Tear down once - // more now that both threads are joined, so no live socket survives *this. - if (auto* cloud = get_orca_cloud_agent()) - cloud->teardown_selected_printer_mqtt(); - - { - std::lock_guard l(state_mutex); - lan_mqtt_connection.reset(); - } -} - -OrcaCloudServiceAgent* OrcaPrinterAgent::get_orca_cloud_agent() -{ - if (!m_cloud_agent) - return nullptr; - - return dynamic_cast(m_cloud_agent.get()); -} - -OrcaMqttConnection* OrcaPrinterAgent::get_appropriate_mqtt_connection(bool is_lan) -{ - if (is_lan) { - std::lock_guard l(state_mutex); - return lan_mqtt_connection.get(); - } - auto* cloud = get_orca_cloud_agent(); - return cloud ? cloud->get_mqtt_connection() : nullptr; -} - -// ============================================================================ -// Orca-dialect -> Bambu-dialect compatibility shim for inbound printer reports. -// -// OrcaSonar and the bridge adapter speak the "Orca Protocol" JSON dialect. -// MachineObject::parse_json (DeviceManager.cpp) only understands the Bambu -// dialect. Until the Orca Protocol is formally specified, this function is the -// ONE place where an inbound Orca-dialect report is rewritten into the Bambu -// shape parse_json already handles. -// -// Rules of this seam: -// 1. parse_json and the rest of DeviceManager are NOT modified to accommodate -// the Orca dialect - every such accommodation is a rule inside this -// function. -// 2. Each rule documents its Orca-dialect source, the Bambu-dialect target -// parse_json expects, and the rewrite between them. Rules are independent -// and can be removed one at a time as parse_json gains native support. -// 3. When parse_json reads the Orca dialect directly, this function and the -// single call in deliver_to_sink can be deleted, state included. Nothing -// else should need to change. -// -// It runs on every inbound report, so it stays cheap for payloads it does not -// touch (substring pre-check, no re-serialize unless something changed) and -// never throws (non-throwing parse, every field access guarded). -// ============================================================================ -std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, const std::string& payload) -{ - if (payload.find("get_capabilities") == std::string::npos && payload.find("push_status") == std::string::npos) - return payload; - - nlohmann::json envelope = nlohmann::json::parse(payload, nullptr, false); - if (!envelope.is_object()) - return payload; - - // Shim-local state, kept here (not on the class) so deleting this function - // removes its storage too. Process-wide and keyed by the globally-unique - // dev_id, with its own mutex; C++11 makes the one-time init thread-safe. - static std::unordered_map nozzle_diameter_cache; - static std::mutex nozzle_diameter_cache_mutex; - - bool modified = false; - - // ---- Rule: nozzle geometry --------------------------------------------- - // Orca dialect : info.capabilities.topology.tools[i].nozzle.diameter_mm, - // delivered once in the get_capabilities reply; push_status - // frames carry no nozzle geometry at all. - // Bambu dialect: parse_json runs DevNozzleSystemParser::ParseV1_0 only for a - // push_status frame that holds BOTH print.nozzle_diameter and - // print.nozzle_type. - // Rewrite : cache the diameter from the capabilities reply per device, - // then stamp print.nozzle_diameter + a neutral print.nozzle_type - // ("N/A" -> NozzleType::ntUndefine, as MoonrakerPrinterAgent - // does; Klipper has no Bambu nozzle type) onto later push_status - // frames that carry no real nozzle data. - const auto info_it = envelope.find("info"); - const bool is_capabilities_reply = info_it != envelope.end() && info_it->is_object() && - info_it->value("command", "") == "get_capabilities"; - - if (is_capabilities_reply) { - double nozzle_dia = 0.0; - const auto caps_it = info_it->find("capabilities"); - if (caps_it != info_it->end() && caps_it->is_object()) { - const auto topology_it = caps_it->find("topology"); - if (topology_it != caps_it->end() && topology_it->is_object()) { - const auto tools_it = topology_it->find("tools"); - if (tools_it != topology_it->end() && tools_it->is_array()) { - // First tool with a usable diameter wins: ParseV1_0 keeps a - // single nozzle (id 0). - for (const auto& tool : *tools_it) { - if (!tool.is_object()) - continue; - const auto nozzle_it = tool.find("nozzle"); - if (nozzle_it == tool.end() || !nozzle_it->is_object()) - continue; - const auto dia_it = nozzle_it->find("diameter_mm"); - if (dia_it != nozzle_it->end() && dia_it->is_number() && dia_it->get() > 0.0) { - nozzle_dia = dia_it->get(); - break; - } - } - } - } - } - if (nozzle_dia > 0.0) { - std::lock_guard l(nozzle_diameter_cache_mutex); - nozzle_diameter_cache[dev_id] = nozzle_dia; - } - // The capabilities reply itself is forwarded unchanged. - } - else { - const auto print_it = envelope.find("print"); - if (print_it != envelope.end() && print_it->is_object() && - print_it->value("command", "") == "push_status" && !print_it->contains("nozzle_diameter")) { - - double nozzle_dia = 0.0; - { - std::lock_guard l(nozzle_diameter_cache_mutex); - const auto it = nozzle_diameter_cache.find(dev_id); - if (it != nozzle_diameter_cache.end()) - nozzle_dia = it->second; - } - if (nozzle_dia > 0.0) { - (*print_it)["nozzle_diameter"] = nozzle_dia; - (*print_it)["nozzle_type"] = "N/A"; - modified = true; - } - } - } - // ---------------------------------------------------------------------- - - return modified ? envelope.dump() : payload; -} - -void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local) -{ - parse_ipcam_info(dev_id, payload); - std::string merged_payload = merge_capabilities(dev_id, payload); - - OnMessageFn fn; - QueueOnMainFn q; - { - std::lock_guard l(state_mutex); - fn = local ? on_local_message_fn : on_message_fn; - q = queue_on_main_fn; - } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering " << (local ? "local" : "cloud") << " report payload dev_id=" << dev_id - << " payload=" << merged_payload - << " callback=" << (fn ? "set" : "null") << " queue_on_main=" << (q ? "set" : "null"); - if (!fn) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping " << (local ? "local" : "cloud") << " message because on_message_fn is not set" - << " dev_id=" << dev_id; - return; - } - if (q) - q([fn, dev_id, merged_payload] { fn(dev_id, merged_payload); }); - else - fn(dev_id, merged_payload); -} - -void OrcaPrinterAgent::dispatch_local_connect(int state, const std::string& dev_id, const std::string& message) -{ - OnLocalConnectedFn callback; - QueueOnMainFn queue; - { - std::lock_guard lock(state_mutex); - callback = on_local_connect_fn; - queue = queue_on_main_fn; - } - - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection callback state=" << state << " dev_id=" << dev_id << " message=" << message - << " callback=" << (callback ? "set" : "null") << " queue_on_main=" << (queue ? "set" : "null"); - if (!callback) - return; - - auto dispatch = [callback, state, dev_id, message] { callback(state, dev_id, message); }; - if (queue) - queue(dispatch); - else - dispatch(); -} - -std::function OrcaPrinterAgent::make_lan_message_handler(uint64_t generation) -{ - return [this, generation](const std::string& id, const std::string& payload) { - if (generation == m_lan_generation.load()) - deliver_to_sink(id, payload, true); - else - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: dropping stale LAN message generation=" << generation - << " current_generation=" << m_lan_generation.load() << " dev_id=" << id; - }; -} +OrcaPrinterAgent::~OrcaPrinterAgent() = default; void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr cloud) -{ - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_cloud_agent: cloud=" << (cloud ? cloud->get_id() : ""); - { - std::lock_guard lock(state_mutex); - m_cloud_agent = cloud; - } - if (!get_orca_cloud_agent()) { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_cloud_agent: cloud is not OrcaCloudServiceAgent"; - return; // BBL provider active - nothing to bridge - } - - // OrcaCloudServiceAgent owns the aggregate MQTT socket; it already strips the - // device//report topic and hands us (dev_id, raw_json). Forward to the - // standard sink. message_arrive_fn self-marshals to the UI thread via CallAfter, - // so being called from the MQTT worker thread is fine. - const int callback_result = get_orca_cloud_agent()->set_printer_status_callback( - [this](std::string dev_id, std::string payload) { deliver_to_sink(dev_id, payload, false); }); - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_cloud_agent: status callback result=" << callback_result; -} - -std::unique_ptr OrcaPrinterAgent::create_camera_signaling_channel(const std::string& dev_id) { std::lock_guard lock(state_mutex); - if (!m_cloud_agent) - return nullptr; - return std::make_unique(m_cloud_agent, dev_id); + m_cloud_agent = cloud; } // ============================================================================ -// Communication +// Communication - All Stubs // ============================================================================ -int OrcaPrinterAgent::send_message(std::string dev_id, std::string json_str, int /*qos*/, int /*flag*/) -{ return route_send(/*is_lan=*/false, dev_id, json_str); } - -int OrcaPrinterAgent::command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) +int OrcaPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) { - int tray_number = 0; - if (!parse_nonnegative_command_id(tray_id, tray_number)) { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid RFID tray id=" << tray_id; - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - - nlohmann::json j; - j["print"]["command"] = "ams_get_rfid"; - j["print"]["sequence_id"] = std::to_string(sequence_id); - j["print"]["tray_id"] = tray_number; - return route_send(lan_mode, dev_id, j.dump()); -} - -int OrcaPrinterAgent::command_ams_calibrate(std::string /*dev_id*/, int /*ams_id*/, int /*sequence_id*/, bool /*lan_mode*/) -{ - // OrcaSonar has no ams_calibrate command. Do not send the Bambu M620 C - // dialect through the vendor-neutral OrcaSonar gcode_line command. - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: AMS calibration is not part of the OrcaSonar API"; - return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; -} - -int OrcaPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) -{ - int tray_number = 0; - if (!parse_nonnegative_command_id(tray_id, tray_number)) { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid AMS target tray id=" << tray_id; - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - - nlohmann::json j; - j["print"]["command"] = "ams_change_filament"; - j["print"]["sequence_id"] = std::to_string(sequence_id); - j["print"]["target"] = tray_number; - return route_send(lan_mode, dev_id, j.dump()); -} - -int OrcaPrinterAgent::command_start_camera(std::string /*dev_id*/) -{ - // OrcaSonar exposes camera.ipcam_* controls, not the legacy start_camera - // operation used by the Bambu agent. - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: camera start is not part of the OrcaSonar API"; - return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; -} - -int OrcaPrinterAgent::command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) -{ - nlohmann::json j; - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = "G90\n"; - j["print"]["sequence_id"] = std::to_string(sequence_id); - return route_send(lan_mode, dev_id, j.dump()); -} - -int OrcaPrinterAgent::command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) -{ - nlohmann::json j; - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = "G29\n"; - j["print"]["sequence_id"] = std::to_string(sequence_id); - return route_send(lan_mode, dev_id, j.dump()); -} - -int OrcaPrinterAgent::command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) -{ - nlohmann::json j; - j["print"]["sequence_id"] = std::to_string(sequence_id); - if (supports_mqtt_homing) { - j["print"]["command"] = "back_to_center"; - } else { - // Preserve the existing safety behavior: never home Z/Y during a print. - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = is_printing ? "G28 X\n" : "G28\n"; - } - return route_send(lan_mode, dev_id, j.dump()); -} - -int OrcaPrinterAgent::command_set_bed(std::string dev_id, int temp, bool /*supports_mqtt_bed_ctrl*/, int sequence_id, bool lan_mode) -{ - nlohmann::json j; - j["print"]["command"] = "set_bed_temp"; - j["print"]["sequence_id"] = std::to_string(sequence_id); - j["print"]["temp"] = temp; - return route_send(lan_mode, dev_id, j.dump()); -} - -int OrcaPrinterAgent::command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) -{ - nlohmann::json j; - j["print"]["command"] = "set_nozzle_temp"; - j["print"]["sequence_id"] = std::to_string(sequence_id); - j["print"]["extruder_index"] = 0; - j["print"]["target_temp"] = temp; - return route_send(lan_mode, dev_id, j.dump()); -} - -int OrcaPrinterAgent::command_axis_control(std::string dev_id, - std::string axis, - double unit, - double input_val, - int /*speed*/, - bool is_core_xy, - bool /*supports_mqtt_axis_control*/, - int sequence_id, - bool lan_mode) -{ - std::transform(axis.begin(), axis.end(), axis.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); - if (axis != "X" && axis != "Y" && axis != "Z" && axis != "E") { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid axis control axis=" << axis; - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - - const double requested_distance = input_val * unit; - if (!std::isfinite(requested_distance) || requested_distance == 0.0) { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid axis control distance input=" << input_val << " unit=" << unit; - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - - int direction = requested_distance > 0.0 ? 1 : -1; - if (!is_core_xy && (axis == "Y" || axis == "Z")) - direction = -direction; - - nlohmann::json j; - j["print"]["command"] = "xyz_ctrl"; - j["print"]["sequence_id"] = std::to_string(sequence_id); - j["print"]["axis"] = axis; - j["print"]["dir"] = direction; - j["print"]["distance"] = requested_distance < 0.0 ? -requested_distance : requested_distance; - return route_send(lan_mode, dev_id, j.dump()); -} - -bool OrcaPrinterAgent::parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port) -{ - std::string s = dev_ip; - if (s.rfind("http://", 0) == 0) - s.erase(0, 7); - else if (s.rfind("https://", 0) == 0) - s.erase(0, 8); - if (const auto slash = s.find('/'); slash != std::string::npos) - s.erase(slash); - if (s.empty()) - return false; - port = "8280"; - // split a trailing :port only for host:port, not an unbracketed IPv6 literal - if (const auto colon = s.rfind(':'); colon != std::string::npos && s.find(']') == std::string::npos) { - port = s.substr(colon + 1); - s.erase(colon); - } - if (s.empty() || port.empty()) - return false; - host = s; - return true; -} - -std::string OrcaPrinterAgent::make_lan_client_id(const std::string& dev_id) -{ - static const std::string suffix = [] { - std::random_device rd; - char buf[9]; - std::snprintf(buf, sizeof(buf), "%08x", static_cast(rd())); - return std::string(buf); - }(); - return "orcaslicer-lan-" + dev_id + "-" + suffix; -} - -bool OrcaPrinterAgent::parse_nonnegative_command_id(const std::string& value, int& result) -{ - if (value.empty()) - return false; - try { - std::size_t consumed = 0; - const long long parsed = std::stoll(value, &consumed); - if (consumed != value.size() || parsed < 0 || parsed > std::numeric_limits::max()) - return false; - result = static_cast(parsed); - return true; - } catch (const std::exception&) { - return false; - } -} - -void OrcaPrinterAgent::parse_ipcam_info(const std::string& dev_id, const std::string& payload) -{ - const nlohmann::json envelope = nlohmann::json::parse(payload, nullptr, false); - if (!envelope.is_object()) - return; - - const auto print_it = envelope.find("print"); - if (print_it == envelope.end() || !print_it->is_object()) - return; - - const nlohmann::json& print = *print_it; - const auto command_it = print.find("command"); - const bool is_push_status = command_it != print.end() && command_it->is_string() && command_it->get() == "push_status"; - bool is_full_snapshot = false; - if (is_push_status) { - const auto msg_it = print.find("msg"); - is_full_snapshot = msg_it == print.end() || (msg_it->is_number_integer() && msg_it->get() == 0); - } - - CameraStreamMode stream_mode = CameraStreamMode::none; - std::string stream_url; - bool has_camera_update = false; - const auto ipcam_it = print.find("ipcam"); - if (ipcam_it != print.end() && ipcam_it->is_object()) { - const auto stream_modes_it = ipcam_it->find("stream_mode"); - if (stream_modes_it != ipcam_it->end() && stream_modes_it->is_array()) { - has_camera_update = true; - for (const auto& stream : *stream_modes_it) { - if (!stream.is_object()) - continue; - const auto mode_it = stream.find("mode"); - const auto url_it = stream.find("url"); - if (mode_it == stream.end() || url_it == stream.end() || !mode_it->is_string() || !url_it->is_string()) - continue; - - const std::string mode = mode_it->get(); - if (mode == "rtsp") - stream_mode = CameraStreamMode::rtsp; - else if (mode == "http") - stream_mode = CameraStreamMode::http; - else if (mode == "http_snapshot") - stream_mode = CameraStreamMode::http_snapshot; - else - continue; - - stream_url = url_it->get(); - break; // OrcaSonar orders entries by preference. - } - } else if (is_full_snapshot) { - has_camera_update = true; - } - } else if (is_full_snapshot) { - // A full push_status without ipcam means the printer has no camera - // stream information. Diff reports omit unchanged domains. - has_camera_update = true; - } - - if (!has_camera_update) - return; - - std::lock_guard lock(state_mutex); - if (m_current_connection != LAN || m_lan_dev_id != dev_id) { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: ignoring camera update for inactive LAN printer dev_id=" << dev_id; - return; - } - - m_camera_stream_mode = stream_mode; - m_camera_url = std::move(stream_url); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: updated camera state dev_id=" << dev_id << " transport=LAN" - << " mode=" << static_cast(m_camera_stream_mode) << " url=" << m_camera_url; -} - -std::string OrcaPrinterAgent::lan_connection_target() const -{ - std::lock_guard l(state_mutex); - return m_lan_url; -} - -std::string OrcaPrinterAgent::seq(int n) { return std::to_string(20000 + (n % 10000)); } - -std::string OrcaPrinterAgent::build_pushing_start(const std::string& sid) -{ return R"({"pushing":{"command":"start","sequence_id":")" + sid + R"("}})"; } -std::string OrcaPrinterAgent::build_pushing_stop(const std::string& sid) -{ return R"({"pushing":{"command":"stop","sequence_id":")" + sid + R"("}})"; } -std::string OrcaPrinterAgent::build_pushall(const std::string& sid) -{ return R"({"pushing":{"command":"pushall","sequence_id":")" + sid + R"(","version":1,"push_target":1}})"; } -std::string OrcaPrinterAgent::build_get_version(const std::string& sid) -{ return R"({"info":{"command":"get_version","sequence_id":")" + sid + R"("}})"; } -std::string OrcaPrinterAgent::build_get_capabilities(const std::string& sid) -{ return R"({"info":{"command":"get_capabilities","sequence_id":")" + sid + R"("}})"; } - -void OrcaPrinterAgent::emit_connect_sequence(const std::string& dev_id, - std::function subscribe, - std::function request) -{ - subscribe(dev_id); - request(build_pushing_start(seq(1))); - request(build_pushall(seq(2))); - request(build_get_version(seq(3))); - request(build_get_capabilities(seq(4))); -} - -void OrcaPrinterAgent::on_connected(const std::string& dev_id, OrcaMqttConnection* conn, uint64_t generation) -{ - // Called from both connect paths with whichever epoch that path captured; the two - // counters are independent, so matching either one means the caller is still live. - if (!conn || (generation != m_lan_generation.load() && generation != m_cloud_generation.load())) - return; - emit_connect_sequence( - dev_id, [conn](const std::string& id) { conn->subscribe(id); }, - [conn, dev_id](const std::string& body) { conn->send_request(dev_id, body); }); // dev_id captured BY VALUE + return BAMBU_NETWORK_SUCCESS; } int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: connect_printer requested dev_id=" << dev_id << " dev_ip=" << dev_ip - << " username=" << (username.empty() ? "" : username) << " password_present=" << (!password.empty()) - << " use_ssl=" << use_ssl; - (void) use_ssl; // OrcaSonar LAN is plaintext ws:// - if (dev_id.empty() || dev_ip.empty()) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: connect_printer rejected missing dev_id or dev_ip"; - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - std::string host, port; - if (!parse_lan_endpoint(dev_ip, host, port)) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: connect_printer rejected unparsable LAN endpoint dev_ip=" << dev_ip; - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - disconnect_printer(); - const uint64_t gen = ++m_lan_generation; - - OrcaMqttConnection::Config cfg; - cfg.url = "ws://" + host + ":" + port + "/mqtt"; - cfg.use_tls = false; - cfg.username = username.empty() ? std::string("orcasonar") : username; - cfg.password = password; - cfg.client_id = make_lan_client_id(dev_id); - cfg.keepalive_seconds = 60; - - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection prepared generation=" << gen << " host=" << host << " port=" << port - << " url=" << cfg.url << " mqtt_username=" << cfg.username << " password_present=" << (!cfg.password.empty()) - << " client_id=" << cfg.client_id; - - OrcaMqttConnection* conn = nullptr; - CurrentConn previous_connection; - { - std::lock_guard l(state_mutex); - previous_connection = m_current_connection; - m_lan_dev_id = dev_id; - m_lan_url = cfg.url; - m_camera_stream_mode = CameraStreamMode::none; - m_camera_url.clear(); - m_current_connection = LAN; - lan_mqtt_connection = std::make_unique(); - conn = lan_mqtt_connection.get(); - } - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: selected LAN printer dev_id=" << dev_id - << " transport=" << connection_type_name(previous_connection) << "->LAN"; - - if (m_lan_connect_thread.joinable()) - m_lan_connect_thread.join(); // disconnect_printer() above already stopped the old conn, so this is fast - m_lan_connect_thread = std::thread([this, conn, cfg, dev_id, gen] { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connect worker started generation=" << gen << " dev_id=" << dev_id - << " url=" << cfg.url; - if (gen != m_lan_generation.load()) { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connect worker abandoned before start generation=" << gen - << " current_generation=" << m_lan_generation.load(); - return; // superseded before we ran: never raise a socket nobody will tear down - } - const bool ok = conn->start(cfg, make_lan_message_handler(gen), [this, gen, dev_id, conn](bool connected, bool initial) { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN MQTT state callback connected=" << connected << " initial=" << initial - << " generation=" << gen << " current_generation=" << m_lan_generation.load() - << " connack_rc=" << conn->last_connack_rc(); - if (gen != m_lan_generation.load()) { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: ignoring stale LAN MQTT state callback generation=" << gen; - return; - } - if (connected && !initial) { - on_connected(dev_id, conn, gen); - dispatch_local_connect(ConnectStatusOk, dev_id, "0"); - } else if (!connected && !initial) { - dispatch_local_connect(ConnectStatusLost, dev_id, "connection_lost"); - } - }); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN MQTT start returned ok=" << ok << " generation=" << gen - << " current_generation=" << m_lan_generation.load() << " connected=" << conn->is_connected() - << " running=" << conn->is_running() << " connack_rc=" << conn->last_connack_rc(); - if (ok && gen == m_lan_generation.load()) { - on_connected(dev_id, conn, gen); - dispatch_local_connect(ConnectStatusOk, dev_id, "0"); - } else if (!ok && gen == m_lan_generation.load() && !conn->is_running()) { - // A refusal with rc 4/5 terminates the transport. Network errors keep - // retrying in OrcaMqttConnection, so leave the UI in its connecting state. - const int rc = conn->last_connack_rc(); - const std::string reason = rc >= 0 ? std::to_string(rc) : "initial_connect_failed"; - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: LAN MQTT connection terminated before readiness" - << " generation=" << gen << " connack_rc=" << rc << " reason=" << reason; - dispatch_local_connect(ConnectStatusFailed, dev_id, reason); - } else if (!ok && gen == m_lan_generation.load()) { - BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: LAN MQTT initial attempt failed but worker is retrying" - << " generation=" << gen << " connack_rc=" << conn->last_connack_rc(); - } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connect worker exiting generation=" << gen - << " current_generation=" << m_lan_generation.load(); - }); - return BAMBU_NETWORK_SUCCESS; } int OrcaPrinterAgent::disconnect_printer() { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: disconnect_printer requested"; - ++m_lan_generation; // fence stale worker callbacks - std::unique_ptr doomed; - std::string prev_dev; - CurrentConn previous_connection; - CurrentConn current_connection; - { - std::lock_guard l(state_mutex); - previous_connection = m_current_connection; - doomed = std::move(lan_mqtt_connection); - prev_dev = m_lan_dev_id; - m_lan_dev_id.clear(); - if (m_current_connection == LAN) { - m_current_connection = NONE; - m_camera_stream_mode = CameraStreamMode::none; - m_camera_url.clear(); - } - current_connection = m_current_connection; - } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN disconnect generation=" << m_lan_generation.load() << " previous_dev_id=" << prev_dev - << " had_connection=" << (doomed ? "yes" : "no") - << " connected=" << (doomed && doomed->is_connected() ? "yes" : "no") - << " transport=" << connection_type_name(previous_connection) << "->" - << connection_type_name(current_connection); - // Tell the printer to stop pushing and drop the report topic before the socket - // goes away (§3.2/§5.4: deselect issues pushing.stop on both transports). - if (doomed && !prev_dev.empty() && doomed->is_connected()) { - doomed->send_request(prev_dev, build_pushing_stop(seq(5))); - doomed->unsubscribe(prev_dev); - } - if (doomed) - doomed->stop(); // joins the OrcaMqttConnection worker; OUTSIDE state_mutex - if (m_lan_connect_thread.joinable()) - m_lan_connect_thread.join(); // start() has returned (doomed->stop above); the thread's raw conn ptr - // is still valid here because `doomed` is not destroyed until we return return BAMBU_NETWORK_SUCCESS; } -int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int /*qos*/, int /*flag*/) -{ return route_send(/*is_lan=*/true, dev_id, json_str); } - -int OrcaPrinterAgent::route_send(bool is_lan, const std::string& dev_id, const std::string& json_str) +int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) { - std::string command = ""; - try { - const nlohmann::json envelope = nlohmann::json::parse(json_str); - for (const char* namespace_name : {"pushing", "info", "print", "system", "camera", "xcam", "upgrade", "event", "files"}) { - const auto namespace_it = envelope.find(namespace_name); - if (namespace_it != envelope.end() && namespace_it->is_object()) { - const auto command_it = namespace_it->find("command"); - if (command_it != namespace_it->end() && command_it->is_string()) { - command = std::string(namespace_name) + "." + command_it->get(); - break; - } - } - } - } catch (const std::exception&) { - // Preserve the transport's existing behavior for malformed payloads; - // the printer will report the protocol error asynchronously. - } - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send is_lan=" << is_lan << " dev_id=" << dev_id << " command=" << command - << " payload_bytes=" << json_str.size(); - if (dev_id.empty()) - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - OrcaMqttConnection* conn = get_appropriate_mqtt_connection(is_lan); - if (!conn) - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - const bool queued = conn->send_request(dev_id, json_str); - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send command=" << command << " queued=" << queued << " is_lan=" << is_lan - << " dev_id=" << dev_id; - return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; + return BAMBU_NETWORK_SUCCESS; } // ============================================================================ // Certificates - All Stubs // ============================================================================ -int OrcaPrinterAgent::check_cert() { return BAMBU_NETWORK_SUCCESS; } - -void OrcaPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) {} - -// ============================================================================ -// Discovery -// ============================================================================ - -bool OrcaPrinterAgent::start_discovery(bool start, bool /*sending*/) +int OrcaPrinterAgent::check_cert() { - if (start) { - std::lock_guard lock(state_mutex); - if (!m_discovery) { - m_discovery = std::make_unique([this](const std::string& machine_alive) { - OnMsgArrivedFn ssdp_fn; - QueueOnMainFn queue_fn; - { - std::lock_guard callback_lock(state_mutex); - ssdp_fn = on_ssdp_msg_fn; - queue_fn = queue_on_main_fn; - } - if (!ssdp_fn) - return; - if (queue_fn) - queue_fn([ssdp_fn, machine_alive] { ssdp_fn(machine_alive); }); - else - ssdp_fn(machine_alive); - }); - } - m_discovery->start(); - return true; - } + return BAMBU_NETWORK_SUCCESS; +} - // The discovery thread invokes the callback, which takes state_mutex. Move the - // owner out first, then join without holding that mutex. - std::unique_ptr discovery; - { - std::lock_guard lock(state_mutex); - discovery = std::move(m_discovery); - } - if (discovery) - discovery->stop(); +void OrcaPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) +{ +} + +// ============================================================================ +// Discovery - Stub +// ============================================================================ + +bool OrcaPrinterAgent::start_discovery(bool start, bool sending) +{ return true; } // ============================================================================ -// Binding +// Binding - All Stubs // ============================================================================ -int OrcaPrinterAgent::ping_bind(std::string ping_code) { return BAMBU_NETWORK_SUCCESS; } - -// Runs on the "Input IP address" dialog worker thread, before any MachineObject -// exists. Probe the address for a live OrcaSonar and hand its real device id back -// so DeviceManager::insert_local_device keys the machine correctly; connect_type -// and bind_state must be set for is_lan_mode_printer()/is_avaliable() to hold, or -// set_selected_machine never routes to the LAN connect path. -int OrcaPrinterAgent::bind_detect(std::string dev_ip, std::string /*sec_link*/, detectResult& detect) +int OrcaPrinterAgent::ping_bind(std::string ping_code) { - std::string host, port; - if (!parse_lan_endpoint(dev_ip, host, port)) { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::bind_detect: unparsable dev_ip=" << dev_ip; - return BAMBU_NETWORK_ERR_INVALID_HANDLE; // -1: dialog shows "Failed to connect to printer." - } - - std::string device_id; - std::string device_name; - std::string model_id; - if (!probe_orcasonar_landing_page(host, port, device_id, device_name, model_id) || device_id.empty()) { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::bind_detect: no OrcaSonar reachable at " << host << ":" << port; - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - - detect.dev_id = device_id; - detect.dev_name = device_name; - detect.model_id = model_id; - detect.version = ""; - detect.connect_type = "lan"; // required by MachineObject::is_lan_mode_printer() - detect.bind_state = "free"; // required by MachineObject::is_avaliable() - detect.result_msg = ""; - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::bind_detect: found OrcaSonar dev_id=" << device_id << " at " << host << ":" << port; return BAMBU_NETWORK_SUCCESS; } -int OrcaPrinterAgent::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) -{ return BAMBU_NETWORK_SUCCESS; } +int OrcaPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) +{ + return BAMBU_NETWORK_SUCCESS; +} -int OrcaPrinterAgent::unbind(std::string dev_id) { return BAMBU_NETWORK_SUCCESS; } +int OrcaPrinterAgent::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) +{ + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaPrinterAgent::unbind(std::string dev_id) +{ + return BAMBU_NETWORK_SUCCESS; +} int OrcaPrinterAgent::request_bind_ticket(std::string* ticket) { @@ -1310,94 +123,8 @@ std::string OrcaPrinterAgent::get_user_selected_machine() int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) { - auto* cloud = get_orca_cloud_agent(); - std::string previous; - CurrentConn previous_connection; - CurrentConn current_connection; - { - std::lock_guard lock(state_mutex); - previous_connection = m_current_connection; - // An empty cloud selection must not clear an independently active LAN - // selection. Conversely, selecting a cloud machine with the same id - // while LAN is active is still a transport switch and must proceed. - const bool same_selection = dev_id == selected_machine; - const bool same_transport = dev_id.empty() ? m_current_connection != CLOUD : m_current_connection == CLOUD; - if (same_selection && same_transport) { - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: unchanged dev_id=" << dev_id - << " transport=" << connection_type_name(m_current_connection); - return BAMBU_NETWORK_SUCCESS; - } - previous = selected_machine; - selected_machine = dev_id; - if (dev_id.empty()) { - if (m_current_connection == CLOUD) { - m_current_connection = NONE; - m_camera_stream_mode = CameraStreamMode::none; - m_camera_url.clear(); - } - } else { - m_current_connection = CLOUD; - m_camera_stream_mode = CameraStreamMode::none; - m_camera_url.clear(); - } - current_connection = m_current_connection; - } - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: previous=" << previous << " new=" << dev_id - << " cloud=" << (cloud ? "set" : "") << " transport=" << connection_type_name(previous_connection) << "->" - << connection_type_name(current_connection); - if (!cloud) { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_user_selected_machine: no Orca cloud agent"; - return BAMBU_NETWORK_SUCCESS; - } - - // Bump ONCE at the top for any change (select or deselect) so a deselect also - // fences an in-flight configure thread started by the previous selection. This is - // the CLOUD epoch only — a cloud selection must not fence a live LAN session. - const uint64_t gen = ++m_cloud_generation; - - auto* conn = cloud->get_mqtt_connection(); - if (!previous.empty()) { - cloud->del_subscribe({previous}); - if (conn && conn->is_connected()) - conn->send_request(previous, build_pushing_stop(seq(5))); - } - - // A previous initial-connect worker may still be blocking in start(). Stop it - // only when no fleet connection has reached CONNACK; an established fleet - // socket survives printer selection changes. - if (m_cloud_connect_thread.joinable()) { - if (conn && !conn->is_connected()) - conn->stop(); - m_cloud_connect_thread.join(); - } - - if (dev_id.empty()) - return BAMBU_NETWORK_SUCCESS; - - if (conn && conn->is_running()) { - on_connected(dev_id, conn, gen); - return BAMBU_NETWORK_SUCCESS; - } - - m_cloud_connect_thread = std::thread([this, cloud, dev_id, gen] { - if (gen != m_cloud_generation.load()) - return; // superseded before we ran: do not raise a socket nobody owns - - auto state_handler = [this](bool connected, bool initial) { - if (!connected || initial) - return; - auto* current_cloud = get_orca_cloud_agent(); - OrcaMqttConnection* current_conn = current_cloud ? current_cloud->get_mqtt_connection() : nullptr; - const std::string selected = get_user_selected_machine(); - if (current_conn && !selected.empty()) - on_connected(selected, current_conn, m_cloud_generation.load()); - }; - - if (cloud->configure_selected_printer_mqtt(dev_id, std::move(state_handler)) == BAMBU_NETWORK_SUCCESS && - gen == m_cloud_generation.load()) { - on_connected(dev_id, cloud->get_mqtt_connection(), gen); - } - }); + std::lock_guard lock(state_mutex); + selected_machine = dev_id; return BAMBU_NETWORK_SUCCESS; } @@ -1405,265 +132,39 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) // Agent Information // ============================================================================ AgentInfo OrcaPrinterAgent::get_agent_info_static() -{ return AgentInfo{ORCA_PRINTER_AGENT_ID, "Orca", OrcaPrinterAgent_VERSION, "Orca Printer Communication Protocol Agent"}; } +{ + return AgentInfo{ORCA_PRINTER_AGENT_ID, "Orca", OrcaPrinterAgent_VERSION, "Orca Printer Communication Protocol Agent"}; +} // ============================================================================ // Print Job Operations - All Stubs // ============================================================================ -// Orchestrates the cloud print workflow: upload the sliced G-code straight to R2 -// via a presigned URL (upload_gcode_via_cloud), then finalize over HTTP -// (start_cloud_print_job). The finalize call is what actually gets the file to -// the printer: the gateway HEAD-verifies the R2 object, then relays -// print.project_file to OrcaSonar over the gateway's OWN cloud relay connection - -// not this agent's MQTT session. See CLOUD_PRINT_JOB_MQTT_DESIGN.md for the -// MQTT-native alternative (publishing print.project_file directly over this -// agent's own cloud connection) and why it isn't used yet. -// -// This deliberately does NOT go through start_sdcard_print/print.gcode_file: -// that command is the generic "start this file already on the printer" primitive -// (also used by the LAN start_local_print path, and meant to stay that way as it -// grows params like filament mapping), and has no download-awareness to give it. int OrcaPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { - (void) wait_fn; - - if (update_fn) - update_fn(PrintingStageCreate, 0, "Preparing..."); - - if (cancel_fn && cancel_fn()) - return BAMBU_NETWORK_ERR_CANCELED; - - auto* cloud = get_orca_cloud_agent(); - if (!cloud || params.dev_id.empty()) - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - - const std::string local_path = resolve_local_gcode_path(params); - boost::system::error_code ec; - if (!fs::exists(local_path, ec) || !fs::is_regular_file(local_path, ec)) { - BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent: G-code file does not exist: " << local_path; - return BAMBU_NETWORK_ERR_FILE_NOT_EXIST; - } - - if (cancel_fn && cancel_fn()) - return BAMBU_NETWORK_ERR_CANCELED; - - if (update_fn) - update_fn(PrintingStageUpload, 0, "Uploading G-code..."); - - std::string job_id; - int result = cloud->upload_gcode_via_cloud(params.dev_id, local_path, &job_id, update_fn, cancel_fn); - if (result != BAMBU_NETWORK_SUCCESS) - return result; - - if (cancel_fn && cancel_fn()) - return BAMBU_NETWORK_ERR_CANCELED; - - if (update_fn) - update_fn(PrintingStageSending, 0, "Starting print..."); - - const int start_rc = cloud->start_cloud_print_job(params.dev_id, job_id, remote_gcode_name(params), /*start=*/true); - if (start_rc != BAMBU_NETWORK_SUCCESS) - return start_rc; - - if (update_fn) - update_fn(PrintingStageFinished, 100, "Print started"); - return BAMBU_NETWORK_SUCCESS; } -int OrcaPrinterAgent::start_local_print_with_record(PrintParams params, +int OrcaPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, - WasCancelledFn cancel_fn, - OnWaitFn wait_fn) -{ return BAMBU_NETWORK_SUCCESS; } + WasCancelledFn cancel_fn, + OnWaitFn wait_fn) +{ + return BAMBU_NETWORK_SUCCESS; +} -// Upload one G-code file to the printer's `gcodes` root over OrcaSonar's -// Moonraker-compatible HTTP facade. No print is started here (print=false); the -// caller issues print.gcode_file over MQTT separately (start_sdcard_print). -int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, - OnUpdateStatusFn update_fn, - WasCancelledFn cancel_fn, - OnWaitFn /*wait_fn*/) +int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { - if (update_fn) - update_fn(PrintingStageCreate, 0, "Preparing..."); - - const std::string local_path = resolve_local_gcode_path(params); - const fs::path source(local_path); - boost::system::error_code ec; - if (!fs::exists(source, ec) || !fs::is_regular_file(source, ec)) { - BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent: G-code file does not exist: " << local_path; - return BAMBU_NETWORK_ERR_FILE_NOT_EXIST; - } - - const std::uintmax_t file_size = fs::file_size(source, ec); - if (ec) { - BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent: cannot stat G-code file " << local_path << ": " << ec.message(); - return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; - } - if (file_size > 1024ull * 1024 * 1024) { // OrcaSonar caps a single upload at 1 GiB - BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent: G-code file too large: " << file_size << " bytes"; - return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; - } - - std::string host, port, origin; - if (parse_lan_endpoint(params.dev_ip, host, port)) - origin = "http://" + host; - else - origin = http_origin_from_lan_ws(lan_connection_target()); - if (origin.empty()) { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: no LAN HTTP endpoint for G-code upload (dev_ip=" << params.dev_ip << ")"; - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - - const std::string upload_name = remote_gcode_name(params); - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: uploading G-code " << local_path << " -> " << origin << "/server/files/upload as " - << upload_name << " (" << file_size << " bytes)"; - - if (update_fn) - update_fn(PrintingStageUpload, 0, "Uploading..."); - - bool canceled = false; - long http_status = 0; - std::string http_error; - std::string response_body; - - // check if printer has enough storage - Http::get(origin + "/server/files/directory?path=gcodes") - .on_complete([&](std::string body, unsigned status) { - if (body.empty()) { - http_status = 400; - http_error = "Failed to get gcodes directory."; - } - - int free = 0; - - nlohmann::json json = nlohmann::json::parse(body); - if (json.contains("result")) { - json = json["result"]; - if (json.contains("disk_usage")) { - json = json["disk_usage"]; - if (json.contains("free")) - free = json["free"].get(); - } - } - - if (free < file_size) { - http_status = 507; - http_error = "Not enough storage on the printer."; - } - }) - .on_error([&](std::string body, std::string err, unsigned status) { - http_status = status; - http_error = std::move(err); - response_body = std::move(body); - }) - .perform_sync(); - - if (http_status >= 400) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " failed with error code: " << http_status << ", " << http_error; - return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; - } - - auto http = Http::post(origin + "/server/files/upload"); - if (!params.password.empty()) - http.header("X-Api-Key", params.password); // trusted LAN facades may not require it; harmless when they do not - http.form_add("root", "gcodes") - .form_add("print", "false") - .form_add_file("file", source, upload_name) - .timeout_connect(5) - .timeout_max(300) // large G-code over a slow link - .on_complete([&](std::string body, unsigned status) { - http_status = status; - response_body = std::move(body); - }) - .on_error([&](std::string body, std::string err, unsigned status) { - http_status = status; - http_error = std::move(err); - response_body = std::move(body); - }) - .on_progress([&](Http::Progress progress, bool& cancel) { - if (cancel_fn && cancel_fn()) { - cancel = true; - canceled = true; - return; - } - if (update_fn && progress.ultotal > 0) { - const int percent = static_cast((progress.ulnow * 100) / progress.ultotal); - update_fn(PrintingStageUpload, percent, "Uploading..."); - } - }) - .perform_sync(); - - if (canceled) { - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: G-code upload canceled by user"; - return BAMBU_NETWORK_ERR_CANCELED; - } - - // OrcaSonar's Moonraker facade returns 201 Created on a successful save. - if (http_status != 200 && http_status != 201) { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: G-code upload failed http_status=" << http_status << " error=" << http_error - << " body=" << response_body; - return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; - } - - if (update_fn) - update_fn(PrintingStageUpload, 100, "File uploaded"); return BAMBU_NETWORK_SUCCESS; } -// Upload the sliced G-code, then start it: the LAN "print now" path. int OrcaPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) { - if (cancel_fn && cancel_fn()) - return BAMBU_NETWORK_ERR_CANCELED; - - const int upload_rc = start_send_gcode_to_sdcard(params, update_fn, cancel_fn, nullptr); - if (upload_rc != BAMBU_NETWORK_SUCCESS) - return upload_rc; - - if (cancel_fn && cancel_fn()) - return BAMBU_NETWORK_ERR_CANCELED; - - return start_sdcard_print(params, update_fn, cancel_fn); + return BAMBU_NETWORK_SUCCESS; } -// Start a file that already lives on the printer by publishing the canonical -// OPCP print.gcode_file command to device//request. The acknowledgement -// and lifecycle progress arrive asynchronously as print.push_status on the -// report topic, which the GUI already consumes. int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) { - if (params.dev_id.empty()) { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: start_sdcard_print rejected missing dev_id"; - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - if (cancel_fn && cancel_fn()) - return BAMBU_NETWORK_ERR_CANCELED; - - // dst_file, when set, names a file already on the printer (print-from-SD flow); - // otherwise start what start_send_gcode_to_sdcard just uploaded to `gcodes`. - const std::string target = params.dst_file.empty() ? remote_gcode_name(params) : fs::path(params.dst_file).filename().string(); - - nlohmann::json j; - j["print"]["command"] = "gcode_file"; - j["print"]["sequence_id"] = next_gcode_file_sequence_id(); - j["print"]["param"] = target; - - if (update_fn) - update_fn(PrintingStageSending, 0, "Starting print..."); - - const bool is_lan = params.connection_type == "lan"; - const int rc = route_send(is_lan, params.dev_id, j.dump()); - if (rc != BAMBU_NETWORK_SUCCESS) { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: start_sdcard_print publish failed rc=" << rc << " dev_id=" << params.dev_id - << " param=" << target; - return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED; - } - - if (update_fn) - update_fn(PrintingStageFinished, 100, "Print started"); return BAMBU_NETWORK_SUCCESS; } @@ -1696,7 +197,6 @@ int OrcaPrinterAgent::set_on_message_fn(OnMessageFn fn) { std::lock_guard lock(state_mutex); on_message_fn = fn; - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_on_message_fn: callback=" << (fn ? "set" : "clear"); return BAMBU_NETWORK_SUCCESS; } @@ -1711,7 +211,6 @@ int OrcaPrinterAgent::set_on_local_connect_fn(OnLocalConnectedFn fn) { std::lock_guard lock(state_mutex); on_local_connect_fn = fn; - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_on_local_connect_fn: callback=" << (fn ? "set" : "clear"); return BAMBU_NETWORK_SUCCESS; } @@ -1719,7 +218,6 @@ int OrcaPrinterAgent::set_on_local_message_fn(OnMessageFn fn) { std::lock_guard lock(state_mutex); on_local_message_fn = fn; - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_on_local_message_fn: callback=" << (fn ? "set" : "clear"); return BAMBU_NETWORK_SUCCESS; } @@ -1730,20 +228,4 @@ int OrcaPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) return BAMBU_NETWORK_SUCCESS; } -CameraStreamMode OrcaPrinterAgent::get_camera_stream_mode() const -{ - std::lock_guard lock(state_mutex); - if (m_current_connection == CLOUD) - return CameraStreamMode::webrtc; - return m_camera_stream_mode; -} - -std::string OrcaPrinterAgent::get_camera_url() const -{ - std::lock_guard lock(state_mutex); - if (m_current_connection != LAN) - return {}; - return m_camera_url; -} - } // namespace Slic3r diff --git a/src/slic3r/Utils/OrcaPrinterAgent.hpp b/src/slic3r/Utils/OrcaPrinterAgent.hpp index 1f50fb7a81..a1613420a5 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.hpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.hpp @@ -3,28 +3,19 @@ #include "IPrinterAgent.hpp" #include "ICloudServiceAgent.hpp" -#include "OrcaCloudServiceAgent.hpp" -#include "OrcaMqttConnection.hpp" -#include -#include -#include #include #include #include -#include namespace Slic3r { -class OrcaCloudServiceAgent; - /** - * OrcaPrinterAgent - OrcaSonar MQTT printer agent. + * OrcaPrinterAgent - Stub implementation for printer operations. * - * LAN and cloud commands use the same OrcaSonar protocol payloads; only the - * MQTT connection selected by route_send() differs. + * All printer-related operations are currently stubs that return success. + * Actual printer connectivity requires the BBL SDK or future Orca implementation. */ -class OrcaPrinterAgent : public IPrinterAgent -{ +class OrcaPrinterAgent : public IPrinterAgent { public: explicit OrcaPrinterAgent(std::string log_dir); ~OrcaPrinterAgent() override; @@ -34,10 +25,6 @@ public: // ======================================================================== void set_cloud_agent(std::shared_ptr cloud) override; - CameraStreamMode get_camera_stream_mode() const override; - std::string get_camera_url() const override; - std::unique_ptr - create_camera_signaling_channel(const std::string& dev_id) override; // Communication int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; @@ -55,13 +42,7 @@ public: // Binding int ping_bind(std::string ping_code) override; int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override; - 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 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; @@ -96,127 +77,10 @@ public: int set_on_local_message_fn(OnMessageFn fn) override; int set_queue_on_main_fn(QueueOnMainFn fn) override; - int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override; - int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override; - int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override; - int command_start_camera(std::string dev_id) override; - int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override; - int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override; - int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) override; - int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode) override; - int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override; - int command_axis_control(std::string dev_id, - std::string axis, - double unit, - double input_val, - int speed, - bool is_core_xy, - bool supports_mqtt_axis_control, - int sequence_id, - bool lan_mode) override; - - // Test-only: drive emit_connect_sequence directly (no socket). - void run_connect_sequence_for_test(const std::string& dev_id) - { - emit_connect_sequence(dev_id, [](const std::string&) {}, [](const std::string&) {}); - } - - // Test-only: advance the LAN connection epoch without a connect/disconnect cycle. - void bump_lan_generation_for_test() { ++m_lan_generation; } - // Test-only: the same for the (independent) cloud selection epoch. - void bump_cloud_generation_for_test() { ++m_cloud_generation; } - -protected: - // Forward one inbound printer message to on_message_fn or on_local_message_fn (marshalled onto the UI - // thread via queue_on_main_fn when set). Body of every connection's MessageHandler. - void deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local); - - // Extract OrcaSonar's print.ipcam.stream_mode from LAN reports before they - // are forwarded to the GUI. The getters below then read this agent-owned state. - void parse_ipcam_info(const std::string& dev_id, const std::string& payload); - - // Orca-dialect -> Bambu-dialect compatibility shim for inbound reports: the single - // place Orca Protocol JSON is rewritten into the shapes MachineObject::parse_json - // already handles, so parse_json needs no Orca-specific changes. Self-contained - // (its cache is a function-local static) and deletable together with its call site - // once parse_json reads the Orca dialect natively. See the definition for the - // per-rule detail. Returns the payload unchanged when no rule applies. - std::string merge_capabilities(const std::string& dev_id, const std::string& payload); - - // Report the asynchronous LAN connection state using the same callback contract as - // the other printer agents. The transport result cannot be returned by - // connect_printer(), which only starts the worker. - void dispatch_local_connect(int state, const std::string& dev_id, const std::string& message); - - // The LAN inbound-message handler for one connection generation: forwards to - // deliver_to_sink only while `generation` is still the live epoch. - std::function make_lan_message_handler(uint64_t generation); - - // Pure LAN-address parsing + client-id. protected static so the test Probe reaches them. - static bool parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port); - static std::string make_lan_client_id(const std::string& dev_id); - // Test hook: the ws:// URL connect_printer built for the current LAN session ("" if none). - std::string lan_connection_target() const; - // Shared post-connect sequence: SUBSCRIBE, then pushing.start, pushall, - // info.get_version, info.get_capabilities. Runs identically on LAN and cloud. - void on_connected(const std::string& dev_id, OrcaMqttConnection* conn, uint64_t generation); - - // The post-connect command sequence, factored behind a seam so a test can - // observe the SUBSCRIBE + 4 request payloads without a live OrcaMqttConnection. - virtual void emit_connect_sequence(const std::string& dev_id, - std::function subscribe, - std::function request); - static std::string seq(int n); // decimal string in the OrcaSlicer 20000..29999 band - static std::string build_pushing_start(const std::string& sequence_id); - static std::string build_pushing_stop(const std::string& sequence_id); - static std::string build_pushall(const std::string& sequence_id); - static std::string build_get_version(const std::string& sequence_id); - static std::string build_get_capabilities(const std::string& sequence_id); - private: - class OrcaSonarDiscovery; - std::string log_dir; std::string selected_machine; - - enum CurrentConn { NONE, CLOUD, LAN }; - static const char* connection_type_name(CurrentConn connection); - - // The transport for the printer currently selected by the UI. LAN and - // cloud sessions have separate connection objects, so this is selection - // state rather than an inference from whichever socket happens to exist. - CurrentConn m_current_connection = NONE; - std::shared_ptr m_cloud_agent; - std::unique_ptr lan_mqtt_connection; - - // Two independent epochs: a cloud (de)selection must not fence the live LAN - // feed, and vice versa. Each transport's connect thread and inbound handler - // compare against their own counter only. - std::atomic m_lan_generation{0}; - std::atomic m_cloud_generation{0}; - - // The short-lived threads that run the blocking initial connect for the current - // LAN / cloud session. Joined members (never detached) so they cannot outlive - // *this or the connection they hold a raw pointer to. - std::thread m_lan_connect_thread; - std::thread m_cloud_connect_thread; - - std::unique_ptr m_discovery; - - std::string m_lan_dev_id; // guarded by state_mutex - std::string m_lan_url; // guarded by state_mutex — the Config.url of the live LAN session - CameraStreamMode m_camera_stream_mode = CameraStreamMode::none; // guarded by state_mutex - std::string m_camera_url; // guarded by state_mutex - - OrcaCloudServiceAgent* get_orca_cloud_agent(); - - OrcaMqttConnection* get_appropriate_mqtt_connection(bool is_lan = true); - static bool parse_nonnegative_command_id(const std::string& value, int& result); - - // Route one command payload to device//request on the LAN or the shared - // cloud connection. The uniform send path for both send_message* overrides. - int route_send(bool is_lan, const std::string& dev_id, const std::string& json_str); // Callbacks OnMsgArrivedFn on_ssdp_msg_fn; diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index 94280e658b..4a90ec3771 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -1,6 +1,5 @@ #include "QidiPrinterAgent.hpp" #include "Http.hpp" -#include "IPrinterAgent.hpp" #include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI_App.hpp" @@ -9,7 +8,6 @@ #include #include #include -#include using json = nlohmann::json; @@ -29,15 +27,6 @@ bool has_visible_base_preset(const PresetCollection& filaments, const std::strin return false; } -// RAII decrement for MoonrakerPrinterAgent::filament_fetch_in_flight — guarantees the -// counter drops back down on every exit path (early return or fall-through) inside the -// detached fetch thread below, so ~MoonrakerPrinterAgent()'s wait loop can't stall forever. -struct InFlightGuard -{ - std::atomic& counter; - ~InFlightGuard() { counter.fetch_sub(1, std::memory_order_relaxed); } -}; - } // anonymous namespace const std::string QidiPrinterAgent_VERSION = "0.0.1"; @@ -51,141 +40,42 @@ AgentInfo QidiPrinterAgent::get_agent_info_static() return AgentInfo{"qidi", "Qidi", QidiPrinterAgent_VERSION, "Qidi printer agent"}; } -FilamentSyncMode QidiPrinterAgent::get_filament_sync_mode() const +bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/) { - if (GUI::wxGetApp().app_config->get_bool("use_printer_agents")) - return FilamentSyncMode::subscription; - return FilamentSyncMode::pull; -} + std::string error; -bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode) -{ - if (sync_mode != get_filament_sync_mode()) + // 1. Fetch device info and infer series_id + std::string series_id; + { + MoonrakerDeviceInfo info; + if (fetch_device_info(device_info.base_url, device_info.api_key, info, error)) { + series_id = infer_series_id(info.model_id, info.dev_name); + } + } + if (series_id.empty()) { + // Fall back to the configured Orca model if Moonraker doesn't expose a usable identifier. + series_id = infer_series_id(device_info.model_id, device_info.model_name); + } + + // 2. Fetch filament dictionary + QidiFilamentDict dict; + if (!fetch_filament_dict(device_info.base_url, device_info.api_key, dict, error)) { + BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error; + } + + // 3. Fetch slot info and build AmsTrayData directly + std::vector trays; + int box_count = 0; + if (!fetch_slot_info(device_info.base_url, device_info.api_key, dict, series_id, trays, box_count, error)) { + BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error; return false; + } - // Snapshot only what the fetch needs, rather than reading device_info live from the - // background thread below — device_info can be concurrently rewritten by a reconnect - // on another thread while this fetch is still in flight. - std::string base_url = device_info.base_url; - std::string api_key = device_info.api_key; - std::string model_id = device_info.model_id; - std::string model_name = device_info.model_name; - - filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed); - - std::thread([this, base_url, api_key, model_id, model_name]() { - InFlightGuard guard{filament_fetch_in_flight}; - - std::string error; - - // 1. Fetch device info and infer series_id - std::string series_id; - { - MoonrakerDeviceInfo info; - if (fetch_device_info(base_url, api_key, info, error)) { - series_id = infer_series_id(info.model_id, info.dev_name); - } - } - if (series_id.empty()) { - // Fall back to the configured Orca model if Moonraker doesn't expose a usable identifier. - series_id = infer_series_id(model_id, model_name); - } - - // 2. Fetch filament dictionary - QidiFilamentDict dict; - if (!fetch_filament_dict(base_url, api_key, dict, error)) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error; - } - - // 3. Fetch slot info and build AmsTrayData directly - std::vector trays; - int box_count = 0; - if (!fetch_slot_info(base_url, api_key, dict, series_id, trays, box_count, error)) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error; - return; - } - - // 4. Build the AMS payload - build_ams_payload(box_count, box_count * 4 - 1, trays); - }).detach(); + // 4. Build the AMS payload + build_ams_payload(box_count, box_count * 4 - 1, trays); return true; } -bool QidiPrinterAgent::apply_box_mapping(const PrintParams& params) const -{ - // enable_box mirrors task_use_ams: engage the multi-color box only when this - // job actually routes filament through it. (See qidi-ams-findings.md §2/§8.3 — - // if firmware treats enable_box as "a box exists" rather than "use it this job", - // switch this gate to HasAms()/box_count instead.) - const int enable = params.task_use_ams ? 1 : 0; - if (!send_gcode(device_info.dev_id, "SAVE_VARIABLE VARIABLE=enable_box VALUE=" + std::to_string(enable))) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to set enable_box"; - return false; - } - - // When the box isn't used this job, leave the existing value_t slot - // assignments untouched (enable_box=0 is enough to disengage it). - if (!enable) - return true; - - if (params.ams_mapping.empty()) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::apply_box_mapping: enable_box set but ams_mapping is empty"; - return true; - } - - // ams_mapping (v0) is a JSON array indexed by filament/tool; each value is the - // physical box slot (-1 = unmapped). Mirror it onto the printer's value_t - // variables: SAVE_VARIABLE VARIABLE=value_t VALUE='slot'. - auto mapping = nlohmann::json::parse(params.ams_mapping, nullptr, /*allow_exceptions*/ false); - if (mapping.is_discarded() || !mapping.is_array()) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: invalid ams_mapping: " << params.ams_mapping; - return false; - } - - for (size_t tool = 0; tool < mapping.size(); ++tool) { - if (!mapping[tool].is_number_integer()) - continue; - const int slot = mapping[tool].get(); - if (slot < 0) - continue; // unmapped filament — skip - const std::string gcode = "SAVE_VARIABLE VARIABLE=value_t" + std::to_string(tool) + - " VALUE=\"'slot" + std::to_string(slot) + "'\""; - if (!send_gcode(device_info.dev_id, gcode)) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to set value_t" << tool; - return false; - } - } - return true; -} - -int QidiPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) -{ - if (!apply_box_mapping(params)) - return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED; - return MoonrakerPrinterAgent::start_local_print(std::move(params), update_fn, cancel_fn); -} - -int QidiPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) -{ - if (!apply_box_mapping(params)) - return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED; - return MoonrakerPrinterAgent::start_print(std::move(params), update_fn, cancel_fn, wait_fn); -} - -int QidiPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) -{ - if (!apply_box_mapping(params)) - return BAMBU_NETWORK_ERR_PRINT_WR_UPLOAD_FTP_FAILED; - return MoonrakerPrinterAgent::start_local_print_with_record(std::move(params), update_fn, cancel_fn, wait_fn); -} - -int QidiPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) -{ - if (!apply_box_mapping(params)) - return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED; - return MoonrakerPrinterAgent::start_sdcard_print(std::move(params), update_fn, cancel_fn); -} - bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, const std::string& api_key, const QidiFilamentDict& dict, @@ -230,10 +120,20 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, return false; } - nlohmann::json status; - nlohmann::json variables; - if (!parse_slot_response(response_body, status, variables, error)) + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + error = "Invalid JSON response"; return false; + } + + if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("save_variables") || + !json["result"]["status"]["save_variables"].contains("variables")) { + error = "Unexpected JSON structure"; + return false; + } + + auto& variables = json["result"]["status"]["save_variables"]["variables"]; + auto& status = json["result"]["status"]; box_count = variables.value("box_count", 1); if (box_count < 0) { @@ -308,31 +208,6 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, return true; } -bool QidiPrinterAgent::parse_slot_response(const std::string& response_body, - nlohmann::json& status, - nlohmann::json& variables, - std::string& error) -{ - auto json = nlohmann::json::parse(response_body, nullptr, false, true); - if (json.is_discarded()) { - error = "Invalid JSON response"; - return false; - } - - if (!json.is_object() || !json.contains("result") || !json["result"].is_object() || !json["result"].contains("status") || - !json["result"]["status"].is_object() || !json["result"]["status"].contains("save_variables") || - !json["result"]["status"]["save_variables"].is_object() || !json["result"]["status"]["save_variables"].contains("variables") || - !json["result"]["status"]["save_variables"]["variables"].is_object()) { - // why: Qidi firmware may send null here, but json::value() throws for it. - error = "Unexpected JSON structure: save_variables.variables must be an object"; - return false; - } - - status = json["result"]["status"]; - variables = status["save_variables"]["variables"]; - return true; -} - bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url, const std::string& api_key, QidiFilamentDict& dict, diff --git a/src/slic3r/Utils/QidiPrinterAgent.hpp b/src/slic3r/Utils/QidiPrinterAgent.hpp index a74cf317d2..ce7b4cda0d 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.hpp +++ b/src/slic3r/Utils/QidiPrinterAgent.hpp @@ -1,9 +1,7 @@ #ifndef __QIDI_PRINTER_AGENT_HPP__ #define __QIDI_PRINTER_AGENT_HPP__ -#include "IPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp" -#include "nlohmann/json_fwd.hpp" #include #include @@ -23,23 +21,7 @@ public: // Override filament sync (Qidi-specific implementation) bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override; - static bool parse_slot_response(const std::string& response_body, - nlohmann::json& status, - nlohmann::json& variables, - std::string& error); - - // Print operations — emit QiDi multi-color box config, then delegate to base. - int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; - int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override; - int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; - int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override; - - FilamentSyncMode get_filament_sync_mode() const override; - private: - // Push enable_box + value_t SAVE_VARIABLEs before a print starts. - // Returns false if any command fails (caller should abort the print). - bool apply_box_mapping(const PrintParams& params) const; struct QidiFilamentDict { std::map colors; diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp index 2ae67f8140..6039eb260f 100644 --- a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp @@ -1,14 +1,10 @@ #include "SnapmakerPrinterAgent.hpp" #include "Http.hpp" -#include "IPrinterAgent.hpp" #include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "nlohmann/json.hpp" #include -#include -#include -#include using json = nlohmann::json; @@ -17,13 +13,6 @@ namespace Slic3r { namespace { constexpr const char* SNAPMAKER_AGENT_VERSION = "0.0.1"; -constexpr int64_t CAMERA_REFRESH_INTERVAL_MS = 300'000; - -int64_t now_ms() -{ - return std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count(); -} // Safely access a parallel array by index, returning a fallback if out of bounds. template @@ -80,31 +69,6 @@ std::string find_closest_color_preset_by_vendor_and_type(const PresetCollection& SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {} -void SnapmakerPrinterAgent::start_camera_monitor() -{ - std::thread([this] { - send_ws_rpc("camera.start_monitor", - {{"domain", "lan"}, {"interval", 0}, {"expect_pw", false}}); - }).detach(); - m_camera_last_fire_ms.store(now_ms()); -} - -void SnapmakerPrinterAgent::on_status_loop_tick(const std::string& dev_id) -{ - (void) dev_id; - const int64_t last = m_camera_last_fire_ms.load(); - if (last == 0 || now_ms() - last >= CAMERA_REFRESH_INTERVAL_MS) { - start_camera_monitor(); - } -} - -int SnapmakerPrinterAgent::command_start_camera(std::string dev_id) -{ - (void) dev_id; - start_camera_monitor(); - return BAMBU_NETWORK_SUCCESS; -} - AgentInfo SnapmakerPrinterAgent::get_agent_info_static() { return AgentInfo{"snapmaker", "Snapmaker", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"}; @@ -140,149 +104,127 @@ std::string SnapmakerPrinterAgent::combine_filament_type(const std::string& type return base; } -bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode) +bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/) { - (void) dev_id; - if (sync_mode != get_filament_sync_mode()) + std::string url = join_url(device_info.base_url, "/printer/objects/query?print_task_config&filament_detect"); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!device_info.api_key.empty()) { + http.header("X-Api-Key", device_info.api_key); + } + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: HTTP request failed: " << http_error; return false; + } - const std::string base_url = device_info.base_url; - const std::string api_key = device_info.api_key; + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Invalid JSON response"; + return false; + } - filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed); + // Navigate to result.status.print_task_config + if (!json.contains("result") || !json["result"].contains("status") || + !json["result"]["status"].contains("print_task_config")) { + BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Missing print_task_config in response"; + return false; + } - std::thread([this, base_url, api_key]() { - struct InFlightGuard - { - std::atomic& counter; - ~InFlightGuard() { counter.fetch_sub(1, std::memory_order_relaxed); } - } guard{filament_fetch_in_flight}; + auto& ptc = json["result"]["status"]["print_task_config"]; - const std::string url = join_url(base_url, "/printer/objects/query?print_task_config&filament_detect"); + // Read parallel arrays from print_task_config + auto filament_exist = ptc.value("filament_exist", std::vector{}); + auto filament_type = ptc.value("filament_type", std::vector{}); + auto filament_sub_type = ptc.value("filament_sub_type", std::vector{}); + auto filament_color = ptc.value("filament_color_rgba", std::vector{}); + auto filament_vendor = ptc.value("filament_vendor", std::vector{}); - std::string response_body; - bool success = false; - std::string http_error; + const int slot_count = static_cast(filament_exist.size()); + if (slot_count == 0) { + BOOST_LOG_TRIVIAL(info) << "SnapmakerPrinterAgent::fetch_filament_info: No filament slots reported"; + return false; + } - auto http = Http::get(url); - if (!api_key.empty()) { - http.header("X-Api-Key", api_key); - } - http.timeout_connect(5) - .timeout_max(10) - .on_complete([&](std::string body, unsigned status) { - if (status == 200) { - response_body = body; - success = true; + // Read NFC filament_detect data for temperature info (optional) + nlohmann::json nfc_info; + if (json["result"]["status"].contains("filament_detect") && + json["result"]["status"]["filament_detect"].contains("info")) { + nfc_info = json["result"]["status"]["filament_detect"]["info"]; + } + + static const std::string empty_str; + static const std::string default_color = "FFFFFFFF"; + + std::vector trays; + trays.reserve(slot_count); + + for (int i = 0; i < slot_count; ++i) { + AmsTrayData tray; + tray.slot_index = i; + tray.has_filament = filament_exist[i]; + + if (tray.has_filament) { + tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), + safe_at(filament_sub_type, i, empty_str)); + tray.tray_color = safe_at(filament_color, i, default_color); + + auto* bundle = GUI::wxGetApp().preset_bundle; + // Try to find a matching preset for this filament based on vendor, type and color. + // If not found, default to traditional search by type only or generic type mapping. + if (bundle) { + std::string vendor = safe_at(filament_vendor, i, empty_str); + std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendor, tray.tray_type, + tray.tray_color); + + if (!filament_id.empty()) { + tray.tray_info_idx = filament_id; + BOOST_LOG_TRIVIAL(warning) << "Filament sync: Found manufacturer-specific profile for slot " << i << ": " + << filament_id; } else { - http_error = "HTTP error: " + std::to_string(status); - } - }) - .on_error([&](std::string body, std::string err, unsigned status) { - http_error = err; - if (status > 0) { - http_error += " (HTTP " + std::to_string(status) + ")"; - } - }) - .perform_sync(); - - if (!success) { - BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: HTTP request failed: " << http_error; - return; - } - - auto json = nlohmann::json::parse(response_body, nullptr, false, true); - if (json.is_discarded()) { - BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Invalid JSON response"; - return; - } - - // Navigate to result.status.print_task_config - if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("print_task_config")) { - BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Missing print_task_config in response"; - return; - } - - auto& ptc = json["result"]["status"]["print_task_config"]; - - // Read parallel arrays from print_task_config - auto filament_exist = ptc.value("filament_exist", std::vector{}); - auto filament_type = ptc.value("filament_type", std::vector{}); - auto filament_sub_type = ptc.value("filament_sub_type", std::vector{}); - auto filament_color = ptc.value("filament_color_rgba", std::vector{}); - auto filament_vendor = ptc.value("filament_vendor", std::vector{}); - - const int slot_count = static_cast(filament_exist.size()); - if (slot_count == 0) { - BOOST_LOG_TRIVIAL(info) << "SnapmakerPrinterAgent::fetch_filament_info: No filament slots reported"; - return; - } - - // Read NFC filament_detect data for temperature info (optional) - nlohmann::json nfc_info; - if (json["result"]["status"].contains("filament_detect") && json["result"]["status"]["filament_detect"].contains("info")) { - nfc_info = json["result"]["status"]["filament_detect"]["info"]; - } - - static const std::string empty_str; - static const std::string default_color = "FFFFFFFF"; - - std::vector trays; - trays.reserve(slot_count); - - for (int i = 0; i < slot_count; ++i) { - AmsTrayData tray; - tray.slot_index = i; - tray.has_filament = filament_exist[i]; - - if (tray.has_filament) { - tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), safe_at(filament_sub_type, i, empty_str)); - tray.tray_color = safe_at(filament_color, i, default_color); - - auto* bundle = GUI::wxGetApp().preset_bundle; - // Try to find a matching preset for this filament based on vendor, type and color. - // If not found, default to traditional search by type only or generic type mapping. - if (bundle) { - std::string vendor = safe_at(filament_vendor, i, empty_str); - std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendor, tray.tray_type, - tray.tray_color); - - if (!filament_id.empty()) { - tray.tray_info_idx = filament_id; - BOOST_LOG_TRIVIAL(warning) - << "Filament sync: Found manufacturer-specific profile for slot " << i << ": " << filament_id; - } else { - tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); - } - } else { - tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); - } - - // Extract NFC temperature data if available - if (nfc_info.is_array() && i < static_cast(nfc_info.size()) && nfc_info[i].is_object()) { - auto& nfc_slot = nfc_info[i]; - std::string vendor = nfc_slot.value("VENDOR", "NONE"); - if (vendor != "NONE" && !vendor.empty()) { - tray.bed_temp = nfc_slot.value("BED_TEMP", 0); - tray.nozzle_temp = nfc_slot.value("FIRST_LAYER_TEMP", 0); - } + tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); } + } else { + tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); } - trays.emplace_back(std::move(tray)); + // Extract NFC temperature data if available + if (nfc_info.is_array() && i < static_cast(nfc_info.size()) && nfc_info[i].is_object()) { + auto& nfc_slot = nfc_info[i]; + std::string vendor = nfc_slot.value("VENDOR", "NONE"); + if (vendor != "NONE" && !vendor.empty()) { + tray.bed_temp = nfc_slot.value("BED_TEMP", 0); + tray.nozzle_temp = nfc_slot.value("FIRST_LAYER_TEMP", 0); + } + } } - build_ams_payload(1, slot_count - 1, trays); - }).detach(); + trays.emplace_back(std::move(tray)); + } + build_ams_payload(1, slot_count - 1, trays); return true; } -FilamentSyncMode SnapmakerPrinterAgent::get_filament_sync_mode() const -{ - if (GUI::wxGetApp().app_config->get_bool("use_printer_agents")) - return FilamentSyncMode::subscription; - return FilamentSyncMode::pull; -} - } // namespace Slic3r diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.hpp b/src/slic3r/Utils/SnapmakerPrinterAgent.hpp index db3ea4622c..d49fe21239 100644 --- a/src/slic3r/Utils/SnapmakerPrinterAgent.hpp +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.hpp @@ -1,10 +1,7 @@ #pragma once -#include "IPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp" -#include -#include #include namespace Slic3r { @@ -19,19 +16,10 @@ public: AgentInfo get_agent_info() override { return get_agent_info_static(); } bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override; - FilamentSyncMode get_filament_sync_mode() const override; - int command_start_camera(std::string dev_id) override; - CameraStreamMode get_camera_stream_mode() const override { return CameraStreamMode::http_snapshot; } - std::string get_camera_url() const override { return device_info.base_url + "/server/files/camera/monitor.jpg"; } private: // Combine filament_type + filament_sub_type into a unified type string static std::string combine_filament_type(const std::string& type, const std::string& sub_type); - - void start_camera_monitor(); - void on_status_loop_tick(const std::string& dev_id) override; - - std::atomic m_camera_last_fire_ms{0}; }; } // namespace Slic3r diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index f1c6431e96..fbf0eaa901 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -14,9 +14,6 @@ add_executable(${_TEST_NAME}_tests test_plugin_capabilities_in_use.cpp test_plugin_status.cpp test_printer_agent.cpp - test_qidi_printer_agent.cpp - test_orca_mqtt_connection.cpp - test_orca_printer_agent.cpp test_plugin_install.cpp test_plugin_lifecycle.cpp test_slicing_pipeline_bindings.cpp diff --git a/tests/slic3rutils/orca_mqtt_mock_broker.hpp b/tests/slic3rutils/orca_mqtt_mock_broker.hpp deleted file mode 100644 index 8545a4344f..0000000000 --- a/tests/slic3rutils/orca_mqtt_mock_broker.hpp +++ /dev/null @@ -1,317 +0,0 @@ -#pragma once - -// In-process plaintext MQTT-over-WebSocket broker for the OrcaMqtt tests. -// -// It speaks just enough of MQTT 3.1.1 to drive OrcaMqttConnection / -// OrcaPrinterAgent end to end without a real network: CONNECT/CONNACK, -// SUBSCRIBE/SUBACK, UNSUBSCRIBE/UNSUBACK, client PUBLISH (QoS 0), PINGREQ and -// DISCONNECT. The outbound PUBLISH frame is built with the production -// OrcaMqttConnection::make_publish_packet() so the tests never depend on a -// second, hand-rolled MQTT encoder. - -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace orca_mqtt_test { - -namespace net = boost::asio; -namespace beast = boost::beast; -namespace ws = boost::beast::websocket; -using tcp = boost::asio::ip::tcp; - -// Decode an MQTT remaining-length varint starting at packet[offset]. -// Returns {value, bytes_consumed}; bytes_consumed == 0 means malformed. -inline std::pair mqtt_decode_remaining_length(const std::string& packet, std::size_t offset) -{ - std::size_t value = 0; - std::size_t multiplier = 1; - std::size_t used = 0; - while (offset + used < packet.size() && used < 4) { - const std::uint8_t byte = static_cast(packet[offset + used]); - value += static_cast(byte & 0x7f) * multiplier; - multiplier *= 128; - ++used; - if ((byte & 0x80) == 0) - return {value, used}; - } - return {0, 0}; -} - -inline bool mqtt_topic_is_request(const std::string& topic) -{ - static const std::string suffix = "/request"; - return topic.size() >= suffix.size() && - topic.compare(topic.size() - suffix.size(), suffix.size(), suffix) == 0; -} - -class MockBroker -{ -public: - // refuse_auth: answer every CONNECT with CONNACK rc 5 (not authorized) and - // close, so the reconnect/refusal paths can be exercised. - explicit MockBroker(bool refuse_auth = false) : m_refuse_auth(refuse_auth), m_acceptor(m_io) - { - const tcp::endpoint endpoint(net::ip::make_address("127.0.0.1"), 0); - m_acceptor.open(endpoint.protocol()); - m_acceptor.set_option(net::socket_base::reuse_address(true)); - m_acceptor.bind(endpoint); - m_acceptor.listen(net::socket_base::max_listen_connections); - m_port = std::to_string(m_acceptor.local_endpoint().port()); - // why: a non-blocking acceptor lets the accept loop poll a stop flag, so - // the destructor never has to interrupt a blocking accept(). - m_acceptor.non_blocking(true); - m_thread = std::thread([this] { run(); }); - } - - ~MockBroker() - { - m_stopping.store(true); - drop_client(); // unblocks the worker's blocking read - if (m_thread.joinable()) - m_thread.join(); - boost::system::error_code ec; - m_acceptor.close(ec); // after join: the acceptor is worker-owned - m_io.stop(); - } - - MockBroker(const MockBroker&) = delete; - MockBroker& operator=(const MockBroker&) = delete; - - std::string ws_url() const { return "ws://127.0.0.1:" + m_port + "/mqtt"; } - - std::pair host_port() const { return {std::string("127.0.0.1"), m_port}; } - - // Server -> client PUBLISH on device//report. - void push_report(const std::string& dev_id, const std::string& payload) - { - const std::vector packet = - Slic3r::OrcaMqttConnection::make_publish_packet("device/" + dev_id + "/report", payload); - std::lock_guard lock(m_mutex); - if (!m_stream || !m_stream_ready) - return; - boost::system::error_code ec; - m_stream->binary(true); - m_stream->write(net::buffer(packet), ec); // a vanished client is not a test failure - } - - // Force-close the live client socket; the worker's read returns an error and - // the accept loop picks up the client's reconnect. - void drop_client() - { - std::lock_guard lock(m_mutex); - close_client_locked(); - } - - // Payloads the client PUBLISHed to any device//request topic. - std::vector received_requests() const - { - std::lock_guard lock(m_mutex); - return m_received_requests; - } - - // MQTT CONNECTs seen; increments again after a reconnect. - int connect_count() const { return m_connect_count.load(); } - -private: - void run() - { - try { - while (!m_stopping.load()) { - tcp::socket socket(m_io); - boost::system::error_code ec; - m_acceptor.accept(socket, ec); - if (ec == net::error::would_block || ec == net::error::try_again) { - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - continue; - } - if (ec) - return; - try { - serve(std::move(socket)); - } catch (...) { - // a client dying mid-session must not take the broker down - } - std::lock_guard lock(m_mutex); - close_client_locked(); - m_stream.reset(); - } - } catch (...) { - // never let an exception escape the broker thread - } - } - - void serve(tcp::socket socket) - { - ws::stream* stream = nullptr; - { - std::lock_guard lock(m_mutex); - m_stream.emplace(std::move(socket)); - m_stream_ready = false; - stream = &*m_stream; - } - // why: no io_context is ever run here, so a tcp_stream timer would never - // fire; the sync operations below carry no timeout of their own. - beast::get_lowest_layer(*stream).expires_never(); - stream->set_option(ws::stream_base::decorator( - [](ws::response_type& res) { res.set("Sec-WebSocket-Protocol", "mqtt"); })); - - boost::system::error_code ec; - stream->accept(ec); - if (ec) - return; - stream->binary(true); - { - std::lock_guard lock(m_mutex); - m_stream_ready = true; - } - read_loop(*stream); - } - - // The client sends every MQTT packet as one binary WebSocket message, so one - // read yields exactly one packet. - void read_loop(ws::stream& stream) - { - beast::flat_buffer buffer; - while (!m_stopping.load()) { - boost::system::error_code ec; - buffer.clear(); - stream.read(buffer, ec); - if (ec) - return; - const std::string packet = beast::buffers_to_string(buffer.data()); - if (packet.empty()) - continue; - if (!handle_packet(stream, packet)) - return; - } - } - - // Returns false when the session must be closed. - bool handle_packet(ws::stream& stream, const std::string& packet) - { - switch (static_cast(packet[0]) & 0xf0) { - case 0x10: { // CONNECT - ++m_connect_count; - if (m_refuse_auth) { - write_packet(stream, {0x20, 0x02, 0x00, 0x05}); // CONNACK not authorized - return false; - } - write_packet(stream, {0x20, 0x02, 0x00, 0x00}); // CONNACK accepted - return true; - } - case 0x80: { // SUBSCRIBE (0x82) - packet id follows the remaining-length varint - const auto id = packet_id(packet); - if (id) - write_packet(stream, {0x90, 0x03, id->first, id->second, 0x00}); // SUBACK, QoS 0 - return true; - } - case 0xa0: { // UNSUBSCRIBE (0xa2) - const auto id = packet_id(packet); - if (id) - write_packet(stream, {0xb0, 0x02, id->first, id->second}); // UNSUBACK - return true; - } - case 0x30: { // PUBLISH, QoS 0 (no packet identifier) - record_publish(packet); - return true; - } - case 0xc0: // PINGREQ - write_packet(stream, {0xd0, 0x00}); - return true; - case 0xe0: // DISCONNECT - return false; - default: - return true; - } - } - - // The two packet-identifier bytes sitting right after the remaining-length varint. - static std::optional> packet_id(const std::string& packet) - { - const auto varint = mqtt_decode_remaining_length(packet, 1); - if (varint.second == 0) - return std::nullopt; - const std::size_t pos = 1 + varint.second; - if (pos + 2 > packet.size()) - return std::nullopt; - return std::make_pair(static_cast(packet[pos]), static_cast(packet[pos + 1])); - } - - void record_publish(const std::string& packet) - { - const auto varint = mqtt_decode_remaining_length(packet, 1); - if (varint.second == 0) - return; - std::size_t pos = 1 + varint.second; - if (pos + 2 > packet.size()) - return; - const std::size_t topic_len = (static_cast(static_cast(packet[pos])) << 8) | - static_cast(packet[pos + 1]); - pos += 2; - if (pos + topic_len > packet.size()) - return; - const std::string topic = packet.substr(pos, topic_len); - pos += topic_len; - const std::size_t end = std::min(packet.size(), 1 + varint.second + varint.first); - if (end < pos) - return; - if (!mqtt_topic_is_request(topic)) - return; - std::lock_guard lock(m_mutex); - m_received_requests.push_back(packet.substr(pos, end - pos)); - } - - // Every write - the worker's own replies and push_report() from the test - // thread - is serialised by m_mutex. beast permits a writer while the worker - // is blocked in read(), which is the same arrangement OrcaMqttConnection uses. - void write_packet(ws::stream& stream, const std::vector& packet) - { - std::lock_guard lock(m_mutex); - boost::system::error_code ec; - stream.binary(true); - stream.write(net::buffer(packet), ec); - } - - void close_client_locked() - { - if (!m_stream) - return; - m_stream_ready = false; - boost::system::error_code ec; - auto& socket = beast::get_lowest_layer(*m_stream).socket(); - socket.cancel(ec); - // shutdown() before close() is what actually wakes a blocking read on the - // worker thread; close() alone does not on POSIX. - socket.shutdown(tcp::socket::shutdown_both, ec); - socket.close(ec); - } - - const bool m_refuse_auth; - net::io_context m_io; - tcp::acceptor m_acceptor; - std::string m_port; - std::thread m_thread; - std::atomic_bool m_stopping{false}; - std::atomic m_connect_count{0}; - mutable std::mutex m_mutex; - std::optional> m_stream; // guarded by m_mutex - bool m_stream_ready = false; // guarded by m_mutex - std::vector m_received_requests; // guarded by m_mutex -}; - -} // namespace orca_mqtt_test diff --git a/tests/slic3rutils/test_orca_mqtt_connection.cpp b/tests/slic3rutils/test_orca_mqtt_connection.cpp deleted file mode 100644 index 60c4e9303b..0000000000 --- a/tests/slic3rutils/test_orca_mqtt_connection.cpp +++ /dev/null @@ -1,229 +0,0 @@ -#include -#include - -#include "orca_mqtt_mock_broker.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -using Slic3r::OrcaMqttConnection; - -// Offset of the CONNECT variable header: 1 (fixed header) + N remaining-length varint bytes. -static size_t mqtt_varheader_offset(const std::vector& p) { - size_t i = 1; - while (i < p.size() && (p[i] & 0x80)) ++i; // skip varint continuation bytes - return i + 1; // + the final varint byte -} - -TEST_CASE("OrcaMqtt parse_endpoint handles ws and wss", "[OrcaMqtt]") { - OrcaMqttConnection::Endpoint ep; - - REQUIRE(OrcaMqttConnection::parse_endpoint("ws://printer.local:8280/mqtt", ep)); - CHECK(ep.host == "printer.local"); - CHECK(ep.port == "8280"); - CHECK(ep.target == "/mqtt"); - - REQUIRE(OrcaMqttConnection::parse_endpoint("ws://10.0.0.5/mqtt", ep)); - CHECK(ep.port == "80"); - - REQUIRE(OrcaMqttConnection::parse_endpoint("wss://api.example.com/api/v1/printers/abc/mqtt", ep)); - CHECK(ep.host == "api.example.com"); - CHECK(ep.port == "443"); - CHECK(ep.target == "/api/v1/printers/abc/mqtt"); - - CHECK_FALSE(OrcaMqttConnection::parse_endpoint("http://x/y", ep)); -} - -TEST_CASE("OrcaMqtt CONNECT packet - no auth (cloud form)", "[OrcaMqtt]") { - auto p = OrcaMqttConnection::make_connect_packet("OrcaSlicer", "", "", 300); - REQUIRE(p.size() >= 12); - CHECK(p[0] == 0x10); // CONNECT fixed header - const size_t v = mqtt_varheader_offset(p); - CHECK(p[v + 0] == 0x00); CHECK(p[v + 1] == 0x04); // protocol name length - CHECK(p[v + 2] == 'M'); CHECK(p[v + 3] == 'Q'); - CHECK(p[v + 4] == 'T'); CHECK(p[v + 5] == 'T'); - CHECK(p[v + 6] == 0x04); // protocol level 3.1.1 - CHECK(p[v + 7] == 0x02); // connect flags: clean session only - CHECK(((p[v + 8] << 8) | p[v + 9]) == 300); // keepalive -} - -TEST_CASE("OrcaMqtt CONNECT packet - username/password (LAN form)", "[OrcaMqtt]") { - auto p = OrcaMqttConnection::make_connect_packet("orcaslicer-lan-x", "orcasonar", "code123", 60); - CHECK(p[0] == 0x10); - const size_t v = mqtt_varheader_offset(p); - CHECK(p[v + 7] == (0x02 | 0x80 | 0x40)); // clean session + username + password flags - const std::string blob(p.begin(), p.end()); - CHECK(blob.find("orcaslicer-lan-x") != std::string::npos); - CHECK(blob.find("orcasonar") != std::string::npos); - CHECK(blob.find("code123") != std::string::npos); -} - -// Auth precedence (spec O3): when a bearer_provider is configured, connect_and_read -// passes empty CONNECT credentials, so the packet must carry clean-session only and -// no username/password flags or payload fields. (The precedence branch itself lives -// in connect_and_read; the [.integration] cloud-style round trip exercises it live.) -TEST_CASE("OrcaMqtt CONNECT omits creds when a bearer is configured", "[OrcaMqtt]") { - auto p = OrcaMqttConnection::make_connect_packet("cid", "", "", 60); - const size_t v = mqtt_varheader_offset(p); - CHECK(p[v + 7] == 0x02); // clean session only: no 0x80 / 0x40 - const std::string blob(p.begin(), p.end()); - CHECK(blob.find("orcasonar") == std::string::npos); -} - -TEST_CASE("OrcaMqtt topic helpers", "[OrcaMqtt]") { - CHECK(OrcaMqttConnection::request_topic("abc") == "device/abc/request"); - CHECK(OrcaMqttConnection::report_topic("abc") == "device/abc/report"); -} - -TEST_CASE("OrcaMqtt PUBLISH packet QoS0", "[OrcaMqtt]") { - auto p = OrcaMqttConnection::make_publish_packet("device/abc/request", "{\"ok\":1}"); - CHECK((p[0] & 0xf0) == 0x30); // PUBLISH - CHECK((p[0] & 0x06) == 0x00); // QoS 0 - const std::string blob(p.begin(), p.end()); - CHECK(blob.find("device/abc/request") != std::string::npos); - CHECK(blob.find("{\"ok\":1}") != std::string::npos); -} - -TEST_CASE("OrcaMqtt SUBSCRIBE packet", "[OrcaMqtt]") { - auto p = OrcaMqttConnection::make_subscribe_packet(7, "device/abc/report", 1); - CHECK(p[0] == 0x82); // SUBSCRIBE + reserved bit - const size_t v = mqtt_varheader_offset(p); - CHECK(((p[v] << 8) | p[v + 1]) == 7); // packet id - CHECK(p.back() == 1); // requested QoS -} - -TEST_CASE("OrcaMqtt send_request refuses when not connected", "[OrcaMqtt]") { - OrcaMqttConnection conn; - CHECK_FALSE(conn.send_request("abc", "{\"pushing\":{\"command\":\"pushall\",\"sequence_id\":\"20001\"}}")); -} - -TEST_CASE("OrcaMqtt start takes a Config", "[OrcaMqtt]") { - OrcaMqttConnection conn; - OrcaMqttConnection::Config cfg; - cfg.url = "ws://127.0.0.1:1/mqtt"; // nothing listening - cfg.keepalive_seconds = 42; - // start() returns false (no server) but must compile with the Config overload - const bool ok = conn.start(cfg, [](auto, auto){}, [](bool, bool){}); - CHECK_FALSE(ok); - CHECK(conn.last_connack_rc() == -1); - conn.stop(); -} - -TEST_CASE("MockBroker starts and reports a url", "[OrcaMqtt][.integration]") { - orca_mqtt_test::MockBroker b; - CHECK(b.ws_url().rfind("ws://127.0.0.1:", 0) == 0); - CHECK(b.connect_count() == 0); -} - -// --- End-to-end integration: OrcaMqttConnection against the in-process MockBroker. -// All hidden behind [.integration] (run explicitly). These prove a LAN-style config -// (CONNECT username/password) and a cloud-style config (bearer on the WS upgrade, -// no CONNECT creds) drive the *same* OrcaMqttConnection code path with identical -// assertions. - -static void run_round_trip(bool use_tls_flag_only) { - orca_mqtt_test::MockBroker broker; - OrcaMqttConnection conn; - OrcaMqttConnection::Config cfg; - cfg.url = broker.ws_url(); // plaintext regardless - cfg.use_tls = false; // the mock is plaintext; the flag path is unit-tested elsewhere - if (use_tls_flag_only) cfg.bearer_provider = []{ return std::string("tok"); }; - else { cfg.username = "orcasonar"; cfg.password = "code"; } - - // A mutex + condition_variable rather than a promise: the handler runs on the MQTT - // worker thread and a second inbound message would throw std::future_error there. - std::mutex got_mutex; - std::condition_variable got_cv; - bool got_any = false; - std::string got_id, got_payload; - - REQUIRE(conn.start(cfg, - [&](const std::string& id, const std::string& payload){ - { - std::lock_guard l(got_mutex); - if (got_any) return; // keep the first message only - got_any = true; got_id = id; got_payload = payload; - } - got_cv.notify_all(); - }, - [](bool,bool){})); - REQUIRE(conn.subscribe("dev-1")); - REQUIRE(conn.send_request("dev-1", R"({"pushing":{"command":"pushall","sequence_id":"20001"}})")); - - broker.push_report("dev-1", R"({"print":{"command":"push_status","sequence_id":"20001","result":"success"}})"); - std::string id, payload; - { - std::unique_lock l(got_mutex); - REQUIRE(got_cv.wait_for(l, std::chrono::seconds(3), [&]{ return got_any; })); - id = got_id; payload = got_payload; - } - CHECK(id == "dev-1"); - CHECK(payload.find("push_status") != std::string::npos); - - // the client's command reached the broker on the request topic. The mock records - // the PUBLISH on its own read-loop thread, so poll rather than check immediately. - std::vector reqs; - for (int i = 0; i < 200; ++i) { - reqs = broker.received_requests(); - if (!reqs.empty()) break; - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - REQUIRE(reqs.size() >= 1); - CHECK(reqs.front().find("pushall") != std::string::npos); - conn.stop(); -} - -TEST_CASE("OrcaMqtt round-trip — LAN-style config", "[OrcaMqtt][.integration]") { run_round_trip(false); } -TEST_CASE("OrcaMqtt round-trip — cloud-style config", "[OrcaMqtt][.integration]") { run_round_trip(true); } - -TEST_CASE("OrcaMqtt reconnects and re-subscribes after a socket drop", "[OrcaMqtt][.integration]") { - orca_mqtt_test::MockBroker broker; - OrcaMqttConnection conn; - OrcaMqttConnection::Config cfg; cfg.url = broker.ws_url(); cfg.use_tls = false; cfg.username = "u"; cfg.password = "p"; - - std::mutex m; std::vector got; - REQUIRE(conn.start(cfg, - [&](const std::string&, const std::string& p){ std::lock_guard l(m); got.push_back(p); }, - [](bool,bool){})); - REQUIRE(conn.subscribe("dev-1")); - - broker.drop_client(); - // the worker reconnects with ~1s backoff - for (int i = 0; i < 300 && broker.connect_count() < 2; ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - CHECK(broker.connect_count() >= 2); - - // a report after the reconnect must still be delivered -> the SUBSCRIBE was re-sent - broker.push_report("dev-1", R"({"print":{"command":"push_status","sequence_id":"20002"}})"); - bool delivered = false; - for (int i = 0; i < 200 && !delivered; ++i) { - { std::lock_guard l(m); delivered = !got.empty(); } - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - CHECK(delivered); - conn.stop(); -} - -TEST_CASE("OrcaMqtt auth rejection is terminal (no retry storm)", "[OrcaMqtt][.integration]") { - orca_mqtt_test::MockBroker broker(/*refuse_auth=*/true); - OrcaMqttConnection conn; - OrcaMqttConnection::Config cfg; cfg.url = broker.ws_url(); cfg.use_tls = false; cfg.username = "u"; cfg.password = "bad"; - - const bool ok = conn.start(cfg, [](const std::string&, const std::string&){}, [](bool,bool){}); - CHECK_FALSE(ok); - CHECK(conn.last_connack_rc() == 5); - // worker must have stopped itself (rc 5 is terminal) — give it a moment - for (int i = 0; i < 100 && conn.is_running(); ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - CHECK_FALSE(conn.is_running()); - // and it must NOT have hammered the broker with retries - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - CHECK(broker.connect_count() <= 2); - conn.stop(); -} diff --git a/tests/slic3rutils/test_orca_printer_agent.cpp b/tests/slic3rutils/test_orca_printer_agent.cpp deleted file mode 100644 index 7ffc4f3d2f..0000000000 --- a/tests/slic3rutils/test_orca_printer_agent.cpp +++ /dev/null @@ -1,204 +0,0 @@ -#include -#include -#include - -#include "orca_mqtt_mock_broker.hpp" - -#include -#include -#include -#include -#include -#include - -using Slic3r::OrcaPrinterAgent; - -namespace { -// Probe exposes the protected internals the tests drive. -struct Probe : OrcaPrinterAgent { - using OrcaPrinterAgent::OrcaPrinterAgent; - using OrcaPrinterAgent::deliver_to_sink; - using OrcaPrinterAgent::parse_lan_endpoint; - using OrcaPrinterAgent::make_lan_client_id; - using OrcaPrinterAgent::lan_connection_target; -}; -} - -TEST_CASE("OrcaPrinterAgent forwards a status payload to on_message_fn", "[OrcaPrinterAgent]") { - Probe agent("/tmp"); - std::string got_id, got_payload; - agent.set_on_message_fn([&](std::string id, std::string p){ got_id = std::move(id); got_payload = std::move(p); }); - agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status"}})", /*local=*/false); - CHECK(got_id == "dev-1"); - CHECK(got_payload.find("push_status") != std::string::npos); -} - -TEST_CASE("OrcaPrinterAgent stamps the get_capabilities nozzle diameter onto push_status frames", "[OrcaPrinterAgent]") { - Probe agent("/tmp"); - std::string last_payload; - agent.set_on_message_fn([&](std::string, std::string p){ last_payload = std::move(p); }); - - // Before any capabilities reply, a push_status frame is forwarded untouched. - agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","mc_percent":10}})", /*local=*/false); - CHECK(last_payload.find("nozzle_diameter") == std::string::npos); - - // The get_capabilities reply is forwarded verbatim; its topology nozzle diameter - // is cached for the device. - agent.deliver_to_sink( - "dev-1", - R"({"info":{"command":"get_capabilities","capabilities":{"topology":{"tools":[{"id":"T0","nozzle":{"diameter_mm":0.4}}]}}}})", - /*local=*/false); - CHECK(last_payload.find("\"command\":\"get_capabilities\"") != std::string::npos); - CHECK(last_payload.find("\"print\"") == std::string::npos); - - // Later push_status frames for that device get the cached diameter plus a neutral - // nozzle_type, so MachineObject::parse_json's legacy nozzle parser can run. - agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","mc_percent":20}})", /*local=*/false); - CHECK(last_payload.find("\"nozzle_diameter\":0.4") != std::string::npos); - CHECK(last_payload.find("\"nozzle_type\":\"N/A\"") != std::string::npos); - - // A different device is unaffected. - agent.deliver_to_sink("dev-2", R"({"print":{"command":"push_status"}})", /*local=*/false); - CHECK(last_payload.find("nozzle_diameter") == std::string::npos); - - // A frame that already carries real nozzle data is not overridden. - agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","nozzle_diameter":0.6}})", /*local=*/false); - CHECK(last_payload.find("\"nozzle_diameter\":0.6") != std::string::npos); - CHECK(last_payload.find("N/A") == std::string::npos); -} - -TEST_CASE("OrcaPrinterAgent::parse_lan_endpoint", "[OrcaPrinterAgent]") { - std::string h, p; - REQUIRE(Probe::parse_lan_endpoint("192.168.1.9", h, p)); - CHECK(h == "192.168.1.9"); CHECK(p == "8280"); - REQUIRE(Probe::parse_lan_endpoint("http://host.local:9000/x", h, p)); - CHECK(h == "host.local"); CHECK(p == "9000"); - CHECK_FALSE(Probe::parse_lan_endpoint("", h, p)); -} - -TEST_CASE("OrcaPrinterAgent::make_lan_client_id is stable and prefixed", "[OrcaPrinterAgent]") { - const auto a = Probe::make_lan_client_id("dev-1"); - const auto b = Probe::make_lan_client_id("dev-1"); - CHECK(a == b); // drawn once per process - CHECK(a.rfind("orcaslicer-lan-dev-1-", 0) == 0); -} - -TEST_CASE("connect_printer wires up a LAN Config", "[OrcaPrinterAgent][.integration]") { - Probe agent("/tmp"); - const int rc = agent.connect_printer("dev-1", "10.255.255.1", "orcasonar", "code", false); - CHECK(rc == BAMBU_NETWORK_SUCCESS); - CHECK(agent.lan_connection_target() == "ws://10.255.255.1:8280/mqtt"); - CHECK(agent.get_user_selected_machine().empty()); // LAN path must not touch the cloud selection - agent.disconnect_printer(); -} - -TEST_CASE("post-connect sequence is subscribe then 4 requests in order", "[OrcaPrinterAgent]") { - struct SeqProbe : OrcaPrinterAgent { - using OrcaPrinterAgent::OrcaPrinterAgent; - std::vector calls; - void emit_connect_sequence(const std::string& dev_id, - std::function /*sub*/, - std::function /*req*/) override { - OrcaPrinterAgent::emit_connect_sequence(dev_id, - [&](const std::string& id){ calls.push_back("sub:" + id); }, - [&](const std::string& body){ calls.push_back(body); }); - } - } probe("/tmp"); - probe.run_connect_sequence_for_test("dev-1"); - REQUIRE(probe.calls.size() == 5); - CHECK(probe.calls[0] == "sub:dev-1"); - CHECK(probe.calls[1].find("\"pushing\"") != std::string::npos); - CHECK(probe.calls[1].find("\"start\"") != std::string::npos); - CHECK(probe.calls[2].find("pushall") != std::string::npos); - CHECK(probe.calls[3].find("get_version") != std::string::npos); - CHECK(probe.calls[4].find("get_capabilities") != std::string::npos); - for (auto& c : probe.calls) - if (auto pos = c.find("sequence_id"); pos != std::string::npos) - CHECK(c.substr(pos).find("\"2") != std::string::npos); -} - -// Hidden: spawns the connect worker and attempts a real (failing) connect. -TEST_CASE("selecting a cloud printer configures the fleet socket", "[OrcaPrinterAgent][.integration]") { - auto cloud = std::make_shared("/tmp"); - cloud->set_api_base_url("api.example.com"); - OrcaPrinterAgent agent("/tmp"); - agent.set_cloud_agent(cloud); - - agent.set_user_selected_machine("printer-uuid-1"); - // The configure runs on the connect worker; poll rather than racing it. - std::string url; - for (int i = 0; i < 300; ++i) { - url = cloud->selected_printer_mqtt_url(); - if (!url.empty()) break; - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - CHECK(url == "wss://api.example.com/api/v1/printers/mqtt"); - - agent.set_user_selected_machine(""); // selection changes do not tear down the fleet socket - CHECK(cloud->selected_printer_mqtt_url() == "wss://api.example.com/api/v1/printers/mqtt"); -} - -TEST_CASE("a stale-generation inbound message is dropped", "[OrcaPrinterAgent]") { - struct GenProbe : OrcaPrinterAgent { - using OrcaPrinterAgent::OrcaPrinterAgent; - using OrcaPrinterAgent::make_lan_message_handler; // expose for the test - }; - GenProbe agent("/tmp"); - int hits = 0; - agent.set_on_message_fn([&](std::string, std::string){ ++hits; }); - auto handler_gen1 = agent.make_lan_message_handler(/*generation=*/1); - // m_lan_generation starts at 0; two bumps -> 2, so the epoch-1 handler is stale. - agent.bump_lan_generation_for_test(); - agent.bump_lan_generation_for_test(); - handler_gen1("dev-1", "{}"); // late callback from gen 1 - CHECK(hits == 0); -} - -TEST_CASE("connect_server does not start an MQTT socket", "[OrcaCloud]") { - auto cloud = std::make_shared("/tmp"); - cloud->set_api_base_url("127.0.0.1:1"); // no session -> connect_server short-circuits before any probe - cloud->connect_server(); - REQUIRE(cloud->get_mqtt_connection() != nullptr); // created in the ctor - CHECK_FALSE(cloud->get_mqtt_connection()->is_running()); // never started - CHECK(cloud->selected_printer_mqtt_url().empty()); -} - -TEST_CASE("send_message* reject when there is no connection", "[OrcaPrinterAgent]") { - OrcaPrinterAgent agent("/tmp"); // no cloud agent, no LAN connection - CHECK(agent.send_message("d", "{}", 0, 0) == BAMBU_NETWORK_ERR_INVALID_HANDLE); - CHECK(agent.send_message_to_printer("d", "{}", 0, 0) == BAMBU_NETWORK_ERR_INVALID_HANDLE); - CHECK(agent.send_message("", "{}", 0, 0) == BAMBU_NETWORK_ERR_INVALID_HANDLE); // empty dev_id -} - -TEST_CASE("send_message_to_printer publishes on the LAN connection", "[OrcaPrinterAgent][.integration]") { - orca_mqtt_test::MockBroker broker; - OrcaPrinterAgent agent("/tmp"); - const auto ep = broker.host_port(); - agent.connect_printer("dev-1", ep.first + ":" + ep.second, "orcasonar", "code", false); - - for (int i = 0; i < 150 && broker.connect_count() == 0; ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - REQUIRE(broker.connect_count() >= 1); - - CHECK(agent.send_message_to_printer("dev-1", R"({"print":{"command":"pause","sequence_id":"20007"}})", 0, 0) - == BAMBU_NETWORK_SUCCESS); - - // on_connected also publishes 4 requests; poll until "pause" specifically shows up. - bool saw_pause = false; - for (int i = 0; i < 150 && !saw_pause; ++i) { - for (const auto& r : broker.received_requests()) - if (r.find("pause") != std::string::npos) { saw_pause = true; break; } - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - } - CHECK(saw_pause); - agent.disconnect_printer(); -} - -TEST_CASE("destroying an agent mid-connect does not hang or crash", "[OrcaPrinterAgent]") { - for (int i = 0; i < 20; ++i) { - auto agent = std::make_unique("/tmp"); - agent->connect_printer("dev-1", "127.0.0.1:1", "orcasonar", "code", false); // nothing listening: instant ECONNREFUSED - agent.reset(); // ~OrcaPrinterAgent must stop the conn, join the thread, and not hang/crash - } - SUCCEED(); -} diff --git a/tests/slic3rutils/test_printer_agent.cpp b/tests/slic3rutils/test_printer_agent.cpp index 7a2fb24e7b..55ba57b253 100644 --- a/tests/slic3rutils/test_printer_agent.cpp +++ b/tests/slic3rutils/test_printer_agent.cpp @@ -1,7 +1,5 @@ #include -#include -#include #include #include @@ -10,164 +8,11 @@ #include #include -#include -#include -#include -#include #include -#include using namespace Slic3r; namespace py = pybind11; -class MoonrakerParserProbe : public MoonrakerPrinterAgent -{ -public: - using MoonrakerPrinterAgent::parse_nozzle_diameter; - - explicit MoonrakerParserProbe(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {} -}; - -TEST_CASE("Moonraker parses nozzle diameter from configfile settings", "[unit][moonraker]") -{ - const auto response = nlohmann::json::parse(R"({ - "result": { - "status": { - "configfile": { - "settings": { - "extruder": { - "nozzle_diameter": 0.6 - } - } - } - } - } - })"); - - CHECK(MoonrakerParserProbe::parse_nozzle_diameter(response) == Catch::Approx(0.6f)); -} - -TEST_CASE("Moonraker parses nozzle diameter from raw config and tolerates missing data", "[unit][moonraker]") -{ - const auto raw_config_response = nlohmann::json::parse(R"({ - "result": { - "status": { - "configfile": { - "config": { - "extruder": { - "nozzle_diameter": "0.8" - } - } - } - } - } - })"); - const auto missing_response = nlohmann::json::object(); - - CHECK(MoonrakerParserProbe::parse_nozzle_diameter(raw_config_response) == Catch::Approx(0.8f)); - CHECK(MoonrakerParserProbe::parse_nozzle_diameter(missing_response) == 0.0f); -} - -// why: these builders preserve the Bambu firmware dialect byte-for-byte, including its trailing space. -TEST_CASE("unit: BBL AMS gcode builders preserve command bytes", "[unit][bbl]") -{ - CHECK(BBLPrinterAgent::ams_refresh_rfid_gcode("123") == "M620 R123 \n"); - CHECK(BBLPrinterAgent::ams_calibrate_gcode(123) == "M620 C123 \n"); - CHECK(BBLPrinterAgent::ams_select_tray_gcode("123") == "M620 P123 \n"); -} - -// why: an agent without a Bambu-dialect translation must refuse these commands before any network or wx path. -TEST_CASE("unit: default AMS commands report not supported", "[unit][moonraker]") -{ - MoonrakerPrinterAgent agent(""); - - CHECK(agent.command_ams_refresh_rfid("dev", "123", 1, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); - CHECK(agent.command_ams_calibrate("dev", 1, 2, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); - CHECK(agent.command_ams_select_tray("dev", "123", 3, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); -} - -TEST_CASE("unit: Moonraker light name matching", "[unit][moonraker]") -{ - CHECK(moonraker_is_light_name("caselight")); - CHECK(moonraker_is_light_name("LED_STRIP")); - CHECK_FALSE(moonraker_is_light_name("beeper")); - CHECK(moonraker_is_light_name("FLASHLIGHT_SWITCH")); - CHECK(moonraker_is_light_name("MODLELIGHT_SWITCH")); -} - -// =========================================================================== -// UNIT - handle_request's not-supported default. -// The agent is the only thing that knows what it can translate, so an untranslated -// command has to say so instead of returning success and letting the UI believe the -// control worked. Guards the inverse too: the pushing namespace is genuinely -// satisfied by the websocket status stream, and it re-fires from the keepalive timer -// roughly once a second, so it must stay a success or it would raise a dialog on a -// timer. Only branches that touch neither the network nor wx are exercised. -// =========================================================================== -TEST_CASE("unit: Moonraker reports untranslated commands as not supported", "[unit][moonraker]") -{ - MoonrakerPrinterAgent agent(""); - - CHECK(agent.send_message("dev", R"({"print":{"command":"ams_change_filament"}})", 0, 0) == - ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); - CHECK(agent.send_message("dev", R"({"system":{"command":"set_door_stat"}})", 0, 0) == - ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); - CHECK(agent.send_message("dev", R"({"xcam":{"command":"xcam_control_set"}})", 0, 0) == - ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); - - CHECK(agent.send_message("dev", R"({"pushing":{"command":"pushall"}})", 0, 0) == BAMBU_NETWORK_SUCCESS); - CHECK(agent.send_message("dev", R"({"pushing":{"command":"start"}})", 0, 0) == BAMBU_NETWORK_SUCCESS); - - // why: malformed input is a different failure than an untranslated command, and the - // default must not swallow it into a misleading not-supported verdict. - CHECK(agent.send_message("dev", "{not json", 0, 0) == BAMBU_NETWORK_ERR_INVALID_RESULT); -} - -// why: IPrinterAgent::fetch_filament_info is the single virtual hook derived agents override -// (MoonrakerPrinterAgent's own override is synchronous, but QidiPrinterAgent's override is -// fire-and-forget: it spawns a detached thread and returns immediately). QidiPrinterAgent is -// `final`, so this probes the same contract with a controllable double instead. -TEST_CASE("unit: a fire-and-forget override of fetch_filament_info is not waited on by the caller", - "[unit][moonraker]") -{ - class RecordingAgent : public Slic3r::MoonrakerPrinterAgent - { - public: - explicit RecordingAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {} - - std::atomic invoked{false}; - std::promise release_gate; - std::promise done_promise; - - bool fetch_filament_info(std::string /*dev_id*/, FilamentSyncMode /*sync_mode*/ = FilamentSyncMode::pull) override - { - std::thread([this]() { - invoked.store(true); - // Block here until the test explicitly releases us, proving the caller - // (fetch_filament_info) does not wait for this to run. - release_gate.get_future().wait(); - done_promise.set_value(); - }).detach(); - return true; - } - }; - - auto agent = std::make_shared(std::string{}); - auto done_future = agent->done_promise.get_future(); - - bool immediate_result = agent->fetch_filament_info("test-dev"); - - // fetch_filament_info must return before its background work completes — prove - // it by confirming the background call is still blocked on the gate right now. - REQUIRE(immediate_result == true); - REQUIRE(done_future.wait_for(std::chrono::milliseconds(100)) == std::future_status::timeout); - - // Now let the background call finish and confirm it actually ran (polymorphic dispatch). - agent->release_gate.set_value(); - REQUIRE(done_future.wait_for(std::chrono::seconds(2)) == std::future_status::ready); - REQUIRE(agent->invoked.load() == true); -} - // =========================================================================== // UNIT - printer-agent registry duplicate handling. // Confirms a duplicate agent id is rejected so a plugin cannot shadow a built-in diff --git a/tests/slic3rutils/test_qidi_printer_agent.cpp b/tests/slic3rutils/test_qidi_printer_agent.cpp deleted file mode 100644 index 2d3307381a..0000000000 --- a/tests/slic3rutils/test_qidi_printer_agent.cpp +++ /dev/null @@ -1,131 +0,0 @@ -#include - -#include - -#include - -#include - -using namespace Slic3r; - -TEST_CASE("Qidi slot response rejects null variables without throwing", "[QidiPrinterAgent]") -{ - const std::string response = R"({ - "result": { - "status": { - "save_variables": { - "variables": null - } - } - } - })"; - nlohmann::json status; - nlohmann::json variables; - std::string error; - bool parsed = true; - - REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response(response, status, variables, error)); - CHECK_FALSE(parsed); - CHECK_THAT(error, Catch::Matchers::ContainsSubstring("variables")); - CHECK_THAT(error, Catch::Matchers::ContainsSubstring("object")); -} - -TEST_CASE("Qidi slot response rejects missing and non-object fields without throwing", "[QidiPrinterAgent]") -{ - std::string response; - - SECTION("missing result") - { - response = R"({})"; - } - - SECTION("non-object result") - { - response = R"({"result":null})"; - } - - SECTION("missing status") - { - response = R"({"result":{}})"; - } - - SECTION("non-object status") - { - response = R"({"result":{"status":null}})"; - } - - SECTION("missing save_variables") - { - response = R"({"result":{"status":{}}})"; - } - - SECTION("non-object save_variables") - { - response = R"({"result":{"status":{"save_variables":null}}})"; - } - - SECTION("missing variables") - { - response = R"({"result":{"status":{"save_variables":{}}}})"; - } - - SECTION("scalar") - { - response = R"({"result":{"status":{"save_variables":{"variables":42}}}})"; - } - - SECTION("array") - { - response = R"({"result":{"status":{"save_variables":{"variables":[]}}}})"; - } - - nlohmann::json status; - nlohmann::json variables; - std::string error; - bool parsed = true; - - REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response(response, status, variables, error)); - CHECK_FALSE(parsed); -} - -TEST_CASE("Qidi slot response exposes valid status and variables", "[QidiPrinterAgent]") -{ - const std::string response = R"({ - "result": { - "status": { - "save_variables": { - "variables": { - "box_count": 2, - "color_slot0": 3 - } - }, - "box_stepper slot0": { - "runout_button": 0 - } - } - } - })"; - nlohmann::json status; - nlohmann::json variables; - std::string error; - bool parsed = false; - - REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response(response, status, variables, error)); - REQUIRE(parsed); - CHECK(status.is_object()); - CHECK(variables.is_object()); - CHECK(variables.at("box_count") == 2); - CHECK(status.contains("box_stepper slot0")); -} - -TEST_CASE("Qidi slot response rejects invalid JSON", "[QidiPrinterAgent]") -{ - nlohmann::json status; - nlohmann::json variables; - std::string error; - bool parsed = true; - - REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response("{not json", status, variables, error)); - CHECK_FALSE(parsed); - CHECK(error == "Invalid JSON response"); -}