From 0d0d281d0c4311bbfc1037bb89996fa412dec5a9 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 20 Aug 2026 20:11:31 +0800 Subject: [PATCH] feat: parse nozzle information for qidi and moonraker printer agents --- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 73 ++++++++++++++++++++-- src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 2 + tests/slic3rutils/test_printer_agent.cpp | 48 ++++++++++++++ 3 files changed, 119 insertions(+), 4 deletions(-) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 149936d77b..22ea0d52f1 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -1364,12 +1364,57 @@ 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) { + auto fetch_json = [&](const std::string& url, nlohmann::json& out, std::string& fetch_error) { std::string response_body; bool success = false; std::string http_error; @@ -1397,13 +1442,13 @@ bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, .perform_sync(); if (!success) { - error = http_error.empty() ? "Connection failed" : http_error; + fetch_error = http_error.empty() ? "Connection failed" : http_error; return false; } out = nlohmann::json::parse(response_body, nullptr, false, true); if (out.is_discarded()) { - error = "Invalid JSON response"; + fetch_error = "Invalid JSON response"; return false; } return true; @@ -1411,7 +1456,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)) { + if (!fetch_json(url, json, error)) { return false; } @@ -1420,6 +1465,17 @@ 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; } @@ -2461,6 +2517,14 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const 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"; @@ -2816,6 +2880,7 @@ 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 diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index 62e5ca7846..f1798f097f 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -90,6 +90,7 @@ protected: std::string dev_name; std::string version; std::string klippy_state; + float nozzle_diameter = 0.0f; bool use_ssl = false; } device_info; @@ -110,6 +111,7 @@ 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; diff --git a/tests/slic3rutils/test_printer_agent.cpp b/tests/slic3rutils/test_printer_agent.cpp index 1d9948164b..81800e6ae8 100644 --- a/tests/slic3rutils/test_printer_agent.cpp +++ b/tests/slic3rutils/test_printer_agent.cpp @@ -17,6 +17,54 @@ 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]") {