Fix device popup, mapping, and status-parse defects

- Allow mapping to EMPTY trays only from the multi-machine send page and keep
  both panels pickable in the LEFT_AND_RIGHT view
- Guard the error-dialog cloud snapshot against stale callbacks and fall back
  to the local illustration on timeout
- Parse the ipcam storage-check ack and axis/chamber pushes defensively
- Strip fan-control telemetry, initialize the upgrade error code, restore the
  .json filter in the model-id scan
- Fix best-position popup tray lookup, gradient placement, and colour-list
  ownership
- Cover switch binding sets and invalid-track transients in DevMapping tests
This commit is contained in:
SoftFever
2026-07-18 01:46:10 +08:00
parent 12720ccbd8
commit 4683c17f6f
11 changed files with 181 additions and 69 deletions

View File

@@ -1059,8 +1059,7 @@ void AmsMapingPopup::show_reset_button() {
m_reset_btn->Show();
}
// Orca: kept surface for the reverse-direction SelectMachine — this helper (dropped by the reference)
// is still consumed by SelectMachine, so it stays as a permanent compatibility surface.
// Orca: dropped by the reference but still consumed by SelectMachine.
std::vector<TrayData> AmsMapingPopup::parse_ams_mapping(const std::map<std::string, DevAms*, NumericStrCompare>& amsList)
{
std::vector<TrayData> m_tray_data;
@@ -1185,7 +1184,7 @@ void AmsMapingPopup::on_left_down(wxMouseEvent &evt)
}
}
if (item->m_tray_data.type == TrayType::EMPTY) return;
if (item->m_tray_data.type == TrayType::EMPTY && !m_mapping_from_multi_machines) return;
if (m_show_type == ShowType::LEFT_AND_RIGHT_DYNAMIC && devPrinterUtil::IsVirtualSlot(item->m_ams_id)) return;
if ((m_show_type == ShowType::LEFT && item->GetParent()->GetName() == "left") ||

View File

@@ -598,9 +598,11 @@ void AmsMapingPopup::add_ams_mapping(std::vector<TrayData> tray_data,
}
else {
if (can_pick_the_item) {
// LEFT_AND_RIGHT — the fallback when a filament has no entry in the
// nozzle map — keeps both panels pickable.
auto parent = container->GetParent();
if (parent == m_left_marea_panel) {
can_pick_the_item = (m_show_type == ShowType::LEFT);
can_pick_the_item = (m_show_type == ShowType::LEFT || m_show_type == ShowType::LEFT_AND_RIGHT);
} else if (parent == m_right_marea_panel) {
if (m_show_type == ShowType::LEFT_AND_RIGHT_DYNAMIC) {
can_pick_the_item = !devPrinterUtil::IsVirtualSlot(m_mapping_item->m_ams_id);
@@ -608,7 +610,7 @@ void AmsMapingPopup::add_ams_mapping(std::vector<TrayData> tray_data,
item_tooltip_msg = _L(
"External spools is not supported since Filament Track Switch has been installed. If you want to use external spool, please uninstall it.");
}
} else if (m_show_type != ShowType::RIGHT) {
} else if (m_show_type != ShowType::RIGHT && m_show_type != ShowType::LEFT_AND_RIGHT) {
can_pick_the_item = false;
}
}
@@ -620,7 +622,9 @@ void AmsMapingPopup::add_ams_mapping(std::vector<TrayData> tray_data,
if (tray_data[i].type == NORMAL && !is_match_material(tray_data[i].filament_type)){
can_pick_the_item = false;
} else if(tray_data[i].type == EMPTY){
can_pick_the_item = false;
// Mapping to an empty slot is only allowed from the multi-machine send page,
// which maps onto EMPTY placeholder trays.
can_pick_the_item = m_mapping_from_multi_machines;
}
}

View File

@@ -61,6 +61,8 @@ std::map<std::string, std::string> DevPrinterConfigUtil::get_all_model_id_with_n
for (wxString file : m_files)
{
if (!file.Lower().ends_with(".json")) continue;
std::string config_file = m_resource_file_path + "/printers/" + file.ToStdString();
boost::nowide::ifstream json_file(config_file.c_str());

View File

@@ -46,29 +46,11 @@ void Slic3r::DevFan::converse_to_duct(bool is_suppt_part_fun, bool is_suppt_aux_
}
}
static std::string _get_string_from_fantype(int type)
{
switch (type) {
case 1: return "cooling_fan";
case 2: return "big_cooling_fan";
case 3: return "chamber_fan";
default: return "";
}
return "";
}
// Old protocol
int Slic3r::DevFan::command_control_fan(int fan_type, int val)
{
// Orca: strip analytics telemetry
std::string gcode = (boost::format("M106 P%1% S%2% \n") % (int) fan_type % (val)).str();
try {
json j;
j["ctrl_type"] = _get_string_from_fantype(fan_type);
j["value"] = val;
NetworkAgent *agent = GUI::wxGetApp().getAgent();
if (agent) agent->track_event("printer_control", j.dump());
} catch (...) {}
return m_owner->publish_gcode(gcode);
}

View File

@@ -60,7 +60,7 @@ private:
std::map<std::string, DevFirmwareVersionInfo> m_new_ver_list; // some protcols have version list
DevFirmwareUpgradeState m_upgrade_display_state = DevFirmwareUpgradeState::DC;
int m_upgrade_err_code;
int m_upgrade_err_code = 0;
std::string m_upgrade_status;
std::string m_upgrade_progress;
std::string m_upgrade_message;

View File

@@ -155,7 +155,10 @@ void DeviceErrorDialog::on_webrequest_state(wxWebRequestEvent& evt)
void DeviceErrorDialog::on_request_timeout(wxTimerEvent& event)
{
if (m_request_cancelled.load()) { return; }
m_error_picture->SetBitmap(make_placeholder_image(_L("Network unavailable")));
// Same fallback order as the cloud callback: local/HTTP illustration first, placeholder last.
if (!get_fail_snapshot_from_local(m_local_img_url)) {
m_error_picture->SetBitmap(make_placeholder_image(_L("Network unavailable")));
}
Layout();
Fit();
}
@@ -207,13 +210,16 @@ bool DeviceErrorDialog::get_fail_snapshot_from_cloud()
// destruction with a liveness token (m_alive, cleared in the dtor) and marshal onto the UI
// thread via the always-valid app handler instead of this->CallAfter. The raw bytes are
// decoded on the UI thread so no wxImage (non-atomic refcount) is shared across threads.
// Assumes a single in-flight request per dialog, which all current callers satisfy.
// m_request_seq guards against a callback from a previous error painting over the
// current one when the dialog is reused by a later show_error_code().
auto alive = m_alive;
const int seq = m_request_seq;
int ret = agent->get_hms_snapshot(m_obj->get_dev_id(), m_obj->m_print_error_img_id,
[this, alive](std::string body, int status) {
[this, alive, seq](std::string body, int status) {
if (!alive->load()) { return; }
wxGetApp().CallAfter([this, alive, body = std::move(body), status]() {
wxGetApp().CallAfter([this, alive, seq, body = std::move(body), status]() {
if (!alive->load()) { return; }
if (seq != m_request_seq) { return; }
m_request_cancelled.store(true);
clear_request_timer();
@@ -304,7 +310,7 @@ void DeviceErrorDialog::init_button_list()
init_button(FILAMENT_LOAD_RESUME, _L("Filament Loaded, Resume"));
init_button(JUMP_TO_LIVEVIEW, _L("View Liveview"));
init_button(NO_REMINDER_NEXT_TIME, _L("No Reminder Next Time"));
init_button(REFRESH_NOZZLE, _L("Recheck")); // Orca: ported REF error action
init_button(REFRESH_NOZZLE, _L("Recheck"));
init_button(IGNORE_NO_REMINDER_NEXT_TIME, _L("Ignore. Don't Remind Next Time"));
init_button(IGNORE_RESUME, _L("Ignore this and Resume"));
init_button(PROBLEM_SOLVED_RESUME, _L("Problem Solved and Resume"));
@@ -313,8 +319,8 @@ void DeviceErrorDialog::init_button_list()
init_button(CANCEL, _L("Cancel"));
init_button(STOP_DRYING, _L("Stop Drying"));
init_button(PROCEED, _L("Proceed"));
init_button(OK_JUMP_RACK, _L("OK")); // Orca: ported REF error action
init_button(ABORT, _L("Abort")); // Orca: ported REF error action
init_button(OK_JUMP_RACK, _L("OK"));
init_button(ABORT, _L("Abort"));
init_button(DISABLE_PURIFICATION, _L("Disable Purification for This Print"));
init_button(DONT_REMIND_NEXT_TIME, _L("Don't Remind Me"));
init_button(DBL_CHECK_CANCEL, _L("Cancel"));
@@ -452,6 +458,7 @@ void DeviceErrorDialog::update_contents(const wxString& title, const wxString& t
// path keeps its existing behavior.
m_local_img_url = image_url;
m_request_cancelled.store(false);
++m_request_seq; // invalidate any snapshot fetch still in flight for the previous error
clear_request_timer();
if (get_fail_snapshot_from_cloud())
{
@@ -489,9 +496,10 @@ void DeviceErrorDialog::update_contents(const wxString& title, const wxString& t
auto text_size = m_error_msg_label->GetBestSize();
if (text_size.y < FromDIP(360))
{
// Orca: also reserve the image area when only a cloud snapshot (m_print_error_img_id)
// is available, so the scroll area sizes correctly even with an empty local image_url.
if (!image_url.empty() || (m_obj && !m_obj->m_print_error_img_id.empty()))
// Orca: reserve the image area exactly when an image is coming (cloud snapshot,
// local illustration, or the loading placeholder) - m_error_picture's visibility
// was decided above, so no blank slot is reserved when neither source is usable.
if (m_error_picture->IsShown())
{
m_scroll_area->SetMinSize(wxSize(FromDIP(320), text_size.y + FromDIP(220)));
}
@@ -604,16 +612,16 @@ void DeviceErrorDialog::on_button_click(ActionButton btn_id)
}
break;
}
case DeviceErrorDialog::REFRESH_NOZZLE: { // Orca: ported REF error action
case DeviceErrorDialog::REFRESH_NOZZLE: {
m_obj->command_refresh_nozzle();
break;
}
case DeviceErrorDialog::OK_JUMP_RACK: { // Orca: ported REF error action
case DeviceErrorDialog::OK_JUMP_RACK: {
Slic3r::GUI::wxGetApp().mainframe->jump_to_monitor();
Slic3r::GUI::wxGetApp().mainframe->m_monitor->jump_to_Rack();
break;
}
case DeviceErrorDialog::ABORT: { // Orca: ported REF error action
case DeviceErrorDialog::ABORT: {
m_obj->command_ams_control("abort");
break;
}

View File

@@ -37,7 +37,7 @@ public:
JUMP_TO_LIVEVIEW,
NO_REMINDER_NEXT_TIME = 23,
REFRESH_NOZZLE = 24, // Orca: ported REF error action (recheck nozzle)
REFRESH_NOZZLE = 24,
IGNORE_NO_REMINDER_NEXT_TIME = 25,
//LOAD_FILAMENT = 26*/
IGNORE_RESUME = 27,
@@ -49,8 +49,8 @@ public:
CANCEL = 37,
REMOVE_CLOSE_BTN = 39, // special case, do not show close button
PROCEED = 41,
OK_JUMP_RACK = 49, // Orca: ported REF error action (jump to the nozzle rack)
ABORT = 51, // Orca: ported REF error action (abort)
OK_JUMP_RACK = 49,
ABORT = 51,
DISABLE_PURIFICATION = 54,
DONT_REMIND_NEXT_TIME = 57,
@@ -107,6 +107,9 @@ private:
wxWebRequest web_request;
wxTimer* m_request_timer{ nullptr };
std::atomic<bool> m_request_cancelled{ false };
// UI-thread-only generation counter: bumped on every update_contents() so a cloud
// snapshot callback from a previous error cannot paint over the current one.
int m_request_seq{ 0 };
wxString m_local_img_url;
// Orca: liveness token for the async cloud snapshot callback (the request has no cancel handle)
std::shared_ptr<std::atomic_bool> m_alive{ std::make_shared<std::atomic_bool>(true) };

View File

@@ -74,8 +74,6 @@ int get_tray_id_by_ams_id_and_slot_id(int ams_id, int slot_id)
}
}
// Orca: REF-additive stringing-prone filament chain, now ported (post-review).
// Consumers arrive in resync clusters 5 (PrintOptionsDialog) and 7 (SelectMachine).
namespace {
// Stringing-prone filament IDs per nozzle-diameter bucket.
@@ -759,8 +757,6 @@ std::string MachineObject::get_filament_display_type(const std::string& ams_id,
return this->get_tray(ams_id, tray_id).get_display_filament_type();
}
// Orca: REF-additive, now ported (post-review). Adapted to Orca's kept fila model:
// filament id resolved via MachineObject::get_filament_id (tray setting_id / tray_info_idx).
bool MachineObject::any_loaded_filament_is_stringing_prone() const
{
if (print_job_filament_mapping.empty()) return false;
@@ -806,7 +802,7 @@ void MachineObject::_parse_ams_status(int ams_status)
ams_status_main = AmsStatusMain::AMS_STATUS_MAIN_SELF_CHECK;
} else if (ams_status_main_int == (int) AmsStatusMain::AMS_STATUS_MAIN_DEBUG) {
ams_status_main = AmsStatusMain::AMS_STATUS_MAIN_DEBUG;
} else if (ams_status_main_int == (int) AmsStatusMain::AMS_STATUS_MAIN_COLD_PULL) { // Orca: REF-additive, now ported (post-review)
} else if (ams_status_main_int == (int) AmsStatusMain::AMS_STATUS_MAIN_COLD_PULL) {
ams_status_main = AmsStatusMain::AMS_STATUS_MAIN_COLD_PULL;
} else {
ams_status_main = AmsStatusMain::AMS_STATUS_MAIN_UNKNOWN;
@@ -959,7 +955,6 @@ void MachineObject::clear_version_info()
laser_version_info = DevFirmwareVersionInfo();
cutting_module_version_info = DevFirmwareVersionInfo();
extinguish_version_info = DevFirmwareVersionInfo();
// Orca: REF-additive accessory firmware versions, now ported (post-review)
rotary_version_info = DevFirmwareVersionInfo();
exhaustfan_version_info = DevFirmwareVersionInfo();
amshub_version_info = DevFirmwareVersionInfo();
@@ -980,16 +975,16 @@ void MachineObject::store_version_info(const DevFirmwareVersionInfo& info)
cutting_module_version_info = info;
} else if (info.isExtinguishSystem()) {
extinguish_version_info = info;
} else if (info.isRotary()) { // Orca: REF-additive, now ported (post-review)
} else if (info.isRotary()) {
rotary_version_info = info;
} else if (info.isWTM()) {
// Route rack-hotend / extruder-nozzle firmware into the nozzle system so
// the rack upgrade UI can read per-nozzle versions. isWTM() is false for every non-rack
// printer's modules, so this branch never fires outside H2C.
m_nozzle_system->AddFirmwareInfoWTM(info);
} else if (info.isExhaustFan()) { // Orca: REF-additive, now ported (post-review)
} else if (info.isExhaustFan()) {
exhaustfan_version_info = info;
} else if (info.isHmshub()) { // Orca: REF-additive, now ported (post-review)
} else if (info.isHmshub()) {
amshub_version_info = info;
} else if (info.isFilaTrackSwitch()) {
filatrack_version_info = info;
@@ -3364,9 +3359,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
int flag3 = jj["flag3"].get<int>();
is_support_filament_setting_inprinting = get_flag_bits(flag3, 3);
is_enable_ams_np = get_flag_bits(flag3, 9);
is_support_fila_change_abort = get_flag_bits(flag3, 13); // Orca: REF-additive, now ported (filament-change Stop button)
is_support_ext_change_assist_old = get_flag_bits(flag3, 16); // Orca: REF-additive, now ported (A/P-series multi-color external change assist)
is_support_filament_32_colors = get_flag_bits(flag3, 17); // Orca: REF-additive, now ported (post-review)
is_support_fila_change_abort = get_flag_bits(flag3, 13); // filament-change Stop button
is_support_ext_change_assist_old = get_flag_bits(flag3, 16); // A/P-series multi-color external change assist
is_support_filament_32_colors = get_flag_bits(flag3, 17);
}
}
if (!key_field_only) {
@@ -3533,9 +3528,14 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
/* temperature */
// Orca: adopt DeviceCore split — populate DevAxis/DevChamber modules
// alongside the inline handling (side-effect-free; inline stays authoritative)
m_axis->ParseAxis(jj);
m_chamber->ParseChamber(jj);
// alongside the inline handling (side-effect-free; inline stays authoritative).
// Contained locally so a malformed field cannot abort the whole status parse.
try {
m_axis->ParseAxis(jj);
m_chamber->ParseChamber(jj);
} catch (...) {
BOOST_LOG_TRIVIAL(warning) << "parse_json: DevAxis/DevChamber parse failed";
}
DevBed::ParseV1_0(jj,m_bed);
@@ -4548,7 +4548,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
} else if (j["camera"]["command"].get<std::string>() == "ipcam_get_media_info") {
if (j["camera"].contains("sub_command") &&
j["camera"]["sub_command"].get<std::string>() == "is_timelapse_storage_enough") {
timelapse_storage_check_result = j["camera"]["result"].get<int>();
timelapse_storage_check_result = j["camera"].value("result", -1);
timelapse_storage_is_enough = j["camera"].value("is_enough", true);
timelapse_storage_file_count = j["camera"].value("file_count", 0);
timelapse_storage_check_done = true;
@@ -5160,8 +5160,7 @@ bool MachineObject::check_enable_np(const json& print) const
return false;
}
// Orca: REF-additive, now ported (post-review). Consumer is the SelectMachine
// color-quantity send gate (arrives in cluster 7). 0 = no explicit upper bound.
// Max filament color count for the send gate; 0 = no explicit upper bound.
int MachineObject::get_max_filament_color_count() const
{
if (is_support_filament_32_colors) return 32;
@@ -5332,8 +5331,7 @@ void MachineObject::parse_new_info(json print)
}
}
/* Orca: REF-additive, now ported (post-review). Per-filament-index AMS slot mapping
(task-level state). Consumers arrive in clusters 5/7. */
/* Per-filament-index AMS slot mapping reported by the printer (task-level state) */
if (print.contains("mapping") && print["mapping"].is_array()) {
std::vector<uint16_t> new_mapping;
new_mapping.reserve(print["mapping"].size());

View File

@@ -79,7 +79,7 @@ void UiStyledAMSPanel::OnPaint(wxPaintEvent& event)
int labelX = offset;
int labelY = m_isTop ? offset : (height - offset - FromDIP(27)); // true=顶部, false=底部
int labelY = m_isTop ? offset : (height - offset - FromDIP(27)); // true=top, false=bottom
wxRect labelRect(labelX, labelY, width - 2 * offset, FromDIP(10));
@@ -340,8 +340,7 @@ void UiAMSSlot::DrawRectangle(wxPaintDC& dc, const wxSize& cli)
for (int i = 0; i < m_bgColours.size(); i++)
{
baseX += step * i;
wxRect rr(baseX, (cli.y - rectangleH) / 2, step, rectangleH);
wxRect rr(baseX + step * i, (cli.y - rectangleH) / 2, step, rectangleH);
dc.SetBrush(wxBrush(LightenColour(m_bgColours[i])));
dc.SetPen(wxPen(LightenColour(m_bgColours[i])));
@@ -1010,8 +1009,9 @@ wxString ReselectMachineDialog::getTrayID(MachineObject* obj, const std::string&
if (filaSys)
{
DevAms* ams = filaSys->GetAmsById(amsID);
int ams_id_int = std::stoi(amsID);
int slot_id_int = std::stoi(slotID);
if (!ams) { return ""; }
int ams_id_int = atoi(amsID.c_str());
int slot_id_int = atoi(slotID.c_str());
int tray_id = 0;
if (ams->GetAmsType() == DevAmsType::AMS || ams->GetAmsType() == DevAmsType::AMS_LITE || ams->GetAmsType() == DevAmsType::N3F) {
tray_id = ams_id_int * 4 + slot_id_int;

View File

@@ -131,10 +131,12 @@ private:
bool IsDark(const wxColour& c)
{
int brightness = (c.Red() * 299 + c.Green() * 587 + c.Blue() * 114) / 1000;
return brightness < 128; // 0-255 range128 mid
return brightness < 128; // 0-255 range, 128 mid
}
private:
const std::vector<wxColour>& m_bgColours;
// Copied, not referenced: the slot repaints long after construction, so it must not
// depend on the caller's vector staying alive.
std::vector<wxColour> m_bgColours;
wxString m_text;
DataStatusType m_status;
wxSize m_size;

View File

@@ -79,3 +79,117 @@ TEST_CASE("Switch-bound AMS trays map to the left extruder", "[DevMapping]")
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<FilamentInfo> result;
std::vector<bool> 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<FilamentInfo> result;
std::vector<bool> 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);
}