diff --git a/src/slic3r/Utils/CrealityPrintAgent.cpp b/src/slic3r/Utils/CrealityPrintAgent.cpp index a86ba559af..53e496f541 100644 --- a/src/slic3r/Utils/CrealityPrintAgent.cpp +++ b/src/slic3r/Utils/CrealityPrintAgent.cpp @@ -294,7 +294,9 @@ bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMod if (sync_mode != get_filament_sync_mode()) return false; - if (device_info.dev_ip.empty()) { + const MoonrakerDeviceInfo info = snapshot_device_info(); + + if (info.dev_ip.empty()) { BOOST_LOG_TRIVIAL(warning) << "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent"; return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id)); @@ -303,11 +305,11 @@ bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMod // Build a CrealityPrint helper so we can use its model detection + WS helpers // (added in upstream PR #13291). DynamicPrintConfig cfg; - cfg.set_key_value("print_host", new ConfigOptionString("http://" + device_info.dev_ip)); + cfg.set_key_value("print_host", new ConfigOptionString("http://" + info.dev_ip)); cfg.set_key_value("print_host_webui", new ConfigOptionString("")); cfg.set_key_value("printhost_cafile", new ConfigOptionString("")); cfg.set_key_value("printhost_port", new ConfigOptionString("")); - cfg.set_key_value("printhost_apikey", new ConfigOptionString(device_info.api_key)); + cfg.set_key_value("printhost_apikey", new ConfigOptionString(info.api_key)); cfg.set_key_value("printhost_ssl_ignore_revoke", new ConfigOptionBool(false)); CrealityPrint host(&cfg); diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 32696654cb..398369ffef 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -343,8 +343,9 @@ void MoonrakerPrinterAgent::shutdown() cmd_thread.join(); } - while (filament_fetch_in_flight.load() > 0) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + { + std::unique_lock lock(fetch_lifecycle_mutex); + fetch_done_cv.wait(lock, [this] { return filament_fetch_in_flight.load() == 0; }); } // Fetches read device_info without the lock; clear it once none can run. @@ -766,8 +767,9 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, queue_fn = queue_on_main_fn; } - std::string dev_id = device_info.dev_id; - std::string model_id = device_info.model_id; + const MoonrakerDeviceInfo info = snapshot_device_info(); + std::string dev_id = info.dev_id; + std::string model_id = info.model_id; auto apply = [dev_id, model_id, ams_count, max_lane_index, trays]() { // Look up MachineObject via DeviceManager @@ -1641,6 +1643,21 @@ MoonrakerPrinterAgent::ConnectionSettings MoonrakerPrinterAgent::get_connection_ return connection; } +MoonrakerPrinterAgent::MoonrakerDeviceInfo MoonrakerPrinterAgent::snapshot_device_info() const +{ + std::lock_guard lock(connect_mutex); + return device_info; +} + +void MoonrakerPrinterAgent::release_fetch_slot() noexcept +{ + { + std::lock_guard lock(fetch_lifecycle_mutex); + filament_fetch_in_flight.fetch_sub(1, std::memory_order_relaxed); + } + fetch_done_cv.notify_all(); +} + void MoonrakerPrinterAgent::configure_http(Http& http, const ConnectionSettings& connection) const { http.tls_verify(connection.use_ssl); @@ -1924,6 +1941,11 @@ bool MoonrakerPrinterAgent::fetch_webcam_info(const ConnectionSettings& connecti for (const auto& webcam : result["webcams"]) { if (webcam.is_object()) { + // /server/webcams/list returns disabled webcams too; skip them. + if (webcam.contains("enabled") && webcam["enabled"].is_boolean() && + !webcam["enabled"].get()) { + continue; + } if (webcam.contains("stream_url") && webcam["stream_url"].is_string() && !webcam["stream_url"].get().empty()) { camera_url = webcam["stream_url"].get(); @@ -1948,7 +1970,9 @@ bool MoonrakerPrinterAgent::fetch_webcam_info(const ConnectionSettings& connecti } if (error.empty()) { - if (camera_url.rfind("http", 0) != 0 && !camera_url.empty() && camera_url.front() == '/') { + if (camera_url.rfind("rtsp://", 0) == 0 || camera_url.rfind("rtsps://", 0) == 0) { + stream_mode = CameraStreamMode::rtsp; + } else 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 = connection.base_url.find("://"); const size_t authority_start = scheme_end == std::string::npos ? 0 : scheme_end + 3; @@ -2202,7 +2226,7 @@ int MoonrakerPrinterAgent::send_version_info(const std::string& dev_id) nlohmann::json module; module["name"] = "ota"; - module["sw_ver"] = device_info.version; + module["sw_ver"] = snapshot_device_info().version; module["product_name"] = "Moonraker"; payload["info"]["module"].push_back(module); @@ -2214,7 +2238,7 @@ int MoonrakerPrinterAgent::send_access_code(const std::string& dev_id) { nlohmann::json payload; payload["system"]["command"] = "get_access_code"; - payload["system"]["access_code"] = device_info.api_key; + payload["system"]["access_code"] = snapshot_device_info().api_key; dispatch_message(dev_id, payload.dump()); return BAMBU_NETWORK_SUCCESS; } @@ -2336,13 +2360,14 @@ 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. + // Wake a blocked synchronous ws.connect()/tls_handshake()/read()/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(); } } + ws_wait_cv.notify_all(); if (ws_thread.joinable()) { ws_thread.join(); } @@ -2366,15 +2391,14 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, ConnectionSett try { MoonrakerWebsocket ws{endpoint.secure, connection.api_key, connection.ca_file}; - ws.connect(endpoint.host, endpoint.port, std::chrono::seconds(10)); - ws.tls_handshake(endpoint.host); - // 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`. + // Allow stop_status_stream() to force this socket shut so a blocked synchronous + // ws.connect()/tls_handshake()/read()/write() returns with an error (Beast's + // expires_after() does not bound synchronous operations). Registered before + // connect() so a stalled handshake is interruptible too; 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; @@ -2386,6 +2410,9 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, ConnectionSett }; } + ws.connect(endpoint.host, endpoint.port, std::chrono::seconds(10)); + ws.tls_handshake(endpoint.host); + std::string host_header = endpoint.host; if (!endpoint.port.empty() && endpoint.port != (endpoint.secure ? "7130" : "7125")) { host_header += ":" + endpoint.port; @@ -2536,9 +2563,13 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, ConnectionSett } // Exponential backoff before reconnection - int delay_ms = base_delay_ms * (1 << std::min(retry_count, 5)); + const int delay_ms = base_delay_ms * (1 << std::min(retry_count, 5)); BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Reconnecting in " << delay_ms << "ms (attempt " << (retry_count + 1) << ")"; - std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms)); + { + // Interruptible so stop_status_stream() doesn't have to wait out a backoff of up to 32 s. + std::unique_lock wait_lock(ws_wait_mutex); + ws_wait_cv.wait_for(wait_lock, std::chrono::milliseconds(delay_ms), [this] { return ws_stop.load(); }); + } retry_count++; } @@ -2831,8 +2862,9 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const // 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; + const float nozzle_diameter = snapshot_device_info().nozzle_diameter; + if (nozzle_diameter > 0.0f) { + payload["print"]["nozzle_diameter"] = nozzle_diameter; payload["print"]["nozzle_type"] = "N/A"; } diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index 860e524261..f06e268a49 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -109,6 +109,10 @@ public: CameraStreamMode get_camera_stream_mode() const override; std::string get_camera_url() const override; + // Called by the fetch-thread RAII guard when a background filament fetch finishes. + // Serialized with the reservation so shutdown()'s wait cannot miss the transition to 0. + void release_fetch_slot() noexcept; + protected: struct ConnectionSettings { @@ -153,6 +157,9 @@ protected: virtual bool init_device_info(const PrinterConnectionParams& params); virtual bool fetch_device_info(const ConnectionSettings& connection, MoonrakerDeviceInfo& info, std::string& error) const; ConnectionSettings get_connection_settings() const; + // Copy of the mutable connection state, taken under connect_mutex. Background threads + // must use this instead of reading device_info directly. + MoonrakerDeviceInfo snapshot_device_info() const; void configure_http(Http& http, const ConnectionSettings& connection) const; static float parse_nozzle_diameter(const nlohmann::json& response); @@ -168,6 +175,7 @@ protected: // Serializes the shutting_down check with the in-flight reservation. std::mutex fetch_lifecycle_mutex; + std::condition_variable fetch_done_cv; // notified when filament_fetch_in_flight reaches 0 // Helpers bool is_numeric(const std::string& value); @@ -316,6 +324,10 @@ private: std::mutex ws_abort_mutex; std::function ws_abort_io; // guarded by ws_abort_mutex + // Interrupts the exponential reconnect backoff in run_status_stream() when stopping. + std::mutex ws_wait_mutex; + std::condition_variable ws_wait_cv; + // 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; diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index f13d52ed18..ec75bcc773 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -41,16 +41,16 @@ bool has_visible_base_preset(const PresetCollection& filaments, const std::strin return false; } -// RAII decrement for the in-flight fetch count; movable so a failed thread start still releases it. +// RAII release of the in-flight fetch slot; movable so a failed thread start still releases it. struct InFlightGuard { - std::atomic* counter; - explicit InFlightGuard(std::atomic& c) noexcept : counter(&c) {} - InFlightGuard(InFlightGuard&& other) noexcept : counter(other.counter) { other.counter = nullptr; } + MoonrakerPrinterAgent* owner; + explicit InFlightGuard(MoonrakerPrinterAgent& o) noexcept : owner(&o) {} + InFlightGuard(InFlightGuard&& other) noexcept : owner(other.owner) { other.owner = nullptr; } InFlightGuard(const InFlightGuard&) = delete; InFlightGuard& operator=(const InFlightGuard&) = delete; InFlightGuard& operator=(InFlightGuard&&) = delete; - ~InFlightGuard() { if (counter) counter->fetch_sub(1, std::memory_order_relaxed); } + ~InFlightGuard() { if (owner) owner->release_fetch_slot(); } }; // nlohmann::json::value() returns the default only when the key is absent; a present but @@ -104,7 +104,7 @@ bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed); } - InFlightGuard guard{filament_fetch_in_flight}; + InFlightGuard guard{*this}; std::thread([this, guard = std::move(guard), connection = std::move(connection), model_id, model_name]() mutable { try { std::string error; @@ -155,8 +155,9 @@ bool QidiPrinterAgent::apply_box_mapping(const PrintParams& params) const // 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))) { + const int enable = params.task_use_ams ? 1 : 0; + const std::string dev_id = get_connection_settings().dev_id; + if (!send_gcode(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; } @@ -188,7 +189,7 @@ bool QidiPrinterAgent::apply_box_mapping(const PrintParams& params) const 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)) { + if (!send_gcode(dev_id, gcode)) { BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to set value_t" << tool; return false; } diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp index 3db2520194..bb1c7b8fdd 100644 --- a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp @@ -35,16 +35,16 @@ int64_t now_ms() std::chrono::steady_clock::now().time_since_epoch()).count(); } -// RAII decrement for the in-flight fetch count; movable so a failed thread start still releases it. +// RAII release of the in-flight fetch slot; movable so a failed thread start still releases it. struct InFlightGuard { - std::atomic* counter; - explicit InFlightGuard(std::atomic& c) noexcept : counter(&c) {} - InFlightGuard(InFlightGuard&& other) noexcept : counter(other.counter) { other.counter = nullptr; } + MoonrakerPrinterAgent* owner; + explicit InFlightGuard(MoonrakerPrinterAgent& o) noexcept : owner(&o) {} + InFlightGuard(InFlightGuard&& other) noexcept : owner(other.owner) { other.owner = nullptr; } InFlightGuard(const InFlightGuard&) = delete; InFlightGuard& operator=(const InFlightGuard&) = delete; InFlightGuard& operator=(InFlightGuard&&) = delete; - ~InFlightGuard() { if (counter) counter->fetch_sub(1, std::memory_order_relaxed); } + ~InFlightGuard() { if (owner) owner->release_fetch_slot(); } }; // nlohmann::json::value() returns the default only when the key is absent; a present but @@ -234,7 +234,7 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed); } - InFlightGuard guard{filament_fetch_in_flight}; + InFlightGuard guard{*this}; std::thread([this, guard = std::move(guard), connection]() { try { const std::string url = join_url(connection.base_url, "/printer/objects/query?print_task_config&filament_detect"); @@ -368,6 +368,11 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync return true; } +std::string SnapmakerPrinterAgent::get_camera_url() const +{ + return get_connection_settings().base_url + "/server/files/camera/monitor.jpg"; +} + FilamentSyncMode SnapmakerPrinterAgent::get_filament_sync_mode() const { if (GUI::wxGetApp().app_config->get_bool("use_printer_agents")) diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.hpp b/src/slic3r/Utils/SnapmakerPrinterAgent.hpp index 8a40434424..34ed2e9ac9 100644 --- a/src/slic3r/Utils/SnapmakerPrinterAgent.hpp +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.hpp @@ -22,7 +22,7 @@ public: 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"; } + std::string get_camera_url() const override; private: // Combine filament_type + filament_sub_type into a unified type string diff --git a/tests/slic3rutils/test_printer_agent.cpp b/tests/slic3rutils/test_printer_agent.cpp index f221ea391e..c19e6caf63 100644 --- a/tests/slic3rutils/test_printer_agent.cpp +++ b/tests/slic3rutils/test_printer_agent.cpp @@ -219,9 +219,9 @@ public: std::thread([this, finish = std::move(allow_finish_p)] { struct InFlightGuard { - std::atomic& counter; - ~InFlightGuard() { counter.fetch_sub(1, std::memory_order_relaxed); } - } guard{filament_fetch_in_flight}; + MoonrakerPrinterAgent& owner; + ~InFlightGuard() { owner.release_fetch_slot(); } + } guard{*this}; g_deferred_fetch_running.fetch_add(1, std::memory_order_relaxed); finish->get_future().wait();