diff --git a/docs/HLSD/multi-extruder-ams.md b/docs/HLSD/multi-extruder-ams.md index 0160ad0041..4c5f9d8e34 100644 --- a/docs/HLSD/multi-extruder-ams.md +++ b/docs/HLSD/multi-extruder-ams.md @@ -93,19 +93,33 @@ property of the unit kind and the reported trays: The tray ids are the payload's slot indices; they are expected to be unique, 0-based, and contiguous within a unit, because the generic tray-index mapping below is cumulative. -Bulk/extended spools are carried by `print.vir_slot`, which `DeviceManager` parses into -`MachineObject::vt_slot`. Two ids are fixed: `VIRTUAL_TRAY_MAIN_ID = 255` (extruder 0) and -`VIRTUAL_TRAY_DEPUTY_ID = 254` (extruder 1); the id convention is `vt_id = 255 - extruder_id`, so -further extruders use 253, 252, … . The `vir_slot` parser recognises the 255/254 pair; the agent -filament descriptor path `DevFilaSystemParser::ParseAgentFilament` is the entry that stores a fuller -descending set in `vt_slot`. +### External spool slots + +External/manual spools are virtual trays in `MachineObject::vt_slot`, separate from the trays owned +by `DevFilaSystem`. Their stable id is `vt_id = VIRTUAL_TRAY_MAIN_ID - extruder_id`: 255 for +extruder 0, 254 for extruder 1, then 253, 252, and so on. The index in `vt_slot` is therefore +`VIRTUAL_TRAY_MAIN_ID - vt_id`, not the order in which a printer happens to report trays. + +For non-Bambu printers, slot topology comes from the live `DevExtderSystem` extruder count, not from +external-spool telemetry. `MachineObject::ensure_external_virtual_tray_slots()` grows `vt_slot` to +one entry per extruder and enables virtual-tray support even when the agent reports no `vir_slot`, +`vt_tray`, or external-spool data. It only adds missing entries, so reported tray metadata is kept. +The shared ensure runs after push-status parsing has updated the extruder count and after the generic +AMS/agent-filament parsers. Thus agents may report external-spool metadata, but do not have to invent +empty slots just to make each toolhead's external-spool row visible. + +When a payload does report virtual trays, `vir_slot` entries are placed by their id and missing +indices are filled with placeholders. Generic agent `external` entries are likewise placed by +extruder id, so sparse or out-of-order reports do not shift a spool onto another toolhead. An empty +placeholder represents the slot's existence, not a claim that the printer reported a loaded spool. +Bambu keeps its fixed 255/254 virtual-tray pair and does not use generic slot synthesis. ## Mapping Several subsystems relate AMS units, trays, and extruders, and more than one place derives a tray -index. The Bambu mapping path (`DevMappingUtil`) computes `ams_id * 4 + slot_id` and knows only the -Bambu unit kinds; `DevFilaSystem` provides the device model's own mapping below. The two agree for -Bambu units and diverge for a non-Bambu unit whose slot count is not four. +index. Bambu mapping computes `ams_id * 4 + slot_id` for its fixed-width units; generic mapping in +`DevFilaSystem` and the sidebar matcher uses the count of lanes in preceding units plus the slot id. +The two agree for Bambu units and differ for a non-Bambu unit whose slot count is not four. ### AMS id to extruder id @@ -128,13 +142,25 @@ used for backup-slot translation, calibration addressing, and the mapping popups - Generic (non-Bambu): a running lane index — the count of trays in preceding units plus the slot id. This depends on each unit's tray ids being 0-based and contiguous. +The sidebar sync matcher uses the same identities. Bambu retains its fixed left/right AMS and +external-spool choices. For a generic printer, a full sync considers AMS units according to their +bound extruder ids and considers every external slot by `255 - vt_id`; a split left/right mapping +still restricts itself to the corresponding legacy extruders 0 and 1. Generic AMS lanes use the +cumulative tray index above, while their `ams_id`/`slot_id` pair remains the address used to update +the matching sidebar entry. The runtime filament-list configs also carry a transient device-extruder +association so generic entries are grouped under the correct toolhead after sync; that association +is removed before the configs enter the slicing pipeline. Bambu keeps the existing key-based +left/right grouping. + ### Virtual-slot recognition `devPrinterUtil::IsVirtualSlot()` decides whether an id denotes a virtual tray. On Bambu printers it is the fixed pair `{255, 254}`. On generic printers it is the descending range -`[255 - count + 1, 255]`, where `count` is the selected printer preset's extruder count. The preset -is expected to match the connected device; when the two disagree the virtual range is wrong, which -is why a mismatch is reported rather than silently accepted. +`[255 - count + 1, 255]`, where `count` is the selected printer preset's extruder count. Slot +topology is grown from the live device extruder count, while this recognition range still uses the +preset count; the preset is expected to match the connected device. A mismatch can therefore make +the recognized virtual-id range disagree with `vt_slot`, and is reported by the existing count +warning rather than silently accepted. ## Device-control UI @@ -148,6 +174,9 @@ The device-control widgets follow the same device-reported counts. toolhead, `CreateAmsDoubleNozzle` for two, and `CreateAmsMultiNozzle` (one preview and one AMS simplebook per toolhead) for three or more. A two-toolhead generic printer therefore keeps the legacy left/right presentation; that is deliberate. +- The status panel builds external-spool entries from `MachineObject::vt_slot`; generic slot + synthesis keeps one empty entry available for each live toolhead even when the printer does not + report external-spool state. - The status panel's `ExtruderImage` renders one per-toolhead state per nozzle and is resized to `GetTotalExtderCount()`. Non-Bambu printers choose the active toolhead through `m_generic_nozzle_selector` (shown when more than one toolhead exists); Bambu keeps its left/right @@ -157,8 +186,9 @@ The device-control widgets follow the same device-reported counts. - **Single dialect.** The agent translates its protocol into this JSON; the model does not learn vendor protocols. Extending the model with a new unit kind or field means extending the dialect. -- **Agent-produced counts.** Toolhead, AMS, and slot counts come from the device payload. Nothing - caps them at two toolheads or four slots except the Bambu contract described above. +- **Runtime counts.** The agent reports toolhead count and AMS unit/tray data. The shared model derives + generic external-spool slot count from the parsed live toolhead count; nothing caps the number of + toolheads or generic slots except the id range and the Bambu contract described above. - **Extruder dialect invariants.** `device.extruder.state`'s count field must equal the length of `info[]`, and `info[]` must be ordered by extruder id with `id` equal to the array index: the parser places toolheads by array position while `GetExtderById()` indexes by id. @@ -185,10 +215,14 @@ The device-control widgets follow the same device-reported counts. `device.extruder` parser - [`DevFilaSystem`](../../src/slic3r/GUI/DeviceCore/DevFilaSystem.h) — AMS/tray model, the AMS parser, and the mapping helpers +- [`DevMapping.cpp`](../../src/slic3r/GUI/DeviceCore/DevMapping.cpp) — filament-to-device tray matching + for Bambu and generic external/AMS slots +- [`PresetBundle.cpp`](../../src/libslic3r/PresetBundle.cpp) — per-extruder grouping of the transient + device filament list - [`DevDefs.h`](../../src/slic3r/GUI/DeviceCore/DevDefs.h) — `DevAmsType`, extruder ids, and virtual tray ids - [`DeviceManager.cpp`](../../src/slic3r/GUI/DeviceManager.cpp) — message dispatch, `vir_slot` - parsing, and device state assembly + parsing, external-slot topology, and device state assembly - [`AMSControl.cpp`](../../src/slic3r/GUI/Widgets/AMSControl.cpp) — per-toolhead AMS panes (`CreateAmsMultiNozzle`) and the generic layout - [`AMSItem.hpp`](../../src/slic3r/GUI/Widgets/AMSItem.hpp) — `AMSModel` and the variable-lane AMS diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 5a9bf1597c..2d8dd182a2 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -4511,6 +4511,53 @@ static void warn_ambiguous_filament_id_match(const PresetCollection &filaments, << "\", also matches " << others; } +// Orca: resolve a compatible system preset from a printer-reported filament type. Prefers an exact +// "Generic " match, then the longest generic whose filament_type appears as a whole word in +// the reported type (e.g. "ASA" in "ASA Sparkle"). On success `generic_name_out` holds the +// "Generic <...>" name used for compatibility messages. Shared by the known-filament_id lookup and +// the data-bearing-but-unmapped tray path so both pick the same preset. +static PresetCollection::Iterator find_generic_preset_for_type(PresetCollection &filaments, const std::string &filament_type, std::string &generic_name_out) +{ + if (filament_type.empty()) + return filaments.end(); + const std::string generic_name = "Generic " + filament_type; + auto iter = std::find_if(filaments.begin(), filaments.end(), [&generic_name](const Preset &f) { + return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, generic_name); + }); + if (iter != filaments.end()) { + generic_name_out = generic_name; + return iter; + } + // Similarity fallback: find a generic preset whose filament_type + // appears as a whole word in the reported type (e.g. "ASA" in "ASA Sparkle"). + auto upper_type = boost::to_upper_copy(filament_type); + auto contains_word = [](const std::string& haystack, const std::string& needle) { + auto pos = haystack.find(needle); + while (pos != std::string::npos) { + bool start_ok = (pos == 0 || !std::isalnum(static_cast(haystack[pos - 1]))); + bool end_ok = (pos + needle.size() >= haystack.size() || + !std::isalnum(static_cast(haystack[pos + needle.size()]))); + if (start_ok && end_ok) + return true; + pos = haystack.find(needle, pos + 1); + } + return false; + }; + // Find the longest-matching preset type to prefer e.g. "PA-CF" over "PA". + size_t best_len = 0; + for (auto it = filaments.begin(); it != filaments.end(); ++it) { + if (!it->is_compatible || !it->is_system || !boost::algorithm::starts_with(it->name, "Generic ")) + continue; + auto preset_type = boost::to_upper_copy(it->config.opt_string("filament_type", 0u)); + if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) { + iter = it; + best_len = preset_type.size(); + generic_name_out = "Generic " + it->config.opt_string("filament_type", 0u); + } + } + return iter; +} + void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info) { combox_info.clear(); @@ -4566,7 +4613,9 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info) unsigned int PresetBundle::sync_ams_list(std::vector> &unknowns, bool use_map, std::map &maps, bool enable_append, MergeFilamentInfo &merge_info, bool color_only) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "use_map:" << use_map << " enable_append:" << enable_append; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": use_map %1% enable_append %2% color_only %3% start_presets %4% start_colors %5%") + % use_map % enable_append % color_only % this->filament_presets.size() + % project_config.option("filament_colour")->values.size(); std::vector ams_filament_presets; std::vector ams_filament_colors; std::vector ams_filament_color_types; @@ -4578,6 +4627,10 @@ unsigned int PresetBundle::sync_ams_list(std::vector("filament_multi_colour")->values; auto ams_id = ams.opt_string("ams_id", 0u); auto slot_id = ams.opt_string("slot_id", 0u); auto is_placeholder = ams.has("filament_slot_placeholder") && ams.opt_bool("filament_slot_placeholder", 0u); - ams_infos.push_back({filament_id.empty() ? false : true, false, is_placeholder, filament_color}); + // Real reported filament data: the device named a type or flagged the slot loaded. A + // topology-only synthetic virtual tray carries neither, so it must not become a filament. + bool has_reported_data = !is_placeholder && filament_id.empty() && (!filament_type.empty() || filament_exist); + AmsInfo ams_info; + ams_info.valid = !filament_id.empty(); + ams_info.is_placeholder = is_placeholder; + ams_info.has_reported_data = has_reported_data; + ams_info.filament_color = filament_color; + // Only the append path of a data-bearing, id-less tray reads these; the known-id path keeps + // taking its color and color type from ams_filament_colors/ams_filament_color_types. + if (has_reported_data) { + if (ams_info.filament_color.empty()) + ams_info.filament_color = "#CECECE"; + ams_info.filament_color_type = filament_color_type.empty() ? "1" : filament_color_type; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": entry %1% ams_id %2% slot_id %3% filament_id %4% type %5% color %6% valid %7% placeholder %8% reported_data %9% changed %10%") + % index % ams_id % slot_id % filament_id % filament_type % filament_color + % (!filament_id.empty()) % is_placeholder % has_reported_data % filament_changed; + ams_infos.push_back(std::move(ams_info)); AMSMapInfo temp = {ams_id, slot_id}; ams_array_maps.push_back(temp); index++; @@ -4617,14 +4690,45 @@ unsigned int PresetBundle::sync_ams_list(std::vectorname); + ams_filament_colors.push_back(append_color); + ams_filament_color_types.push_back(filament_color_type.empty() ? "1" : filament_color_type); + if (filament_multi_color.size() == 0) + filament_multi_color.push_back(append_color); + ams_multi_color_filment.push_back(filament_multi_color); + continue; + } + ams_filament_presets.push_back(""); + ams_filament_colors.push_back(""); + ams_filament_color_types.push_back(""); + ams_multi_color_filment.push_back({}); + } else { + ams_filament_presets.push_back("Generic PLA");//for unknow matieral + auto default_unknown_color = "#CECECE"; + ams_filament_colors.push_back(default_unknown_color); + ams_filament_color_types.push_back("1"); + if (filament_multi_color.size() == 0) { + filament_multi_color.push_back(default_unknown_color); + } + ams_multi_color_filment.push_back(filament_multi_color); } - ams_multi_color_filment.push_back(filament_multi_color); } continue; } @@ -4636,49 +4740,19 @@ unsigned int PresetBundle::sync_ams_list(std::vector(haystack[pos - 1]))); - bool end_ok = (pos + needle.size() >= haystack.size() || - !std::isalnum(static_cast(haystack[pos + needle.size()]))); - if (start_ok && end_ok) - return true; - pos = haystack.find(needle, pos + 1); - } - return false; - }; - // Find the longest-matching preset type to prefer e.g. "PA-CF" over "PA". - size_t best_len = 0; - for (auto it = filaments.begin(); it != filaments.end(); ++it) { - if (!it->is_compatible || !it->is_system || !boost::algorithm::starts_with(it->name, "Generic ")) - continue; - auto preset_type = boost::to_upper_copy(it->config.opt_string("filament_type", 0u)); - if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) { - iter = it; - best_len = preset_type.size(); - filament_type = "Generic " + it->config.opt_string("filament_type", 0u); - } - } - } + if (generic != filaments.end()) { + iter = generic; + filament_type = resolved_generic; } if (iter == filaments.end()) { // Prefer old selection @@ -4838,6 +4912,8 @@ unsigned int PresetBundle::sync_ams_list(std::vectorvalues = exist_colors; ams_multi_color_filment = exist_multi_color_filment; merge_info.merges.clear(); @@ -4897,13 +4973,18 @@ unsigned int PresetBundle::sync_ams_list(std::vector= ams_filament_presets.size()) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (check append)"; continue; } ams_infos[i].filament_preset = ams_filament_presets[i]; ams_infos[i].mutli_filament_color = ams_multi_color_filment[i]; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": entry %1% mapped %2% preset %3% color %4% valid %5% reported_data %6%") + % i % ams_infos[i].is_map % ams_infos[i].filament_preset % ams_infos[i].filament_color + % ams_infos[i].valid % ams_infos[i].has_reported_data; if (!ams_infos[i].is_map) { need_append_colors.emplace_back(ams_infos[i]); ams_filament_colors[i] = ""; @@ -4913,6 +4994,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector &value) { return value.empty(); }), ams_multi_color_filment.end()); if (need_append_colors.size() > 0 && enable_append) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "need_append_colors.size() > 0 && enable_append"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append candidates %1% (start existing %2%)") % need_append_colors.size() % exist_filament_presets.size(); auto get_idx_in_array = [](std::vector &presets, std::vector &colors, const std::string &preset, const std::string &color) -> int { for (size_t i = 0; i < presets.size(); i++) { if (presets[i] == preset && colors[i] == color) { @@ -4941,17 +5023,22 @@ unsigned int PresetBundle::sync_ams_list(std::vector= MAXIMUM_AMS_SYNC_FILAMENT_NUMBER){ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append capped at %1%, %2% candidate(s) left") % MAXIMUM_AMS_SYNC_FILAMENT_NUMBER % (need_append_colors.size() - i); break; } auto idx = get_idx_in_array(exist_filament_presets, exist_colors, need_append_colors[i].filament_preset, need_append_colors[i].filament_color); if (idx >= 0) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append dedup preset %1% color %2% already at %3%") % need_append_colors[i].filament_preset % need_append_colors[i].filament_color % idx; continue; } exist_filament_presets.push_back(need_append_colors[i].filament_preset); exist_colors.push_back(need_append_colors[i].filament_color); exist_color_types.push_back(need_append_colors[i].filament_color_type); exist_multi_color_filment.push_back(need_append_colors[i].mutli_filament_color); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": appended preset %1% color %2% type %3% -> slot %4%") % need_append_colors[i].filament_preset % need_append_colors[i].filament_color % need_append_colors[i].filament_color_type % (exist_filament_presets.size() - 1); } + } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append skipped candidates %1% enable_append %2%") % need_append_colors.size() % enable_append; } filament_color->values = exist_colors; filament_color_type->values = exist_color_types; @@ -5067,7 +5154,7 @@ unsigned int PresetBundle::sync_ams_list(std::vectorfilament_presets.size() % filament_color->values.size(); return this->filament_presets.size(); } @@ -5146,6 +5233,20 @@ std::vector> PresetBundle::get_extruder_filament if (extruder_nums > 1) { filament_infos.resize(extruder_nums, std::vector()); for (auto ams_item : filament_ams_list) { + // A generic slot carries the extruder it actually belongs to, so it lands in that + // extruder's bucket instead of the legacy left/right pair. Bambu entries (and anything + // without the field) keep the existing 0x10000 key convention. + const bool has_extruder_id = ams_item.second.has("device_tray_extruder_id"); + const int entry_extruder = has_extruder_id ? ams_item.second.opt_int("device_tray_extruder_id") : -1; + if (has_extruder_id) { + if (entry_extruder >= 0 && entry_extruder < extruder_nums) { + // Drop the transient association before the config reaches the slicing pipeline. + DynamicPrintConfig entry = ams_item.second; + entry.erase("device_tray_extruder_id"); + filament_infos[entry_extruder].push_back(std::move(entry)); + } + continue; + } if (ams_item.first & 0x10000) { // right filament_infos[1].push_back(ams_item.second); } else { // left diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 57f7fc9176..de02f20e90 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -2361,13 +2362,18 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man return false; } - if (!this->plater) - return false; + const Preset &selected_preset = preset_bundle->printers.get_selected_preset(); + const Preset *parent_preset = preset_bundle->printers.get_preset_parent(selected_preset); + const bool inherits_system_preset = selected_preset.is_user() && parent_preset && parent_preset->is_system; + if (!inherits_system_preset) { + if (!this->plater) + return false; - this->plater->update_objects_position_when_select_preset([machine_preset]() { - Tab *printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER); - printer_tab->select_preset(machine_preset->name); - }); + this->plater->update_objects_position_when_select_preset([machine_preset]() { + Tab *printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER); + printer_tab->select_preset(machine_preset->name); + }); + } } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " go on sync_extruder_list"; const Preset &cur_preset = preset_bundle->printers.get_selected_preset(); @@ -2379,7 +2385,19 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man assert(physical_extruder_map->values.size() == extruder_nums); extruder_map = physical_extruder_map->values; } - assert(obj->GetExtderSystem()->GetTotalExtderCount() == extruder_nums); + const int device_extruder_count = obj->GetExtderSystem()->GetTotalExtderCount(); + if (device_extruder_count != extruder_nums) { + MessageDialog dlg( + this->plater, + format_wxstr( + _L("The connected printer reports %1% extruder(s), but the selected printer preset has %2%. " + "Update the preset to match the printer before syncing."), + device_extruder_count, extruder_nums), + _L("Sync printer information"), + wxOK | wxICON_WARNING); + dlg.ShowModal(); + return false; + } // Multi-nozzle: resolve the nozzle option (pops MultiNozzleSyncDialog when is_manual). No-op for every // existing printer — support_multi_nozzle is false unless some extruder_max_nozzle_count entry is a real @@ -2458,26 +2476,60 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man int main_index = obj->is_main_extruder_on_left() ? 0 : 1; int deputy_index = obj->is_main_extruder_on_left() ? 1 : 0; + // Nozzle diameters are not changed by device sync. If the device reports a different or unavailable + // diameter, warn and leave the preset and controls alone while syncing AMS and other printer info. + const ConfigOptionFloats *preset_nozzle_diameter = + preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter"); + bool nozzle_diameters_match = false; + if (preset_nozzle_diameter != nullptr && preset_nozzle_diameter->values.size() == nozzle_diameters.size()) { + nozzle_diameters_match = true; + for (size_t index = 0; index < nozzle_diameters.size(); ++index) { + // A diameter the device does not report (missing, zero, negative or not a finite number) never matches. + const double device_diameter = nozzle_diameters[index]; + const double preset_diameter = preset_nozzle_diameter->values[index]; + if (!std::isfinite(device_diameter) || device_diameter <= 0.0 || + !std::isfinite(preset_diameter) || preset_diameter <= 0.0 || + std::fabs(device_diameter - preset_diameter) > EPSILON) { + nozzle_diameters_match = false; + break; + } + } + } + if (!nozzle_diameters_match) { + if (is_manual) { + MessageDialog dlg( + this->plater, + _L("The nozzle diameter reported by the connected printer does not match the current printer preset. " + "The nozzle diameter and the printer preset will not be synchronized. " + "Select the printer preset that matches the nozzle installed on the printer, then sync again."), + _L("Sync printer information"), wxICON_WARNING | wxOK); + dlg.ShowModal(); + } else { + // An automatic sync runs on every device update, so log the mismatch instead of popping a dialog. + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << __LINE__ + << " nozzle diameter mismatch between connected printer and printer preset," + " skip syncing the nozzle diameter"; + } + } + if (extruder_nums > 1) { - int left_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0])); - int right_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[1])); - assert(left_index != -1 && right_index != -1); - left_extruder->combo_diameter->SetSelection(left_index); - right_extruder->combo_diameter->SetSelection(right_index); - is_switching_diameter = true; - switch_diameter(false); - is_switching_diameter = false; + if (nozzle_diameters_match) { + int left_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0])); + int right_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[1])); + assert(left_index != -1 && right_index != -1); + left_extruder->combo_diameter->SetSelection(left_index); + right_extruder->combo_diameter->SetSelection(right_index); + } AMSCountPopupWindow::SetAMSCount(deputy_index, deputy_4, deputy_1); AMSCountPopupWindow::SetAMSCount(main_index, main_4, main_1); AMSCountPopupWindow::UpdateAMSCount(0, left_extruder); AMSCountPopupWindow::UpdateAMSCount(1, right_extruder); } else { - int index = single_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0])); - assert(index != -1); - single_extruder->combo_diameter->SetSelection(index); - is_switching_diameter = true; - switch_diameter(true); - is_switching_diameter = false; + if (nozzle_diameters_match) { + int index = single_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0])); + assert(index != -1); + single_extruder->combo_diameter->SetSelection(index); + } } // set nozzle volume type after switching prset, so this value can override the old value stored in conf @@ -6069,7 +6121,9 @@ void Sidebar::on_bed_type_change(BedType bed_type) * * Return Value: * - Map key encoding: - * - Virtual trays: 0x10000 + vt_tray.id (first/main extruder), or just vt_tray.id (secondary) + * - Virtual trays: 0x10000 + vt_tray.id for the main extruder's slot, or just vt_tray.id for the + * others. Bambu's fixed pair is 255 (main) / 254 (secondary); a generic printer identifies each + * slot by its own id (extruder = VIRTUAL_TRAY_MAIN_ID - id: 255 -> 0, 254 -> 1, ...) * - AMS trays: 0x10000 + (ams_id * 4 + slot_id) (main extruder), or (ams_id * 4 + slot_id) (secondary) * - Generic (non-Bambu) units are not four slots wide: their trays index by a cumulative lane * count across units instead of ams_id * 4 + slot_id @@ -6092,7 +6146,13 @@ std::map Sidebar::build_filament_ams_list(MachineObject } } - auto build_tray_config = [](DevAmsTray const &tray, std::string const &name, std::string ams_id, std::string slot_id) { + const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor(); + + // Diagnostic: keys produced from virtual (external) trays, so the final summary can label each + // output entry's origin as virtual vs AMS. + std::set virtual_slot_keys; + + auto build_tray_config = [is_bbl_vendor](DevAmsTray const &tray, std::string const &name, std::string ams_id, std::string slot_id, int extruder_id) { BOOST_LOG_TRIVIAL(info) << boost::format("build_filament_ams_list: name %1% setting_id %2% type %3% color %4%") % name % tray.setting_id % tray.m_fila_type % tray.color; DynamicPrintConfig tray_config; @@ -6107,6 +6167,12 @@ std::map 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}); + // Transient (never persisted) per-extruder association of a generic slot, so the sync and + // the slicing per-extruder grouping keep the real extruder instead of the legacy left/right + // buckets. Bambu entries omit it and keep the legacy key-only convention. + if (!is_bbl_vendor) { + tray_config.set_key_value("device_tray_extruder_id", new ConfigOptionInt(extruder_id)); + } std::optional info; if (wxGetApp().preset_bundle) { info = wxGetApp().preset_bundle->get_filament_by_filament_id(tray.setting_id); @@ -6119,10 +6185,44 @@ std::map Sidebar::build_filament_ams_list(MachineObject }; if (obj->ams_support_virtual_tray) { - int extruder = 0x10000; // Main (first) extruder at right - for (auto & vt_tray : obj->vt_slot) { - filament_ams_list.emplace(extruder + stoi(vt_tray.id), build_tray_config(vt_tray, "Ext",vt_tray.id, "0"));//254 or 255 - extruder = 0; + for (size_t index = 0; index < obj->vt_slot.size(); ++index) { + auto & vt_tray = obj->vt_slot[index]; + int vt_id = 0; + if (vt_tray.id.empty()) continue; + if (!std::all_of(vt_tray.id.begin(), vt_tray.id.end(), [](unsigned char c) { return std::isdigit(c) != 0; })) continue; + try { vt_id = std::stoi(vt_tray.id); } catch (...) { continue; } + if (vt_id < 0 || vt_id > VIRTUAL_TRAY_MAIN_ID) continue; + // Enumerate the extruder ordinal (the vt_slot index) and validate the expected tray id + // instead of trusting the vector size: a stray filler or a stale id for a non-live + // extruder must never surface as a real tray. Bambu keeps its fixed pair untouched. + if (!is_bbl_vendor) { + const int expected_id = VIRTUAL_TRAY_MAIN_ID - static_cast(index); + if (vt_id != expected_id || !devPrinterUtil::IsVirtualSlot(vt_id)) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skip vt_slot index %1% id %2% expected %3%") % index % vt_id % expected_id; + continue; + } + } + // Bambu keeps its fixed pair with the leading (main/right) slot marked 0x10000. On a + // generic printer every external slot is identified by its own virtual tray id + // (extruder = VIRTUAL_TRAY_MAIN_ID - id: 255 -> 0, 254 -> 1, ...), so mark the main + // extruder's slot by identity rather than by position in vt_slot. The map key then + // keeps each slot bound to its extruder across refreshes and sparse reports. + const bool is_main_slot = is_bbl_vendor ? (&vt_tray == &obj->vt_slot.front()) + : (vt_id == VIRTUAL_TRAY_MAIN_ID); + const int key = (is_main_slot ? 0x10000 : 0) + vt_id; + const auto inserted = filament_ams_list.emplace(key, + build_tray_config(vt_tray, "Ext", vt_tray.id, "0", VIRTUAL_TRAY_MAIN_ID - vt_id)); + virtual_slot_keys.insert(key); + if (!inserted.second) { + const auto &existing = inserted.first->second; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": virtual insert collision key %1% incoming_id %2% existing_origin %3% existing_ams %4% existing_slot %5% existing_type %6% existing_id %7%") + % key % vt_id + % (virtual_slot_keys.count(key) ? "virtual" : "ams") + % existing.opt_string("ams_id", 0u) + % existing.opt_string("slot_id", 0u) + % existing.opt_string("filament_type", 0u) + % existing.opt_string("filament_id", 0u); + } } } @@ -6138,12 +6238,12 @@ std::map Sidebar::build_filament_ams_list(MachineObject return std::string(); }; - const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor(); - auto list = obj->GetFilaSystem()->GetAmsList(); for (auto ams : list) { int ams_id = std::stoi(ams.first); int extruder = ams.second->GetExtruderId() ? 0 : 0x10000; // Main (first) extruder at right + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": AMS unit %1% extruder %2% trays %3%") + % ams_id % ams.second->GetExtruderId() % ams.second->GetTrays().size(); // Bambu units are four slots wide; a generic unit reports its own lane count and its trays // carry a cumulative lane index. The AMS map is keyed by string id, so count preceding units // numerically to keep this the same index the parser and the agent use. @@ -6154,12 +6254,49 @@ std::map Sidebar::build_filament_ams_list(MachineObject generic_tray_base += static_cast(other.second->GetTrays().size()); } } + size_t inserted_trays = 0; + size_t rejected_trays = 0; for (auto tray : ams.second->GetTrays()) { int slot_id = std::stoi(tray.first); const int tray_index = is_bbl_vendor ? ams_id * 4 + slot_id : generic_tray_base + slot_id; - filament_ams_list.emplace(extruder + tray_index, - build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), std::to_string(ams_id), std::to_string(slot_id))); + const int key = extruder + tray_index; + auto inserted = filament_ams_list.emplace(key, + build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), std::to_string(ams_id), std::to_string(slot_id), + ams.second->GetExtruderId())); + if (inserted.second) { + ++inserted_trays; + } else { + ++rejected_trays; + const auto &existing = inserted.first->second; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": AMS insert collision key %1% incoming_ams %2% incoming_slot %3% incoming_extruder %4% existing_origin %5% existing_ams %6% existing_slot %7% existing_extruder %8% existing_type %9% existing_id %10%") + % key % ams_id % slot_id % ams.second->GetExtruderId() + % (virtual_slot_keys.count(key) ? "virtual" : "ams") + % existing.opt_string("ams_id", 0u) + % existing.opt_string("slot_id", 0u) + % (existing.has("device_tray_extruder_id") ? existing.opt_int("device_tray_extruder_id") : -1) + % existing.opt_string("filament_type", 0u) + % existing.opt_string("filament_id", 0u); + } } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": AMS unit %1% tray inserts accepted %2% rejected %3%") + % ams_id % inserted_trays % rejected_trays; + } + + // Diagnostic: dump the final output map (origin, key, identity/data flags) so parser output can + // be correlated with the entries logged at the sync boundary. + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": output entries %1%") % filament_ams_list.size(); + for (const auto &entry : filament_ams_list) { + const auto &cfg = entry.second; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": out %1% key %2% ams_id %3% slot_id %4% extruder %5% type %6% id %7% exist %8% placeholder %9%") + % (virtual_slot_keys.count(entry.first) ? "virtual" : "ams") + % entry.first + % cfg.opt_string("ams_id", 0u) + % cfg.opt_string("slot_id", 0u) + % (cfg.has("device_tray_extruder_id") ? cfg.opt_int("device_tray_extruder_id") : -1) + % cfg.opt_string("filament_type", 0u) + % cfg.opt_string("filament_id", 0u) + % cfg.opt_bool("filament_exist", 0u) + % cfg.opt_bool("filament_slot_placeholder", 0u); } return filament_ams_list; } @@ -6396,6 +6533,20 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) std::vector> unknowns; auto enable_append = wxGetApp().app_config->get_bool("enable_append_color_by_sync_ams"); auto sync_color_only = wxGetApp().app_config->get("sync_ams_filament_mode") == "1"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": sync_color_only %1% enable_append %2% direct_sync %3% effective_use_map %4% filament_ams_list %5%") + % sync_color_only % enable_append % sync_result.direct_sync % (!sync_result.direct_sync) % list.size(); + for (const auto &entry : list) { + const auto &tray = entry.second; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": tray %1% ams_id %2% slot_id %3% filament_id %4% type %5% color %6% filament_exist %7% placeholder %8%") + % entry.first + % tray.opt_string("ams_id", 0u) + % tray.opt_string("slot_id", 0u) + % tray.opt_string("filament_id", 0u) + % tray.opt_string("filament_type", 0u) + % tray.opt_string("filament_colour", 0u) + % (tray.has("filament_exist") && tray.opt_bool("filament_exist", 0u)) + % (tray.has("filament_slot_placeholder") && tray.opt_bool("filament_slot_placeholder", 0u)); + } auto n = wxGetApp().preset_bundle->sync_ams_list(unknowns, !sync_result.direct_sync, sync_result.sync_maps, enable_append, merge_info, sync_color_only); wxString detail; for (auto & uk : unknowns) { diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index c6e1ea312e..381e1d6bc3 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -1155,7 +1155,7 @@ void SelectMachineDialog::sync_ams_mapping_result(std::vector &res wxString ams_id; wxColour ams_col; - if (f->tray_id == VIRTUAL_TRAY_MAIN_ID || f->tray_id == VIRTUAL_TRAY_DEPUTY_ID) + if (devPrinterUtil::IsVirtualSlot(f->ams_id)) { ams_id = _L("Ext"); }else if (f->tray_id >= 0) { @@ -1347,6 +1347,8 @@ bool SelectMachineDialog::get_ams_mapping_result(std::string &mapping_array_str, if (m_ams_mapping_result.empty()) return false; + const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor(); + bool valid_mapping_result = true; int invalid_count = 0; for (int i = 0; i < m_ams_mapping_result.size(); i++) { @@ -1402,7 +1404,10 @@ bool SelectMachineDialog::get_ams_mapping_result(std::string &mapping_array_str, // convert #RRGGBB to RRGGBBAA mapping_item["sourceColor"] = m_filaments[k].color; mapping_item["targetColor"] = m_ams_mapping_result[k].color; - if (tray_id == VIRTUAL_TRAY_MAIN_ID || tray_id == VIRTUAL_TRAY_DEPUTY_ID) { tray_id = -1; } + // The legacy v0 mapping array encodes an external spool as -1, a BBL-firmware + // contract. A generic printer keeps the virtual tray id (255 - extruder) so + // the payload still says which extruder's external slot was mapped. + if (is_bbl_vendor && (tray_id == VIRTUAL_TRAY_MAIN_ID || tray_id == VIRTUAL_TRAY_DEPUTY_ID)) { tray_id = -1; } /*new ams mapping data*/ try { diff --git a/src/slic3r/GUI/SyncAmsInfoDialog.cpp b/src/slic3r/GUI/SyncAmsInfoDialog.cpp index 5daeb6b3d2..1162461495 100644 --- a/src/slic3r/GUI/SyncAmsInfoDialog.cpp +++ b/src/slic3r/GUI/SyncAmsInfoDialog.cpp @@ -1245,7 +1245,7 @@ void SyncAmsInfoDialog::sync_ams_mapping_result(std::vector &resul wxString ams_id; wxColour ams_col; - if (f->tray_id == VIRTUAL_TRAY_MAIN_ID || f->tray_id == VIRTUAL_TRAY_DEPUTY_ID) { + if (devPrinterUtil::IsVirtualSlot(f->ams_id)) { ams_id = _L("Ext"); } @@ -1393,6 +1393,8 @@ bool SyncAmsInfoDialog::get_ams_mapping_result(std::string &mapping_array_str, s if (m_ams_mapping_result.empty()) return false; + const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor(); + bool valid_mapping_result = true; int invalid_count = 0; for (int i = 0; i < m_ams_mapping_result.size(); i++) { @@ -1449,7 +1451,10 @@ bool SyncAmsInfoDialog::get_ams_mapping_result(std::string &mapping_array_str, s // convert #RRGGBB to RRGGBBAA mapping_item["sourceColor"] = m_filaments[k].color; mapping_item["targetColor"] = m_ams_mapping_result[k].color; - if (tray_id == VIRTUAL_TRAY_MAIN_ID || tray_id == VIRTUAL_TRAY_DEPUTY_ID) { tray_id = -1; } + // The legacy v0 mapping array encodes an external spool as -1, a BBL-firmware + // contract. A generic printer keeps the virtual tray id (255 - extruder) so the + // payload still says which extruder's external slot was mapped. + if (is_bbl_vendor && (tray_id == VIRTUAL_TRAY_MAIN_ID || tray_id == VIRTUAL_TRAY_DEPUTY_ID)) { tray_id = -1; } /*new ams mapping data*/ try { diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 47f5c80cca..774017037c 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -2304,6 +2304,105 @@ TEST_CASE("set_num_filaments keeps mixed-color arrays in step with the filament } } +namespace { + +// One system "Generic PLA" the sync can resolve a reported "PLA" tray to, with a real +// filament_id so a tray that does report one still maps through the known-id path. +void add_generic_pla(PresetBundle &bundle) +{ + Preset &pla = add_inmemory_preset(bundle.filaments, "Generic PLA"); + pla.is_system = true; + pla.is_compatible = true; + pla.filament_id = "GFL99"; +} + +// A device tray as build_filament_ams_list builds it: a reported type, color and loaded flag, +// an optional preset id, and the placeholder flag pull-mode agents set on empty slots. +DynamicPrintConfig ams_tray(const std::string &filament_id, const std::string &filament_type, bool exist, bool placeholder) +{ + DynamicPrintConfig tray; + tray.set_key_value("filament_id", new ConfigOptionStrings{filament_id}); + tray.set_key_value("filament_type", new ConfigOptionStrings{filament_type}); + tray.set_key_value("filament_colour", new ConfigOptionStrings{"#FF0000"}); + tray.set_key_value("filament_colour_type", new ConfigOptionStrings{"1"}); + tray.set_key_value("filament_multi_colour", new ConfigOptionStrings{}); + tray.set_key_value("filament_exist", new ConfigOptionBools{exist}); + tray.set_key_value("filament_slot_placeholder", new ConfigOptionBools{placeholder}); + tray.set_key_value("ams_id", new ConfigOptionStrings{"0"}); + tray.set_key_value("slot_id", new ConfigOptionStrings{"0"}); + return tray; +} + +// Run the mapped sync over the bundle's single tray and return the filament count it reports, +// which is what the sidebar uses to rebuild the Prepare filament list. +unsigned int sync_one_tray(PresetBundle &bundle, bool enable_append) +{ + std::vector> unknowns; + std::map maps; + MergeFilamentInfo merge; + return bundle.sync_ams_list(unknowns, /*use_map=*/true, maps, enable_append, merge, /*color_only=*/false); +} + +} // namespace + +// A generic device reports type/color/loaded for a spool but no preset id. The Device tab shows +// that spool, so the Prepare sidebar has to count it: the sync appends the generic preset its +// reported type resolves to. A tray that only carried topology (no type, not loaded) must not. +TEST_CASE("A data-bearing AMS tray without a preset id appends a generic filament on sync", "[Preset][Bundle]") +{ + PresetBundle bundle; + add_generic_pla(bundle); + bundle.set_num_filaments(1, std::string("#00FF00")); + bundle.filament_ams_list[0] = ams_tray(/*filament_id=*/{}, "PLA", /*exist=*/true, /*placeholder=*/false); + + const size_t before = bundle.filament_presets.size(); + const auto count = sync_one_tray(bundle, /*enable_append=*/true); + + CHECK(count == before + 1); + CHECK(bundle.filament_presets.back() == "Generic PLA"); +} + +TEST_CASE("An empty synthetic AMS tray does not append a filament on sync", "[Preset][Bundle]") +{ + PresetBundle bundle; + add_generic_pla(bundle); + bundle.set_num_filaments(1, std::string("#00FF00")); + bundle.filament_ams_list[0] = ams_tray(/*filament_id=*/{}, /*filament_type=*/{}, /*exist=*/false, /*placeholder=*/false); + + const size_t before = bundle.filament_presets.size(); + const auto count = sync_one_tray(bundle, /*enable_append=*/true); + + CHECK(count == before); +} + +TEST_CASE("An empty AMS placeholder does not append a filament on sync", "[Preset][Bundle]") +{ + PresetBundle bundle; + add_generic_pla(bundle); + bundle.set_num_filaments(1, std::string("#00FF00")); + bundle.filament_ams_list[0] = ams_tray(/*filament_id=*/{}, /*filament_type=*/{}, /*exist=*/false, /*placeholder=*/true); + + const size_t before = bundle.filament_presets.size(); + const auto count = sync_one_tray(bundle, /*enable_append=*/true); + + CHECK(count == before); +} + +// The append preference still governs a data-bearing tray: with append off the sidebar count is +// unchanged, exactly as for a mapped-but-unlisted tray. +TEST_CASE("The append preference gates a data-bearing AMS tray without a preset id", "[Preset][Bundle]") +{ + PresetBundle bundle; + add_generic_pla(bundle); + bundle.set_num_filaments(1, std::string("#00FF00")); + bundle.filament_ams_list[0] = ams_tray(/*filament_id=*/{}, "PLA", /*exist=*/true, /*placeholder=*/false); + + const size_t before = bundle.filament_presets.size(); + const auto count = sync_one_tray(bundle, /*enable_append=*/false); + + CHECK(count == before); +} + // A mix is described by 1-based indices into the project's filament list, which Orca rebuilds // from the selected printer's snapshot (filament_%02u / filament_colors) at startup and on every // printer selection. Held anywhere but that same per-printer snapshot, the mixed arrays end up