From e5e30ee0e86551fef80ddb66c9d961bdf4e40a74 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 8 Oct 2026 17:39:44 +0800 Subject: [PATCH] fix: harden generic N-extruder device model and status/AMS UI, add N-toolhead ExtruderImage sizing, tests, and HLSD --- docs/HLSD/multi-extruder-ams.md | 16 ++-- src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp | 22 +++-- src/slic3r/GUI/DeviceManager.cpp | 19 ++-- src/slic3r/GUI/StatusPanel.cpp | 95 ++++++++++++++++++-- src/slic3r/GUI/StatusPanel.hpp | 1 + src/slic3r/GUI/Widgets/AMSControl.cpp | 23 +++-- src/slic3r/GUI/Widgets/MultiNozzleSync.cpp | 2 +- tests/slic3rutils/test_dev_extder_system.cpp | 76 +++++++++++++++- tests/slic3rutils/test_dev_mapping.cpp | 9 +- 9 files changed, 224 insertions(+), 39 deletions(-) diff --git a/docs/HLSD/multi-extruder-ams.md b/docs/HLSD/multi-extruder-ams.md index 99305246f3..0160ad0041 100644 --- a/docs/HLSD/multi-extruder-ams.md +++ b/docs/HLSD/multi-extruder-ams.md @@ -144,10 +144,10 @@ The device-control widgets follow the same device-reported counts. `N3F_AMS`, `N3S_AMS`. A plain `AMS` maps to `GENERIC_AMS`. - On non-Bambu printers `use_generic_ams_layout()` routes `GENERIC_AMS` units through a variable-lane rendering that draws one lane per reported tray; Bambu keeps its fixed four-slot rendering. -- `AMSControl` picks its layout by vendor: Bambu keeps the left/right presentation - (`CreateAmsSingleNozzle` for one toolhead, `CreateAmsDoubleNozzle` for two), while non-Bambu - printers use `CreateAmsMultiNozzle`, which builds one preview and one AMS simplebook per toolhead - for any count. +- `AMSControl` picks its layout by toolhead count, not vendor: `CreateAmsSingleNozzle` for one + 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'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 @@ -162,10 +162,14 @@ The device-control widgets follow the same device-reported counts. - **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. +- **One filament road per toolhead.** Each toolhead renders a single AMS filament road rather than an + independent per-toolhead road state. A generic scaffold that allowed the latter was removed as + unused; the one-extruder-per-toolhead road is the intended model. - **Preset/device agreement.** The generic layout and virtual-slot range derive their count from the selected printer preset. A printer preset must therefore declare the same number of - `nozzle_diameter` entries as the device has toolheads; a mismatch is surfaced because the sidebar - and extruder logic would otherwise disagree. + `nozzle_diameter` entries as the device has toolheads. A mismatch is surfaced only as a log warning + (`StatusPanel::check_extruder_count_mismatch()`), deliberately log-only, because the sidebar and + extruder logic would otherwise silently disagree. - **Bambu compatibility.** Bambu-specific presentation (left/right pair, fixed 4-slot units, fixed virtual ids, physical/logical inversion) is preserved behind the vendor check. Generic paths do not alter it. diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp index 2198b96e59..f37519c719 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp @@ -37,6 +37,9 @@ namespace Slic3r { bool devPrinterUtil::IsVirtualSlot(int ams_id) { + // When the app or preset bundle is unavailable we cannot know the vendor's + // extruder count, so fall back deterministically to the fixed Bambu pair + // {255, 254} rather than guessing from an unknown generic layout. if (wxTheApp == nullptr || GUI::wxGetApp().preset_bundle == nullptr || GUI::wxGetApp().preset_bundle->is_bbl_vendor()) return ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID; @@ -46,6 +49,14 @@ bool devPrinterUtil::IsVirtualSlot(int ams_id) bool devPrinterUtil::IsVirtualSlot(const std::string& ams_id) { + // Strict: only a fully numeric string is a valid slot id. std::stoi alone + // would accept trailing garbage (e.g. "255abc" -> 255). + if (ams_id.empty()) + return false; + for (char c : ams_id) { + if (c < '0' || c > '9') + return false; + } try { return IsVirtualSlot(std::stoi(ams_id)); } catch (...) { @@ -220,6 +231,11 @@ wxString DevAms::GetDisplayName() const int DevAms::GetSlotCount() const { + // N3S is a 1-slot Bambu contract, so report 1 regardless of whether the + // preset bundle is momentarily unavailable (m_trays.size() can be 0). + if (GetAmsType() == N3S) + return 1; + if (wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor()) { // GetAmsType() maps AMS_LITE_MIXED -> AMS_LITE, so N9 reports 4 slots like AMS-Lite. @@ -228,10 +244,6 @@ int DevAms::GetSlotCount() const { return 4; } - else if (ams_type == N3S) - { - return 1; - } } return static_cast(m_trays.size()); @@ -1057,7 +1069,7 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob if (!e.is_object()) continue; const int ext = e.value("extruder", MAIN_EXTRUDER_ID); - if (ext < MAIN_EXTRUDER_ID || ext > VIRTUAL_TRAY_MAIN_ID) + if (ext < MAIN_EXTRUDER_ID || ext >= VIRTUAL_TRAY_MAIN_ID) continue; const int vt_id = VIRTUAL_TRAY_MAIN_ID - ext; diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index cf80dc4802..d0f9c42100 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -808,8 +808,12 @@ bool MachineObject::is_extrusion_cali_finished() DevAmsTray *MachineObject::get_curr_tray() { const std::string& cur_ams_id = m_extder_system->GetCurrentAmsId(); - if (cur_ams_id.compare(std::to_string(VIRTUAL_TRAY_MAIN_ID)) == 0) { - return &vt_slot[0]; + if (devPrinterUtil::IsVirtualSlot(cur_ams_id)) { + for (auto& tray : vt_slot) { + if (tray.id == cur_ams_id) + return &tray; + } + return nullptr; } DevAms* curr_ams = get_curr_Ams(); @@ -2634,13 +2638,10 @@ void MachineObject::reset() json empty_j; print_json.diff2all_base_reset(empty_j); - for (auto i = 0; i < vt_slot.size(); i++) { - vt_slot[i].reset(); - - if (i == 1) { - vt_slot.erase(vt_slot.begin() + 1); - } - } + for (auto& tray : vt_slot) + tray.reset(); + if (vt_slot.size() > 1) + vt_slot.erase(vt_slot.begin() + 1, vt_slot.end()); // why: reset reuses MachineObject, so release its lazy subtask // before dropping the pointer to prevent reconnect leaks. if (subtask_) { diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index c2758f4cf8..4ea546a6ee 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -226,6 +226,8 @@ ExtruderImage::ExtruderImage(wxWindow* parent, wxWindowID id, int nozzle_num, co wxWindow::Create(parent, id, pos, wxSize(FromDIP(45), FromDIP(112))); SetBackgroundColour(*wxWHITE); m_nozzle_num = nozzle_num; + // Legacy Bambu single/dual page size. The generic (non-Bambu) grid grows + // past this when it holds more than two toolheads; see updateGenericSize(). SetSize(wxSize(FromDIP(45), FromDIP(112))); SetMinSize(wxSize(FromDIP(45), FromDIP(112))); SetMaxSize(wxSize(FromDIP(45), FromDIP(112))); @@ -298,6 +300,7 @@ void ExtruderImage::setExtruderCount(int nozzle_num) m_nozzle_num = normalized_count; m_multi_extruder_states.resize(static_cast(m_nozzle_num), ExtruderState::EMPTY_LOAD); + updateGenericSize(); Refresh(); } @@ -307,9 +310,37 @@ void ExtruderImage::setGenericNozzleDisplay(bool enabled) return; m_generic_nozzle_display = enabled; + updateGenericSize(); Refresh(); } +void ExtruderImage::updateGenericSize() +{ + // The generic (non-Bambu) grid must stay legible for any toolhead count, so + // give it whole natural-size icons (24x62 DIP) and wrap into extra rows + // instead of squeezing everything into the fixed 45x112 Bambu page. doRender() + // balances the columns/rows inside the area we hand it, so sizing the page to + // a grid of natural-size cells keeps every icon at its full size. + // Bambu <=2 toolheads and generic <=2 toolheads keep the legacy page size. + wxSize size(FromDIP(45), FromDIP(112)); + if (m_generic_nozzle_display && m_nozzle_num > 2) { + const wxSize icon = m_left_extruder_active_filled->GetBmpSize(); + if (icon.x > 0 && icon.y > 0) { + // Fit as many columns as the fixed-width extruder panel (143 DIP) allows. + const int columns = std::max(1, std::min(m_nozzle_num, FromDIP(143) / icon.x)); + const int rows = (m_nozzle_num + columns - 1) / columns; + size = wxSize(columns * icon.x, rows * icon.y); + } + } + + if (GetMinSize() == size && GetMaxSize() == size) + return; + + SetMinSize(size); + SetMaxSize(size); + SetSize(size); +} + void ExtruderImage::setExtruderUsed(std::string loc) { //current_nozzle_idx = nozzle_id; @@ -352,6 +383,7 @@ void ExtruderImage::update(ExtruderState state, int idx) { return; m_multi_extruder_states[idx] = state; + Refresh(); } void ExtruderImage::paintEvent(wxPaintEvent& evt) @@ -1954,6 +1986,8 @@ void StatusBasePanel::ensure_nozzle_temp_controls(size_t count) if (!m_temp_nozzle_parent || !m_temp_nozzle_sizer) return; + // Widgets beyond the current toolhead count are intentionally retained + // (hidden, never destroyed) so a later count increase reuses them. bool changed = count != m_temp_nozzle_active_count; while (m_tempCtrl_nozzles.size() < count) { TempInput* temp_ctrl = create_nozzle_temp_control(m_temp_nozzle_parent, m_nozzle_temp_control_id); @@ -3715,6 +3749,9 @@ void StatusPanel::update_misc_ctrl(MachineObject *obj) ExtruderImage* image = m_extruderImage.front(); image->setGenericNozzleDisplay(true); image->setExtruderCount(m_nozzle_num); + // Re-apply every update so a DPI change (which the setters above skip on + // steady state) still resizes the grid. + image->updateGenericSize(); const int current_nozzle_id = extder_system->GetCurrentExtderId(); const int selected_nozzle_id = current_nozzle_id >= 0 && current_nozzle_id < m_nozzle_num ? current_nozzle_id : -1; @@ -3740,7 +3777,31 @@ void StatusPanel::update_misc_ctrl(MachineObject *obj) m_generic_nozzle_selector->Enable(); } - if (bbl_selector_was_shown != m_nozzle_btn_panel->IsShown() || + // The generic >2 toolhead page is larger than the legacy 45x112 Bambu page, + // so drive the book's size from the page that is shown. The book is only + // given an explicit size while a >2 generic grid is shown (or to clear a + // previous one); a plain Bambu session leaves the book's natural 45x112 + // best size untouched. + bool book_size_changed = false; + { + const bool generic_grid = !is_bbl_vendor && m_nozzle_num > 2; + const bool book_has_explicit_size = + m_extruder_book->GetMinSize() != wxDefaultSize || + m_extruder_book->GetMaxSize() != wxDefaultSize; + if (generic_grid || book_has_explicit_size) { + const wxSize legacy_size(FromDIP(45), FromDIP(112)); + const wxSize wanted_size = generic_grid ? m_extruderImage.front()->GetMinSize() : legacy_size; + if (m_extruder_book->GetMinSize() != wanted_size || + m_extruder_book->GetMaxSize() != wanted_size) { + m_extruder_book->SetMinSize(wanted_size); + m_extruder_book->SetMaxSize(wanted_size); + book_size_changed = true; + } + } + } + + if (book_size_changed || + bbl_selector_was_shown != m_nozzle_btn_panel->IsShown() || generic_selector_was_shown != m_generic_nozzle_selector->IsShown()) m_nozzle_btn_panel->GetParent()->Layout(); @@ -3870,6 +3931,16 @@ void StatusPanel::update_ams(MachineObject *obj) AMSinfo info; info.ams_id = ams->first; if (ams->second->IsExist() && info.parse_ams_info(obj, ams->second, obj->GetFilaSystem()->IsDetectRemainEnabled(), obj->is_support_ams_humidity)) { + // A non-Bambu unit reported as AMS_LITE but not matching the fixed + // four-lane AMS_LITE grid (e.g. a generic unit advertising a + // different slot count) has no fixed layout to render into and would + // show an empty card. Treat it as GENERIC_AMS so it uses the same + // variable-lane rendering. Bambu's fixed rendering is untouched. + if (info.ams_type == AMSModel::AMS_LITE && + info.cans.size() != GENERIC_AMS_SLOT_NUM && + (!wxGetApp().preset_bundle || !wxGetApp().preset_bundle->is_bbl_vendor())) { + info.ams_type = AMSModel::GENERIC_AMS; + } ams_info.push_back(info); } } @@ -3889,6 +3960,7 @@ void StatusPanel::update_ams(MachineObject *obj) const bool show_generic_nozzle_switch = obj->GetExtderSystem()->GetTotalExtderCount() > 2 && !wxGetApp().preset_bundle->is_bbl_vendor(); + const bool ams_nozzle_switch_was_shown = m_ams_nozzle_switch->IsShown(); if (show_generic_nozzle_switch) { std::vector nozzle_options; const int nozzle_count = obj->GetExtderSystem()->GetTotalExtderCount(); @@ -3903,7 +3975,8 @@ void StatusPanel::update_ams(MachineObject *obj) } else { m_ams_nozzle_switch->Hide(); } - m_ams_nozzle_switch->GetParent()->Layout(); + if (ams_nozzle_switch_was_shown != m_ams_nozzle_switch->IsShown()) + m_ams_nozzle_switch->GetParent()->Layout(); m_ams_control->UpdateAmsDryControl(obj); last_tray_exist_bits = obj->tray_exist_bits; @@ -5607,6 +5680,12 @@ void StatusPanel::on_nozzle_selected(wxCommandEvent &event) m_generic_nozzle_selector->Disable(); if (obj->GetCtrl()->command_select_extruder(nozzle_id) == 0) { + if (!is_bbl_vendor) { + // The command did not start; restore the previous selection and + // re-enable the selector so the user is not stuck. + m_generic_nozzle_selector->SetSelection(current_selection); + m_generic_nozzle_selector->Enable(); + } return; } } @@ -5735,11 +5814,16 @@ void StatusPanel::set_default() m_switch_cham_fan_timeout = 0; m_show_ams_group = false; m_show_filament_group = false; - m_generic_nozzle_selector->Hide(); + if (m_generic_nozzle_selector) + m_generic_nozzle_selector->Hide(); m_nozzle_btn_panel->Hide(); - m_generic_nozzle_selector->Clear(); + if (m_generic_nozzle_selector) + m_generic_nozzle_selector->Clear(); m_generic_nozzle_selector_count = 0; m_extruder_book->SetSelection(0); + // Clear any generic >2 toolhead grid growth from a previous device. + m_extruder_book->SetMinSize(wxSize(FromDIP(45), FromDIP(112))); + m_extruder_book->SetMaxSize(wxSize(FromDIP(45), FromDIP(112))); m_nozzle_btn_panel->GetParent()->Layout(); reset_printing_values(); @@ -5765,7 +5849,8 @@ void StatusPanel::set_default() m_ams_control->Reset(); m_ams_rack_switch->updateState("left"); m_ams_rack_switch->Hide(); - m_ams_nozzle_switch->Hide(); + if (m_ams_nozzle_switch) + m_ams_nozzle_switch->Hide(); m_panel_nozzle_rack->Hide(); m_scale_panel->Hide(); m_filament_load_box->Hide(); diff --git a/src/slic3r/GUI/StatusPanel.hpp b/src/slic3r/GUI/StatusPanel.hpp index 85cadc2b27..c6d25d8ae4 100644 --- a/src/slic3r/GUI/StatusPanel.hpp +++ b/src/slic3r/GUI/StatusPanel.hpp @@ -168,6 +168,7 @@ public: void update(ExtruderState state, int idx); void msw_rescale(); + void updateGenericSize(); void setExtruderCount(int nozzle_num); void setGenericNozzleDisplay(bool enabled); void setExtruderUsed(std::string loc); diff --git a/src/slic3r/GUI/Widgets/AMSControl.cpp b/src/slic3r/GUI/Widgets/AMSControl.cpp index c1dc58b87c..f128f3831f 100644 --- a/src/slic3r/GUI/Widgets/AMSControl.cpp +++ b/src/slic3r/GUI/Widgets/AMSControl.cpp @@ -638,6 +638,7 @@ void AMSControl::ClearAms() { for (auto &pane : m_nozzle_panes) { if (pane.preview_panel) pane.preview_panel->Destroy(); if (pane.ams_book) pane.ams_book->Destroy(); + if (pane.page) pane.page->Destroy(); } } m_item_ids.assign(std::max(m_total_ext_count, 2), {}); @@ -1329,17 +1330,25 @@ void AMSControl::createAms(wxSimplebook* parent, int& idx, AMSinfo info, AMSPane // Above eight lanes, AmsItem is a fixed-minimum viewport over a wider // lane strip. Expand the viewport through its page's sizers rather // than propagating the strip's virtual width into the card/book. + // Reset back to the centered fixed layout for eight or fewer lanes so a + // later small unit is not left in expand mode. const bool generic_lane_layout = !wxGetApp().preset_bundle || !wxGetApp().preset_bundle->is_bbl_vendor(); - if (generic_lane_layout && info.cans.size() > 8) { + if (generic_lane_layout) { if (auto *page = parent->GetParent()) { auto pane = std::find_if(m_nozzle_panes.begin(), m_nozzle_panes.end(), [page](const NozzleAmsPane &candidate) { return candidate.page == page; }); - if (pane != m_nozzle_panes.end()) { - pane->ams_area->GetItem(pane->ams_book)->SetProportion(1); - pane->ams_area->GetItem(pane->ams_book)->SetFlag(wxEXPAND); - pane->page_sizer->GetItem(pane->ams_area)->SetFlag(wxEXPAND | wxTOP); - m_sizer_ams_items->GetItem(m_nozzle_book)->SetFlag(wxEXPAND); - m_sizer_body->GetItem(m_sizer_ams_items)->SetFlag(wxEXPAND); + if (pane != m_nozzle_panes.end() && m_nozzle_book) { + const bool expand_viewport = info.cans.size() > 8; + if (auto *book_item = pane->ams_area->GetItem(pane->ams_book)) { + book_item->SetProportion(expand_viewport ? 1 : 0); + book_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER); + } + if (auto *area_item = pane->page_sizer->GetItem(pane->ams_area)) + area_item->SetFlag(expand_viewport ? (wxEXPAND | wxTOP) : (wxALIGN_CENTER | wxTOP)); + if (auto *nozzle_book_item = m_sizer_ams_items->GetItem(m_nozzle_book)) + nozzle_book_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER); + if (auto *items_item = m_sizer_body->GetItem(m_sizer_ams_items)) + items_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER); m_amswin->Layout(); } } diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp index 26a72b8d5e..f30e7a870d 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp @@ -877,7 +877,7 @@ void MultiNozzleStatusTable::UpdateRackInfo(std::weak_ptr rack) auto& nozzle = elem.second; int extruder_id{}; - if (wxGetApp().preset_bundle->is_bbl_vendor()) { + if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor()) { extruder_id = nozzle.AtLeftExtruder() ? 0 : 1; if (nozzle.AtRightExtruder()) has_right = true; diff --git a/tests/slic3rutils/test_dev_extder_system.cpp b/tests/slic3rutils/test_dev_extder_system.cpp index dcbeae18be..e78dc36a90 100644 --- a/tests/slic3rutils/test_dev_extder_system.cpp +++ b/tests/slic3rutils/test_dev_extder_system.cpp @@ -3,7 +3,6 @@ // 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. -#include "catch2/catch_message.hpp" #include #include #include @@ -17,7 +16,6 @@ #include #endif -#include #include #include @@ -84,3 +82,77 @@ TEST_CASE("Extruder state decodes a generic toolhead set", "[DevExtderSystem]") CHECK(ext->GetNozzleId() == id); } } + +TEST_CASE("Legacy single-toolhead parser fills only the main extruder", "[DevExtderSystem]") +{ + MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); + REQUIRE(obj.GetExtderSystem()->GetTotalExtderCount() == 1); + + // ParseV1_0 is the legacy single/dual fallback. It returns unless the system reports exactly + // one toolhead, so it only ever writes MAIN_EXTRUDER_ID's fields. + const json legacy = { + {"nozzle_temper", 210}, + {"nozzle_target_temper", 245}, + {"ams", {{"tray_now", "2"}, {"tray_tar", "3"}}} + }; + ExtderSystemParser::ParseV1_0(legacy, obj.GetExtderSystem()); + + const auto ext = obj.GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID); + REQUIRE(ext.has_value()); + CHECK(ext->GetCurrentTemp() == 210); + CHECK(ext->GetTargetTemp() == 245); + + // Below 0x80 the tray pointer encodes ams_id = value / 4, slot_id = value % 4. + CHECK(ext->GetSlotNow().ams_id == "0"); + CHECK(ext->GetSlotNow().slot_id == "2"); + CHECK(ext->GetSlotTarget().ams_id == "0"); + CHECK(ext->GetSlotTarget().slot_id == "3"); + CHECK(obj.GetExtderSystem()->IsBusyLoading()); + CHECK(obj.GetExtderSystem()->GetLoadingExtderId() == MAIN_EXTRUDER_ID); +} + +// ParseV2_0 does not enforce the documented extruder dialect invariants: the count in `state` and +// the id ordering of `info[]` are trusted, not checked. This test pins the resulting behavior. +TEST_CASE("Malformed extruder payload is stored by info array position", "[DevExtderSystem]") +{ + const auto extder_info = [](int id) { + // Every key ParseV2_0 reads must be present. + return json{ {"id", id}, {"filam_bak", json::array()}, {"info", 0}, {"temp", 0}, + {"spre", 0}, {"snow", 0}, {"star", 0}, {"stat", 0}, {"hnow", 0} }; + }; + + SECTION("state count disagrees with info length") { + MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); + + // `state` reports 2 toolheads but info[] carries 3. + json info = json::array(); + for (int id = 0; id < 3; ++id) + info.push_back(extder_info(id)); + ExtderSystemParser::ParseV2_0(json{ {"state", 2}, {"info", info} }, obj.GetExtderSystem()); + + // The info array wins: three toolheads are stored. m_total_extder_count still holds 2, so + // GetTotalExtderCount() would trip its debug assert; only the stored vector is inspected. + CHECK(obj.GetExtderSystem()->GetTotalExtderSize() == 3); + for (int id = 0; id < 3; ++id) { + const auto ext = obj.GetExtderSystem()->GetExtderById(id); + REQUIRE(ext.has_value()); + CHECK(ext->GetExtId() == id); + } + } + + SECTION("info id differs from the array index") { + MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); + + // ids 1 and 2 sit at array positions 0 and 1. + json info = json::array({extder_info(1), extder_info(2)}); + ExtderSystemParser::ParseV2_0(json{ {"state", 2}, {"info", info} }, obj.GetExtderSystem()); + + // Toolheads are stored by array position; the payload id is kept but is not the lookup key. + const auto first = obj.GetExtderSystem()->GetExtderById(0); + REQUIRE(first.has_value()); + CHECK(first->GetExtId() == 1); + const auto second = obj.GetExtderSystem()->GetExtderById(1); + REQUIRE(second.has_value()); + CHECK(second->GetExtId() == 2); + } +} diff --git a/tests/slic3rutils/test_dev_mapping.cpp b/tests/slic3rutils/test_dev_mapping.cpp index 92c0cf1117..5f7007e947 100644 --- a/tests/slic3rutils/test_dev_mapping.cpp +++ b/tests/slic3rutils/test_dev_mapping.cpp @@ -3,8 +3,6 @@ // 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. -#include -#include #ifdef WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN @@ -15,11 +13,14 @@ #include #endif +#include +#include +#include +#include + #include #include "slic3r/GUI/DeviceCore/DevDefs.h" #include "libslic3r/ProjectTask.hpp" -#include -#include #include #include