diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 10cce3e330..eb8a595860 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -1461,9 +1461,10 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: } response["print"]["param"] = gcode; - send_gcode_async(dev_id, gcode, [this, dev_id, response](bool success) mutable { + MessageRouter router = make_message_router(dev_id); + send_gcode_async(dev_id, gcode, [router, response](bool success) mutable { response["print"]["result"] = success ? "success" : "failed"; - dispatch_message(dev_id, response.dump()); + router.deliver(response.dump()); }); return BAMBU_NETWORK_SUCCESS; } @@ -2905,6 +2906,36 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const return payload; } +void MoonrakerPrinterAgent::MessageRouter::deliver(std::string payload) const +{ + auto dispatch = [local = local_fn, cloud = cloud_fn, dev = dev_id, payload = std::move(payload)]() { + if (local) { + local(dev, payload); + return; + } + if (cloud) { + cloud(dev, payload); + } + }; + + if (queue_fn) { + queue_fn(dispatch); + } else { + dispatch(); + } +} + +MoonrakerPrinterAgent::MessageRouter MoonrakerPrinterAgent::make_message_router(const std::string& dev_id) const +{ + MessageRouter router; + router.dev_id = dev_id; + std::lock_guard lock(state_mutex); + router.local_fn = on_local_message_fn; + router.cloud_fn = on_message_fn; + router.queue_fn = queue_on_main_fn; + return router; +} + void MoonrakerPrinterAgent::dispatch_message(const std::string& dev_id, const std::string& payload) { OnMessageFn local_fn; diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index c021d5c2c6..d79ba1bb7c 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -209,6 +209,20 @@ private: void announce_printhost_device(); void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg); void dispatch_printer_connected(const std::string& dev_id); + + // Self-contained snapshot of the message callbacks. Async completions capture this + // by value so they never dereference `this` after the agent may have been destroyed. + struct MessageRouter + { + OnMessageFn local_fn; + OnMessageFn cloud_fn; + QueueOnMainFn queue_fn; + std::string dev_id; + + void deliver(std::string payload) const; + }; + + MessageRouter make_message_router(const std::string& dev_id) const; void dispatch_message(const std::string& dev_id, const std::string& payload); void start_status_stream(const std::string& dev_id, ConnectionSettings connection); void stop_status_stream(); diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index 09b5649953..f13d52ed18 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -53,6 +53,14 @@ struct InFlightGuard ~InFlightGuard() { if (counter) counter->fetch_sub(1, std::memory_order_relaxed); } }; +// nlohmann::json::value() returns the default only when the key is absent; a present but +// null/wrong-typed value throws. Firmware JSON is untrusted, so read defensively. +int read_int_or(const nlohmann::json& obj, const std::string& key, int fallback) +{ + auto it = obj.find(key); + return (it != obj.end() && it->is_number_integer()) ? it->get() : fallback; +} + } // anonymous namespace const std::string QidiPrinterAgent_VERSION = "0.0.1"; @@ -98,37 +106,45 @@ bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode InFlightGuard guard{filament_fetch_in_flight}; std::thread([this, guard = std::move(guard), connection = std::move(connection), model_id, model_name]() mutable { - std::string error; + try { + std::string error; - // 1. Fetch device info and infer series_id - std::string series_id; - { - MoonrakerDeviceInfo info; - if (fetch_device_info(connection, info, error)) { - series_id = infer_series_id(info.model_id, info.dev_name); + // 1. Fetch device info and infer series_id + std::string series_id; + { + MoonrakerDeviceInfo info; + if (fetch_device_info(connection, 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); } - } - 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(connection, dict, error)) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error; - } + // 2. Fetch filament dictionary + QidiFilamentDict dict; + if (!fetch_filament_dict(connection, 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(connection, dict, series_id, trays, box_count, error)) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error; - return; - } + // 3. Fetch slot info and build AmsTrayData directly + std::vector trays; + int box_count = 0; + if (!fetch_slot_info(connection, 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); + // 4. Build the AMS payload + build_ams_payload(box_count, box_count * 4 - 1, trays); + } catch (const std::exception& e) { + // why: an exception escaping a detached thread is std::terminate, and firmware + // JSON is untrusted; mirror run_command_worker and swallow it here. + BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: unhandled exception: " << e.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: unhandled exception"; + } }).detach(); return true; } @@ -257,7 +273,7 @@ bool QidiPrinterAgent::fetch_slot_info(const ConnectionSettings& connection, if (!parse_slot_response(response_body, status, variables, error)) return false; - box_count = variables.value("box_count", 1); + box_count = read_int_or(variables, "box_count", 1); if (box_count < 0) { box_count = 0; } @@ -280,9 +296,9 @@ bool QidiPrinterAgent::fetch_slot_info(const ConnectionSettings& connection, tray.slot_index = i; // Read slot variables - const int color_index = variables.value("color_slot" + std::to_string(i), 1); - const int filament_type = variables.value("filament_slot" + std::to_string(i), 1); - const int vendor_type = variables.value("vendor_slot" + std::to_string(i), 0); + const int color_index = read_int_or(variables, "color_slot" + std::to_string(i), 1); + const int filament_type = read_int_or(variables, "filament_slot" + std::to_string(i), 1); + const int vendor_type = read_int_or(variables, "vendor_slot" + std::to_string(i), 0); // Check filament presence via runout sensor std::string box_stepper_key = "box_stepper slot" + std::to_string(i); @@ -290,7 +306,7 @@ bool QidiPrinterAgent::fetch_slot_info(const ConnectionSettings& connection, if (status.contains(box_stepper_key)) { auto& box_stepper = status[box_stepper_key]; if (box_stepper.contains("runout_button") && !box_stepper["runout_button"].is_null()) { - int runout_button = box_stepper["runout_button"].template get(); + const int runout_button = read_int_or(box_stepper, "runout_button", 0); tray.has_filament = (runout_button == 0); } } diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp index 2a09ffa71d..726eb09629 100644 --- a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp @@ -47,6 +47,58 @@ struct InFlightGuard ~InFlightGuard() { if (counter) counter->fetch_sub(1, std::memory_order_relaxed); } }; +// nlohmann::json::value() returns the default only when the key is absent; a present but +// null/wrong-typed value throws. Firmware JSON is untrusted, so read defensively. +int read_int_or(const nlohmann::json& obj, const char* key, int fallback) +{ + auto it = obj.find(key); + return (it != obj.end() && it->is_number_integer()) ? it->get() : fallback; +} + +std::vector read_string_array_or(const nlohmann::json& obj, const char* key) +{ + auto it = obj.find(key); + if (it == obj.end() || !it->is_array()) + return {}; + std::vector out; + out.reserve(it->size()); + for (const auto& v : *it) + out.push_back(v.is_string() ? v.get() : std::string{}); + return out; +} + +std::vector read_bool_array_or(const nlohmann::json& obj, const char* key) +{ + auto it = obj.find(key); + if (it == obj.end() || !it->is_array()) + return {}; + std::vector out; + out.reserve(it->size()); + for (const auto& v : *it) + out.push_back(v.is_boolean() ? v.get() : false); + return out; +} + +// Parse a hex colour, stopping at the first non-hex character. std::stoul throws on +// empty/non-hex input, which must not escape the detached fetch thread. +unsigned int parse_hex_color(const std::string& hex) +{ + unsigned int value = 0; + for (char c : hex) { + int digit; + if (c >= '0' && c <= '9') + digit = c - '0'; + else if (c >= 'a' && c <= 'f') + digit = c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + digit = c - 'A' + 10; + else + break; + value = (value << 4) | static_cast(digit); + } + return value; +} + // Safely access a parallel array by index, returning a fallback if out of bounds. template T safe_at(const std::vector& vec, int index, const T& fallback) @@ -68,14 +120,16 @@ std::string find_closest_color_preset_by_vendor_and_type(const PresetCollection& filaments.get_preset_base(p) == &p && p.config.opt_string("filament_vendor", 0u) == vendor_name && p.config.opt_string("filament_type", 0u) == filament_type) { // The printer returns RGBA in the format RRGGBBAA, but profiles store color as #RRGGBB, - // so we must remove # and ignore alpha channel for distance calculation - unsigned int target_color_value = std::stoul(color_rgba.substr(0, color_rgba.length() - 2), nullptr, 16); + // so we must remove # and ignore alpha channel for distance calculation. Firmware + // colours are untrusted; parse_hex_color tolerates empty/non-hex instead of throwing. + unsigned int target_color_value = + parse_hex_color(color_rgba.substr(0, color_rgba.size() >= 2 ? color_rgba.size() - 2 : 0)); std::string p_color = p.config.opt_string("default_filament_colour", 0u); - unsigned int p_color_value; + unsigned int p_color_value = 0; if (!p_color.empty()) { - size_t hash_pos = p_color.find("#"); - p_color_value = std::stoul(p_color.substr(hash_pos != std::string::npos ? hash_pos + 1 : 0), nullptr, 16); + size_t hash_pos = p_color.find("#"); + p_color_value = parse_hex_color(p_color.substr(hash_pos != std::string::npos ? hash_pos + 1 : 0)); } else { // Default to black if no color specified in profile. Assume other profiles might be a closer color match. // Could be a problem if the target color is also black and there exist a specific profile for that type, vendor and color @@ -186,121 +240,132 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync InFlightGuard guard{filament_fetch_in_flight}; std::thread([this, guard = std::move(guard), base_url, api_key]() { - const std::string url = join_url(base_url, "/printer/objects/query?print_task_config&filament_detect"); + try { + const std::string url = join_url(base_url, "/printer/objects/query?print_task_config&filament_detect"); - std::string response_body; - bool success = false; - std::string http_error; + std::string response_body; + bool success = false; + std::string http_error; - 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; - } 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; + 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; } else { - tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); + http_error = "HTTP error: " + std::to_string(status); } - } else { - tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); - } + }) + .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(); - // 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); - } - } + if (!success) { + BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: HTTP request failed: " << http_error; + return; } - trays.emplace_back(std::move(tray)); - } + 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; + } - build_ams_payload(1, slot_count - 1, trays); + // 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 = read_bool_array_or(ptc, "filament_exist"); + auto filament_type = read_string_array_or(ptc, "filament_type"); + auto filament_sub_type = read_string_array_or(ptc, "filament_sub_type"); + auto filament_color = read_string_array_or(ptc, "filament_color_rgba"); + auto filament_vendor = read_string_array_or(ptc, "filament_vendor"); + + 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 = "NONE"; + if (auto vendor_it = nfc_slot.find("VENDOR"); vendor_it != nfc_slot.end() && vendor_it->is_string()) { + vendor = vendor_it->get(); + } + if (vendor != "NONE" && !vendor.empty()) { + tray.bed_temp = read_int_or(nfc_slot, "BED_TEMP", 0); + tray.nozzle_temp = read_int_or(nfc_slot, "FIRST_LAYER_TEMP", 0); + } + } + } + + trays.emplace_back(std::move(tray)); + } + + build_ams_payload(1, slot_count - 1, trays); + } catch (const std::exception& e) { + // why: an exception escaping a detached thread is std::terminate, and firmware + // JSON is untrusted; mirror run_command_worker and swallow it here. + BOOST_LOG_TRIVIAL(error) << "SnapmakerPrinterAgent::fetch_filament_info: unhandled exception: " << e.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "SnapmakerPrinterAgent::fetch_filament_info: unhandled exception"; + } }).detach(); return true;