mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-14 11:21:18 +00:00
fix: snapshot Moonraker connection state and make agent teardown responsive
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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<std::mutex> 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<std::recursive_mutex> lock(connect_mutex);
|
||||
return device_info;
|
||||
}
|
||||
|
||||
void MoonrakerPrinterAgent::release_fetch_slot() noexcept
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<bool>()) {
|
||||
continue;
|
||||
}
|
||||
if (webcam.contains("stream_url") && webcam["stream_url"].is_string() &&
|
||||
!webcam["stream_url"].get<std::string>().empty()) {
|
||||
camera_url = webcam["stream_url"].get<std::string>();
|
||||
@@ -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<std::mutex> 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<std::mutex> 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<std::mutex> 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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void()> 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;
|
||||
|
||||
@@ -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<int>* counter;
|
||||
explicit InFlightGuard(std::atomic<int>& 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;
|
||||
}
|
||||
|
||||
@@ -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<int>* counter;
|
||||
explicit InFlightGuard(std::atomic<int>& 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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -219,9 +219,9 @@ public:
|
||||
std::thread([this, finish = std::move(allow_finish_p)] {
|
||||
struct InFlightGuard
|
||||
{
|
||||
std::atomic<int>& 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();
|
||||
|
||||
Reference in New Issue
Block a user