fix: nozzle & preset sync

This commit is contained in:
Ian Chua
2026-10-16 20:51:22 +08:00
parent f68fbfed58
commit 5390b789f1
6 changed files with 493 additions and 98 deletions
+49 -15
View File
@@ -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
+149 -48
View File
@@ -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 <type>" 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<unsigned char>(haystack[pos - 1])));
bool end_ok = (pos + needle.size() >= haystack.size() ||
!std::isalnum(static_cast<unsigned char>(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<std::pair<DynamicPrintConfig *,std::string>> &unknowns, bool use_map, std::map<int, AMSMapInfo> &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<ConfigOptionStrings>("filament_colour")->values.size();
std::vector<std::string> ams_filament_presets;
std::vector<std::string> ams_filament_colors;
std::vector<std::string> ams_filament_color_types;
@@ -4578,6 +4627,10 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
bool valid{false};
bool is_map{false};
bool is_placeholder{false};
// Orca: set for a tray that reports filament data (type and/or loaded flag) but carries
// no preset id. It is not `valid` (no id to map through), yet it still holds a real spool
// and is eligible for the same append step as a mapped tray.
bool has_reported_data{false};
std::string filament_color = "";
std::string filament_color_type = "";
std::string filament_preset = "";
@@ -4591,12 +4644,32 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
auto filament_id = ams.opt_string("filament_id", 0u);
auto filament_color = ams.opt_string("filament_colour", 0u);
auto filament_color_type = ams.opt_string("filament_colour_type", 0u);
auto filament_type = ams.opt_string("filament_type", 0u);
auto filament_exist = ams.has("filament_exist") && ams.opt_bool("filament_exist", 0u);
auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed");
auto filament_multi_color = ams.opt<ConfigOptionStrings>("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::vector<std::pair<DynamicPrintConfi
ams_filament_color_types.push_back("");
ams_multi_color_filment.push_back({});
} else if (use_map) {
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);
if (has_reported_data) {
// Orca: the tray reports a filament but no preset id, so it never reaches the
// filament_id lookup. Resolve the same generic preset the id path would pick
// from the reported type and keep the reported color; the append step below (if
// enabled) then turns it into a sidebar filament. Without a generic match the
// slot is dropped rather than invented, exactly like an empty tray.
std::string resolved_generic;
auto generic = find_generic_preset_for_type(filaments, filament_type, resolved_generic);
if (generic == filaments.end()) {
// Reported loaded but no usable type: fall back to the first compatible
// generic, matching the id path's fallback for an unknown model.
generic = std::find_if(filaments.begin(), filaments.end(), [](const Preset &f) {
return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, "Generic ");
});
}
if (generic != filaments.end()) {
auto append_color = filament_color.empty() ? std::string("#CECECE") : filament_color;
ams_filament_presets.push_back(generic->name);
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<std::pair<DynamicPrintConfi
continue;
}
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; });
warn_ambiguous_filament_id_match(filaments, iter, 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()) {
auto original_type = filament_type;
std::string resolved_generic;
auto generic = find_generic_preset_for_type(filaments, filament_type, resolved_generic);
if (!filament_type.empty())
filament_type = "Generic " + filament_type;
iter = std::find_if(filaments.begin(), filaments.end(), [&filament_type](auto &f) {
return f.is_compatible && f.is_system
&& boost::algorithm::starts_with(f.name, filament_type);
});
if (iter == filaments.end()) {
// Similarity fallback: find a generic preset whose filament_type
// appears as a whole word in the AMS type (e.g. "ASA" in "ASA Sparkle").
auto upper_type = boost::to_upper_copy(original_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<unsigned char>(haystack[pos - 1])));
bool end_ok = (pos + needle.size() >= haystack.size() ||
!std::isalnum(static_cast<unsigned char>(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::vector<std::pair<DynamicPrintConfi
}
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": color_only mapped_any %1% maps %2% entries %3%")
% mapped_any % maps.size() % ams_infos.size();
filament_color->values = 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<std::pair<DynamicPrintConfi
}
}
for (size_t i = 0; i < ams_infos.size(); i++) {// check append
if (ams_infos[i].valid) {
// A mapped tray (valid id) and a data-bearing tray without an id both append the same
// way; only a tray with neither contributes nothing.
if (ams_infos[i].valid || ams_infos[i].has_reported_data) {
if (i >= 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<std::pair<DynamicPrintConfi
}
}
else {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": entry %1% dropped (no valid id or reported data)") % i;
ams_filament_colors[i] = "";
ams_filament_color_types[i] = "";
ams_filament_presets[i] = "";
@@ -4930,7 +5012,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
[](std::vector<std::string> &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<std::string> &presets, std::vector<std::string> &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<std::pair<DynamicPrintConfi
};
for (size_t i = 0; i < need_append_colors.size(); i++){
if (exist_filament_presets.size() >= 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::vector<std::pair<DynamicPrintConfi
// Update ams_multi_color_filment
update_filament_multi_color();
update_multi_material_filament_presets();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "finish sync ams list";
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finish sync ams list, end_presets %1% end_colors %2%") % this->filament_presets.size() % filament_color->values.size();
return this->filament_presets.size();
}
@@ -5146,6 +5233,20 @@ std::vector<std::vector<DynamicPrintConfig>> PresetBundle::get_extruder_filament
if (extruder_nums > 1) {
filament_infos.resize(extruder_nums, std::vector<DynamicPrintConfig>());
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
+182 -31
View File
@@ -20,6 +20,7 @@
#include <clocale>
#include <cstddef>
#include <algorithm>
#include <cctype>
#include <ios>
#include <iomanip>
#include <cstring>
@@ -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<ConfigOptionFloats>("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<int, DynamicPrintConfig> 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<int> 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<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});
// 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<FilamentBaseInfo> info;
if (wxGetApp().preset_bundle) {
info = wxGetApp().preset_bundle->get_filament_by_filament_id(tray.setting_id);
@@ -6119,10 +6185,44 @@ std::map<int, DynamicPrintConfig> 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<int>(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<int, DynamicPrintConfig> 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<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
generic_tray_base += static_cast<int>(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<std::pair<DynamicPrintConfig *,std::string>> 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) {
+7 -2
View File
@@ -1155,7 +1155,7 @@ void SelectMachineDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &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 {
+7 -2
View File
@@ -1245,7 +1245,7 @@ void SyncAmsInfoDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &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 {
@@ -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<std::pair<DynamicPrintConfig *, std::string>> unknowns;
std::map<int, AMSMapInfo> 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