diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 768d0c2f31..ac5602c569 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -3,6 +3,8 @@ #include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/DeviceCore/DevFilaSystem.h" +#include "slic3r/GUI/DeviceCore/DevManager.h" #include "nlohmann/json.hpp" #include #include @@ -454,7 +456,239 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) return BAMBU_NETWORK_SUCCESS; } -bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) { return false; } +void MoonrakerPrinterAgent::build_ams_payload(int ams_count, const std::vector& trays) +{ + + // Look up MachineObject via DeviceManager + auto* dev_manager = GUI::wxGetApp().getDeviceManager(); + if (!dev_manager) { + return; + } + MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id); + if (!obj) { + return; + } + + + // Color normalization helper (handles #RRGGBB, 0xRRGGBB -> RRGGBBAA) + auto normalize_color = [](const std::string& color) -> std::string { + std::string value = color; + boost::trim(value); + + // Remove 0x or 0X prefix if present + if (value.size() >= 2 && (value.rfind("0x", 0) == 0 || value.rfind("0X", 0) == 0)) { + value = value.substr(2); + } + // Remove # prefix if present + if (!value.empty() && value[0] == '#') { + value = value.substr(1); + } + + // Extract only hex digits + std::string normalized; + for (char c : value) { + if (std::isxdigit(static_cast(c))) { + normalized.push_back(static_cast(std::toupper(static_cast(c)))); + } + } + + // If 6 hex digits, add FF alpha + if (normalized.size() == 6) { + normalized += "FF"; + } + + // Validate length - return default if invalid + if (normalized.size() != 8) { + return "00000000"; + } + + return normalized; + }; + + // Build BBL-format JSON for DevFilaSystemParser::ParseV1_0 + nlohmann::json ams_json = nlohmann::json::object(); + nlohmann::json ams_array = nlohmann::json::array(); + + // Calculate ams_exist_bits and tray_exist_bits + unsigned long ams_exist_bits = 0; + unsigned long tray_exist_bits = 0; + + for (int ams_id = 0; ams_id < ams_count; ++ams_id) { + ams_exist_bits |= (1 << ams_id); + + nlohmann::json ams_unit = nlohmann::json::object(); + ams_unit["id"] = std::to_string(ams_id); + ams_unit["info"] = "2100"; // AMS_LITE type (2), main extruder (0) + + nlohmann::json tray_array = nlohmann::json::array(); + for (int slot_id = 0; slot_id < 4; ++slot_id) { + int slot_index = ams_id * 4 + slot_id; + + // Find tray with matching slot_index + const AmsTrayData* tray = nullptr; + for (const auto& t : trays) { + if (t.slot_index == slot_index) { + tray = &t; + break; + } + } + + nlohmann::json tray_json = nlohmann::json::object(); + tray_json["id"] = std::to_string(slot_id); + tray_json["tag_uid"] = "0000000000000000"; + + if (tray && tray->has_filament) { + tray_exist_bits |= (1 << slot_index); + + tray_json["tray_info_idx"] = tray->tray_info_idx; + tray_json["tray_type"] = tray->tray_type; + tray_json["tray_color"] = normalize_color(tray->tray_color); + + // Add temperature data if provided + if (tray->bed_temp > 0) { + tray_json["bed_temp"] = std::to_string(tray->bed_temp); + } + if (tray->nozzle_temp > 0) { + tray_json["nozzle_temp_max"] = std::to_string(tray->nozzle_temp); + } + } else { + tray_json["tray_info_idx"] = ""; + tray_json["tray_type"] = ""; + tray_json["tray_color"] = "00000000"; + } + + tray_array.push_back(tray_json); + } + ams_unit["tray"] = tray_array; + ams_array.push_back(ams_unit); + } + + // Format as hex strings (matching BBL protocol) + std::ostringstream ams_exist_ss; + ams_exist_ss << std::hex << std::uppercase << ams_exist_bits; + std::ostringstream tray_exist_ss; + tray_exist_ss << std::hex << std::uppercase << tray_exist_bits; + + ams_json["ams"] = ams_array; + ams_json["ams_exist_bits"] = ams_exist_ss.str(); + ams_json["tray_exist_bits"] = tray_exist_ss.str(); + + // Wrap in the expected structure for ParseV1_0 + nlohmann::json print_json = nlohmann::json::object(); + print_json["ams"] = ams_json; + + // Call the parser to populate DevFilaSystem + DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem(), false); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays"; + + // Set push counters so is_info_ready() returns true for pull-mode agents. + if (obj->m_push_count == 0) { + obj->m_push_count = 1; + } + if (obj->m_full_msg_count == 0) { + obj->m_full_msg_count = 1; + } + obj->last_push_time = std::chrono::system_clock::now(); +} + +bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) +{ + // Fetch AFC lane data from Moonraker database (inline) + std::string url = join_url(device_info.base_url, "/server/database/item?namespace=lane_data"); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!device_info.api_key.empty()) { + http.header("X-Api-Key", device_info.api_key); + } + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_filament_info: Failed to fetch lane data: " << http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_filament_info: Invalid JSON response"; + return false; + } + + // Expected structure: { "result": { "namespace": "lane_data", "value": { "lane1": {...}, ... } } } + if (!json.contains("result") || !json["result"].contains("value") || !json["result"]["value"].is_object()) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_filament_info: Unexpected JSON structure or no lane_data found"; + return false; + } + + // Parse response into AmsTrayData + const auto& value = json["result"]["value"]; + std::vector trays; + int max_lane_index = 0; + + for (const auto& [lane_key, lane_obj] : value.items()) { + if (!lane_obj.is_object()) { + continue; + } + + // Extract lane index from the "lane" field (tool number, 1-based) + std::string lane_str = lane_obj.value("lane", ""); + int lane_index = -1; + if (!lane_str.empty()) { + try { + lane_index = std::stoi(lane_str) - 1; // Convert to 0-based + } catch (...) { + lane_index = -1; + } + } + + if (lane_index < 0) { + continue; + } + + AmsTrayData tray; + tray.slot_index = lane_index; + tray.tray_color = lane_obj.value("color", ""); + tray.tray_type = lane_obj.value("material", ""); + tray.bed_temp = lane_obj.value("bed_temp", 0); + tray.nozzle_temp = lane_obj.value("nozzle_temp", 0); + tray.has_filament = !tray.tray_type.empty(); + tray.tray_info_idx = ""; // AFC doesn't provide setting IDs + + max_lane_index = std::max(max_lane_index, lane_index); + trays.push_back(tray); + } + + if (trays.empty()) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: No AFC lanes found"; + return false; + } + + // Calculate AMS count from max lane index (4 trays per AMS unit) + int ams_count = (max_lane_index + 4) / 4; + + // Build and parse the AMS payload + build_ams_payload(ams_count, trays); + return true; +} int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) { diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index 29b7f98715..b301ae0fd9 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -88,6 +88,20 @@ protected: bool use_ssl = false; } device_info; + // Shared tray data for AMS payload building (used by derived classes like QidiPrinterAgent) + struct AmsTrayData { + int slot_index = 0; // 0-based slot index + bool has_filament = false; + std::string tray_type; // Material type (e.g., "PLA", "ASA") + std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA) + std::string tray_info_idx; // Setting ID (optional) + int bed_temp = 0; // Optional + int nozzle_temp = 0; // Optional + }; + + // Build ams JSON and call parser + void build_ams_payload(int ams_count, const std::vector& trays); + // Methods that derived classes may need to override or access virtual bool init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index b0f4847c4c..46aa623610 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -1,8 +1,5 @@ #include "QidiPrinterAgent.hpp" #include "Http.hpp" -#include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/GUI/DeviceCore/DevFilaSystem.h" -#include "slic3r/GUI/DeviceCore/DevManager.h" #include "nlohmann/json.hpp" #include @@ -26,129 +23,43 @@ AgentInfo QidiPrinterAgent::get_agent_info_static() bool QidiPrinterAgent::fetch_filament_info(std::string dev_id) { - // Look up MachineObject via DeviceManager - auto* dev_manager = GUI::wxGetApp().getDeviceManager(); - if (!dev_manager) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: DeviceManager is null"; - return false; - } - MachineObject* obj = dev_manager->get_my_machine(dev_id); - if (!obj) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: MachineObject not found for dev_id=" << dev_id; - return false; - } - - std::vector slots; - int box_count = 0; - std::string error; - if (!fetch_slot_info(device_info.base_url, device_info.api_key, slots, box_count, error)) { - BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error; - return false; + std::string error; + + // 1. Fetch device info and infer series_id + std::string series_id; + { + MoonrakerDeviceInfo info; + if (fetch_device_info(device_info.base_url, device_info.api_key, info, error)) { + series_id = infer_series_id(info.model_id, info.dev_name); + } } + // 2. Fetch filament dictionary QidiFilamentDict dict; if (!fetch_filament_dict(device_info.base_url, device_info.api_key, dict, error)) { BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error; } - std::string series_id; - { - MoonrakerDeviceInfo info; - std::string device_error; - if (fetch_device_info(device_info.base_url, device_info.api_key, info, device_error)) { - series_id = infer_series_id(info.model_id, info.dev_name); - } + // 3. Fetch slot info and build AmsTrayData directly + std::vector trays; + int box_count = 0; + if (!fetch_slot_info(device_info.base_url, device_info.api_key, dict, series_id, trays, box_count, error)) { + BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error; + return false; } - auto build_setting_id = [&](const QidiSlotInfo& slot, const std::string& tray_type) { - const int vendor = (slot.vendor_type == 1) ? 1 : 0; - if (is_numeric(series_id) && slot.filament_type > 0) { - return "QD_" + series_id + "_" + std::to_string(vendor) + "_" + std::to_string(slot.filament_type); - } - return map_filament_type_to_setting_id(tray_type); - }; - - // Build BBL-format JSON for DevFilaSystemParser::ParseV1_0 - // The expected format matches BBL's print.push_status AMS subset - nlohmann::json ams_json = nlohmann::json::object(); - nlohmann::json ams_array = nlohmann::json::array(); - - // Calculate ams_exist_bits and tray_exist_bits - unsigned long ams_exist_bits = 0; - unsigned long tray_exist_bits = 0; - - for (int ams_id = 0; ams_id < box_count; ++ams_id) { - ams_exist_bits |= (1 << ams_id); - - nlohmann::json ams_unit = nlohmann::json::object(); - ams_unit["id"] = std::to_string(ams_id); - ams_unit["info"] = "2100"; // AMS_LITE type (2), main extruder (0) - - nlohmann::json tray_array = nlohmann::json::array(); - for (int slot_id = 0; slot_id < 4; ++slot_id) { - const int slot_index = ams_id * 4 + slot_id; - const QidiSlotInfo slot = slot_index < static_cast(slots.size()) ? slots[slot_index] : QidiSlotInfo{}; - - nlohmann::json tray_json = nlohmann::json::object(); - tray_json["id"] = std::to_string(slot_id); - tray_json["tag_uid"] = "0000000000000000"; - - if (slot.filament_exists) { - tray_exist_bits |= (1 << slot_index); - - std::string filament_type = "PLA"; - auto filament_it = dict.filaments.find(slot.filament_type); - if (filament_it != dict.filaments.end()) { - filament_type = filament_it->second; - } - std::string tray_type = normalize_filament_type(filament_type); - std::string setting_id = build_setting_id(slot, tray_type); - - std::string color = "FFFFFFFF"; - auto color_it = dict.colors.find(slot.color_index); - if (color_it != dict.colors.end()) { - color = normalize_color(color_it->second); - } - - tray_json["tray_info_idx"] = setting_id; - tray_json["tray_type"] = tray_type; - tray_json["tray_color"] = color; - } else { - tray_json["tray_info_idx"] = ""; - tray_json["tray_type"] = ""; - tray_json["tray_color"] = "00000000"; - } - - tray_array.push_back(tray_json); - } - ams_unit["tray"] = tray_array; - ams_array.push_back(ams_unit); - } - - // Format as hex strings (matching BBL protocol) - std::ostringstream ams_exist_ss; - ams_exist_ss << std::hex << std::uppercase << ams_exist_bits; - std::ostringstream tray_exist_ss; - tray_exist_ss << std::hex << std::uppercase << tray_exist_bits; - - ams_json["ams"] = ams_array; - ams_json["ams_exist_bits"] = ams_exist_ss.str(); - ams_json["tray_exist_bits"] = tray_exist_ss.str(); - - // Wrap in the expected structure for ParseV1_0 - nlohmann::json print_json = nlohmann::json::object(); - print_json["ams"] = ams_json; - - // Call the parser to populate DevFilaSystem - DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem(), false); + // 4. Build the AMS payload + build_ams_payload(box_count, trays); return true; } -bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, - const std::string& api_key, - std::vector& slots, - int& box_count, - std::string& error) const +bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, + const std::string& api_key, + const QidiFilamentDict& dict, + const std::string& series_id, + std::vector& trays, + int& box_count, + std::string& error) { std::string url = join_url(base_url, "/printer/objects/query?save_variables=variables"); for (int i = 0; i < 16; ++i) { @@ -207,26 +118,58 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, } const int max_slots = box_count * 4; - slots.clear(); - slots.reserve(max_slots); + trays.clear(); + trays.reserve(max_slots); + + // Lambda to build setting_id from slot data + auto build_setting_id = [&](int filament_type_idx, int vendor_type, const std::string& tray_type) { + const int vendor = (vendor_type == 1) ? 1 : 0; + if (is_numeric(series_id) && filament_type_idx > 0) { + return "QD_" + series_id + "_" + std::to_string(vendor) + "_" + std::to_string(filament_type_idx); + } + return map_filament_type_to_setting_id(tray_type); + }; for (int i = 0; i < max_slots; ++i) { - QidiSlotInfo slot; - slot.slot_index = i; - slot.color_index = variables.value("color_slot" + std::to_string(i), 1); - slot.filament_type = variables.value("filament_slot" + std::to_string(i), 1); - slot.vendor_type = variables.value("vendor_slot" + std::to_string(i), 0); + AmsTrayData tray; + tray.slot_index = i; + // Read slot variables + const int color_index = variables.value("color_slot" + std::to_string(i), 1); + const int filament_type = variables.value("filament_slot" + std::to_string(i), 1); + const int vendor_type = variables.value("vendor_slot" + std::to_string(i), 0); + + // Check filament presence via runout sensor std::string box_stepper_key = "box_stepper slot" + std::to_string(i); - slot.filament_exists = false; + tray.has_filament = false; if (status.contains(box_stepper_key)) { auto& box_stepper = status[box_stepper_key]; if (box_stepper.contains("runout_button") && !box_stepper["runout_button"].is_null()) { - int runout_button = box_stepper["runout_button"].get(); - slot.filament_exists = (runout_button == 0); + int runout_button = box_stepper["runout_button"].template get(); + tray.has_filament = (runout_button == 0); } } - slots.push_back(slot); + + if (tray.has_filament) { + // Look up filament type name from dictionary + std::string filament_name = "PLA"; + auto filament_it = dict.filaments.find(filament_type); + if (filament_it != dict.filaments.end()) { + filament_name = filament_it->second; + } + tray.tray_type = normalize_filament_type(filament_name); + tray.tray_info_idx = build_setting_id(filament_type, vendor_type, tray.tray_type); + + // Look up color from dictionary + auto color_it = dict.colors.find(color_index); + if (color_it != dict.colors.end()) { + tray.tray_color = color_it->second; + } else { + tray.tray_color = "FFFFFFFF"; + } + } + + trays.push_back(tray); } return true; @@ -348,31 +291,6 @@ void QidiPrinterAgent::parse_filament_sections(const std::string& content, std:: } } -std::string QidiPrinterAgent::normalize_color(const std::string& color) -{ - std::string value = color; - boost::trim(value); - if (value.rfind("0x", 0) == 0 || value.rfind("0X", 0) == 0) { - value = value.substr(2); - } - if (!value.empty() && value[0] == '#') { - value = value.substr(1); - } - std::string normalized; - for (char c : value) { - if (std::isxdigit(static_cast(c))) { - normalized.push_back(static_cast(std::toupper(static_cast(c)))); - } - } - if (normalized.size() == 6) { - normalized += "FF"; - } - if (normalized.size() != 8) { - return "00000000"; - } - return normalized; -} - std::string QidiPrinterAgent::map_filament_type_to_setting_id(const std::string& filament_type) { std::string upper = filament_type; diff --git a/src/slic3r/Utils/QidiPrinterAgent.hpp b/src/slic3r/Utils/QidiPrinterAgent.hpp index 27556ebd92..a8326edb53 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.hpp +++ b/src/slic3r/Utils/QidiPrinterAgent.hpp @@ -22,16 +22,6 @@ public: bool fetch_filament_info(std::string dev_id) override; private: - // Qidi-specific device info (extends base MoonrakerDeviceInfo with model_id) - struct QidiSlotInfo - { - int slot_index = 0; - int color_index = 0; - int filament_type = 0; - int vendor_type = 0; - bool filament_exists = false; - }; - struct QidiFilamentDict { std::map colors; @@ -39,11 +29,13 @@ private: }; // 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_slot_info(const std::string& base_url, + const std::string& api_key, + const QidiFilamentDict& dict, + const std::string& series_id, + std::vector& trays, + int& box_count, + std::string& error); bool fetch_filament_dict(const std::string& base_url, const std::string& api_key, QidiFilamentDict& dict, std::string& error) const; std::string normalize_filament_type(const std::string& filament_type); std::string infer_series_id(const std::string& model_id, const std::string& dev_name); @@ -52,7 +44,6 @@ private: // 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); };