// Match the include environment that libslic3r_gui TUs get from pchheader.hpp: Windows.h with // WIN32_LEAN_AND_MEAN/NOMINMAX must come first so rpcndr.h's `byte` is processed before // makes std::byte a competing candidate (otherwise the Windows COM headers pulled in via // DeviceManager.hpp error with an ambiguous `byte`). wx/timer.h must precede DeviceManager.hpp, // which includes DeviceErrorDialog.hpp (uses wxTimerEvent) before its own wx/timer.h include. #ifdef WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #ifndef NOMINMAX #define NOMINMAX #endif #include #endif #include #include #include #include #include #include #include "slic3r/GUI/DeviceCore/DevDefs.h" #include "libslic3r/ProjectTask.hpp" #include #include #include #include "slic3r/GUI/DeviceManager.hpp" #include "slic3r/GUI/DeviceCore/DevMapping.h" #include "slic3r/GUI/DeviceCore/DevFilaSystem.h" #include #include "slic3r/GUI/DeviceCore/DevFilaSwitch.h" #include "slic3r/GUI/DeviceCore/DevUtil.h" using json = nlohmann::json; using namespace Slic3r; TEST_CASE("AMS tray placeholder state follows the latest status", "[DevFilaSystem]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); const json empty_slot = json::parse(R"({ "ams": { "tray_exist_bits": "0", "ams": [ { "id": "0", "info": "00000001", "tray": [ { "id": "0", "tray_slot_placeholder": "1", "tray_color": "00000000" } ] } ] } })"); DevFilaSystemParser::ParseV1_0(empty_slot, &obj, obj.GetFilaSystem().get(), false); DevAmsTray* tray = obj.GetFilaSystem()->GetAmsTray("0", "0"); REQUIRE(tray != nullptr); REQUIRE(tray->is_slot_placeholder); const json loaded_slot = json::parse(R"({ "ams": { "tray_exist_bits": "1", "ams": [ { "id": "0", "info": "00000001", "tray": [ { "id": "0", "tray_color": "FF0000FF" } ] } ] } })"); DevFilaSystemParser::ParseV1_0(loaded_slot, &obj, obj.GetFilaSystem().get(), false); CHECK_FALSE(tray->is_slot_placeholder); } TEST_CASE("Agent filament slots preserve every extruder and AMS binding", "[DevFilaSystem]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); const json filament = json::parse(R"({ "units": [ { "id": "0", "extruder": 0, "slots": [ { "index": 0, "loaded": true, "material": "PLA", "color": "FF5733FF" }, { "index": 1, "loaded": true, "material": "PLA", "color": "33C1FFFF" }, { "index": 2, "loaded": true, "material": "PLA", "color": "8D33FFFF" }, { "index": 3, "loaded": false, "material": "", "color": "00000000" } ] }, { "id": "1", "extruder": 1, "slots": [ { "index": 0, "loaded": true, "material": "ASA", "color": "33C1FFFF" }, { "index": 1, "loaded": true, "material": "ASA", "color": "8D33FFFF" }, { "index": 2, "loaded": true, "material": "ASA", "color": "FF5733FF" }, { "index": 3, "loaded": false, "material": "", "color": "00000000" } ] }, { "id": "2", "extruder": 2, "slots": [ { "index": 0, "loaded": true, "material": "ABS", "color": "8D33FFFF" }, { "index": 1, "loaded": true, "material": "ABS", "color": "FF5733FF" }, { "index": 2, "loaded": true, "material": "ABS", "color": "33C1FFFF" }, { "index": 3, "loaded": false, "material": "", "color": "00000000" } ] }, { "id": "3", "extruder": 3, "slots": [ { "index": 0, "loaded": true, "material": "PETG", "color": "FF5733FF" }, { "index": 1, "loaded": true, "material": "PETG", "color": "33C1FFFF" }, { "index": 2, "loaded": true, "material": "PETG", "color": "8D33FFFF" }, { "index": 3, "loaded": false, "material": "", "color": "00000000" } ] } ], "external": [ { "extruder": 3, "loaded": true, "material": "PETG", "preset_id": "P3" }, { "extruder": 0, "loaded": true, "material": "PLA", "preset_id": "P0" }, { "extruder": 2, "loaded": false, "material": "ABS", "preset_id": "P2" }, { "extruder": 1, "loaded": true, "material": "ASA", "preset_id": "P1" } ] })"); DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get()); REQUIRE(obj.vt_slot.size() == 4); CHECK(obj.vt_slot[0].id == "255"); CHECK(obj.vt_slot[1].id == "254"); CHECK(obj.vt_slot[2].id == "253"); CHECK(obj.vt_slot[3].id == "252"); CHECK(obj.vt_slot[0].setting_id == "P0"); CHECK(obj.vt_slot[1].setting_id == "P1"); CHECK(obj.vt_slot[2].setting_id == "P2"); CHECK(obj.vt_slot[3].setting_id == "P3"); REQUIRE(obj.GetFilaSystem()->GetAmsCount() == 4); for (int extruder = 0; extruder < 4; ++extruder) { const auto& ams = obj.GetFilaSystem()->GetAmsList().at(std::to_string(extruder)); CHECK(ams->GetExtruderId() == extruder); CHECK(ams->GetBindedExtruderSet() == std::set{extruder}); REQUIRE(ams->GetTrays().size() == 4); CHECK(ams->GetTrays().at("0")->color != ams->GetTrays().at("1")->color); CHECK(ams->GetTrays().at("1")->color != ams->GetTrays().at("2")->color); CHECK_FALSE(ams->GetTrays().at("3")->is_exists); } CHECK(obj.contains_tray("253", "0")); CHECK(obj.get_tray("253", "0").setting_id == "P2"); CHECK(obj.GetFilaSystem()->GetExtruderIdByAmsId("253") == 2); const auto tray_index_map = obj.GetFilaSystem()->GetTrayIndexMap(); REQUIRE(tray_index_map.count(252) == 1); CHECK(tray_index_map.at(252).first == 252); CHECK(tray_index_map.at(252).second == 0); } TEST_CASE("External virtual tray slots cover each extruder without shifting existing data", "[DevFilaSystem]") { std::vector slots; slots.emplace_back("255"); slots[0].setting_id = "main-spool"; // Ensure slots for three extruders, preserving the main spool's reported metadata. for (int extruder_id = 0; extruder_id < 3; ++extruder_id) { grow_virtual_tray_slots(slots, static_cast(VIRTUAL_TRAY_MAIN_ID - extruder_id)); } REQUIRE(slots.size() == 3); CHECK(slots[0].id == "255"); CHECK(slots[1].id == "254"); CHECK(slots[2].id == "253"); CHECK(slots[0].setting_id == "main-spool"); // Repeating the topology update is idempotent and leaves existing tray data intact. slots[2].setting_id = "third-spool"; for (int extruder_id = 0; extruder_id < 3; ++extruder_id) { grow_virtual_tray_slots(slots, static_cast(VIRTUAL_TRAY_MAIN_ID - extruder_id)); } REQUIRE(slots.size() == 3); CHECK(slots[2].setting_id == "third-spool"); } TEST_CASE("Sparse external spool reports retain their extruder slot IDs", "[DevFilaSystem]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); const json filament = { {"units", json::array()}, {"external", json::array({ {{"extruder", 2}, {"loaded", true}, {"material", "PETG"}, {"preset_id", "third-spool"}}, {{"extruder", 0}, {"loaded", true}, {"material", "PLA"}, {"preset_id", "main-spool"}} })} }; DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get()); REQUIRE(obj.vt_slot.size() == 3); CHECK(obj.vt_slot[0].id == "255"); CHECK(obj.vt_slot[0].setting_id == "main-spool"); CHECK(obj.vt_slot[1].id == "254"); CHECK(obj.vt_slot[2].id == "253"); CHECK(obj.vt_slot[2].setting_id == "third-spool"); } TEST_CASE("Generic AMS tray index map uses cumulative lane counts", "[DevMapping]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); // A well-formed generic payload pairs "units" with "external". An empty external array resets // vt_slot, clearing the default virtual tray (255) MachineObject's constructor seeds; otherwise // that phantom external tray lands in GetTrayIndexMap and inflates the map size. json filament = { {"units", json::array()}, {"external", json::array()} }; for (int ams_id = 0; ams_id < 2; ++ams_id) { json unit = { {"id", std::to_string(ams_id)}, {"extruder", ams_id}, {"slots", json::array()} }; for (int slot_id = 0; slot_id < 5; ++slot_id) unit["slots"].push_back({{"index", slot_id}, {"loaded", false}}); filament["units"].push_back(std::move(unit)); } DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get()); const auto tray_index_map = obj.GetFilaSystem()->GetTrayIndexMap(); REQUIRE(tray_index_map.size() == 10); CHECK(tray_index_map.at(0).first == 0); CHECK(tray_index_map.at(0).second == 0); CHECK(tray_index_map.at(4).first == 0); CHECK(tray_index_map.at(4).second == 4); CHECK(tray_index_map.at(5).first == 1); CHECK(tray_index_map.at(5).second == 0); CHECK(tray_index_map.at(9).first == 1); CHECK(tray_index_map.at(9).second == 4); } TEST_CASE("Generic tray index map handles unequal per-unit slot counts", "[DevMapping]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); // An "external" array must accompany "units" so vt_slot is reset (see the cumulative test above). json filament = { {"units", json::array()}, {"external", json::array()} }; const std::vector slot_counts = {4, 12}; for (int ams_id = 0; ams_id < static_cast(slot_counts.size()); ++ams_id) { json unit = { {"id", std::to_string(ams_id)}, {"extruder", ams_id}, {"slots", json::array()} }; for (int slot_id = 0; slot_id < slot_counts[ams_id]; ++slot_id) unit["slots"].push_back({{"index", slot_id}, {"loaded", false}}); filament["units"].push_back(std::move(unit)); } DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get()); const auto tray_index_map = obj.GetFilaSystem()->GetTrayIndexMap(); REQUIRE(tray_index_map.size() == 16); // Unit 0 occupies global lanes 0..3; unit 1 continues at 4..15 because the counter advances // by the reported tray count, not by a fixed four-slot stride. CHECK(tray_index_map.at(0).first == 0); CHECK(tray_index_map.at(0).second == 0); CHECK(tray_index_map.at(3).first == 0); CHECK(tray_index_map.at(3).second == 3); CHECK(tray_index_map.at(4).first == 1); CHECK(tray_index_map.at(4).second == 0); CHECK(tray_index_map.at(15).first == 1); CHECK(tray_index_map.at(15).second == 11); } TEST_CASE("A generic AMS unit reports its reported tray count", "[DevFilaSystem]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); json filament = { {"units", json::array()}, {"external", json::array()} }; const std::vector slot_counts = {8, 1}; for (int ams_id = 0; ams_id < static_cast(slot_counts.size()); ++ams_id) { json unit = { {"id", std::to_string(ams_id)}, {"extruder", ams_id}, {"slots", json::array()} }; for (int slot_id = 0; slot_id < slot_counts[ams_id]; ++slot_id) unit["slots"].push_back({{"index", slot_id}, {"loaded", false}}); filament["units"].push_back(std::move(unit)); } DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get()); // Non-Bambu units are not capped at the four-slot Bambu contract; the reported tray count wins. const auto& ams_list = obj.GetFilaSystem()->GetAmsList(); REQUIRE(ams_list.at("0")->GetSlotCount() == 8); REQUIRE(ams_list.at("1")->GetSlotCount() == 1); } TEST_CASE("Switch-bound AMS trays map to the left extruder", "[DevMapping]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); // aux bit 29 = Filament Track Switch installed (DevFilaSwitch.cpp:69-77) obj.GetFilaSwitch()->ParseFilaSwitchInfo(json::parse(R"({"aux":"20000000"})")); REQUIRE(obj.GetFilaSwitch()->IsInstalled()); // info bits: 0-3 type(1=AMS), 8-11 extruder(0xE=switch-bound), 24-27 bind_switch_in(0) // tray_exist_bits bit 0 marks AMS 0 / tray 0 present so the mapping result survives the // is_exists check in is_valid_mapping_result (DevFilaSystem.cpp:769). // tray_info_idx/tray_type are intentionally omitted: the tray parse resolves the display // filament type via MachineObject::setting_id_to_type(), which reads the GUI preset bundle // (wxGetApp().preset_bundle) — unavailable in this headless unit test. The tray filament // type (only needed for the type-match below) is set directly after the parse instead. json print_push = json::parse(R"({ "ams": { "tray_exist_bits": "1", "ams": [ { "id": "0", "info": "00000E01", "tray": [ { "id": "0", "tray_color": "FF0000FF" } ] } ] } })"); DevFilaSystemParser::ParseV1_0(print_push, &obj, obj.GetFilaSystem().get(), false); const auto& ams_list = obj.GetFilaSystem()->GetAmsList(); REQUIRE(ams_list.count("0") == 1); REQUIRE(ams_list.at("0")->GetBindedExtruderSet().count(MAIN_EXTRUDER_ID) == 1); REQUIRE(ams_list.at("0")->GetBindedExtruderSet().count(DEPUTY_EXTRUDER_ID) == 1); DevAmsTray* tray = obj.GetFilaSystem()->GetAmsTray("0", "0"); REQUIRE(tray != nullptr); tray->m_fila_type = "PLA"; FilamentInfo fila; fila.id = 0; fila.type = "PLA"; fila.color = "FF0000FF"; std::vector result; std::vector map_opt(4, false); // MappingOption: LEFT_AMS,RIGHT_AMS,LEFT_EXT,RIGHT_EXT (DevMapping.h:13-19) map_opt[MappingOption::USE_LEFT_AMS] = true; DevMappingUtil::ams_filament_mapping(&obj, {fila}, result, map_opt, {}, false); // A switch-bound AMS feeds BOTH extruders, so a left-only mapping request // must still land the filament on the AMS tray. REQUIRE(result.size() == 1); CHECK(result[0].tray_id == 0); CHECK(result[0].ams_id == "0"); } TEST_CASE("Without a switch the binding set equals the single bound extruder", "[DevMapping]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); REQUIRE_FALSE(obj.GetFilaSwitch()->IsInstalled()); // AMS 0: info extruder nibble = MAIN (right). AMS 1: nibble = DEPUTY (left). // AMS 2: no "info" key at all (old X1/P1 firmware) -> must default to MAIN. // tray_exist_bits: bit ams_id*4+tray_id -> 0x111 marks tray 0 of each AMS. json print_push = json::parse(R"({ "ams": { "tray_exist_bits": "111", "ams": [ { "id": "0", "info": "00000001", "tray": [ { "id": "0", "tray_color": "FF0000FF" } ] }, { "id": "1", "info": "00000101", "tray": [ { "id": "0", "tray_color": "00FF00FF" } ] }, { "id": "2", "tray": [ { "id": "0", "tray_color": "0000FFFF" } ] } ] } })"); DevFilaSystemParser::ParseV1_0(print_push, &obj, obj.GetFilaSystem().get(), false); // The invariant that keeps the binding-set mapping filter behavior-preserving for // ordinary printers: GetBindedExtruderSet() == { GetExtruderId() }, info key or not. const auto& ams_list = obj.GetFilaSystem()->GetAmsList(); REQUIRE(ams_list.count("0") == 1); REQUIRE(ams_list.count("1") == 1); REQUIRE(ams_list.count("2") == 1); for (const char* id : {"0", "1", "2"}) { const auto& ams = ams_list.at(id); INFO("ams " << id); REQUIRE(ams->GetBindedExtruderSet().size() == 1); REQUIRE(ams->GetBindedExtruderSet().count(ams->GetExtruderId()) == 1); } REQUIRE(ams_list.at("0")->GetExtruderId() == MAIN_EXTRUDER_ID); REQUIRE(ams_list.at("1")->GetExtruderId() == DEPUTY_EXTRUDER_ID); REQUIRE(ams_list.at("2")->GetExtruderId() == MAIN_EXTRUDER_ID); for (const char* id : {"0", "1", "2"}) { DevAmsTray* tray = obj.GetFilaSystem()->GetAmsTray(id, "0"); REQUIRE(tray != nullptr); tray->m_fila_type = "PLA"; } // A left-only request must exclude the MAIN-bound AMSes: the red filament exactly // matches AMS 0's red tray, so landing anywhere but AMS 1 (or unmapped) means the // exclusion is broken. FilamentInfo fila; fila.id = 0; fila.type = "PLA"; fila.color = "FF0000FF"; std::vector result; std::vector map_opt(4, false); map_opt[MappingOption::USE_LEFT_AMS] = true; DevMappingUtil::ams_filament_mapping(&obj, {fila}, result, map_opt, {}, false); REQUIRE(result.size() == 1); CHECK(result[0].ams_id != "0"); CHECK(result[0].ams_id != "2"); // The mirrored right-only request maps to the exact-match MAIN-bound AMS. result.clear(); map_opt[MappingOption::USE_LEFT_AMS] = false; map_opt[MappingOption::USE_RIGHT_AMS] = true; DevMappingUtil::ams_filament_mapping(&obj, {fila}, result, map_opt, {}, false); REQUIRE(result.size() == 1); CHECK(result[0].ams_id == "0"); } TEST_CASE("Switch-bound AMS with an invalid track is excluded from mapping", "[DevMapping]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); obj.GetFilaSwitch()->ParseFilaSwitchInfo(json::parse(R"({"aux":"20000000"})")); REQUIRE(obj.GetFilaSwitch()->IsInstalled()); // info bits 24-27 = 0xF: switch-bound (0xE) but the input track is not yet valid - // the transient while the device is still homing the switch. The AMS must survive // (display keeps working) with an EMPTY binding set that excludes it from mapping. json print_push = json::parse(R"({ "ams": { "tray_exist_bits": "1", "ams": [ { "id": "0", "info": "0F000E01", "tray": [ { "id": "0", "tray_color": "FF0000FF" } ] } ] } })"); DevFilaSystemParser::ParseV1_0(print_push, &obj, obj.GetFilaSystem().get(), false); const auto& ams_list = obj.GetFilaSystem()->GetAmsList(); REQUIRE(ams_list.count("0") == 1); REQUIRE(ams_list.at("0")->GetBindedExtruderSet().empty()); REQUIRE_FALSE(ams_list.at("0")->GetSwitcherPos().has_value()); REQUIRE_FALSE(obj.GetFilaSwitch()->IsReady()); DevAmsTray* tray = obj.GetFilaSystem()->GetAmsTray("0", "0"); REQUIRE(tray != nullptr); tray->m_fila_type = "PLA"; FilamentInfo fila; fila.id = 0; fila.type = "PLA"; fila.color = "FF0000FF"; std::vector result; std::vector map_opt(4, false); map_opt[MappingOption::USE_LEFT_AMS] = true; map_opt[MappingOption::USE_RIGHT_AMS] = true; DevMappingUtil::ams_filament_mapping(&obj, {fila}, result, map_opt, {}, false); REQUIRE(result.size() == 1); CHECK(result[0].tray_id == -1); // Without the switch, the same 0xE AMS is dropped from the list entirely. MachineObject obj_no_switch(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); REQUIRE_FALSE(obj_no_switch.GetFilaSwitch()->IsInstalled()); DevFilaSystemParser::ParseV1_0(print_push, &obj_no_switch, obj_no_switch.GetFilaSystem().get(), false); REQUIRE(obj_no_switch.GetFilaSystem()->GetAmsList().count("0") == 0); } TEST_CASE("Generic external spools map by their virtual tray id", "[DevMapping]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); // One external spool per extruder: 255 -> extruder 0, 254 -> 1, 253 -> 2, 252 -> 3. const json filament = { {"units", json::array()}, {"external", json::array({ {{"extruder", 0}, {"loaded", true}, {"material", "PLA"}, {"color", "FF5733FF"}, {"preset_id", "P0"}}, {{"extruder", 1}, {"loaded", true}, {"material", "ASA"}, {"color", "33C1FFFF"}, {"preset_id", "P1"}}, {{"extruder", 2}, {"loaded", true}, {"material", "ABS"}, {"color", "8D33FFFF"}, {"preset_id", "P2"}}, {{"extruder", 3}, {"loaded", true}, {"material", "PETG"}, {"color", "44AA22FF"}, {"preset_id", "P3"}} })} }; DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get()); REQUIRE(obj.vt_slot.size() == 4); REQUIRE(obj.vt_slot[2].id == "253"); // Matches only extruder 2's external spool (ABS / 8D33FFFF). FilamentInfo fila; fila.id = 0; fila.type = "ABS"; fila.color = "8D33FFFF"; std::vector result; std::vector map_opt(4, false); map_opt[MappingOption::USE_LEFT_EXT] = true; map_opt[MappingOption::USE_RIGHT_EXT] = true; DevMappingUtil::ams_filament_mapping(&obj, {fila}, result, map_opt, {}, false); REQUIRE(result.size() == 1); // The external spool keeps its virtual tray id (253), never the AMS ams_id * 4 + slot_id form. CHECK(result[0].tray_id == 253); CHECK(result[0].ams_id == "253"); } TEST_CASE("Mapping validation accepts only reported generic virtual slots", "[DevMapping]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); const json filament = { {"units", json::array()}, {"external", json::array({ {{"extruder", 0}, {"loaded", true}, {"material", "PLA"}, {"preset_id", "P0"}}, {{"extruder", 1}, {"loaded", true}, {"material", "ASA"}, {"preset_id", "P1"}}, {{"extruder", 2}, {"loaded", true}, {"material", "ABS"}, {"preset_id", "P2"}} })} }; DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get()); REQUIRE(obj.vt_slot.size() == 3); FilamentInfo reported; reported.id = 0; reported.tray_id = 253; reported.ams_id = "253"; // extruder 2's external slot, present in vt_slot reported.slot_id = "0"; { std::vector result{reported}; CHECK(DevMappingUtil::is_valid_mapping_result(&obj, result, false)); } // A virtual id the printer never reported is rejected and reset. FilamentInfo missing; missing.id = 1; missing.tray_id = 252; missing.ams_id = "252"; missing.slot_id = "0"; { std::vector result{missing}; CHECK_FALSE(DevMappingUtil::is_valid_mapping_result(&obj, result, false)); CHECK(result[0].tray_id == -1); } } TEST_CASE("Generic AMS units on every extruder join the mapping", "[DevMapping]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); const std::vector materials = {"PLA", "ASA", "ABS", "PETG", "TPU"}; const std::vector colors = {"FF5733FF", "33C1FFFF", "8D33FFFF", "44AA22FF", "FFAA00FF"}; json filament = { {"units", json::array()}, {"external", json::array()} }; for (int extruder = 0; extruder < 5; ++extruder) { json unit = { {"id", std::to_string(extruder)}, {"extruder", extruder}, {"slots", json::array()} }; unit["slots"].push_back({{"index", 0}, {"loaded", true}, {"material", materials[extruder]}, {"color", colors[extruder]}}); filament["units"].push_back(std::move(unit)); } DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get()); REQUIRE(obj.GetFilaSystem()->GetAmsCount() == 5); // PETG / 44AA22FF is extruder 3's AMS slot, with cumulative generic tray index 3. FilamentInfo fila; fila.id = 0; fila.type = "PETG"; fila.color = "44AA22FF"; // The sidebar sync enables both legacy AMS flags, so every extruder participates. std::vector result; std::vector map_opt(4, false); map_opt[MappingOption::USE_LEFT_AMS] = true; map_opt[MappingOption::USE_RIGHT_AMS] = true; DevMappingUtil::ams_filament_mapping(&obj, {fila}, result, map_opt, {}, false); REQUIRE(result.size() == 1); CHECK(result[0].ams_id == "3"); CHECK(result[0].tray_id == 3); // SelectMachine's right-side split passes only the right flag: extruder 3's unit stays out and // nothing in extruder 0's unit matches PETG, so the filament is left unmapped. std::vector split_result; map_opt[MappingOption::USE_LEFT_AMS] = false; DevMappingUtil::ams_filament_mapping(&obj, {fila}, split_result, map_opt, {}, false); REQUIRE(split_result.size() == 1); CHECK(split_result[0].tray_id == -1); } TEST_CASE("Generic external spool is suppressed when its extruder has AMS filament", "[DevMapping]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); // One AMS unit on extruder 2 (red ABS) plus one external spool per extruder; extruder 2's // external spool is green ABS, which is what the sliced filament matches exactly. const json filament = { {"units", json::array({ {{"id", "2"}, {"extruder", 2}, {"slots", json::array({ {{"index", 0}, {"loaded", true}, {"material", "ABS"}, {"color", "FF0000FF"}} })}} })}, {"external", json::array({ {{"extruder", 0}, {"loaded", true}, {"material", "PLA"}, {"color", "FF5733FF"}}, {{"extruder", 1}, {"loaded", true}, {"material", "ASA"}, {"color", "33C1FFFF"}}, {{"extruder", 2}, {"loaded", true}, {"material", "ABS"}, {"color", "00FF00FF"}}, {{"extruder", 3}, {"loaded", true}, {"material", "PETG"}, {"color", "44AA22FF"}}, {{"extruder", 4}, {"loaded", true}, {"material", "TPU"}, {"color", "FFAA00FF"}} })} }; DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get()); REQUIRE(obj.vt_slot.size() == 5); REQUIRE(obj.vt_slot[2].id == "253"); FilamentInfo fila; fila.id = 0; fila.type = "ABS"; fila.color = "00FF00FF"; std::vector map_opt(4, false); map_opt[MappingOption::USE_LEFT_AMS] = true; map_opt[MappingOption::USE_RIGHT_AMS] = true; map_opt[MappingOption::USE_LEFT_EXT] = true; map_opt[MappingOption::USE_RIGHT_EXT] = true; // With suppression off the exact-match external spool (253) wins over the red AMS tray. std::vector result; DevMappingUtil::ams_filament_mapping(&obj, {fila}, result, map_opt, {}, false); REQUIRE(result.size() == 1); CHECK(result[0].tray_id == 253); CHECK(result[0].ams_id == "253"); // With suppression on, extruder 2 already has AMS filament in this mapping, so its external // spool is dropped and the AMS tray is the only candidate left. This fixture has only that // single generic unit, so its cumulative tray index is 0. std::vector suppressed; DevMappingUtil::ams_filament_mapping(&obj, {fila}, suppressed, map_opt, {}, true); REQUIRE(suppressed.size() == 1); CHECK(suppressed[0].ams_id == "2"); CHECK(suppressed[0].tray_id == 0); }