From 7ec07ee5a47e3e440eea1dd8602aedb8ba669044 Mon Sep 17 00:00:00 2001 From: Florian Stamer Date: Mon, 18 May 2026 14:50:27 +0200 Subject: [PATCH] Matching for Filaments by Vendor Name, Material and Special Name --- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 261 +++++++++++++-------- src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 5 +- 2 files changed, 161 insertions(+), 105 deletions(-) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index ff18d98a818..c29549bbd3e 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -4,6 +4,7 @@ #include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/DeviceCore/DevFilaSystem.h" +#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h" #include "slic3r/GUI/DeviceCore/DevManager.h" #include "../GUI/DeviceCore/DevStorage.h" #include "../GUI/DeviceCore/DevFirmware.h" @@ -37,68 +38,6 @@ struct WsEndpoint bool secure = false; }; -// Find best-matching visible filament preset by vendor + type + color. -// Same algorithm as SnapmakerPrinterAgent's helper. Allows the KX-Bridge -// (which knows the user's slot brand selection) to direct OrcaSlicer to a -// specific preset instead of falling back to "first sichtbares PETG" lookup. -std::string find_closest_color_preset_by_vendor_and_type(const Slic3r::PresetCollection& filaments, - const std::string& vendor_name, - const std::string& filament_type, - const std::string& color_rgba) -{ - std::string best_match_id = ""; - int best_color_distance = 0xffffffff; - if (vendor_name.empty() || filament_type.empty()) return best_match_id; - - auto parse_color = [](const std::string& s) -> unsigned int { - if (s.empty()) return 0; - std::string h = s; - if (!h.empty() && h.front() == '#') h.erase(0, 1); - if (h.size() >= 8) h = h.substr(0, 6); // strip alpha - try { return std::stoul(h, nullptr, 16); } catch (...) { return 0; } - }; - - unsigned int target = parse_color(color_rgba); - - // Pass 1: compatible + visible presets (ideal match) - for (const auto& p : filaments.get_presets()) { - if (!(p.is_visible && p.is_compatible)) continue; - if (filaments.get_preset_base(p) != &p) continue; - if (p.config.opt_string("filament_vendor", 0u) != vendor_name) continue; - if (p.config.opt_string("filament_type", 0u) != filament_type) continue; - - unsigned int pc = parse_color(p.config.opt_string("default_filament_colour", 0u)); - int dr = ((target ) & 0xff) - ((pc ) & 0xff); - int dg = ((target >> 8) & 0xff) - ((pc >> 8) & 0xff); - int db = ((target >> 16) & 0xff) - ((pc >> 16) & 0xff); - unsigned int distance = dr * dr + dg * dg + db * db; - if (distance < (unsigned int)best_color_distance) { - best_color_distance = (int)distance; - best_match_id = p.filament_id; - } - } - if (!best_match_id.empty()) return best_match_id; - - // Pass 2: fallback — any base preset matching vendor+type, even if not compatible/instantiatable. - // Covers vendors like Elegoo that have a @base preset but no printer-specific variant for this printer. - for (const auto& p : filaments.get_presets()) { - if (p.config.opt_string("filament_vendor", 0u) != vendor_name) continue; - if (p.config.opt_string("filament_type", 0u) != filament_type) continue; - if (p.filament_id.empty()) continue; - - unsigned int pc = parse_color(p.config.opt_string("default_filament_colour", 0u)); - int dr = ((target ) & 0xff) - ((pc ) & 0xff); - int dg = ((target >> 8) & 0xff) - ((pc >> 8) & 0xff); - int db = ((target >> 16) & 0xff) - ((pc >> 16) & 0xff); - unsigned int distance = dr * dr + dg * dg + db * db; - if (distance < (unsigned int)best_color_distance) { - best_color_distance = (int)distance; - best_match_id = p.filament_id; - } - } - return best_match_id; -} - bool parse_ws_endpoint(const std::string& base_url, WsEndpoint& endpoint) { if (base_url.empty()) { @@ -150,6 +89,88 @@ std::string map_moonraker_state(std::string state) return "IDLE"; } +std::string normalize_filament_name_for_match(const std::string& input) +{ + std::string normalized = input; + boost::trim(normalized); + // Ignore profile suffixes like " @0.4 nozzle" for name matching. + if (const auto suffix_pos = normalized.find(" @"); suffix_pos != std::string::npos) { + normalized = normalized.substr(0, suffix_pos); + } + std::transform(normalized.begin(), normalized.end(), normalized.begin(), + [](unsigned char c) { return static_cast(std::toupper(c)); }); + return normalized; +} + +bool filament_name_match_relaxed(const std::string& wanted, const std::string& candidate) +{ + if (wanted == candidate) { + return true; + } + + // Allow lane names with trailing color descriptors, e.g.: + // "ELEGOO RAPID PETG GREY" -> "ELEGOO RAPID PETG". + if (!candidate.empty() && boost::starts_with(wanted, candidate + " ")) { + return true; + } + return false; +} + +std::vector vendor_match_candidates(std::string vendor) +{ + std::vector candidates; + boost::trim(vendor); + if (vendor.empty()) { + return candidates; + } + + candidates.push_back(vendor); + + // Also try first token (e.g. "Bambu Lab" -> "Bambu") without hardcoded aliases. + const auto first_space = vendor.find_first_of(" \t"); + if (first_space != std::string::npos) { + std::string first = vendor.substr(0, first_space); + boost::trim(first); + if (!first.empty() && !boost::iequals(first, vendor)) { + candidates.push_back(first); + } + } + return candidates; +} + +std::string filament_id_by_name(const Slic3r::PresetCollection& filaments, const std::string& filament_name) +{ + if (filament_name.empty()) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher received empty filament name"; + return ""; + } + + const std::string wanted = normalize_filament_name_for_match(filament_name); + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher lookup requested='" << filament_name + << "' normalized='" << wanted << "'"; + for (size_t i = 0; i < filaments.size(); ++i) { + const auto& preset = filaments.preset(i); + if (!preset.is_visible || !preset.is_compatible || preset.filament_id.empty()) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher skip preset='" << preset.name + << "' visible=" << preset.is_visible << " compatible=" << preset.is_compatible + << " filament_id_empty=" << preset.filament_id.empty(); + continue; + } + const std::string candidate = normalize_filament_name_for_match(preset.name); + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher compare preset='" << preset.name + << "' normalized='" << candidate << "' filament_id='" << preset.filament_id << "'"; + if (filament_name_match_relaxed(wanted, candidate)) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: filament matcher matched requested='" << filament_name + << "' normalized='" << wanted << "' to preset='" << preset.name + << "' filament_id='" << preset.filament_id << "'"; + return preset.filament_id; + } + } + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: filament matcher found no match for requested='" << filament_name + << "' normalized='" << wanted << "'"; + return ""; +} + } // namespace namespace Slic3r { @@ -510,7 +531,7 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) return BAMBU_NETWORK_SUCCESS; } -void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays) +void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays, int active_lane_index) { // Look up MachineObject via DeviceManager @@ -561,6 +582,7 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, tray_json["tray_info_idx"] = tray->tray_info_idx; tray_json["tray_type"] = tray->tray_type; + tray_json["tray_sub_brands"] = tray->tray_sub_brands.empty() ? tray->tray_type : tray->tray_sub_brands; tray_json["tray_color"] = normalize_color_value(tray->tray_color); // Add temperature data if provided @@ -592,6 +614,11 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, ams_json["ams"] = ams_array; ams_json["ams_exist_bits"] = ams_exist_ss.str(); ams_json["tray_exist_bits"] = tray_exist_ss.str(); + if (active_lane_index >= 0) { + const std::string active_lane = std::to_string(active_lane_index); + ams_json["tray_now"] = active_lane; + ams_json["tray_tar"] = active_lane; + } // Wrap in the expected structure for ParseV1_0 nlohmann::json print_json = nlohmann::json::object(); @@ -599,6 +626,7 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, // Call the parser to populate DevFilaSystem DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem(), false); + ExtderSystemParser::ParseV1_0(print_json, obj->GetExtderSystem()); BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays"; // Set printer_type so update_sync_status() can match it against the preset's printer type. @@ -632,6 +660,7 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) { std::vector trays; int max_lane_index = 0; + int active_lane_index = -1; // Try Moonraker filament data (more generic, supports any filament changer // software that reports lane data to Moonraker like AFC and recent Happy @@ -640,16 +669,16 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Moonraker filament system with " << (max_lane_index + 1) << " lanes"; int ams_count = (max_lane_index + 4) / 4; - build_ams_payload(ams_count, max_lane_index, trays); + build_ams_payload(ams_count, max_lane_index, trays, active_lane_index); return true; } // Attempt Happy Hare first (more widely adopted, supports more filament changers) - if (fetch_hh_filament_info(trays, max_lane_index)) { + if (fetch_hh_filament_info(trays, max_lane_index, active_lane_index)) { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Happy Hare MMU with " << (max_lane_index + 1) << " gates"; int ams_count = (max_lane_index + 4) / 4; - build_ams_payload(ams_count, max_lane_index, trays); + build_ams_payload(ams_count, max_lane_index, trays, active_lane_index); return true; } @@ -838,7 +867,6 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vectorfilaments.get_presets()) { - if (p.is_compatible && p.filament_id == tray.tray_info_idx) { - idx_has_compatible_preset = true; - break; + if (bundle) { + const auto vendor_candidates = vendor_match_candidates(lane_vendor); + auto match_with_vendor_prefix = [&](const std::string& suffix) -> std::string { + if (suffix.empty()) { + return ""; } - } - } + for (const auto& vendor_candidate : vendor_candidates) { + const std::string requested = vendor_candidate + " " + suffix; + std::string match_id = filament_id_by_name(bundle->filaments, requested); + if (!match_id.empty()) { + return match_id; + } + } + return ""; + }; - if (bundle && !tray.filament_vendor.empty() && (tray.tray_info_idx.empty() || !idx_has_compatible_preset)) { - std::string mid = find_closest_color_preset_by_vendor_and_type( - bundle->filaments, tray.filament_vendor, tray.tray_type, tray.tray_color); - if (!mid.empty()) tray.tray_info_idx = mid; - } - if (tray.tray_info_idx.empty()) { - tray.tray_info_idx = bundle - ? bundle->filaments.filament_id_by_type(tray.tray_type) - : map_filament_type_to_generic_id(tray.tray_type); + // Prefer the most specific lane identity first, then broader vendor/material mapping. + tray.tray_info_idx = match_with_vendor_prefix(lane_name); + if (tray.tray_info_idx.empty()) { + tray.tray_info_idx = match_with_vendor_prefix(tray.tray_type); + } + if (tray.tray_info_idx.empty() && !lane_name.empty()) { + tray.tray_info_idx = filament_id_by_name(bundle->filaments, lane_name); + } + if (tray.tray_info_idx.empty()) { + tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); + } + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: lane='" << lane_key + << "' index=" << lane_index << " material='" << tray.tray_type + << "' vendor='" << lane_vendor << "' vendor_candidates=" << vendor_candidates.size() + << "' name='" << lane_name + << "' mapped_by='preset_bundle' tray_info_idx='" << tray.tray_info_idx << "'"; + } else { + tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: lane='" << lane_key + << "' index=" << lane_index << " material='" << tray.tray_type + << "' mapped_by='generic_fallback' tray_info_idx='" << tray.tray_info_idx << "'"; } max_lane_index = std::max(max_lane_index, lane_index); @@ -909,15 +947,11 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector& trays, int& max_lane_index) +bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector& trays, int& max_lane_index, int& active_lane_index) { // Query Happy Hare MMU status std::string url = join_url(device_info.base_url, "/printer/objects/query?mmu"); @@ -989,8 +1023,18 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector& tra // Get arrays const auto& gate_status = mmu.contains("gate_status") ? mmu["gate_status"] : nlohmann::json::array(); const auto& gate_material = mmu.contains("gate_material") ? mmu["gate_material"] : nlohmann::json::array(); + const auto& gate_filament_name = mmu.contains("gate_filament_name") ? mmu["gate_filament_name"] : nlohmann::json::array(); const auto& gate_color = mmu.contains("gate_color") ? mmu["gate_color"] : nlohmann::json::array(); const auto& gate_temperature = mmu.contains("gate_temperature") ? mmu["gate_temperature"] : nlohmann::json::array(); + const int active_gate = safe_json_int(mmu, "gate"); + active_lane_index = active_gate; + std::string active_filament_name; + if (mmu.contains("active_filament") && mmu["active_filament"].is_object()) { + active_filament_name = safe_json_string(mmu["active_filament"], "filament_name"); + if (active_filament_name.empty()) { + active_filament_name = safe_json_string(mmu["active_filament"], "material"); + } + } if (!gate_status.is_array() || !gate_material.is_array() || !gate_color.is_array() || !gate_temperature.is_array()) { @@ -1011,8 +1055,13 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector& tra // Extract gate data std::string material = safe_array_string(gate_material, gate_idx); + std::string filament_name = safe_array_string(gate_filament_name, gate_idx); std::string color = safe_array_string(gate_color, gate_idx); int nozzle_temp = safe_array_int(gate_temperature, gate_idx); + // For the active gate, prefer active_filament from MMU state. + if (gate_idx == active_gate && !active_filament_name.empty()) { + filament_name = active_filament_name; + } // Skip if no material type (empty gate) if (material.empty()) { @@ -1022,15 +1071,21 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector& tra AmsTrayData tray; tray.slot_index = gate_idx; tray.tray_type = material; + tray.tray_sub_brands = filament_name; tray.tray_color = color; tray.nozzle_temp = nozzle_temp; tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays tray.has_filament = true; auto* bundle = GUI::wxGetApp().preset_bundle; - tray.tray_info_idx = bundle - ? bundle->filaments.filament_id_by_type(tray.tray_type) - : map_filament_type_to_generic_id(tray.tray_type); + if (bundle) { + tray.tray_info_idx = filament_id_by_name(bundle->filaments, filament_name); + if (tray.tray_info_idx.empty()) { + tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); + } + } else { + tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); + } max_lane_index = std::max(max_lane_index, gate_idx); trays.push_back(tray); diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index 47f58462452..2eeb1099df3 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -92,6 +92,7 @@ protected: 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_sub_brands; // Human-readable filament name std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA) std::string tray_info_idx; // Setting ID (optional) std::string filament_vendor; // Vendor hint from bridge (optional, KX-Bridge sendet das) @@ -100,7 +101,7 @@ protected: }; // Build ams JSON and call parser - void build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays); + void build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays, int active_lane_index = -1); // 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); @@ -162,7 +163,7 @@ private: uint64_t generation); // System-specific filament fetch methods - bool fetch_hh_filament_info(std::vector& trays, int& max_lane_index); + bool fetch_hh_filament_info(std::vector& trays, int& max_lane_index, int& active_lane_index); bool fetch_moonraker_filament_data(std::vector& trays, int& max_lane_index); // JSON helper methods