feat(v2.4): KX-Linie auf Upstream 2.4.0 konsolidieren (2.4.0-kx1)
Neue 2.4-stable-Linie auf frischem orca-upstream/release/v2.4 (2.4.0): - KX-Moonraker-Bridge (MoonrakerPrinterAgent.cpp/.hpp) übernommen - Preset.cpp: KX-filament_id-Patches per 3way auf neuen Upstream appliziert (Upstream-Fixes 10.-21.06. erhalten) - Issue #52: Vendor-Filter bei User-Presets überspringen - AMS-Leerslot-Fix (DevFilaSystem.cpp + AMSItem.cpp): leerer Slot zeigt kein altes Filament mehr - About-Hinweis, verify_build.sh, Filament-Bridge-Doku, README - Version 2.4.0-kx1
This commit is contained in:
@@ -43,6 +43,8 @@
|
||||
#include <boost/uuid/uuid_generators.hpp>
|
||||
#include <boost/locale.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/uuid/detail/md5.hpp>
|
||||
#include <boost/algorithm/hex.hpp>
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include "Utils.hpp"
|
||||
@@ -671,6 +673,31 @@ void Preset::save(DynamicPrintConfig* parent_config)
|
||||
//BBS: add project embedded preset logic
|
||||
if (this->is_project_embedded)
|
||||
return;
|
||||
|
||||
// Generate a unique filament_id for user filament presets that don't have one yet.
|
||||
// Inherited presets (e.g. "My PLA" inheriting "Generic PLA @System") previously had
|
||||
// no filament_id which caused AMS sync to fall back to the parent's Generic ID.
|
||||
// Also generate a new ID if filament_id is inherited from the parent (== base_id).
|
||||
// This happens when a user preset is saved for the first time without having its own ID.
|
||||
// A user preset needs its own filament_id if:
|
||||
// - it has no filament_id at all, OR
|
||||
// - its filament_id does not start with "P" (user preset IDs start with "P",
|
||||
// system IDs start with GFL/OGFL/GFA/etc.)
|
||||
bool needs_unique_filament_id = this->is_user() && !this->name.empty() &&
|
||||
this->type == Preset::TYPE_FILAMENT &&
|
||||
(this->filament_id.empty() || this->filament_id.front() != 'P');
|
||||
if (needs_unique_filament_id) {
|
||||
boost::uuids::detail::md5 hash;
|
||||
boost::uuids::detail::md5::digest_type digest;
|
||||
hash.process_bytes(this->name.data(), this->name.size());
|
||||
hash.get_digest(digest);
|
||||
const auto char_digest = reinterpret_cast<const char *>(&digest);
|
||||
std::string result;
|
||||
boost::algorithm::hex(char_digest, char_digest + sizeof(boost::uuids::detail::md5::digest_type), std::back_inserter(result));
|
||||
this->filament_id = "P" + result.substr(0, 7);
|
||||
BOOST_LOG_TRIVIAL(info) << "Preset::save: generated filament_id='" << this->filament_id << "' for user preset '" << this->name << "'";
|
||||
}
|
||||
|
||||
//BBS: change to json format
|
||||
//this->config.save(this->file);
|
||||
std::string from_str;
|
||||
@@ -726,6 +753,8 @@ void Preset::save(DynamicPrintConfig* parent_config)
|
||||
opt_dst->set(opt_src);
|
||||
}
|
||||
}
|
||||
if (!filament_id.empty())
|
||||
temp_config.set_key_value(BBL_JSON_KEY_FILAMENT_ID, new ConfigOptionString(filament_id));
|
||||
temp_config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
|
||||
} else if (!filament_id.empty() && inherits().empty()) {
|
||||
DynamicPrintConfig temp_config = config;
|
||||
@@ -1040,9 +1069,6 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"lateral_lattice_angle_1",
|
||||
"lateral_lattice_angle_2",
|
||||
"infill_overhang_angle",
|
||||
"lightning_overhang_angle",
|
||||
"lightning_prune_angle",
|
||||
"lightning_straightening_angle",
|
||||
"top_surface_pattern",
|
||||
"bottom_surface_pattern",
|
||||
"infill_direction",
|
||||
@@ -1107,13 +1133,10 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"print_order",
|
||||
"support_remove_small_overhang",
|
||||
"filename_format",
|
||||
"outer_wall_filament_id",
|
||||
"inner_wall_filament_id",
|
||||
"wall_filament",
|
||||
"support_bottom_z_distance",
|
||||
"sparse_infill_filament_id",
|
||||
"internal_solid_filament_id",
|
||||
"top_surface_filament_id",
|
||||
"bottom_surface_filament_id",
|
||||
"sparse_infill_filament",
|
||||
"solid_infill_filament",
|
||||
"support_filament",
|
||||
"support_interface_filament",
|
||||
"support_interface_not_for_body",
|
||||
@@ -1135,7 +1158,6 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"infill_wall_overlap",
|
||||
"top_bottom_infill_wall_overlap",
|
||||
"bridge_flow",
|
||||
"bridge_line_width",
|
||||
"internal_bridge_flow",
|
||||
"elefant_foot_compensation",
|
||||
"elefant_foot_compensation_layers",
|
||||
@@ -1200,7 +1222,6 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"small_perimeter_threshold",
|
||||
"bridge_angle",
|
||||
"internal_bridge_angle",
|
||||
"relative_bridge_angle",
|
||||
"filter_out_gap_fill",
|
||||
"travel_acceleration",
|
||||
"inner_wall_acceleration",
|
||||
@@ -1361,7 +1382,7 @@ static std::vector<std::string> s_Preset_machine_limits_options {
|
||||
|
||||
static std::vector<std::string> s_Preset_printer_options {
|
||||
"printer_technology",
|
||||
"printable_area", "extruder_printable_area", "support_parallel_printheads", "parallel_printheads_count", "parallel_printheads_bed_exclude_areas", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor",
|
||||
"printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor",
|
||||
"fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
|
||||
"single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode",
|
||||
"printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type",
|
||||
@@ -1372,7 +1393,7 @@ static std::vector<std::string> s_Preset_printer_options {
|
||||
"scan_first_layer", "enable_power_loss_recovery", "wrapping_detection_layers", "wrapping_exclude_area", "machine_load_filament_time", "machine_unload_filament_time", "machine_tool_change_time", "time_cost", "machine_pause_gcode", "template_custom_gcode",
|
||||
"nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", "travel_slope", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","printer_structure",
|
||||
"best_object_pos", "head_wrap_detect_zone",
|
||||
"host_type", "print_host", "printhost_apikey", "flashforge_serial_number", "bbl_use_printhost", "printer_agent",
|
||||
"host_type", "print_host", "printhost_apikey", "bbl_use_printhost", "printer_agent",
|
||||
"print_host_webui",
|
||||
"printhost_cafile","printhost_port","printhost_authorization_type",
|
||||
"printhost_user", "printhost_password", "printhost_ssl_ignore_revoke", "thumbnails", "thumbnails_format",
|
||||
@@ -1679,7 +1700,10 @@ void PresetCollection::load_presets(
|
||||
const Preset& default_preset = this->default_preset_for(config);
|
||||
if (inherit_preset) {
|
||||
preset.config = inherit_preset->config;
|
||||
preset.filament_id = inherit_preset->filament_id;
|
||||
// Only inherit filament_id from parent if this preset has no own ID in JSON.
|
||||
// User presets with a P-prefix ID (generated by Preset::save) must keep their own ID.
|
||||
if (preset.filament_id.empty())
|
||||
preset.filament_id = inherit_preset->filament_id;
|
||||
extend_default_config_length(config, false, {});
|
||||
preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||
}
|
||||
@@ -2880,8 +2904,20 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
|
||||
|
||||
if (m_type == Preset::TYPE_PRINT)
|
||||
preset.config.option<ConfigOptionString>("print_settings_id", true)->value = new_name;
|
||||
else if (m_type == Preset::TYPE_FILAMENT)
|
||||
else if (m_type == Preset::TYPE_FILAMENT) {
|
||||
preset.config.option<ConfigOptionStrings>("filament_settings_id", true)->values[0] = new_name;
|
||||
// Generate a unique filament_id for user presets that don't have one yet (PR #13315).
|
||||
if (preset.filament_id.empty() || preset.filament_id.front() != 'P') {
|
||||
boost::uuids::detail::md5 hash;
|
||||
boost::uuids::detail::md5::digest_type digest;
|
||||
hash.process_bytes(new_name.data(), new_name.size());
|
||||
hash.get_digest(digest);
|
||||
const auto char_digest = reinterpret_cast<const char *>(&digest);
|
||||
std::string result;
|
||||
boost::algorithm::hex(char_digest, char_digest + sizeof(boost::uuids::detail::md5::digest_type), std::back_inserter(result));
|
||||
preset.filament_id = "P" + result.substr(0, 7);
|
||||
}
|
||||
}
|
||||
else if (m_type == Preset::TYPE_PRINTER)
|
||||
preset.config.option<ConfigOptionString>("printer_settings_id", true)->value = new_name;
|
||||
final_inherits = preset.inherits();
|
||||
@@ -3920,7 +3956,6 @@ static std::vector<std::string> s_PhysicalPrinter_opts {
|
||||
"print_host",
|
||||
"print_host_webui",
|
||||
"printhost_apikey",
|
||||
"flashforge_serial_number",
|
||||
"printhost_cafile",
|
||||
"printhost_port",
|
||||
"printhost_authorization_type",
|
||||
|
||||
@@ -3216,9 +3216,28 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
}
|
||||
bool has_type = false;
|
||||
auto filament_type = ams.opt_string("filament_type", 0u);
|
||||
auto iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) {
|
||||
has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
|
||||
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
|
||||
auto sub_brands = ams.opt_string("filament_sub_brands", 0u);
|
||||
// If sub_brands (filament brand name) is known, prefer a preset whose name starts with it
|
||||
// to resolve ID collisions where two different vendors share the same filament_id.
|
||||
auto iter = filaments.end();
|
||||
if (!sub_brands.empty() && !filament_id.empty()) {
|
||||
iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type, &sub_brands](auto &f) {
|
||||
has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
|
||||
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id
|
||||
&& boost::algorithm::istarts_with(f.name, sub_brands); });
|
||||
}
|
||||
if (iter == filaments.end()) {
|
||||
iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) {
|
||||
has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
|
||||
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
|
||||
}
|
||||
// Also search user presets (not base presets) with matching filament_id.
|
||||
// User presets have P-prefix IDs and are not returned by get_preset_base(f) == &f.
|
||||
if (iter == filaments.end() && !filament_id.empty()) {
|
||||
iter = std::find_if(filaments.begin(), filaments.end(), [&filament_id, &has_type, filament_type](auto &f) {
|
||||
has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
|
||||
return f.is_compatible && !f.is_system && f.filament_id == filament_id; });
|
||||
}
|
||||
if (iter == filaments.end()) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
|
||||
if (!filament_type.empty()) {
|
||||
|
||||
@@ -269,6 +269,7 @@ AboutDialog::AboutDialog()
|
||||
text_list.push_back(_L("Open-source slicing stands on a tradition of collaboration and attribution. Slic3r, created by Alessandro Ranellucci and the RepRap community, laid the foundation. PrusaSlicer by Prusa Research built on that work, Bambu Studio forked from PrusaSlicer, and SuperSlicer extended it with community-driven enhancements. Each project carried the work of its predecessors forward, crediting those who came before."));
|
||||
text_list.push_back(_L("OrcaSlicer began in that same spirit, drawing from PrusaSlicer, BambuStudio, SuperSlicer, and CuraSlicer. But it has since grown far beyond its origins — introducing advanced calibration tools, precise wall and seam control and hundreds of other features."));
|
||||
text_list.push_back(_L("Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry."));
|
||||
text_list.push_back(_L("This build includes additional patches for KX-Bridge (Anycubic Kobra X Moonraker bridge). GNU AGPL-3.0: upstream https://github.com/SoftFever/OrcaSlicer + KX patches https://gitea.it-drui.de/viewit/OrcaSlicer-KX/src/branch/release/v2.4"));
|
||||
|
||||
text_sizer->Add( 0, 0, 0, wxTOP, FromDIP(33));
|
||||
bool is_zh = wxGetApp().app_config->get("language") == "zh_CN";
|
||||
|
||||
@@ -612,9 +612,10 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
{
|
||||
curr_tray->remain = -1;
|
||||
}
|
||||
if (tray_it->contains("tray_slot_placeholder")) {
|
||||
curr_tray->is_slot_placeholder = true;
|
||||
}
|
||||
// Reset the placeholder flag every update so a slot that
|
||||
// was empty before but is now loaded does not stay marked
|
||||
// as a placeholder (and vice versa).
|
||||
curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder");
|
||||
int ams_id_int = 0;
|
||||
int tray_id_int = 0;
|
||||
try
|
||||
@@ -638,6 +639,21 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
// An empty slot (placeholder or exist-bit not set) must not
|
||||
// keep the previously loaded filament's name/type/color from
|
||||
// an earlier update. The Moonraker/AMS-Lite bridge reports
|
||||
// empty slots with a placeholder + cleared fields, but the
|
||||
// tray object is merged (not rebuilt) between updates, so the
|
||||
// stale values would otherwise survive and the slot would
|
||||
// render as loaded. Reset them explicitly.
|
||||
if (curr_tray->is_slot_placeholder || !curr_tray->is_exists)
|
||||
{
|
||||
curr_tray->setting_id = "";
|
||||
curr_tray->m_fila_type = "";
|
||||
curr_tray->color = "";
|
||||
curr_tray->sub_brands = "";
|
||||
curr_tray->cols.clear();
|
||||
}
|
||||
if (tray_it->contains("setting_id"))
|
||||
{
|
||||
curr_tray->filament_setting_id = (*tray_it)["setting_id"].get<std::string>();
|
||||
|
||||
@@ -3360,6 +3360,9 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
|
||||
tray_config.set_key_value("filament_colour_type", new ConfigOptionStrings{std::to_string(tray.ctype)});
|
||||
tray_config.set_key_value("filament_exist", new ConfigOptionBools{tray.is_exists});
|
||||
tray_config.set_key_value("filament_slot_placeholder", new ConfigOptionBools{tray.is_slot_placeholder});
|
||||
// KX: pass the human-readable sub-brand/filament name through to the
|
||||
// preset matcher (consumed in PresetBundle, see filament_sub_brands).
|
||||
tray_config.set_key_value("filament_sub_brands", new ConfigOptionStrings{tray.sub_brands});
|
||||
std::optional<FilamentBaseInfo> info;
|
||||
if (wxGetApp().preset_bundle) {
|
||||
info = wxGetApp().preset_bundle->get_filament_by_filament_id(tray.setting_id);
|
||||
|
||||
@@ -267,6 +267,13 @@ int PresetComboBox::update_ams_color()
|
||||
auto color_pack = static_cast<ConfigOptionStrings *>(cfg->option("filament_multi_colour")->clone()); // multi color (all colors in all kinds of filament)
|
||||
auto color_type = static_cast<ConfigOptionStrings*>(cfg->option("filament_colour_type")->clone()); // color type
|
||||
|
||||
// KX: guard against out-of-range filament indices (e.g. AMS slots beyond the
|
||||
// current project's filament count) and default an empty color type.
|
||||
if (m_filament_idx >= color_head->values.size()) color_head->values.resize(m_filament_idx + 1);
|
||||
if (m_filament_idx >= color_pack->values.size()) color_pack->values.resize(m_filament_idx + 1);
|
||||
if (m_filament_idx >= color_type->values.size()) color_type->values.resize(m_filament_idx + 1);
|
||||
if (ctype.empty()) ctype = "1";
|
||||
|
||||
color_head->values[m_filament_idx] = color;
|
||||
color_type->values[m_filament_idx] = ctype;
|
||||
std::string color_str = ""; // Translate multi color info to config storage format
|
||||
|
||||
@@ -69,7 +69,12 @@ bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag,
|
||||
auto it = ams->GetTrays().find(std::to_string(i));
|
||||
Caninfo info;
|
||||
// tray is exists
|
||||
if (it != ams->GetTrays().end() && it->second->is_exists) {
|
||||
// A placeholder slot is explicitly empty (reported by the Moonraker/AMS-Lite
|
||||
// bridge): treat it as empty regardless of any stale is_exists/filament data
|
||||
// left over from a previous update, so it renders grey at its position
|
||||
// instead of showing the previously loaded filament.
|
||||
if (it != ams->GetTrays().end() && it->second->is_exists
|
||||
&& !it->second->is_slot_placeholder) {
|
||||
if (it->second->is_tray_info_ready()) {
|
||||
info.can_id = it->second->id;
|
||||
info.ctype = it->second->ctype;
|
||||
|
||||
@@ -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"
|
||||
@@ -88,6 +89,166 @@ 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);
|
||||
}
|
||||
// Remove non-name symbols (e.g. trademark signs) while preserving separators
|
||||
// commonly used in filament names.
|
||||
std::string cleaned;
|
||||
cleaned.reserve(normalized.size());
|
||||
for (unsigned char c : normalized) {
|
||||
if (std::isalnum(c) || c == '-' || c == '+' || c == '/' || std::isspace(c)) {
|
||||
cleaned.push_back(static_cast<char>(std::toupper(c)));
|
||||
} else {
|
||||
cleaned.push_back(' ');
|
||||
}
|
||||
}
|
||||
|
||||
// Collapse repeated whitespace.
|
||||
std::string collapsed;
|
||||
collapsed.reserve(cleaned.size());
|
||||
bool prev_space = true;
|
||||
for (unsigned char c : cleaned) {
|
||||
if (std::isspace(c)) {
|
||||
if (!prev_space) {
|
||||
collapsed.push_back(' ');
|
||||
}
|
||||
prev_space = true;
|
||||
} else {
|
||||
collapsed.push_back(static_cast<char>(c));
|
||||
prev_space = false;
|
||||
}
|
||||
}
|
||||
boost::trim(collapsed);
|
||||
return collapsed;
|
||||
}
|
||||
|
||||
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,
|
||||
const std::vector<std::string>& vendor_filters = {})
|
||||
{
|
||||
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);
|
||||
std::vector<std::string> normalized_vendor_filters;
|
||||
normalized_vendor_filters.reserve(vendor_filters.size());
|
||||
for (const auto& vendor_filter : vendor_filters) {
|
||||
const std::string normalized_vendor = normalize_filament_name_for_match(vendor_filter);
|
||||
if (!normalized_vendor.empty()) {
|
||||
normalized_vendor_filters.push_back(normalized_vendor);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher lookup requested='" << filament_name
|
||||
<< "' normalized='" << wanted << "' vendor_filters=" << normalized_vendor_filters.size();
|
||||
|
||||
// Two-pass search: Pass 1 = compatible presets only (ideal), Pass 2 = all visible presets
|
||||
// (fallback for vendors like eSUN/Eryone that have no printer-specific Kobra X profile).
|
||||
for (int pass = 1; pass <= 3; ++pass) {
|
||||
for (size_t i = 0; i < filaments.size(); ++i) {
|
||||
const auto& preset = filaments.preset(i);
|
||||
// Pass 1: compatible + visible only
|
||||
// Pass 2: visible but not necessarily compatible (vendors without printer-specific profile)
|
||||
// Pass 3: any preset including invisible (vendors not installed as printer)
|
||||
if (pass <= 2 && !preset.is_visible) {
|
||||
continue;
|
||||
}
|
||||
// User presets (created via "Save As") may have no filament_id yet.
|
||||
// We still match them by name and use their name as the identifier.
|
||||
const std::string preset_id = preset.filament_id.empty() ? preset.name : preset.filament_id;
|
||||
if (pass == 1 && !preset.is_compatible) {
|
||||
continue; // Pass 1: only compatible presets
|
||||
}
|
||||
// Skip the vendor filter for user presets: a preset derived from a
|
||||
// vendor-specific parent (e.g. "Anycubic PLA Matte @...") resolves
|
||||
// filament_vendor from the PARENT ("Anycubic"), not from the user
|
||||
// preset itself ("Tinmorry"), so the filter would wrongly drop it
|
||||
// and the AMS sync falls back to the vendor preset (Issue #52). The
|
||||
// strict name match below is discriminating enough for user presets.
|
||||
if (!preset.is_user() && !normalized_vendor_filters.empty()) {
|
||||
const std::string preset_vendor = normalize_filament_name_for_match(preset.config.opt_string("filament_vendor", 0u));
|
||||
bool vendor_match = false;
|
||||
for (const auto& vendor_filter : normalized_vendor_filters) {
|
||||
if (preset_vendor == vendor_filter) {
|
||||
vendor_match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!vendor_match) {
|
||||
if (pass == 1) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher skip preset='" << preset.name
|
||||
<< "' reason=vendor_filter_miss preset_vendor='" << preset_vendor << "'";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const std::string candidate = normalize_filament_name_for_match(preset.name);
|
||||
BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher compare (pass=" << pass << ") preset='" << preset.name
|
||||
<< "' normalized='" << candidate << "' preset_id='" << preset_id << "'";
|
||||
if (filament_name_match_relaxed(wanted, candidate)) {
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: filament matcher matched (pass=" << pass << ") requested='" << filament_name
|
||||
<< "' normalized='" << wanted << "' to preset='" << preset.name
|
||||
<< "' preset_id='" << preset_id << "'";
|
||||
return preset_id;
|
||||
}
|
||||
}
|
||||
if (pass == 1) {
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: filament matcher pass 1 (compatible) found no match for '"
|
||||
<< filament_name << "', trying pass 2 (all visible)";
|
||||
} else if (pass == 2) {
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: filament matcher pass 2 (all visible) found no match for '"
|
||||
<< filament_name << "', trying pass 3 (all presets incl. invisible)";
|
||||
}
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: filament matcher found no match for requested='" << filament_name
|
||||
<< "' normalized='" << wanted << "'";
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -448,7 +609,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
|
||||
@@ -499,6 +660,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
|
||||
@@ -530,6 +692,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();
|
||||
@@ -537,6 +704,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.
|
||||
@@ -570,6 +738,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
|
||||
@@ -578,16 +747,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;
|
||||
}
|
||||
|
||||
@@ -801,13 +970,51 @@ 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();
|
||||
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) {
|
||||
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, vendor_candidates);
|
||||
if (!match_id.empty()) {
|
||||
return match_id;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// 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, vendor_candidates);
|
||||
}
|
||||
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);
|
||||
trays.push_back(tray);
|
||||
@@ -822,7 +1029,7 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayDat
|
||||
}
|
||||
|
||||
// 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");
|
||||
@@ -894,8 +1101,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()) {
|
||||
@@ -916,8 +1133,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()) {
|
||||
@@ -927,15 +1149,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);
|
||||
|
||||
@@ -92,14 +92,16 @@ 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)
|
||||
int bed_temp = 0; // Optional
|
||||
int nozzle_temp = 0; // Optional
|
||||
};
|
||||
|
||||
// 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);
|
||||
@@ -161,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
|
||||
|
||||
Reference in New Issue
Block a user