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

This commit is contained in:
Lam Wei Lun
2026-09-25 11:05:51 +08:00
parent d3003dfa5e
commit d69d14d6f8
9 changed files with 120 additions and 21 deletions
+6 -3
View File
@@ -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
+14 -1
View File
@@ -11,6 +11,8 @@
#include "DevUtil.h"
#include "DevUtilBackend.h"
#include <algorithm>
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>();
std::string type = MachineObject::setting_id_to_type(curr_tray->setting_id, (*tray_it)["tray_type"].get<std::string>());
// 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");
+5 -2
View File
@@ -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
}// namespace Slic3r
+2 -1
View File
@@ -5247,7 +5247,7 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
//std::string type = vtray["tray_type"].get<std::string>();
std::string type = setting_id_to_type(vt_tray.setting_id, vtray["tray_type"].get<std::string>());
// 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;
@@ -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
} // namespace Slic3r
+9 -7
View File
@@ -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));
}
+2
View File
@@ -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 &&
+1
View File
@@ -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
+75
View File
@@ -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 <Windows.h>
#endif
#include <catch2/catch_all.hpp>
#include <wx/timer.h>
#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);
}