From d69d14d6f848b7b5d95d8e030320ad3826fd62a9 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 25 Sep 2026 11:05:51 +0800 Subject: [PATCH] Fixes issue where it would display a question mark instead of empty when there's an empty filament slot in the AMS or external slot --- docs/HLSD/printer-agent.md | 9 ++- src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp | 15 +++- src/slic3r/GUI/DeviceCore/DevFilaSystem.h | 7 +- src/slic3r/GUI/DeviceManager.cpp | 3 +- .../GUI/DeviceTab/uiAMSBestPositionPopup.cpp | 13 ++-- src/slic3r/GUI/Widgets/AMSItem.cpp | 16 ++-- src/slic3r/GUI/Widgets/AMSItem.hpp | 2 + tests/slic3rutils/CMakeLists.txt | 1 + tests/slic3rutils/test_ams_item.cpp | 75 +++++++++++++++++++ 9 files changed, 120 insertions(+), 21 deletions(-) create mode 100644 tests/slic3rutils/test_ams_item.cpp diff --git a/docs/HLSD/printer-agent.md b/docs/HLSD/printer-agent.md index 14c0dabd5f..f13acfa99d 100644 --- a/docs/HLSD/printer-agent.md +++ b/docs/HLSD/printer-agent.md @@ -169,9 +169,12 @@ Orca agent does not poll the Moonraker `lane_data` namespace, which exists for Moonraker-channel consumers. Slot presence is the user's declaration, not sensed material: `tray_exist_bits` and the presence of a `vir_slot` entry mark a slot as present even with an empty `tray_type`, which is the `is_exists` state the filament UI reads. A full status frame -(`msg=0`, or a LAN frame with no `msg`) is authoritative for removals, so a virtual tray id -absent from a populated `vir_slot` is dropped; a delta frame (`msg=1`) only updates the -entries it names and leaves omitted entries held. +(`msg=0`, or a LAN frame with no `msg`) updates each virtual-tray ID it contains but does not +remove IDs already observed during the current connection. An observed ID remains if omitted +from a populated `vir_slot`, if `vir_slot` is empty, or if the field is absent. Before any +virtual tray has been observed, a full frame with no virtual-tray entries clears the initial +placeholder. `MachineObject::reset()` clears retained IDs on reconnect. Delta frames update +named entries only. This retention rule is specific to the native Orca agent. Writes follow the same edge-translation rule as the rest of the agent. The shared `MachineObject` command builders emit Bambu-shaped `print.ams_*` payloads, and the agent's diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp index 8c58a20b7b..0c44c4f554 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp @@ -11,6 +11,8 @@ #include "DevUtil.h" #include "DevUtilBackend.h" +#include + using namespace nlohmann; namespace Slic3r { @@ -42,6 +44,15 @@ void DevAmsTray::UpdateColorFromStr(const std::string& color) } } +void DevAmsTray::UpdateEmptyState(bool material_fields_present) +{ + const auto is_zero_or_empty = [](const std::string& value) { + return value.empty() || std::all_of(value.begin(), value.end(), [](char c) { return c == '0'; }); + }; + is_empty = material_fields_present && setting_id.empty() && m_fila_type.empty() && + is_zero_or_empty(color) && is_zero_or_empty(tag_uid); +} + void DevAmsTray::reset() { tag_uid = ""; @@ -63,6 +74,7 @@ void DevAmsTray::reset() k = 0.0f; n = 0.0f; is_bbl = false; + is_empty = false; hold_count = 0; remain = 0; } @@ -662,7 +674,7 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS //std::string type = (*tray_it)["tray_type"].get(); std::string type = MachineObject::setting_id_to_type(curr_tray->setting_id, (*tray_it)["tray_type"].get()); // curr_tray->setting_id is our OF id; GFS00/GFS01 are the printer's own support-filament ids. - auto* agent = GUI::wxGetApp().getAgent(); + auto* agent = wxTheApp != nullptr ? GUI::wxGetApp().getAgent() : nullptr; const std::string printer_filament_id = agent ? agent->from_orca_filament_id(curr_tray->setting_id) : curr_tray->setting_id; if (printer_filament_id == "GFS00") { @@ -762,6 +774,7 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS { curr_tray->remain = -1; } + curr_tray->UpdateEmptyState(tray_it->contains("tray_info_idx") && tray_it->contains("tray_type")); // The tray objects are reused across status updates. Reset this // state when a previously empty slot receives a filament again. curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder"); diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h index f3593eaaf4..38addd23dd 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h @@ -83,6 +83,7 @@ public: wxColour wx_color; bool is_bbl; bool is_exists = false; + bool is_empty = false; // Explicitly reported as having no filament. bool is_slot_placeholder = false; // Orca: True for empty tray slots from pull-mode agents int hold_count = 0; int remain = 0; // filament remain: 0 ~ 100 @@ -91,13 +92,15 @@ public: // operators bool operator==(DevAmsTray const& o) const { - return id == o.id && m_fila_type == o.m_fila_type && filament_setting_id == o.filament_setting_id && color == o.color; + return id == o.id && m_fila_type == o.m_fila_type && filament_setting_id == o.filament_setting_id && + color == o.color && is_empty == o.is_empty; } bool operator!=(DevAmsTray const& o) const { return !operator==(o); } // setters void reset(); void UpdateColorFromStr(const std::string& color); + void UpdateEmptyState(bool material_fields_present); void set_hold_count() { hold_count = HOLD_COUNT_MAX; } // getter @@ -432,4 +435,4 @@ struct DevFilamentDryingPreset float filament_dev_ams_drying_heat_distortion_temperature = 0.0f; }; -}// namespace Slic3r \ No newline at end of file +}// namespace Slic3r diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 3d0cac2f67..f9c9ed20b2 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -5247,7 +5247,7 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray) //std::string type = vtray["tray_type"].get(); std::string type = setting_id_to_type(vt_tray.setting_id, vtray["tray_type"].get()); // vt_tray.setting_id is our OF id (translated on the way in); the two support ids below are the printer's own. - auto* agent = GUI::wxGetApp().getAgent(); + auto* agent = wxTheApp != nullptr ? GUI::wxGetApp().getAgent() : nullptr; const std::string printer_filament_id = agent ? agent->from_orca_filament_id(vt_tray.setting_id) : vt_tray.setting_id; if (printer_filament_id == "GFS00") { vt_tray.m_fila_type = "PLA-S"; @@ -5342,6 +5342,7 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray) else { vt_tray.remain = -1; } + vt_tray.UpdateEmptyState(vtray.contains("tray_info_idx") && vtray.contains("tray_type")); } return vt_tray; diff --git a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp index 8505a559d6..efb50d3666 100644 --- a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp +++ b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp @@ -943,7 +943,11 @@ int ReselectMachineDialog::CaculateSwitcherDistribution(MachineObject* obj, cons const auto& can = ams.cans[j]; auto id = getTrayID(obj, ams.ams_id, can.can_id); auto material = can.material_name; - if (can.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND || + if (can.is_empty || can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) + { + material = L("Empty"); + } + else if (can.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND || can.material_state == AMSCanType::AMS_CAN_TYPE_BRAND || can.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL) { @@ -952,11 +956,6 @@ int ReselectMachineDialog::CaculateSwitcherDistribution(MachineObject* obj, cons material = L("?"); } } - if (can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) - { - material = "Empty"; - } - auto itOK = std::find_if(posOK.begin(), posOK.end(), [&](const trayHelper& tray){ auto amsID = std::get<0>(tray); auto slotID = std::get<1>(tray); @@ -1043,4 +1042,4 @@ ReselectMachineDialog::~ReselectMachineDialog() } // namespace GUI -} // namespace Slic3r \ No newline at end of file +} // namespace Slic3r diff --git a/src/slic3r/GUI/Widgets/AMSItem.cpp b/src/slic3r/GUI/Widgets/AMSItem.cpp index 9d5ec9c1c1..7c4ec10162 100644 --- a/src/slic3r/GUI/Widgets/AMSItem.cpp +++ b/src/slic3r/GUI/Widgets/AMSItem.cpp @@ -72,6 +72,7 @@ bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag, Caninfo info; // tray is exists if (it != ams->GetTrays().end() && it->second->is_exists) { + info.is_empty = it->second->is_empty; if (it->second->is_tray_info_ready()) { info.can_id = it->second->id; info.ctype = it->second->ctype; @@ -139,6 +140,7 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) { this->ams_type = AMSModel::EXT_AMS; Caninfo info; info.can_id = std::to_string(0); + info.is_empty = tray.is_empty; this->cans.clear(); if (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID)) @@ -1101,9 +1103,9 @@ void AMSLib::render_lite_text(wxDC& dc) dc.SetTextForeground(temp_text_colour); auto libsize = GetSize(); - if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND + if (!m_info.is_empty && (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND || m_info.material_state == AMSCanType::AMS_CAN_TYPE_BRAND - || m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL) { + || m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)) { if (m_info.material_name.empty()) { auto tsize = dc.GetMultiLineTextExtent("?"); @@ -1151,7 +1153,7 @@ void AMSLib::render_lite_text(wxDC& dc) } } - if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) { + if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || m_info.is_empty) { auto tsize = dc.GetMultiLineTextExtent(_L("/")); auto pot = wxPoint((libsize.x - tsize.x) / 2 + FromDIP(2), (libsize.y - tsize.y) / 2 + FromDIP(3)); dc.DrawText(_L("/"), pot); @@ -1202,9 +1204,9 @@ void AMSLib::render_generic_text(wxDC &dc) } auto libsize = GetSize(); - if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND + if (!m_info.is_empty && (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND || m_info.material_state == AMSCanType::AMS_CAN_TYPE_BRAND - || m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL) { + || m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)) { if (m_info.material_name.empty() /*&& m_info.material_state != AMSCanType::AMS_CAN_TYPE_VIRTUAL*/) { auto tsize = dc.GetMultiLineTextExtent("?"); @@ -1284,7 +1286,7 @@ void AMSLib::render_generic_text(wxDC &dc) } } - if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) { + if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || m_info.is_empty) { auto tsize = dc.GetMultiLineTextExtent(_L("Empty")); auto pot = wxPoint((libsize.x - tsize.x) / 2, (libsize.y - tsize.y) / 2 + FromDIP(3)); dc.DrawText(_L("Empty"), pot); @@ -2781,7 +2783,7 @@ void AMSPreview::doRender(wxDC &dc) } else { wxRect rect(left, (size.y - AMS_ITEM_CUBE_SIZE.y) / 2, AMS_ITEM_CUBE_SIZE.x, AMS_ITEM_CUBE_SIZE.y); - if (iter->material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) { + if (iter->material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || iter->is_empty) { dc.SetPen(wxPen(wxColor(0, 0, 0))); dc.DrawLine(rect.GetRight() - FromDIP(1), rect.GetTop() + FromDIP(1), rect.GetLeft() + FromDIP(1), rect.GetBottom() - FromDIP(1)); } diff --git a/src/slic3r/GUI/Widgets/AMSItem.hpp b/src/slic3r/GUI/Widgets/AMSItem.hpp index ac4283b7e8..a7908a456e 100644 --- a/src/slic3r/GUI/Widgets/AMSItem.hpp +++ b/src/slic3r/GUI/Widgets/AMSItem.hpp @@ -183,6 +183,7 @@ struct Caninfo wxString material_name; wxColour material_colour = {*wxWHITE}; AMSCanType material_state; + bool is_empty = false; int ctype=0; int material_remain = 100; int cali_idx = -1; @@ -198,6 +199,7 @@ public: material_name == other.material_name && material_colour == other.material_colour && material_state == other.material_state && + is_empty == other.is_empty && ctype == other.ctype && material_remain == other.material_remain && cali_idx == other.cali_idx && diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 9c8e75429a..7f56a6c969 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -7,6 +7,7 @@ add_executable(${_TEST_NAME}_tests test_filament_bitmap_utils.cpp test_device_progress.cpp test_device_manager.cpp + test_ams_item.cpp test_device_manager_integration.cpp test_web_media_controller.cpp test_network_versions.cpp diff --git a/tests/slic3rutils/test_ams_item.cpp b/tests/slic3rutils/test_ams_item.cpp new file mode 100644 index 0000000000..d756e32012 --- /dev/null +++ b/tests/slic3rutils/test_ams_item.cpp @@ -0,0 +1,75 @@ +// why: match the GUI include order to avoid rpcndr.h byte/std::byte +// ambiguity in the Windows COM headers. +#ifdef WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include +#endif + +#include + +#include + +#include "slic3r/GUI/Widgets/AMSItem.hpp" +#include "slic3r/GUI/DeviceCore/DevFilaSystem.h" + +using namespace Slic3r; +using namespace Slic3r::GUI; + +TEST_CASE("Configured empty AMS trays remain distinct from unknown trays", "[AMSItem]") +{ + MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1"); + machine.printer_agent_id = "orca"; + + machine.parse_json("lan", R"({"print":{"command":"push_status","msg":0,"ams":{ + "ams_exist_bits":"1","tray_exist_bits":"3","ams":[ + {"id":"0","info":"0001","tray":[ + {"id":"0","tag_uid":"0000000000000000","tray_info_idx":"","tray_type":"","tray_color":"00000000"}, + {"id":"1"} + ]} + ] + }}})", false); + + const auto& ams_list = machine.GetFilaSystem()->GetAmsList(); + const auto ams_it = ams_list.find("0"); + REQUIRE(ams_it != ams_list.end()); + auto* ams = ams_it->second; + REQUIRE(ams != nullptr); + REQUIRE(ams->GetTray("0") != nullptr); + CHECK(ams->GetTray("0")->is_exists); + AMSinfo info; + REQUIRE(info.parse_ams_info(&machine, ams)); + REQUIRE(info.cans.size() == 2); + + CHECK(info.cans[0].is_empty); + CHECK(info.cans[0].material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND); + CHECK_FALSE(info.cans[1].is_empty); + CHECK(info.cans[1].material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND); +} + +TEST_CASE("Empty external slots remain distinct from unknown slots", "[AMSItem]") +{ + MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1"); + machine.printer_agent_id = "orca"; + + machine.parse_json("lan", R"({"print":{"command":"push_status","msg":0,"vir_slot":[ + {"id":"255","tag_uid":"0000000000000000","tray_info_idx":"","tray_type":"","tray_color":"00000000"}, + {"id":"254"} + ]}})", false); + + REQUIRE(machine.vt_slot.size() == 2); + CHECK(machine.vt_slot[0].is_exists); + AMSinfo empty_info; + empty_info.parse_ext_info(&machine, machine.vt_slot[0]); + CHECK(empty_info.cans[0].is_empty); + CHECK(empty_info.cans[0].material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL); + + AMSinfo unknown_info; + unknown_info.parse_ext_info(&machine, machine.vt_slot[1]); + CHECK_FALSE(unknown_info.cans[0].is_empty); + CHECK(unknown_info.cans[0].material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL); +}