Matching for Filaments by Vendor Name, Material and Special Name

This commit is contained in:
Florian Stamer
2026-05-18 14:50:27 +02:00
committed by viewit
parent 4ca3deebba
commit 7ec07ee5a4
2 changed files with 161 additions and 105 deletions

View File

@@ -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<char>(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<std::string> vendor_match_candidates(std::string vendor)
{
std::vector<std::string> 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<AmsTrayData>& trays)
void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& 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<AmsTrayData> 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::vector<AmsTrayDat
const auto& value = json["result"]["value"];
trays.clear();
max_lane_index = 0;
bool has_populated_lane = false;
for (const auto& [lane_key, lane_obj] : value.items()) {
if (!lane_obj.is_object()) {
@@ -864,40 +892,50 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayDat
tray.slot_index = lane_index;
tray.tray_color = safe_json_string(lane_obj, "color");
tray.tray_type = safe_json_string(lane_obj, "material");
const std::string lane_name = safe_json_string(lane_obj, "name");
const std::string lane_vendor = safe_json_string(lane_obj, "vendor_name");
tray.tray_sub_brands = lane_name;
tray.bed_temp = safe_json_int(lane_obj, "bed_temp");
tray.nozzle_temp = safe_json_int(lane_obj, "nozzle_temp");
tray.has_filament = !tray.tray_type.empty();
has_populated_lane = has_populated_lane || tray.has_filament;
// Bridge-Hints: tray_info_idx (konkrete Filament-ID) + filament_vendor.
// KX-Bridge sendet beide, wenn der User in der Bridge-UI ein konkretes
// Slot-Profil gewählt hat (Polymaker/eSUN/…).
tray.tray_info_idx = safe_json_string(lane_obj, "tray_info_idx");
tray.filament_vendor = safe_json_string(lane_obj, "filament_vendor");
auto* bundle = GUI::wxGetApp().preset_bundle;
// If tray_info_idx is set but no compatible preset exists for it,
// and we have a vendor hint, prefer vendor+type+color match instead.
// This handles cases where the bridge sends a generic/other-printer ID
// (e.g. GFL99 for Bambu Lab) that has no compatible preset for this printer.
bool idx_has_compatible_preset = false;
if (!tray.tray_info_idx.empty() && bundle) {
for (const auto& p : bundle->filaments.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<AmsTrayDat
return false;
}
if (!has_populated_lane) {
return false;
}
return true;
}
// Fetch filament info from Happy Hare MMU
bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector<AmsTrayData>& trays, int& max_lane_index)
bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector<AmsTrayData>& 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<AmsTrayData>& 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<AmsTrayData>& 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<AmsTrayData>& 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);

View File

@@ -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<AmsTrayData>& trays);
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& 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<AmsTrayData>& trays, int& max_lane_index);
bool fetch_hh_filament_info(std::vector<AmsTrayData>& trays, int& max_lane_index, int& active_lane_index);
bool fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index);
// JSON helper methods