#include "MoonrakerPrinterAgent.hpp" #include "Http.hpp" #include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "nlohmann/json.hpp" #include #include #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; 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()) { return ""; } if (path.empty()) { return base_url; } if (base_url.back() == '/' && path.front() == '/') { return base_url.substr(0, base_url.size() - 1) + path; } if (base_url.back() != '/' && path.front() != '/') { return base_url + "/" + 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; } // Sanitize filename to prevent path traversal attacks // Extracts only the basename, removing any path components std::string sanitize_filename(const std::string& filename) { if (filename.empty()) { return "print.gcode"; } namespace fs = boost::filesystem; fs::path p(filename); std::string basename = p.filename().string(); if (basename.empty() || basename == "." || basename == "..") { return "print.gcode"; } return basename; } 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"; } } // namespace namespace Slic3r { const std::string MoonrakerPrinterAgent_VERSION = "1.0.0"; MoonrakerPrinterAgent::MoonrakerPrinterAgent(std::string log_dir) : m_cloud_agent(nullptr) { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Constructor - log_dir=" << log_dir; (void) log_dir; } MoonrakerPrinterAgent::~MoonrakerPrinterAgent() { disconnect_printer(); // This will handle thread cleanup } AgentInfo MoonrakerPrinterAgent::get_agent_info_static() { return AgentInfo{.id = "moonraker", .name = "Moonraker Printer Agent", .version = MoonrakerPrinterAgent_VERSION, .description = "Klipper/Moonraker printer agent"}; } void MoonrakerPrinterAgent::set_cloud_agent(std::shared_ptr cloud) { std::lock_guard lock(state_mutex); m_cloud_agent = cloud; BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: Cloud agent set"; } int MoonrakerPrinterAgent::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 MoonrakerPrinterAgent::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 MoonrakerPrinterAgent::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) << "MoonrakerPrinterAgent: connect_printer missing host"; dispatch_local_connect(ConnectStatusFailed, dev_id, "host_missing"); return BAMBU_NETWORK_ERR_INVALID_HANDLE; } if (dev_id.empty()) { dev_id = extract_host(base_url); } // Check if connection already in progress { std::lock_guard lock(connect_mutex); if (connect_in_progress.load()) { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Connection already in progress, waiting..."; // Don't reject - wait for previous connection to complete // This can happen if MonitorPanel triggers connect while previous connect is still running } else { connect_in_progress.store(true); connect_stop_requested.store(false); } } // Wait for previous connection thread to finish if (connect_thread.joinable()) { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Waiting for previous connection thread..."; connect_thread.join(); } // Now we can start a new connection { std::lock_guard lock(connect_mutex); connect_in_progress.store(true); connect_stop_requested.store(false); } // Stop existing status stream and clear state stop_status_stream(); { std::lock_guard lock(payload_mutex); status_cache = nlohmann::json::object(); } ws_last_emit_ms.store(0); store_host(dev_id, base_url, api_key); // Launch connection in background thread connect_thread = std::thread([this, dev_id, base_url, api_key]() { perform_connection_async(dev_id, base_url, api_key); }); // Return immediately - UI is not blocked BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: connect_printer launched in background - dev_id=" << dev_id; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::disconnect_printer() { // Stop connection thread if running { std::lock_guard lock(connect_mutex); if (connect_in_progress.load()) { connect_stop_requested.store(true); // Wake up any sleeping connect_cv.notify_all(); } } // Wait for connection thread to finish (with timeout) if (connect_thread.joinable()) { connect_thread.join(); } stop_status_stream(); return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::check_cert() { BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: check_cert (stub)"; return BAMBU_NETWORK_SUCCESS; } void MoonrakerPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) { BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: install_device_cert (stub) - dev_id=" << dev_id << ", lan_only=" << lan_only; } bool MoonrakerPrinterAgent::start_discovery(bool start, bool sending) { (void) sending; if (start) { announce_printhost_device(); } return true; } int MoonrakerPrinterAgent::ping_bind(std::string ping_code) { BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: ping_bind (stub) - ping_code=" << ping_code; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::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); MoonrakerDeviceInfo info; std::string error; if (!fetch_device_info(base_url, api_key, info, error)) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: bind_detect failed: " << error; return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; } detect.dev_id = info.dev_id.empty() ? dev_ip : info.dev_id; // 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; detect.model_id = "moonraker"; detect.version = info.version; detect.connect_type = "lan"; detect.bind_state = "free"; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::bind( std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) { BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: bind (stub) - dev_id=" << dev_id; (void) dev_ip; (void) sec_link; (void) timezone; (void) improved; (void) update_fn; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::unbind(std::string dev_id) { BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: unbind (stub) - dev_id=" << dev_id; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::request_bind_ticket(std::string* ticket) { BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: request_bind_ticket (stub)"; if (ticket) *ticket = ""; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::set_server_callback(OnServerErrFn fn) { std::lock_guard lock(state_mutex); on_server_err_fn = fn; return BAMBU_NETWORK_SUCCESS; } std::string MoonrakerPrinterAgent::get_user_selected_machine() { std::lock_guard lock(state_mutex); return selected_machine; } int MoonrakerPrinterAgent::set_user_selected_machine(std::string dev_id) { std::lock_guard lock(state_mutex); selected_machine = dev_id; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: start_print (stub) - task_name=" << params.task_name; (void) update_fn; (void) cancel_fn; (void) wait_fn; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: start_local_print_with_record (stub)"; (void) params; (void) update_fn; (void) cancel_fn; (void) wait_fn; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { (void) wait_fn; if (update_fn) update_fn(PrintingStageCreate, 0, "Preparing..."); const std::string base_url = resolve_host(params.dev_id); if (base_url.empty()) { return BAMBU_NETWORK_ERR_INVALID_HANDLE; } const std::string api_key = resolve_api_key(params.dev_id, params.password); std::string filename = params.filename; if (filename.empty()) { filename = params.task_name; } if (!boost::iends_with(filename, ".gcode")) { filename += ".gcode"; } // Sanitize filename to prevent path traversal attacks std::string safe_filename = sanitize_filename(filename); // Upload only, don't start print if (!upload_gcode(params.filename, safe_filename, base_url, api_key, update_fn, cancel_fn)) { return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; } if (update_fn) update_fn(PrintingStageFinished, 100, "File uploaded"); return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) { if (update_fn) update_fn(PrintingStageCreate, 0, "Preparing..."); // Check cancellation if (cancel_fn && cancel_fn()) { return BAMBU_NETWORK_ERR_CANCELED; } const std::string base_url = resolve_host(params.dev_id); if (base_url.empty()) { return BAMBU_NETWORK_ERR_INVALID_HANDLE; } const std::string api_key = resolve_api_key(params.dev_id, params.password); // Determine the G-code file to upload // params.filename may be .3mf, params.dst_file contains actual G-code std::string gcode_path = params.filename; if (!params.dst_file.empty()) { gcode_path = params.dst_file; } // Check if file exists and has .gcode extension namespace fs = boost::filesystem; fs::path source_path(gcode_path); if (!fs::exists(source_path)) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: G-code file does not exist: " << gcode_path; return BAMBU_NETWORK_ERR_FILE_NOT_EXIST; } // Extract filename for upload (relative to gcodes root) std::string upload_filename = source_path.filename().string(); if (!boost::iends_with(upload_filename, ".gcode")) { upload_filename += ".gcode"; } // Sanitize filename to prevent path traversal attacks (extra safety) upload_filename = sanitize_filename(upload_filename); // Upload file if (update_fn) update_fn(PrintingStageUpload, 0, "Uploading G-code..."); if (!upload_gcode(gcode_path, upload_filename, base_url, api_key, update_fn, cancel_fn)) { return BAMBU_NETWORK_ERR_PRINT_LP_UPLOAD_FTP_FAILED; } // Check cancellation if (cancel_fn && cancel_fn()) { return BAMBU_NETWORK_ERR_CANCELED; } // Start print via gcode script (simpler than JSON-RPC) if (update_fn) update_fn(PrintingStageSending, 0, "Starting print..."); std::string gcode = "SDCARD_PRINT_FILE FILENAME=" + upload_filename; if (!send_gcode(params.dev_id, gcode)) { return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED; } if (update_fn) update_fn(PrintingStageFinished, 100, "Print started"); return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) { BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: start_sdcard_print (stub)"; (void) params; (void) update_fn; (void) cancel_fn; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::set_on_ssdp_msg_fn(OnMsgArrivedFn fn) { { std::lock_guard lock(state_mutex); on_ssdp_msg_fn = fn; } // Call announce_printhost_device() outside the lock to avoid deadlock // since announce_printhost_device() also acquires state_mutex if (fn) { announce_printhost_device(); } return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::set_on_printer_connected_fn(OnPrinterConnectedFn fn) { std::lock_guard lock(state_mutex); on_printer_connected_fn = fn; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) { std::lock_guard lock(state_mutex); on_subscribe_failure_fn = fn; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::set_on_message_fn(OnMessageFn fn) { std::lock_guard lock(state_mutex); on_message_fn = fn; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::set_on_user_message_fn(OnMessageFn fn) { std::lock_guard lock(state_mutex); on_user_message_fn = fn; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::set_on_local_connect_fn(OnLocalConnectedFn fn) { std::lock_guard lock(state_mutex); on_local_connect_fn = fn; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::set_on_local_message_fn(OnMessageFn fn) { std::lock_guard lock(state_mutex); on_local_message_fn = fn; return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) { std::lock_guard lock(state_mutex); queue_on_main_fn = fn; return BAMBU_NETWORK_SUCCESS; } 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; } int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) { BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: handle_request received: " << json_str; auto json = nlohmann::json::parse(json_str, nullptr, false); if (json.is_discarded()) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Invalid JSON request"; return BAMBU_NETWORK_ERR_INVALID_RESULT; } // Handle info commands 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); } } // Handle system commands 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); } } // Handle print commands if (json.contains("print") && json["print"].contains("command")) { const auto& command = json["print"]["command"]; if (!command.is_string()) { BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: print command is not a string"; return BAMBU_NETWORK_ERR_INVALID_RESULT; } const std::string cmd = command.get(); BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Received print command: " << cmd; // Handle gcode_line command - this is how G-code commands are sent from OrcaSlicer if (cmd == "gcode_line") { if (!json["print"].contains("param") || !json["print"]["param"].is_string()) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: gcode_line missing param value, full json: " << json_str; return BAMBU_NETWORK_ERR_INVALID_RESULT; } std::string gcode = json["print"]["param"].get(); // Extract sequence_id from request if present std::string sequence_id; if (json["print"].contains("sequence_id") && json["print"]["sequence_id"].is_string()) { sequence_id = json["print"]["sequence_id"].get(); } nlohmann::json response; response["print"]["command"] = "gcode_line"; if (!sequence_id.empty()) { response["print"]["sequence_id"] = sequence_id; } response["print"]["param"] = gcode; if (send_gcode(dev_id, gcode)) { response["print"]["result"] = "success"; dispatch_message(dev_id, response.dump()); return BAMBU_NETWORK_SUCCESS; } response["print"]["result"] = "failed"; dispatch_message(dev_id, response.dump()); return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; } // ===== NEW: Print control commands ===== if (cmd == "pause") { return pause_print(dev_id); } if (cmd == "resume") { return resume_print(dev_id); } if (cmd == "stop") { return cancel_print(dev_id); } // Bed temperature - UI sends "temp" field if (cmd == "set_bed_temp") { if (json["print"].contains("temp") && json["print"]["temp"].is_number()) { int temp = json["print"]["temp"].get(); std::string gcode = "SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=" + std::to_string(temp); send_gcode(dev_id, gcode); return BAMBU_NETWORK_SUCCESS; } } // Nozzle temperature - UI sends "target_temp" and "extruder_index" fields if (cmd == "set_nozzle_temp") { if (json["print"].contains("target_temp") && json["print"]["target_temp"].is_number()) { int temp = json["print"]["target_temp"].get(); int extruder_idx = 0; // Default to main extruder if (json["print"].contains("extruder_index") && json["print"]["extruder_index"].is_number()) { extruder_idx = json["print"]["extruder_index"].get(); } std::string heater = (extruder_idx == 0) ? "extruder" : "extruder" + std::to_string(extruder_idx); std::string gcode = "SET_HEATER_TEMPERATURE HEATER=" + heater + " TARGET=" + std::to_string(temp); send_gcode(dev_id, gcode); return BAMBU_NETWORK_SUCCESS; } } if (cmd == "home") { return send_gcode(dev_id, "G28") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; } } return BAMBU_NETWORK_SUCCESS; } bool MoonrakerPrinterAgent::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_name = printer_cfg.opt_string("printer_model"); config.base_url = normalize_base_url(config.host, config.port); return !config.base_url.empty(); } 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) { 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, "/server/info"); if (!fetch_json(url, json)) { return false; } 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", ""); return true; } bool MoonrakerPrinterAgent::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 MoonrakerPrinterAgent::fetch_server_info_json(const std::string& base_url, const std::string& api_key, nlohmann::json& info, 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; } info = nlohmann::json::parse(response_body, nullptr, false, true); if (info.is_discarded()) { error = "Invalid JSON response"; return false; } return true; } bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const { std::string url = join_url(base_url, "/printer/objects/query?print_stats&virtual_sdcard&extruder&heater_bed&fan"); 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_code) { if (status_code == 200) { response_body = body; success = true; } else { http_error = "HTTP error: " + std::to_string(status_code); } }) .on_error([&](std::string body, std::string err, unsigned status_code) { http_error = err; if (status_code > 0) { http_error += " (HTTP " + std::to_string(status_code) + ")"; } }) .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; } if (!json.contains("result") || !json["result"].contains("status")) { error = "Unexpected JSON structure"; return false; } status = json["result"]["status"]; return true; } bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::string& gcode) const { const std::string base_url = resolve_host(dev_id); if (base_url.empty()) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: send_gcode - empty base_url for dev_id=" << dev_id; return false; } const std::string api_key = resolve_api_key(dev_id, ""); nlohmann::json payload; payload["script"] = gcode; std::string payload_str = payload.dump(); BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: send_gcode to " << base_url << " with payload: " << payload_str; std::string response_body; bool success = false; std::string http_error; auto http = Http::post(join_url(base_url, "/printer/gcode/script")); if (!api_key.empty()) { http.header("X-Api-Key", api_key); } http.header("Content-Type", "application/json") .set_post_body(payload_str) .timeout_connect(10) .timeout_max(30) .on_complete([&](std::string body, unsigned status_code) { BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: send_gcode response status=" << status_code << " body=" << body; if (status_code == 200) { response_body = body; success = true; } else { http_error = "HTTP error: " + std::to_string(status_code); } }) .on_error([&](std::string body, std::string err, unsigned status_code) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: send_gcode error - body=" << body << " err=" << err << " status=" << status_code; http_error = err; if (status_code > 0) { http_error += " (HTTP " + std::to_string(status_code) + ")"; } }) .perform_sync(); if (!success) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: send_gcode failed: " << http_error; return false; } BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: sent gcode successfully: " << gcode; return true; } bool MoonrakerPrinterAgent::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 MoonrakerPrinterAgent::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) << "MoonrakerPrinterAgent: 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 MoonrakerPrinterAgent::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 MoonrakerPrinterAgent::announce_printhost_device() { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device() called"; PrinthostConfig config; if (!get_printhost_config(config)) { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - no printhost config"; return; } const std::string base_url = config.base_url; if (base_url.empty()) { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - empty base_url"; return; } BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - base_url=" << base_url; OnMsgArrivedFn ssdp_fn; { std::lock_guard lock(state_mutex); ssdp_fn = on_ssdp_msg_fn; if (!ssdp_fn) { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - no ssdp callback"; return; } if (ssdp_announced_host == base_url && !ssdp_announced_id.empty()) { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: announce_printhost_device - already announced"; return; } } const std::string dev_id = extract_host(base_url); const std::string api_key = normalize_api_key(config.api_key); // Try to fetch actual device name from Moonraker // Priority: 1) Moonraker hostname, 2) Preset model name, 3) Generic fallback std::string dev_name; MoonrakerDeviceInfo info; std::string fetch_error; if (fetch_device_info(base_url, api_key, info, fetch_error) && !info.dev_name.empty()) { dev_name = info.dev_name; BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Got device name from printer: " << dev_name; } else { dev_name = config.model_name.empty() ? "Moonraker Printer" : config.model_name; BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Using fallback device name: " << dev_name << " (fetch_error=" << fetch_error << ")"; } 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); app_config->set_str("user_access_code", dev_id, access_code); } store_host(dev_id, base_url, 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"] = "moonraker"; 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; // Set this as the selected machine if nothing is currently selected // This ensures auto-connect works when MonitorPanel opens if (selected_machine.empty()) { selected_machine = dev_id; BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Auto-selected machine: " << dev_id; } } } void MoonrakerPrinterAgent::dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg) { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: dispatch_local_connect state=" << state << " dev_id=" << dev_id << " msg=" << 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) { BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: dispatch_local_connect - no callback registered!"; return; } auto dispatch = [state, dev_id, msg, local_fn]() { local_fn(state, dev_id, msg); }; if (queue_fn) { queue_fn(dispatch); } else { dispatch(); } } void MoonrakerPrinterAgent::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 MoonrakerPrinterAgent::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 MoonrakerPrinterAgent::stop_status_stream() { ws_stop.store(true); if (ws_thread.joinable()) { ws_thread.join(); } } void MoonrakerPrinterAgent::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) << "MoonrakerPrinterAgent: websocket endpoint invalid for base_url=" << base_url; return; } if (endpoint.secure) { BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket wss not supported for base_url=" << base_url; return; } // Reconnection logic ws_reconnect_requested.store(false); // Reset reconnect flag int retry_count = 0; const int max_retries = 10; const int base_delay_ms = 1000; while (!ws_stop.load() && retry_count < max_retries) { bool connection_lost = false; // Flag to distinguish clean shutdown from unexpected disconnect 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); // Send client identification nlohmann::json identify; identify["jsonrpc"] = "2.0"; identify["method"] = "server.connection.identify"; identify["params"]["client_name"] = "OrcaSlicer"; identify["params"]["version"] = MoonrakerPrinterAgent_VERSION; identify["params"]["type"] = "agent"; identify["params"]["url"] = "https://github.com/SoftFever/OrcaSlicer"; identify["id"] = 0; ws.write(net::buffer(identify.dump())); 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)) { // Store available_objects in member variable for feature detection { std::lock_guard lock(payload_mutex); this->available_objects = std::move(available_objects); } std::string objects_str; for (const auto& name : this->available_objects) { if (!objects_str.empty()) objects_str += ", "; objects_str += name; } if (this->available_objects.count("heater_bed") != 0) { subscribe_objects.insert("heater_bed"); } // Only subscribe to "fan" if it exists (standard Moonraker API) if (this->available_objects.count("fan") != 0) { subscribe_objects.insert("fan"); } else { } // Add toolhead for homing status if (this->available_objects.count("toolhead") != 0) { subscribe_objects.insert("toolhead"); } // Add display_status for layer info (if available) if (this->available_objects.count("display_status") != 0) { subscribe_objects.insert("display_status"); } for (const auto& name : this->available_objects) { if (name == "extruder" || name.rfind("extruder", 0) == 0) { subscribe_objects.insert(name); if (name == "extruder") { break; } } } } else { subscribe_objects.insert("extruder"); subscribe_objects.insert("heater_bed"); subscribe_objects.insert("toolhead"); // Add toolhead as fallback subscribe_objects.insert("fan"); // Try to subscribe to fan as fallback } 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())); // Read loop 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(); } dispatch_message(dev_id, message.dump()); ws_last_emit_ms.store(now_ms); } continue; } if (ec == websocket::error::closed) { connection_lost = true; break; } if (ec) { BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket read error: " << ec.message(); connection_lost = true; break; } handle_ws_message(dev_id, beast::buffers_to_string(buffer.data())); // Check if handle_ws_message triggered reconnection request if (ws_reconnect_requested.exchange(false)) { connection_lost = true; break; } } beast::error_code ec; ws.close(websocket::close_code::normal, ec); // Only reset retry count on clean shutdown (not connection_lost) if (!connection_lost && !ws_stop.load()) { retry_count = 0; } } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket disconnected: " << e.what(); connection_lost = true; } // Exit loop on clean shutdown if (!connection_lost) { break; } // Check if we should stop reconnection attempts if (ws_stop.load()) { break; } // Exponential backoff before reconnection 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)); retry_count++; } if (retry_count >= max_retries) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Max reconnection attempts reached"; dispatch_local_connect(ConnectStatusLost, dev_id, "max_retries"); } } void MoonrakerPrinterAgent::handle_ws_message(const std::string& dev_id, const std::string& payload) { auto json = nlohmann::json::parse(payload, nullptr, false); if (json.is_discarded()) { BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Invalid WebSocket message JSON"; return; } bool updated = false; // Check for subscription response (has "result.status") if (json.contains("result") && json["result"].contains("status") && json["result"]["status"].is_object()) { update_status_cache(json["result"]["status"]); updated = true; } // Check for status update notifications 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; } // Handle Klippy disconnect - update status and trigger reconnection else if (method == "notify_klippy_disconnected") { // Klippy disconnected - update status to reflect disconnect state nlohmann::json updates; updates["print_stats"]["state"] = "error"; update_status_cache(updates); updated = true; // Set flag to trigger reconnection after dispatching the status update ws_reconnect_requested.store(true); BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Klippy disconnected, triggering reconnection"; } } if (updated) { nlohmann::json message; { std::lock_guard lock(payload_mutex); message = build_print_payload_locked(); } BOOST_LOG_TRIVIAL(trace) << "MoonrakerPrinterAgent: Dispatching payload: " << message.dump(); 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 MoonrakerPrinterAgent::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 MoonrakerPrinterAgent::build_print_payload_locked() const { nlohmann::json payload; payload["print"]["command"] = "push_status"; payload["print"]["msg"] = 0; payload["print"]["support_mqtt_alive"] = true; 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; // ===== NEW: Print Stage ===== // Map Moonraker state to Bambu stage numbers int mc_print_stage = 0; if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("state")) { std::string mr_state = status_cache["print_stats"]["state"].get(); if (mr_state == "printing") mc_print_stage = 1; else if (mr_state == "paused") mc_print_stage = 2; else if (mr_state == "complete") mc_print_stage = 3; else if (mr_state == "error") mc_print_stage = 4; } payload["print"]["mc_print_stage"] = mc_print_stage; // ===== NEW: Error Codes ===== // Leave mc_print_error_code and print_error at 0 // UI expects numeric HMS codes - setting to 1 shows generic error dialog // Only set if real mapping from Moonraker error strings to HMS codes is defined payload["print"]["mc_print_error_code"] = 0; payload["print"]["print_error"] = 0; // ===== NEW: Home Flag ===== // Map homed axes to bit field: X=bit0, Y=bit1, Z=bit2 // WARNING: This only sets bits 0-2, clearing support flags (bit 3+) // Bit 3 = 220V voltage, bit 4 = auto recovery, etc. // This is acceptable for Moonraker (no AMS, different feature set) int home_flag = 0; if (status_cache.contains("toolhead") && status_cache["toolhead"].contains("homed_axes")) { std::string homed = status_cache["toolhead"]["homed_axes"].get(); if (homed.find('X') != std::string::npos) home_flag |= 1; // bit 0 if (homed.find('Y') != std::string::npos) home_flag |= 2; // bit 1 if (homed.find('Z') != std::string::npos) home_flag |= 4; // bit 2 } payload["print"]["home_flag"] = home_flag; // ===== NEW: Temperature Ranges ===== // Moonraker doesn't provide this via API - use hardcoded defaults payload["print"]["nozzle_temp_range"] = {100, 370}; // Typical Klipper range payload["print"]["bed_temp_range"] = {0, 120}; // Typical bed range // ===== NEW: Feature Flags ===== payload["print"]["support_send_to_sd"] = true; // Detect bed_leveling support from available objects (bed_mesh or probe) // Default to 0 (not supported) if neither object exists bool has_bed_leveling = (available_objects.count("bed_mesh") != 0 || available_objects.count("probe") != 0); payload["print"]["support_bed_leveling"] = has_bed_leveling ? 1 : 0; 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(); } } // Handle fan speed - only if Moonraker provides "fan" object (standard API) if (status_cache.contains("fan") && status_cache["fan"].is_object() && !status_cache["fan"].empty()) { 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; } else if (fan.contains("power") && fan["power"].is_number()) { double power = fan["power"].get(); int pwm = static_cast(power * 255.0 + 0.5); pwm = std::clamp(pwm, 0, 255); payload["print"]["fan_gear"] = pwm; } } // If "fan" object doesn't exist, don't include fan_gear in payload 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(); } // ===== NEW: G-code File Path ===== if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("filename")) { payload["print"]["gcode_file"] = status_cache["print_stats"]["filename"]; } 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; BOOST_LOG_TRIVIAL(trace) << "MoonrakerPrinterAgent: Built payload with gcode_state=" << state; return payload; } std::string MoonrakerPrinterAgent::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 ""; } std::string MoonrakerPrinterAgent::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 MoonrakerPrinterAgent::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; } } void MoonrakerPrinterAgent::dispatch_message(const std::string& dev_id, const std::string& payload) { BOOST_LOG_TRIVIAL(trace) << "MoonrakerPrinterAgent: dispatch_message dev_id=" << dev_id << " payload_size=" << payload.size(); 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; } if (!local_fn && !cloud_fn) { BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: dispatch_message - no message callback registered!"; return; } 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(); } } bool MoonrakerPrinterAgent::upload_gcode( const std::string& local_path, const std::string& filename, const std::string& base_url, const std::string& api_key, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) { namespace fs = boost::filesystem; // Validate file exists fs::path source_path(local_path); if (!fs::exists(source_path)) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: File does not exist: " << local_path; return false; } // Check file size std::uintmax_t file_size = fs::file_size(source_path); if (file_size > 1024 * 1024 * 1024) { // 1GB limit BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: File too large: " << file_size << " bytes"; return false; } // Sanitize filename to prevent path traversal attacks std::string safe_filename = sanitize_filename(filename); bool result = true; std::string http_error; // Use Http::form_add and Http::form_add_file auto http = Http::post(join_url(base_url, "/server/files/upload")); if (!api_key.empty()) { http.header("X-Api-Key", api_key); } http.form_add("root", "gcodes") // Upload to gcodes directory .form_add("print", "false") // Don't auto-start print .form_add_file("file", source_path.string(), safe_filename) .timeout_connect(10) .timeout_max(300) // 5 minutes for large files .on_complete([&](std::string body, unsigned status) { BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: Upload complete: HTTP " << status << " body: " << body; }) .on_error([&](std::string body, std::string err, unsigned status) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Upload error: " << err << " HTTP " << status; http_error = err; result = false; }) .on_progress([&](Http::Progress progress, bool& cancel) { // Check for cancellation via WasCancelledFn if (cancel_fn && cancel_fn()) { cancel = true; result = false; return; } // Report progress via OnUpdateStatusFn if (update_fn && progress.ultotal > 0) { int percent = static_cast((progress.ulnow * 100) / progress.ultotal); update_fn(PrintingStageUpload, percent, "Uploading..."); } }) .perform_sync(); if (!result) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Upload failed: " << http_error; return false; } return true; } int MoonrakerPrinterAgent::pause_print(const std::string& dev_id) { const std::string base_url = resolve_host(dev_id); const std::string api_key = resolve_api_key(dev_id, ""); nlohmann::json request; request["jsonrpc"] = "2.0"; request["method"] = "printer.print.pause"; request["id"] = next_jsonrpc_id++; std::string response; // For JSON-RPC over HTTP, we need to use POST to /printer/print/pause // But Moonraker also supports this via WebSocket // For now, send via gcode script which is simpler std::string gcode = "PAUSE"; return send_gcode(dev_id, gcode) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; } int MoonrakerPrinterAgent::resume_print(const std::string& dev_id) { std::string gcode = "RESUME"; return send_gcode(dev_id, gcode) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; } int MoonrakerPrinterAgent::cancel_print(const std::string& dev_id) { std::string gcode = "CANCEL_PRINT"; return send_gcode(dev_id, gcode) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; } bool MoonrakerPrinterAgent::send_jsonrpc_command( const std::string& base_url, const std::string& api_key, const nlohmann::json& request, std::string& response) const { std::string request_str = request.dump(); std::string url = join_url(base_url, "/printer/print/start"); bool success = false; std::string http_error; auto http = Http::post(url); if (!api_key.empty()) { http.header("X-Api-Key", api_key); } http.header("Content-Type", "application/json") .set_post_body(request_str) .timeout_connect(10) .timeout_max(30) .on_complete([&](std::string body, unsigned status) { if (status == 200) { response = body; success = true; } else { http_error = "HTTP " + std::to_string(status); } }) .on_error([&](std::string body, std::string err, unsigned status) { http_error = err; }) .perform_sync(); if (!success) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: JSON-RPC command failed: " << http_error; } return success; } void MoonrakerPrinterAgent::perform_connection_async( const std::string& dev_id, const std::string& base_url, const std::string& api_key) { int result = BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; std::string error_msg; try { // Check Klippy state nlohmann::json server_info; if (!fetch_server_info_json(base_url, api_key, server_info, error_msg)) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Failed to fetch server info: " << error_msg; dispatch_local_connect(ConnectStatusFailed, dev_id, "server_info_failed"); finish_connection(); return; } nlohmann::json result_json = server_info.contains("result") ? server_info["result"] : server_info; std::string klippy_state = result_json.value("klippy_state", ""); // Poll for Klippy ready state (with stop check) if (klippy_state == "startup") { for (int i = 0; i < 30; i++) { // 30 second max { std::unique_lock lock(connect_mutex); if (connect_stop_requested.load()) { result = BAMBU_NETWORK_ERR_CANCELED; break; } } std::this_thread::sleep_for(std::chrono::seconds(1)); if (fetch_server_info_json(base_url, api_key, server_info, error_msg)) { result_json = server_info.contains("result") ? server_info["result"] : server_info; klippy_state = result_json.value("klippy_state", ""); if (klippy_state == "ready") break; } } } // Check final state if (klippy_state != "ready" && result == BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED) { std::string state_message = result_json.value("state_message", "Unknown error"); BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Klippy not ready: " << klippy_state << " - " << state_message; error_msg = "klippy_not_ready:" + klippy_state; dispatch_local_connect(ConnectStatusFailed, dev_id, error_msg); finish_connection(); return; } // Query initial status nlohmann::json initial_status; if (query_printer_status(base_url, api_key, initial_status, error_msg)) { { update_status_cache(initial_status); } BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Initial status queried successfully"; } else { BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Initial status query failed: " << error_msg; } // Start WebSocket status stream start_status_stream(dev_id, base_url, api_key); // Success! result = BAMBU_NETWORK_SUCCESS; } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Connection exception: " << e.what(); error_msg = std::string("exception: ") + e.what(); result = BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; } // Dispatch final result to UI if (result == BAMBU_NETWORK_SUCCESS) { dispatch_local_connect(ConnectStatusOk, dev_id, "0"); dispatch_printer_connected(dev_id); BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: connect_printer completed - dev_id=" << dev_id; } else if (result != BAMBU_NETWORK_ERR_CANCELED) { dispatch_local_connect(ConnectStatusFailed, dev_id, error_msg); } finish_connection(); } void MoonrakerPrinterAgent::finish_connection() { std::lock_guard lock(connect_mutex); connect_in_progress.store(false); } } // namespace Slic3r