diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 2792264830..52cae222da 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -356,6 +356,13 @@ int MoonrakerPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, } detect.dev_id = info.dev_id.empty() ? dev_ip : info.dev_id; + if (!info.model_id.empty()) { + detect.model_id = info.model_id; + } else if (!config.model_id.empty()) { + detect.model_id = config.model_id; + } else { + detect.model_id = config.model_name; + } // Prefer fetched hostname, then preset model name, then generic fallback std::string fallback_name = config.model_name.empty() ? "Moonraker Printer" : config.model_name; detect.dev_name = info.dev_name.empty() ? fallback_name : info.dev_name; @@ -600,7 +607,9 @@ void MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) { // Moonraker doesn't have standard filament tracking like Qidi // This is a no-op for standard Moonraker installations - BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: fetch_filament_info (no-op) - dev_id=" << dev_id; + // Note: QidiPrinterAgent overrides this method with actual implementation + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info (base class no-op) called for dev_id=" << dev_id + << " - if you see this for Qidi printer, virtual dispatch is broken!"; } int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) @@ -741,6 +750,7 @@ bool MoonrakerPrinterAgent::get_printhost_config(PrinthostConfig& config) const config.api_key = host_cfg->opt_string("printhost_apikey"); config.model_name = printer_cfg.opt_string("printer_model"); config.base_url = normalize_base_url(config.host, config.port); + config.model_id = preset.get_printer_type(preset_bundle); return !config.base_url.empty(); } @@ -797,9 +807,13 @@ bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, } nlohmann::json result = json.contains("result") ? json["result"] : json; - info.dev_name = result.value("hostname", "Moonraker Printer"); - info.dev_id = result.value("hostname", ""); - info.version = result.value("moonraker_version", ""); + info.dev_name = result.value("machine_name", result.value("hostname", "")); + info.dev_id = result.value("machine_uuid", ""); + if (info.dev_id.empty()) { + info.dev_id = result.value("serial_number", ""); + } + info.model_id = result.value("model", ""); + info.version = result.value("software_version", result.value("firmware_version", "")); return true; } @@ -1156,6 +1170,8 @@ void MoonrakerPrinterAgent::announce_printhost_device() << " (fetch_error=" << fetch_error << ")"; } + const std::string model_id = config.model_id; + if (auto* app_config = GUI::wxGetApp().app_config) { const std::string access_code = api_key.empty() ? k_no_api_key : api_key; app_config->set_str("access_code", dev_id, access_code); @@ -1168,7 +1184,7 @@ void MoonrakerPrinterAgent::announce_printhost_device() payload["dev_name"] = dev_name; payload["dev_id"] = dev_id; payload["dev_ip"] = extract_host(base_url); - payload["dev_type"] = "moonraker"; + payload["dev_type"] = model_id.empty() ? dev_name : model_id; payload["dev_signal"] = "0"; payload["connect_type"] = "lan"; payload["bind_state"] = "free"; diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index 977cfcf6a9..b6af532bd9 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -15,7 +15,7 @@ namespace Slic3r { -class MoonrakerPrinterAgent final : public IPrinterAgent +class MoonrakerPrinterAgent : public IPrinterAgent { public: explicit MoonrakerPrinterAgent(std::string log_dir); @@ -70,16 +70,18 @@ public: 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; } - void fetch_filament_info(std::string dev_id) override; + virtual FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } + virtual void fetch_filament_info(std::string dev_id) override; -private: +protected: + // Types exposed for derived classes struct PrinthostConfig { std::string host; std::string port; std::string api_key; std::string base_url; + std::string model_id; std::string model_name; }; @@ -87,24 +89,35 @@ private: { std::string dev_id; std::string dev_name; + std::string model_id; std::string version; }; + // Methods that derived classes may need to override or access + virtual bool get_printhost_config(PrinthostConfig& config) const; + virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; + virtual std::string get_dev_type() const { return "moonraker"; } + + // Host resolution methods + std::string resolve_host(const std::string& dev_id) const; + std::string resolve_api_key(const std::string& dev_id, const std::string& fallback) const; + void store_host(const std::string& dev_id, const std::string& host, const std::string& api_key); + + // State access for derived classes + mutable std::recursive_mutex state_mutex; + std::map host_by_device; + std::map api_key_by_device; + +private: int handle_request(const std::string& dev_id, const std::string& json_str); int send_version_info(const std::string& dev_id); int send_access_code(const std::string& dev_id); - bool get_printhost_config(PrinthostConfig& config) const; - bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; bool fetch_server_info(const std::string& base_url, const std::string& api_key, std::string& version, std::string& error) const; 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; - std::string resolve_host(const std::string& dev_id) const; - std::string resolve_api_key(const std::string& dev_id, const std::string& fallback) const; - void store_host(const std::string& dev_id, const std::string& host, const std::string& api_key); - 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); @@ -140,9 +153,6 @@ private: const std::string& api_key); void finish_connection(); - mutable std::recursive_mutex state_mutex; - std::map host_by_device; - std::map api_key_by_device; std::string ssdp_announced_host; std::string ssdp_announced_id; std::shared_ptr m_cloud_agent; diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index 86bcdeb8c0..f3d740f209 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -8,87 +8,18 @@ #include "nlohmann/json.hpp" #include -#include -#include -#include -#include #include #include -#include -#include #include #include namespace { -namespace beast = boost::beast; -namespace http = beast::http; -namespace websocket = beast::websocket; -namespace net = boost::asio; -using tcp = net::ip::tcp; - -std::string to_hex_string(uint64_t value) -{ - std::ostringstream stream; - stream << std::hex << std::uppercase << value; - return stream.str(); -} - -bool looks_like_host(const std::string& value) -{ - if (value.empty()) { - return false; - } - if (value.find(' ') != std::string::npos) { - return false; - } - return value.find('.') != std::string::npos || value.find(':') != std::string::npos; -} - -constexpr const char* k_no_api_key = "__NO_API_KEY__"; - bool is_numeric(const std::string& value) { return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c) != 0; }); } -std::string normalize_base_url(std::string host, const std::string& port) -{ - boost::trim(host); - if (host.empty()) { - return ""; - } - - std::string value = host; - if (is_numeric(port) && value.find("://") == std::string::npos && value.find(':') == std::string::npos) { - value += ":" + port; - } - - if (!boost::istarts_with(value, "http://") && !boost::istarts_with(value, "https://")) { - value = "http://" + value; - } - - if (value.size() > 1 && value.back() == '/') { - value.pop_back(); - } - - return value; -} - -std::string extract_host(const std::string& base_url) -{ - std::string host = base_url; - auto pos = host.find("://"); - if (pos != std::string::npos) { - host = host.substr(pos + 3); - } - pos = host.find('/'); - if (pos != std::string::npos) { - host = host.substr(0, pos); - } - return host; -} - std::string join_url(const std::string& base_url, const std::string& path) { if (base_url.empty()) { @@ -106,14 +37,6 @@ std::string join_url(const std::string& base_url, const std::string& path) return base_url + path; } -std::string normalize_api_key(const std::string& api_key) -{ - if (api_key.empty() || api_key == k_no_api_key) { - return ""; - } - return api_key; -} - std::string normalize_model_key(std::string value) { boost::algorithm::to_lower(value); @@ -151,65 +74,6 @@ std::string infer_series_id(const std::string& model_id, const std::string& dev_ return ""; } -struct WsEndpoint -{ - std::string host; - std::string port; - std::string target; - bool secure = false; -}; - -bool parse_ws_endpoint(const std::string& base_url, WsEndpoint& endpoint) -{ - if (base_url.empty()) { - return false; - } - - std::string url = base_url; - if (boost::istarts_with(url, "https://")) { - endpoint.secure = true; - url = url.substr(8); - } else if (boost::istarts_with(url, "http://")) { - url = url.substr(7); - } - - auto slash = url.find('/'); - if (slash != std::string::npos) { - url = url.substr(0, slash); - } - if (url.empty()) { - return false; - } - - endpoint.host = url; - endpoint.port = endpoint.secure ? "443" : "80"; - if (auto colon = url.rfind(':'); colon != std::string::npos && url.find(']') == std::string::npos) { - endpoint.host = url.substr(0, colon); - endpoint.port = url.substr(colon + 1); - } - - endpoint.target = "/websocket"; - return !endpoint.host.empty() && !endpoint.port.empty(); -} - -std::string map_moonraker_state(std::string state) -{ - boost::algorithm::to_lower(state); - if (state == "printing") { - return "RUNNING"; - } - if (state == "paused") { - return "PAUSE"; - } - if (state == "complete") { - return "FINISH"; - } - if (state == "error" || state == "cancelled") { - return "FAILED"; - } - return "IDLE"; -} - std::string normalize_filament_type(const std::string& filament_type) { std::string trimmed = filament_type; @@ -236,181 +100,23 @@ std::string normalize_filament_type(const std::string& filament_type) return trimmed; } + } // namespace namespace Slic3r { const std::string QidiPrinterAgent_VERSION = "0.0.1"; -QidiPrinterAgent::QidiPrinterAgent(std::string log_dir) : OrcaPrinterAgent(std::move(log_dir)) +QidiPrinterAgent::QidiPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) { BOOST_LOG_TRIVIAL(info) << "QidiPrinterAgent: Constructor"; } -QidiPrinterAgent::~QidiPrinterAgent() -{ - stop_status_stream(); -} - AgentInfo QidiPrinterAgent::get_agent_info_static() { return AgentInfo{.id = "qidi", .name = "Qidi Printer Agent", .version = QidiPrinterAgent_VERSION, .description = "Qidi printer agent"}; } -int QidiPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) -{ - (void) qos; - (void) flag; - return handle_request(dev_id, json_str); -} - -int QidiPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) -{ - (void) qos; - (void) flag; - return handle_request(dev_id, json_str); -} - -int QidiPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) -{ - (void) username; - (void) use_ssl; - std::string base_url = normalize_base_url(dev_ip, ""); - std::string api_key = normalize_api_key(password); - - PrinthostConfig config; - if (get_printhost_config(config)) { - if (base_url.empty()) { - base_url = config.base_url; - } - if (api_key.empty()) { - api_key = normalize_api_key(config.api_key); - } - } - - if (base_url.empty()) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent: connect_printer missing host for dev_id=" << dev_id; - dispatch_local_connect(ConnectStatusFailed, dev_id, "host_missing"); - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - - if (dev_id.empty()) { - dev_id = extract_host(base_url); - } - - { - std::lock_guard lock(payload_mutex); - status_cache = nlohmann::json::object(); - last_ams_payload = nlohmann::json(); - } - ws_last_emit_ms.store(0); - - store_host(dev_id, base_url, api_key); - start_status_stream(dev_id, base_url, api_key); - dispatch_local_connect(ConnectStatusOk, dev_id, "0"); - dispatch_printer_connected(dev_id); - BOOST_LOG_TRIVIAL(info) << "QidiPrinterAgent: connect_printer - dev_id=" << dev_id << ", dev_ip=" << dev_ip; - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::disconnect_printer() -{ - stop_status_stream(); - return BAMBU_NETWORK_SUCCESS; -} - -bool QidiPrinterAgent::start_discovery(bool start, bool sending) -{ - (void) sending; - if (start) { - announce_printhost_device(); - } - return true; -} - -int QidiPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) -{ - (void) sec_link; - - std::string base_url = normalize_base_url(dev_ip, ""); - if (base_url.empty()) { - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - - PrinthostConfig config; - get_printhost_config(config); - const std::string api_key = normalize_api_key(config.api_key); - - QidiDeviceInfo info; - std::string error; - if (!fetch_device_info(base_url, api_key, info, error)) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent: bind_detect failed: " << error; - return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; - } - - detect.dev_id = info.dev_id.empty() ? dev_ip : info.dev_id; - if (!info.model_id.empty()) { - detect.model_id = info.model_id; - } else if (!config.model_id.empty()) { - detect.model_id = config.model_id; - } else { - detect.model_id = config.model_name; - } - detect.dev_name = info.dev_name.empty() ? config.model_name : info.dev_name; - detect.version = info.version; - detect.connect_type = "lan"; - detect.bind_state = "free"; - - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::set_on_ssdp_msg_fn(OnMsgArrivedFn fn) -{ - { - std::lock_guard lock(state_mutex); - on_ssdp_msg_fn = fn; - } - if (fn) { - announce_printhost_device(); - } - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::set_on_printer_connected_fn(OnPrinterConnectedFn fn) -{ - std::lock_guard lock(state_mutex); - on_printer_connected_fn = fn; - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::set_on_message_fn(OnMessageFn fn) -{ - std::lock_guard lock(state_mutex); - on_message_fn = fn; - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::set_on_local_connect_fn(OnLocalConnectedFn fn) -{ - std::lock_guard lock(state_mutex); - on_local_connect_fn = fn; - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::set_on_local_message_fn(OnMessageFn fn) -{ - std::lock_guard lock(state_mutex); - on_local_message_fn = fn; - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) -{ - std::lock_guard lock(state_mutex); - queue_on_main_fn = fn; - return BAMBU_NETWORK_SUCCESS; -} - void QidiPrinterAgent::fetch_filament_info(std::string dev_id) { // Look up MachineObject via DeviceManager @@ -447,7 +153,7 @@ void QidiPrinterAgent::fetch_filament_info(std::string dev_id) std::string series_id; { - QidiDeviceInfo info; + MoonrakerDeviceInfo info; std::string device_error; if (fetch_device_info(base_url, api_key, info, device_error)) { series_id = infer_series_id(info.model_id, info.dev_name); @@ -540,750 +246,6 @@ void QidiPrinterAgent::fetch_filament_info(std::string dev_id) << box_count << " AMS units"; } -int QidiPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) -{ - auto json = nlohmann::json::parse(json_str, nullptr, false); - if (json.is_discarded()) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent: Invalid JSON request"; - return BAMBU_NETWORK_ERR_INVALID_RESULT; - } - - if (json.contains("info") && json["info"].contains("command")) { - const auto& command = json["info"]["command"]; - if (command.is_string() && command.get() == "get_version") { - return send_version_info(dev_id); - } - } - - if (json.contains("system") && json["system"].contains("command")) { - const auto& command = json["system"]["command"]; - if (command.is_string() && command.get() == "get_access_code") { - return send_access_code(dev_id); - } - } - - // if (json.contains("pushing") && json["pushing"].contains("command")) { - // const auto& command = json["pushing"]["command"]; - // if (command.is_string()) { - // const auto cmd = command.get(); - // if (cmd == "pushall" || cmd == "start") { - // return sync_filament_list(dev_id); - // } - // } - // } - - return BAMBU_NETWORK_SUCCESS; -} - -bool QidiPrinterAgent::get_printhost_config(PrinthostConfig& config) const -{ - auto* preset_bundle = GUI::wxGetApp().preset_bundle; - if (!preset_bundle) { - return false; - } - - auto& preset = preset_bundle->printers.get_edited_preset(); - const auto& printer_cfg = preset.config; - const DynamicPrintConfig* host_cfg = &printer_cfg; - config.host = host_cfg->opt_string("print_host"); - if (config.host.empty()) { - if (auto* physical_cfg = preset_bundle->physical_printers.get_selected_printer_config()) { - if (!physical_cfg->opt_string("print_host").empty()) { - host_cfg = physical_cfg; - config.host = host_cfg->opt_string("print_host"); - } - } - } - if (config.host.empty()) { - return false; - } - - config.port = host_cfg->opt_string("printhost_port"); - config.api_key = host_cfg->opt_string("printhost_apikey"); - config.model_id = preset.get_printer_type(preset_bundle); - config.model_name = printer_cfg.opt_string("printer_model"); - config.base_url = normalize_base_url(config.host, config.port); - - return !config.base_url.empty(); -} - -bool QidiPrinterAgent::fetch_device_info(const std::string& base_url, - const std::string& api_key, - QidiDeviceInfo& info, - std::string& error) const -{ - auto fetch_json = [&](const std::string& url, nlohmann::json& out) { - 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(10) - .timeout_max(30) - .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) { - 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"; - return false; - } - return true; - }; - - nlohmann::json json; - std::string url = join_url(base_url, "/machine/device_info"); - if (!fetch_json(url, json)) { - url = join_url(base_url, "/printer/info"); - if (!fetch_json(url, json)) { - return false; - } - } - - nlohmann::json result = json.contains("result") ? json["result"] : json; - info.dev_name = result.value("machine_name", result.value("hostname", "")); - info.dev_id = result.value("machine_uuid", ""); - if (info.dev_id.empty()) { - info.dev_id = result.value("serial_number", ""); - } - info.model_id = result.value("model", ""); - info.version = result.value("software_version", result.value("firmware_version", "")); - - return true; -} - -bool QidiPrinterAgent::fetch_server_info(const std::string& base_url, - const std::string& api_key, - std::string& version, - std::string& error) const -{ - std::string response_body; - bool success = false; - std::string http_error; - - auto http = Http::get(join_url(base_url, "/server/info")); - if (!api_key.empty()) { - http.header("X-Api-Key", api_key); - } - http.timeout_connect(10) - .timeout_max(30) - .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) { - error = http_error.empty() ? "Connection failed" : http_error; - return false; - } - - auto json = nlohmann::json::parse(response_body, nullptr, false, true); - if (json.is_discarded()) { - error = "Invalid JSON response"; - return false; - } - - nlohmann::json result = json.contains("result") ? json["result"] : json; - if (result.contains("moonraker_version") && result["moonraker_version"].is_string()) { - version = result["moonraker_version"].get(); - } else if (result.contains("version") && result["version"].is_string()) { - version = result["version"].get(); - } - - return true; -} - -bool QidiPrinterAgent::fetch_object_list(const std::string& base_url, - const std::string& api_key, - std::set& objects, - std::string& error) const -{ - std::string response_body; - bool success = false; - std::string http_error; - - auto http = Http::get(join_url(base_url, "/printer/objects/list")); - if (!api_key.empty()) { - http.header("X-Api-Key", api_key); - } - http.timeout_connect(10) - .timeout_max(30) - .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) { - error = http_error.empty() ? "Connection failed" : http_error; - return false; - } - - auto json = nlohmann::json::parse(response_body, nullptr, false, true); - if (json.is_discarded()) { - error = "Invalid JSON response"; - return false; - } - - nlohmann::json result = json.contains("result") ? json["result"] : json; - if (!result.contains("objects") || !result["objects"].is_array()) { - error = "Unexpected JSON structure"; - return false; - } - - objects.clear(); - for (const auto& entry : result["objects"]) { - if (entry.is_string()) { - objects.insert(entry.get()); - } - } - - return !objects.empty(); -} - -int QidiPrinterAgent::send_version_info(const std::string& dev_id) -{ - const std::string base_url = resolve_host(dev_id); - if (base_url.empty()) { - return BAMBU_NETWORK_ERR_INVALID_HANDLE; - } - const std::string api_key = resolve_api_key(dev_id, ""); - - std::string version; - std::string error; - if (!fetch_server_info(base_url, api_key, version, error)) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: Failed to fetch server info: " << error; - } - if (version.empty()) { - version = "moonraker"; - } - - nlohmann::json payload; - payload["info"]["command"] = "get_version"; - payload["info"]["result"] = "success"; - payload["info"]["module"] = nlohmann::json::array(); - - nlohmann::json module; - module["name"] = "ota"; - module["sw_ver"] = version; - module["product_name"] = "Moonraker"; - payload["info"]["module"].push_back(module); - - dispatch_message(dev_id, payload.dump()); - return BAMBU_NETWORK_SUCCESS; -} - -int QidiPrinterAgent::send_access_code(const std::string& dev_id) -{ - nlohmann::json payload; - payload["system"]["command"] = "get_access_code"; - payload["system"]["access_code"] = resolve_api_key(dev_id, ""); - dispatch_message(dev_id, payload.dump()); - return BAMBU_NETWORK_SUCCESS; -} - -void QidiPrinterAgent::announce_printhost_device() -{ - PrinthostConfig config; - if (!get_printhost_config(config)) { - return; - } - - const std::string base_url = config.base_url; - if (base_url.empty()) { - return; - } - - OnMsgArrivedFn ssdp_fn; - { - std::lock_guard lock(state_mutex); - ssdp_fn = on_ssdp_msg_fn; - if (!ssdp_fn) { - return; - } - if (ssdp_announced_host == base_url && !ssdp_announced_id.empty()) { - return; - } - } - - const std::string dev_id = extract_host(base_url); - const std::string dev_name = config.model_name.empty() ? "Qidi Printer" : config.model_name; - const std::string model_id = config.model_id; - - if (auto* app_config = GUI::wxGetApp().app_config) { - const std::string access_code = normalize_api_key(config.api_key).empty() ? k_no_api_key : config.api_key; - app_config->set_str("access_code", dev_id, access_code); - app_config->set_str("user_access_code", dev_id, access_code); - } - - store_host(dev_id, base_url, normalize_api_key(config.api_key)); - - nlohmann::json payload; - payload["dev_name"] = dev_name; - payload["dev_id"] = dev_id; - payload["dev_ip"] = extract_host(base_url); - payload["dev_type"] = model_id.empty() ? dev_name : model_id; - payload["dev_signal"] = "0"; - payload["connect_type"] = "lan"; - payload["bind_state"] = "free"; - payload["sec_link"] = "secure"; - payload["ssdp_version"] = "v1"; - - ssdp_fn(payload.dump()); - - std::lock_guard lock(state_mutex); - ssdp_announced_host = base_url; - ssdp_announced_id = dev_id; -} - -void QidiPrinterAgent::dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg) -{ - OnLocalConnectedFn local_fn; - QueueOnMainFn queue_fn; - { - std::lock_guard lock(state_mutex); - local_fn = on_local_connect_fn; - queue_fn = queue_on_main_fn; - } - if (!local_fn) { - return; - } - - auto dispatch = [state, dev_id, msg, local_fn]() { local_fn(state, dev_id, msg); }; - if (queue_fn) { - queue_fn(dispatch); - } else { - dispatch(); - } -} - -void QidiPrinterAgent::dispatch_printer_connected(const std::string& dev_id) -{ - OnPrinterConnectedFn connected_fn; - QueueOnMainFn queue_fn; - { - std::lock_guard lock(state_mutex); - connected_fn = on_printer_connected_fn; - queue_fn = queue_on_main_fn; - } - if (!connected_fn) { - return; - } - - auto dispatch = [dev_id, connected_fn]() { connected_fn(dev_id); }; - if (queue_fn) { - queue_fn(dispatch); - } else { - dispatch(); - } -} - -void QidiPrinterAgent::start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key) -{ - stop_status_stream(); - if (base_url.empty()) { - return; - } - - ws_stop.store(false); - ws_thread = std::thread([this, dev_id, base_url, api_key]() { - run_status_stream(dev_id, base_url, api_key); - }); -} - -void QidiPrinterAgent::stop_status_stream() -{ - ws_stop.store(true); - if (ws_thread.joinable()) { - ws_thread.join(); - } -} - -void QidiPrinterAgent::run_status_stream(std::string dev_id, std::string base_url, std::string api_key) -{ - WsEndpoint endpoint; - if (!parse_ws_endpoint(base_url, endpoint)) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: websocket endpoint invalid for base_url=" << base_url; - return; - } - if (endpoint.secure) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: websocket wss not supported for base_url=" << base_url; - return; - } - - try { - net::io_context ioc; - tcp::resolver resolver{ioc}; - beast::tcp_stream stream{ioc}; - - stream.expires_after(std::chrono::seconds(10)); - auto const results = resolver.resolve(endpoint.host, endpoint.port); - stream.connect(results); - - 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 (!endpoint.port.empty() && endpoint.port != "80") { - host_header += ":" + endpoint.port; - } - ws.handshake(host_header, endpoint.target); - ws.text(true); - - std::set subscribe_objects = {"print_stats", "virtual_sdcard"}; - std::set available_objects; - std::string list_error; - if (fetch_object_list(base_url, api_key, available_objects, list_error)) { - if (available_objects.count("heater_bed") != 0) { - subscribe_objects.insert("heater_bed"); - } - if (available_objects.count("fan") != 0) { - subscribe_objects.insert("fan"); - } - - for (const auto& name : available_objects) { - if (name == "extruder" || name.rfind("extruder", 0) == 0) { - subscribe_objects.insert(name); - if (name == "extruder") { - break; - } - } - } - } else { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: object list unavailable: " << list_error; - subscribe_objects.insert("extruder"); - subscribe_objects.insert("heater_bed"); - subscribe_objects.insert("fan"); - } - - nlohmann::json subscribe; - subscribe["jsonrpc"] = "2.0"; - subscribe["method"] = "printer.objects.subscribe"; - nlohmann::json objects = nlohmann::json::object(); - for (const auto& name : subscribe_objects) { - objects[name] = nullptr; - } - subscribe["params"]["objects"] = std::move(objects); - subscribe["id"] = 1; - ws.write(net::buffer(subscribe.dump())); - - while (!ws_stop.load()) { - ws.next_layer().expires_after(std::chrono::seconds(2)); - beast::flat_buffer buffer; - beast::error_code ec; - ws.read(buffer, ec); - if (ec == beast::error::timeout) { - const auto now_ms = static_cast( - std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count()); - const auto last_ms = ws_last_emit_ms.load(); - if (last_ms == 0 || now_ms - last_ms >= 10000) { - nlohmann::json message; - { - std::lock_guard lock(payload_mutex); - message = build_print_payload_locked(nullptr); - } - dispatch_message(dev_id, message.dump()); - ws_last_emit_ms.store(now_ms); - } - continue; - } - if (ec == websocket::error::closed) { - break; - } - if (ec) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: websocket read error: " << ec.message(); - break; - } - handle_ws_message(dev_id, beast::buffers_to_string(buffer.data())); - } - - beast::error_code ec; - ws.close(websocket::close_code::normal, ec); - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent: websocket exception: " << e.what(); - } -} - -void QidiPrinterAgent::handle_ws_message(const std::string& dev_id, const std::string& payload) -{ - auto json = nlohmann::json::parse(payload, nullptr, false); - if (json.is_discarded()) { - return; - } - - bool updated = false; - if (json.contains("result") && json["result"].contains("status") && - json["result"]["status"].is_object()) { - update_status_cache(json["result"]["status"]); - updated = true; - } - - if (json.contains("method") && json["method"].is_string()) { - const std::string method = json["method"].get(); - if (method == "notify_status_update" && json.contains("params") && - json["params"].is_array() && !json["params"].empty() && - json["params"][0].is_object()) { - update_status_cache(json["params"][0]); - updated = true; - } else if (method == "notify_klippy_ready") { - nlohmann::json updates; - updates["print_stats"]["state"] = "standby"; - update_status_cache(updates); - updated = true; - } else if (method == "notify_klippy_shutdown") { - nlohmann::json updates; - updates["print_stats"]["state"] = "error"; - update_status_cache(updates); - updated = true; - } - } - - if (updated) { - nlohmann::json message; - { - std::lock_guard lock(payload_mutex); - message = build_print_payload_locked(nullptr); - } - dispatch_message(dev_id, message.dump()); - const auto now_ms = static_cast( - std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count()); - ws_last_emit_ms.store(now_ms); - } -} - -void QidiPrinterAgent::update_status_cache(const nlohmann::json& updates) -{ - if (!updates.is_object()) { - return; - } - - std::lock_guard lock(payload_mutex); - if (!status_cache.is_object()) { - status_cache = nlohmann::json::object(); - } - - for (const auto& item : updates.items()) { - if (item.value().is_object()) { - nlohmann::json& target = status_cache[item.key()]; - if (!target.is_object()) { - target = nlohmann::json::object(); - } - for (const auto& field : item.value().items()) { - target[field.key()] = field.value(); - } - } else { - status_cache[item.key()] = item.value(); - } - } -} - -nlohmann::json QidiPrinterAgent::build_print_payload_locked(const nlohmann::json* ams_override) const -{ - nlohmann::json payload; - payload["print"]["command"] = "push_status"; - payload["print"]["msg"] = 0; - payload["print"]["support_mqtt_alive"] = true; - - if (ams_override) { - payload["print"]["ams"] = *ams_override; - } else if (!last_ams_payload.is_null()) { - payload["print"]["ams"] = last_ams_payload; - } - - std::string state = "IDLE"; - if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("state") && - status_cache["print_stats"]["state"].is_string()) { - state = map_moonraker_state(status_cache["print_stats"]["state"].get()); - } - payload["print"]["gcode_state"] = state; - - const nlohmann::json* extruder = nullptr; - if (status_cache.contains("extruder") && status_cache["extruder"].is_object()) { - extruder = &status_cache["extruder"]; - } else { - for (const auto& item : status_cache.items()) { - if (item.value().is_object() && item.key().rfind("extruder", 0) == 0) { - extruder = &item.value(); - break; - } - } - } - - if (extruder) { - if (extruder->contains("temperature") && (*extruder)["temperature"].is_number()) { - payload["print"]["nozzle_temper"] = (*extruder)["temperature"].get(); - } - if (extruder->contains("target") && (*extruder)["target"].is_number()) { - payload["print"]["nozzle_target_temper"] = (*extruder)["target"].get(); - } - } - - if (status_cache.contains("heater_bed") && status_cache["heater_bed"].is_object()) { - const auto& bed = status_cache["heater_bed"]; - if (bed.contains("temperature") && bed["temperature"].is_number()) { - payload["print"]["bed_temper"] = bed["temperature"].get(); - } - if (bed.contains("target") && bed["target"].is_number()) { - payload["print"]["bed_target_temper"] = bed["target"].get(); - } - } - - if (status_cache.contains("fan") && status_cache["fan"].is_object()) { - const auto& fan = status_cache["fan"]; - if (fan.contains("speed") && fan["speed"].is_number()) { - double speed = fan["speed"].get(); - int pwm = 0; - if (speed <= 1.0) { - pwm = static_cast(speed * 255.0 + 0.5); - } else { - pwm = static_cast(speed + 0.5); - } - pwm = std::clamp(pwm, 0, 255); - payload["print"]["fan_gear"] = pwm; - } - } - - if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("filename") && - status_cache["print_stats"]["filename"].is_string()) { - payload["print"]["subtask_name"] = status_cache["print_stats"]["filename"].get(); - } - - int mc_percent = -1; - if (status_cache.contains("virtual_sdcard") && - status_cache["virtual_sdcard"].contains("progress") && - status_cache["virtual_sdcard"]["progress"].is_number()) { - const double progress = status_cache["virtual_sdcard"]["progress"].get(); - if (progress >= 0.0) { - mc_percent = std::clamp(static_cast(progress * 100.0 + 0.5), 0, 100); - } - } - if (mc_percent >= 0) { - payload["print"]["mc_percent"] = mc_percent; - } - - 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; - } - } - - const auto now_ms = static_cast( - std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count()); - payload["t_utc"] = now_ms; - - return payload; -} - -std::string QidiPrinterAgent::resolve_host(const std::string& dev_id) const -{ - { - std::lock_guard lock(state_mutex); - auto it = host_by_device.find(dev_id); - if (it != host_by_device.end()) { - return it->second; - } - } - - PrinthostConfig config; - if (get_printhost_config(config)) { - return config.base_url; - } - - return looks_like_host(dev_id) ? normalize_base_url(dev_id, "") : ""; -} - -std::string QidiPrinterAgent::resolve_api_key(const std::string& dev_id, const std::string& fallback) const -{ - std::string api_key = normalize_api_key(fallback); - if (!api_key.empty()) { - return api_key; - } - - { - std::lock_guard lock(state_mutex); - auto it = api_key_by_device.find(dev_id); - if (it != api_key_by_device.end() && !it->second.empty()) { - return it->second; - } - } - - PrinthostConfig config; - if (get_printhost_config(config)) { - return normalize_api_key(config.api_key); - } - - return ""; -} - -void QidiPrinterAgent::store_host(const std::string& dev_id, const std::string& host, const std::string& api_key) -{ - if (host.empty()) { - return; - } - std::lock_guard lock(state_mutex); - host_by_device[dev_id] = host; - if (!api_key.empty()) { - api_key_by_device[dev_id] = api_key; - } -} - bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, const std::string& api_key, std::vector& slots, @@ -1372,6 +334,52 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, return true; } +bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url, + const std::string& api_key, + QidiFilamentDict& dict, + std::string& error) const +{ + std::string url = join_url(base_url, "/server/files/config/officiall_filas_list.cfg"); + + 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(10) + .timeout_max(30) + .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) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + dict.colors.clear(); + dict.filaments.clear(); + parse_ini_section(response_body, "colordict", dict.colors); + parse_filament_sections(response_body, dict.filaments); + + return !dict.colors.empty(); +} + void QidiPrinterAgent::parse_ini_section(const std::string& content, const std::string& section_name, std::map& result) { std::istringstream stream(content); @@ -1442,52 +450,6 @@ void QidiPrinterAgent::parse_filament_sections(const std::string& content, std:: } } -bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url, - const std::string& api_key, - QidiFilamentDict& dict, - std::string& error) const -{ - std::string url = join_url(base_url, "/server/files/config/officiall_filas_list.cfg"); - - 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(10) - .timeout_max(30) - .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) { - error = http_error.empty() ? "Connection failed" : http_error; - return false; - } - - dict.colors.clear(); - dict.filaments.clear(); - parse_ini_section(response_body, "colordict", dict.colors); - parse_filament_sections(response_body, dict.filaments); - - return !dict.colors.empty(); -} - std::string QidiPrinterAgent::normalize_color(const std::string& color) { std::string value = color; @@ -1534,33 +496,4 @@ std::string QidiPrinterAgent::map_filament_type_to_setting_id(const std::string& return ""; } -void QidiPrinterAgent::dispatch_message(const std::string& dev_id, const std::string& payload) -{ - OnMessageFn local_fn; - OnMessageFn cloud_fn; - QueueOnMainFn queue_fn; - { - std::lock_guard lock(state_mutex); - local_fn = on_local_message_fn; - cloud_fn = on_message_fn; - queue_fn = queue_on_main_fn; - } - - auto dispatch = [dev_id, payload, local_fn, cloud_fn]() { - if (local_fn) { - local_fn(dev_id, payload); - return; - } - if (cloud_fn) { - cloud_fn(dev_id, payload); - } - }; - - if (queue_fn) { - queue_fn(dispatch); - } else { - dispatch(); - } -} - } // namespace Slic3r diff --git a/src/slic3r/Utils/QidiPrinterAgent.hpp b/src/slic3r/Utils/QidiPrinterAgent.hpp index 0da9761dd9..a2a51c423b 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.hpp +++ b/src/slic3r/Utils/QidiPrinterAgent.hpp @@ -1,64 +1,28 @@ #ifndef __QIDI_PRINTER_AGENT_HPP__ #define __QIDI_PRINTER_AGENT_HPP__ -#include "OrcaPrinterAgent.hpp" +#include "MoonrakerPrinterAgent.hpp" -#include -#include #include -#include -#include #include -#include #include namespace Slic3r { -class QidiPrinterAgent final : public OrcaPrinterAgent +class QidiPrinterAgent final : public MoonrakerPrinterAgent { public: explicit QidiPrinterAgent(std::string log_dir); - ~QidiPrinterAgent() override; + ~QidiPrinterAgent() override = default; static AgentInfo get_agent_info_static(); AgentInfo get_agent_info() override { return get_agent_info_static(); } - int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; - int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) 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; - bool start_discovery(bool start, bool sending) override; - int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override; - - int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override; - int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override; - int set_on_message_fn(OnMessageFn fn) override; - 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; - - FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } + // Override filament sync (Qidi-specific implementation) void fetch_filament_info(std::string dev_id) override; private: - struct PrinthostConfig - { - std::string host; - std::string port; - std::string api_key; - std::string base_url; - std::string model_id; - std::string model_name; - }; - - struct QidiDeviceInfo - { - std::string dev_id; - std::string dev_name; - std::string model_id; - std::string version; - }; - + // Qidi-specific device info (extends base MoonrakerDeviceInfo with model_id) struct QidiSlotInfo { int slot_index = 0; @@ -74,62 +38,15 @@ private: std::map filaments; }; - int handle_request(const std::string& dev_id, const std::string& json_str); - int send_version_info(const std::string& dev_id); - int send_access_code(const std::string& dev_id); - - bool get_printhost_config(PrinthostConfig& config) const; - bool fetch_device_info(const std::string& base_url, const std::string& api_key, QidiDeviceInfo& info, std::string& error) const; - bool fetch_server_info(const std::string& base_url, const std::string& api_key, std::string& version, std::string& error) const; - bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set& objects, std::string& error) const; - - std::string resolve_host(const std::string& dev_id) const; - std::string resolve_api_key(const std::string& dev_id, const std::string& fallback) const; - void store_host(const std::string& dev_id, const std::string& host, const std::string& api_key); - - bool fetch_slot_info(const std::string& base_url, - const std::string& api_key, - std::vector& slots, - int& box_count, - std::string& error) const; + // Qidi-specific methods + bool fetch_slot_info(const std::string& base_url, const std::string& api_key, std::vector& slots, int& box_count, std::string& error) const; bool fetch_filament_dict(const std::string& base_url, const std::string& api_key, QidiFilamentDict& dict, std::string& error) const; + // Static helpers static void parse_ini_section(const std::string& content, const std::string& section_name, std::map& result); static void parse_filament_sections(const std::string& content, std::map& result); - static std::string normalize_color(const std::string& color); static std::string map_filament_type_to_setting_id(const std::string& filament_type); - - 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); - void dispatch_message(const std::string& dev_id, const std::string& payload); - 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(const std::string& dev_id, const std::string& payload); - void update_status_cache(const nlohmann::json& updates); - nlohmann::json build_print_payload_locked(const nlohmann::json* ams_override) const; - - mutable std::mutex state_mutex; - std::map host_by_device; - std::map api_key_by_device; - std::string ssdp_announced_host; - std::string ssdp_announced_id; - OnMsgArrivedFn on_ssdp_msg_fn; - OnPrinterConnectedFn on_printer_connected_fn; - OnLocalConnectedFn on_local_connect_fn; - OnMessageFn on_message_fn; - OnMessageFn on_local_message_fn; - QueueOnMainFn queue_on_main_fn; - - mutable std::mutex payload_mutex; - nlohmann::json status_cache; - nlohmann::json last_ams_payload; - - std::atomic ws_stop{false}; - std::atomic ws_last_emit_ms{0}; - std::thread ws_thread; }; } // namespace Slic3r