From 4683c17f6fbfd17ceebfe723ce0fa2c3c3d0eb9f Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 18 Jul 2026 01:46:10 +0800 Subject: [PATCH] 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 --- src/slic3r/GUI/AmsMappingPopup.cpp | 5 +- src/slic3r/GUI/AmsMappingPopupUpdate.cpp | 10 +- src/slic3r/GUI/DeviceCore/DevConfigUtil.cpp | 2 + src/slic3r/GUI/DeviceCore/DevFan.cpp | 20 +-- src/slic3r/GUI/DeviceCore/DevUpgrade.h | 2 +- src/slic3r/GUI/DeviceErrorDialog.cpp | 34 ++++-- src/slic3r/GUI/DeviceErrorDialog.hpp | 9 +- src/slic3r/GUI/DeviceManager.cpp | 38 +++--- .../GUI/DeviceTab/uiAMSBestPositionPopup.cpp | 10 +- .../GUI/DeviceTab/uiAMSBestPositionPopup.hpp | 6 +- tests/slic3rutils/test_dev_mapping.cpp | 114 ++++++++++++++++++ 11 files changed, 181 insertions(+), 69 deletions(-) diff --git a/src/slic3r/GUI/AmsMappingPopup.cpp b/src/slic3r/GUI/AmsMappingPopup.cpp index 1309348751..53fe1adee3 100644 --- a/src/slic3r/GUI/AmsMappingPopup.cpp +++ b/src/slic3r/GUI/AmsMappingPopup.cpp @@ -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 AmsMapingPopup::parse_ams_mapping(const std::map& amsList) { std::vector 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") || diff --git a/src/slic3r/GUI/AmsMappingPopupUpdate.cpp b/src/slic3r/GUI/AmsMappingPopupUpdate.cpp index e294cf7922..7d8215f55d 100644 --- a/src/slic3r/GUI/AmsMappingPopupUpdate.cpp +++ b/src/slic3r/GUI/AmsMappingPopupUpdate.cpp @@ -598,9 +598,11 @@ void AmsMapingPopup::add_ams_mapping(std::vector 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 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 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; } } diff --git a/src/slic3r/GUI/DeviceCore/DevConfigUtil.cpp b/src/slic3r/GUI/DeviceCore/DevConfigUtil.cpp index 5826cb3c80..b86d3cde98 100644 --- a/src/slic3r/GUI/DeviceCore/DevConfigUtil.cpp +++ b/src/slic3r/GUI/DeviceCore/DevConfigUtil.cpp @@ -61,6 +61,8 @@ std::map 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()); diff --git a/src/slic3r/GUI/DeviceCore/DevFan.cpp b/src/slic3r/GUI/DeviceCore/DevFan.cpp index 35cbaae2da..3b5cb40702 100644 --- a/src/slic3r/GUI/DeviceCore/DevFan.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFan.cpp @@ -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); } diff --git a/src/slic3r/GUI/DeviceCore/DevUpgrade.h b/src/slic3r/GUI/DeviceCore/DevUpgrade.h index 0290d5f7d4..a9d9b7c957 100644 --- a/src/slic3r/GUI/DeviceCore/DevUpgrade.h +++ b/src/slic3r/GUI/DeviceCore/DevUpgrade.h @@ -60,7 +60,7 @@ private: std::map 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; diff --git a/src/slic3r/GUI/DeviceErrorDialog.cpp b/src/slic3r/GUI/DeviceErrorDialog.cpp index 77b069f3dc..2a55d77d24 100644 --- a/src/slic3r/GUI/DeviceErrorDialog.cpp +++ b/src/slic3r/GUI/DeviceErrorDialog.cpp @@ -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; } diff --git a/src/slic3r/GUI/DeviceErrorDialog.hpp b/src/slic3r/GUI/DeviceErrorDialog.hpp index fe5ff25425..a08135bf38 100644 --- a/src/slic3r/GUI/DeviceErrorDialog.hpp +++ b/src/slic3r/GUI/DeviceErrorDialog.hpp @@ -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 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 m_alive{ std::make_shared(true) }; diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index b84094027f..5499694686 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -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(); 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() == "ipcam_get_media_info") { if (j["camera"].contains("sub_command") && j["camera"]["sub_command"].get() == "is_timelapse_storage_enough") { - timelapse_storage_check_result = j["camera"]["result"].get(); + 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 new_mapping; new_mapping.reserve(print["mapping"].size()); diff --git a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp index 98f2a37f73..c55b01acf4 100644 --- a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp +++ b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp @@ -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; diff --git a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.hpp b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.hpp index 455576d3cc..427a5c6a73 100644 --- a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.hpp +++ b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.hpp @@ -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 range,128 mid + return brightness < 128; // 0-255 range, 128 mid } private: - const std::vector& 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 m_bgColours; wxString m_text; DataStatusType m_status; wxSize m_size; diff --git a/tests/slic3rutils/test_dev_mapping.cpp b/tests/slic3rutils/test_dev_mapping.cpp index 02ec2abc60..1c1528f818 100644 --- a/tests/slic3rutils/test_dev_mapping.cpp +++ b/tests/slic3rutils/test_dev_mapping.cpp @@ -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 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); +}