WIP fix for filament type syncing

This commit is contained in:
Lam Wei Lun
2026-09-25 19:44:02 +08:00
parent 5f589ff909
commit 78e5ecdcd7
12 changed files with 423 additions and 102 deletions
+56 -27
View File
@@ -4,6 +4,7 @@
#include "GUI_App.hpp"
#include "GUI.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/Utils/AmsPayload.hpp"
#include "libslic3r/Preset.hpp"
#include "I18N.hpp"
#include <algorithm>
@@ -146,6 +147,7 @@ void AMSMaterialsSetting::create_panel_normal(wxWindow* parent)
#else
m_comboBox_filament = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
#endif
m_comboBox_filament->SetClientData(&m_comboBox_from_printer);
m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0);
@@ -483,10 +485,7 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
m_filament_type = "";
long nozzle_temp_min_int = 0;
long nozzle_temp_max_int = 0;
wxColour color = *wxWHITE;
char col_buf[10];
sprintf(col_buf, "%02X%02X%02X00", (int)color.Red(), (int)color.Green(), (int)color.Blue());
std::string color_str; // reset use empty string
std::string color_str; // empty; command_ams_filament_settings maps it to the empty-tray color
std::string selected_ams_id;
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
@@ -503,7 +502,7 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
if (obj) {
if(m_is_third){
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, std::string(col_buf), m_filament_type, nozzle_temp_min_int,
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, color_str, m_filament_type, nozzle_temp_min_int,
nozzle_temp_max_int);
}
@@ -674,7 +673,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
wxString nozzle_temp_max = m_input_nozzle_max->GetTextCtrl()->GetValue();
long nozzle_temp_min_int, nozzle_temp_max_int;
long nozzle_temp_min_int = 0, nozzle_temp_max_int = 0;
nozzle_temp_min.ToLong(&nozzle_temp_min_int);
nozzle_temp_max.ToLong(&nozzle_temp_max_int);
wxColour color = m_clr_picker->m_colour;
@@ -878,12 +877,22 @@ void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
}
std::vector<wxColour> ams_colors;
obj->GetFilaSystem()->CollectAmsColors(ams_colors);
// A fixed-vocabulary printer can only show its own palette; anything else
// would offer a color its screen cannot hold.
const FilamentMetadataValues metadata = filament_metadata_values(obj->get_dev_id());
const bool constrained = metadata.constrained && !metadata.colors.empty();
if (constrained) {
for (const std::string& color : metadata.colors)
ams_colors.push_back(DevAmsTray::decode_color(color));
} else {
obj->GetFilaSystem()->CollectAmsColors(ams_colors);
}
wxPoint img_pos = m_clr_picker->ClientToScreen(wxPoint(0, 0));
wxPoint popup_pos(img_pos.x - m_color_picker_popup.GetSize().x - FromDIP(95), img_pos.y - FromDIP(65));
m_color_picker_popup.Position(popup_pos, wxSize(0, 0));
m_color_picker_popup.set_ams_colours(ams_colors);
m_color_picker_popup.set_constrained(constrained);
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour);
m_color_picker_popup.Popup();
}
@@ -1008,8 +1017,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
if (alias.empty())
continue;
filament_items.push_back(alias);
_collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types);
// A printer with a fixed vocabulary only offers the types it can
// hold, so an incompatible preset never reaches the list.
if (!filament_material_compatible(obj->get_dev_id(), query_filament_types[alias].ToStdString()))
continue;
filament_items.push_back(alias);
FilamentInfos filament_infos;
filament_infos.filament_id = filament_it->filament_id;
@@ -1208,7 +1221,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
}
m_comboBox_filament->Set(filament_items);
m_comboBox_filament->SetClientData(new int(1));
m_comboBox_from_printer = 1;
m_comboBox_filament->SetSelection(selection_idx);
post_select_event(selection_idx);
@@ -1245,8 +1258,7 @@ void AMSMaterialsSetting::on_select_cali_result(wxCommandEvent &evt)
void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
{
// Get the flag whether to open the filament setting dialog from the device page
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData());
const bool initial_printer_selection = from_printer && *from_printer == 1;
const bool initial_printer_selection = m_comboBox_from_printer == 1;
m_filament_type = "";
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
@@ -1330,7 +1342,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
m_input_k_val->GetTextCtrl()->SetValue(wxEmptyString);
m_input_n_val->GetTextCtrl()->SetValue(wxEmptyString);
}
m_comboBox_filament->SetClientData(new int(0));
m_comboBox_from_printer = 0;
return;
}
else {
@@ -1417,7 +1429,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
m_comboBox_cali_result->Set(items);
if (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID) {
if (from_printer && (*from_printer == 1)) {
if (m_comboBox_from_printer == 1) {
for (auto slot : obj->vt_slot) {
if (slot.id == std::to_string(ams_id))
cali_select_idx = CalibUtils::get_selected_calib_idx(m_pa_profile_items, slot.cali_idx);
@@ -1434,7 +1446,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
}
}
else {
if (from_printer && (*from_printer == 1)) {
if (m_comboBox_from_printer == 1) {
DevAmsTray* selected_tray = this->obj->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id));
if (!selected_tray)
{
@@ -1476,7 +1488,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
}
}
m_comboBox_filament->SetClientData(new int(0));
m_comboBox_from_printer = 0;
}
void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
@@ -1738,6 +1750,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
cp->set_selected(false);
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
m_color_pickers.push_back(cp);
m_def_color_pickers.push_back(cp);
fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
set_def_colour(cp->m_colour);
@@ -1750,27 +1763,27 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
}
wxBoxSizer* m_sizer_other = new wxBoxSizer(wxHORIZONTAL);
auto m_title_other = new wxStaticText(m_def_color_box, wxID_ANY, _L("Other Color"), wxDefaultPosition, wxDefaultSize, 0);
m_title_other = new wxStaticText(m_def_color_box, wxID_ANY, _L("Other Color"), wxDefaultPosition, wxDefaultSize, 0);
m_title_other->SetFont(::Label::Body_14);
m_title_other->SetBackgroundColour(wxColour(238, 238, 238));
m_sizer_other->Add(m_title_other, 0, wxALL, 5);
auto other_line = new wxPanel(m_def_color_box, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL);
other_line->SetMinSize(wxSize(-1, 1));
other_line->SetMaxSize(wxSize(-1, 1));
other_line->SetBackgroundColour(wxColour("#CECECE"));
m_sizer_other->Add(other_line, 1, wxALIGN_CENTER, 0);
m_other_line = new wxPanel(m_def_color_box, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL);
m_other_line->SetMinSize(wxSize(-1, 1));
m_other_line->SetMaxSize(wxSize(-1, 1));
m_other_line->SetBackgroundColour(wxColour("#CECECE"));
m_sizer_other->Add(m_other_line, 1, wxALIGN_CENTER, 0);
//custom color
wxBoxSizer* m_sizer_custom = new wxBoxSizer(wxHORIZONTAL);
auto m_title_custom = new wxStaticText(m_def_color_box, wxID_ANY, _L("Custom Color"), wxDefaultPosition, wxDefaultSize, 0);
m_title_custom = new wxStaticText(m_def_color_box, wxID_ANY, _L("Custom Color"), wxDefaultPosition, wxDefaultSize, 0);
m_title_custom->SetFont(::Label::Body_14);
m_title_custom->SetBackgroundColour(wxColour(238, 238, 238));
auto custom_line = new wxPanel(m_def_color_box, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL);
custom_line->SetBackgroundColour(wxColour("#CECECE"));
custom_line->SetMinSize(wxSize(-1, 1));
custom_line->SetMaxSize(wxSize(-1, 1));
m_custom_line = new wxPanel(m_def_color_box, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL);
m_custom_line->SetBackgroundColour(wxColour("#CECECE"));
m_custom_line->SetMinSize(wxSize(-1, 1));
m_custom_line->SetMaxSize(wxSize(-1, 1));
m_sizer_custom->Add(m_title_custom, 0, wxALL, 5);
m_sizer_custom->Add(custom_line, 1, wxALIGN_CENTER, 0);
m_sizer_custom->Add(m_custom_line, 1, wxALIGN_CENTER, 0);
m_custom_cp = new StaticBox(m_def_color_box);
m_custom_cp->SetSize(FromDIP(60), FromDIP(25));
@@ -1910,6 +1923,22 @@ void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
Fit();
}
// A printer with a fixed vocabulary only shows its own palette: the default
// swatches and the custom picker would offer colors its screen cannot hold.
void ColorPickerPopup::set_constrained(bool constrained)
{
const bool show = !constrained;
m_title_other->Show(show);
m_other_line->Show(show);
for (ColorPicker* cp : m_def_color_pickers)
cp->Show(show);
m_title_custom->Show(show);
m_custom_line->Show(show);
m_custom_cp->Show(show);
Layout();
Fit();
}
void ColorPickerPopup::set_def_colour(wxColour col)
{
m_def_col = col;
+14
View File
@@ -7,6 +7,8 @@
#include "DeviceManager.hpp"
#include "wx/clrpicker.h"
#include "wx/colourdata.h"
#include "wx/panel.h"
#include "wx/stattext.h"
#include "Widgets/RadioBox.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/RoundedRectangle.hpp"
@@ -77,12 +79,20 @@ public:
std::vector<wxColour> m_ams_colors;
std::vector<ColorPicker*> m_color_pickers;
std::vector<ColorPicker*> m_ams_color_pickers;
// Sections a fixed-vocabulary printer hides in set_constrained: its own
// palette is the only color its screen can show.
std::vector<ColorPicker*> m_def_color_pickers;
wxStaticText* m_title_other{nullptr};
wxPanel* m_other_line{nullptr};
wxStaticText* m_title_custom{nullptr};
wxPanel* m_custom_line{nullptr};
public:
ColorPickerPopup(wxWindow* parent);
~ColorPickerPopup() {};
void on_custom_clr_picker(wxMouseEvent& event);
void set_ams_colours(std::vector<wxColour> ams);
void set_constrained(bool constrained);
void set_def_colour(wxColour col);
void paintEvent(wxPaintEvent& evt);
virtual void OnDismiss() wxOVERRIDE;
@@ -123,6 +133,10 @@ public:
std::string ams_setting_id;
bool m_is_third;
// Marks whether the filament combo was seeded from the printer's own tray.
// Stored as a member so the combo's client data never owns an allocation.
int m_comboBox_from_printer{0};
// Orca: view-only mode (laser/cut). When set, the dialog is inspectable but every
// editing control is disabled and no command is sent.
bool m_view_only = false;
+20 -11
View File
@@ -20,23 +20,31 @@ static int _hex_digit_to_int(const char c) { return (c >= '0' && c <= '9') ? c -
wxColour DevAmsTray::decode_color(const std::string &color)
{
std::array<int, 4> ret = {0, 0, 0, 0};
// 6 hex digits (RRGGBB) and 8 (RRGGBBAA) are both legal on the wire;
// normalize_ams_color pads the short form, so accept it here too.
const size_t hex_digits = (color.size() == 6 || color.size() == 8) ? color.size() : 0;
if (hex_digits == 0) return wxColour(255, 255, 255, 255);
std::array<int, 4> ret = {0, 0, 0, 255};
const char * c = color.data();
if (color.size() == 8) {
for (size_t j = 0; j < 4; ++j) {
int digit1 = _hex_digit_to_int(*c++);
int digit2 = _hex_digit_to_int(*c++);
if (digit1 == -1 || digit2 == -1) break;
ret[j] = static_cast<float>(digit1 * 16 + digit2);
}
} else { return wxColour(255, 255, 255, 255); }
for (size_t j = 0; j < hex_digits / 2; ++j) {
int digit1 = _hex_digit_to_int(*c++);
int digit2 = _hex_digit_to_int(*c++);
if (digit1 == -1 || digit2 == -1) break;
ret[j] = digit1 * 16 + digit2;
}
return wxColour(ret[0], ret[1], ret[2], ret[3]);
}
void DevAmsTray::UpdateColorFromStr(const std::string& color)
{
if (color.empty()) return;
// An empty color clears, exactly like a payload that omits the key.
if (color.empty()) {
wx_color = wxColour();
this->color.clear();
return;
}
if (this->color != color)
{
wx_color = "#" + wxString::FromUTF8(color);
@@ -729,7 +737,8 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
}
else
{
curr_tray->color = "";
// Absent and empty carry the same meaning.
curr_tray->UpdateColorFromStr(std::string());
}
if (tray_it->contains("nozzle_temp_max"))
{
+43 -36
View File
@@ -565,6 +565,37 @@ bool MachineObject::ams_filament_ack_failed(const nlohmann::json& jj, std::strin
return true;
}
bool MachineObject::ams_filament_native_sync_pending(const nlohmann::json& jj)
{
return jj.contains("native_sync") && jj["native_sync"].is_string() &&
jj["native_sync"].get<std::string>() == "pending";
}
bool MachineObject::ams_filament_connector_only(const nlohmann::json& jj)
{
return jj.contains("native_sync") && jj["native_sync"].is_string() &&
jj["native_sync"].get<std::string>() == "connector_only";
}
void MachineObject::fill_ams_filament_ack_tray(DevAmsTray& tray, const nlohmann::json& jj)
{
// Every field is optional: `jj` is a mutable copy, so a bare operator[]
// would insert null and the get<> below would throw type_error.
if (jj.contains("nozzle_temp_max") && jj["nozzle_temp_max"].is_number())
tray.nozzle_temp_max = std::to_string(jj["nozzle_temp_max"].get<int>());
if (jj.contains("nozzle_temp_min") && jj["nozzle_temp_min"].is_number())
tray.nozzle_temp_min = std::to_string(jj["nozzle_temp_min"].get<int>());
if (jj.contains("tray_color") && jj["tray_color"].is_string())
tray.color = jj["tray_color"].get<std::string>();
if (jj.contains("tray_info_idx") && jj["tray_info_idx"].is_string())
tray.setting_id = jj["tray_info_idx"].get<std::string>();
if (jj.contains("tray_type") || jj.contains("tray_info_idx")) {
const std::string type = (jj.contains("tray_type") && jj["tray_type"].is_string()) ? jj["tray_type"].get<std::string>() : tray.m_fila_type;
tray.m_fila_type = setting_id_to_type(tray.setting_id, type);
}
tray.set_hold_count();
}
PrinterArch MachineObject::get_printer_arch() const
{
return DevPrinterConfigUtil::get_printer_arch(printer_type);
@@ -1791,8 +1822,10 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
j["print"]["tray_id"] = tag_tray_id;
j["print"]["tray_info_idx"] = filament_id;
j["print"]["setting_id"] = setting_id;
// format "FFFFFFFF" RGBA
j["print"]["tray_color"] = tray_color;
// Always present: Bambu firmware echoes it into the tray handler, and a
// slim request would make that echo throw. An empty value becomes the
// protocol's own empty-tray color.
j["print"]["tray_color"] = tray_color.empty() ? std::string("00000000") : tray_color;
j["print"]["nozzle_temp_min"] = nozzle_temp_min;
j["print"]["nozzle_temp_max"] = nozzle_temp_max;
j["print"]["tray_type"] = tray_type;
@@ -4257,6 +4290,10 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
if (!ack_reason.empty())
text += wxString::FromUTF8(": ") + wxString::FromUTF8(ack_reason);
GUI::wxGetApp().push_notification(this, text);
} else if (ams_filament_native_sync_pending(jj)) {
GUI::wxGetApp().push_notification(this, _L("Filament settings are saved and will be applied when the printer reconnects."));
} else if (ams_filament_connector_only(jj)) {
GUI::wxGetApp().push_notification(this, _L("Filament settings were saved by OrcaSonar but could not be applied to the printer."));
}
if (!ack_failed && jj["ams_id"].is_number()) {
@@ -4267,39 +4304,15 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
}
if (ams_id == 255 && tray_id == VIRTUAL_TRAY_MAIN_ID && !vt_slot.empty()) {
BOOST_LOG_TRIVIAL(info) << "ams_filament_setting, parse tray info";
vt_slot[0].nozzle_temp_max = std::to_string(jj["nozzle_temp_max"].get<int>());
vt_slot[0].nozzle_temp_min = std::to_string(jj["nozzle_temp_min"].get<int>());
vt_slot[0].color = jj["tray_color"].get<std::string>();
vt_slot[0].setting_id = jj["tray_info_idx"].get<std::string>();
//vt_tray.type = jj["tray_type"].get<std::string>();
vt_slot[0].m_fila_type = setting_id_to_type(vt_slot[0].setting_id, jj["tray_type"].get<std::string>());
fill_ams_filament_ack_tray(vt_slot[0], jj);
vt_slot[0].is_exists = true;
// delay update
vt_slot[0].set_hold_count();
} else {
auto ams = m_fila_system->GetAmsById(std::to_string(ams_id));
if (ams) {
auto tray_it = ams->GetTrays().find(std::to_string(tray_id));
if (tray_it != ams->GetTrays().end()) {
BOOST_LOG_TRIVIAL(trace) << "ams_filament_setting, parse tray info";
tray_it->second->nozzle_temp_max = std::to_string(jj["nozzle_temp_max"].get<int>());
tray_it->second->nozzle_temp_min = std::to_string(jj["nozzle_temp_min"].get<int>());
//tray_it->second->type = jj["tray_type"].get<std::string>();
tray_it->second->color = jj["tray_color"].get<std::string>();
/*tray_it->second->cols.clear();
if (jj.contains("cols")) {
if (jj["cols"].is_array()) {
for (auto it = jj["cols"].begin(); it != jj["cols"].end(); it++) {
tray_it->second->cols.push_back(it.value().get<std::string>());
}
}
}*/
tray_it->second->setting_id = jj["tray_info_idx"].get<std::string>();
tray_it->second->m_fila_type = setting_id_to_type(tray_it->second->setting_id, jj["tray_type"].get<std::string>());
// delay update
tray_it->second->set_hold_count();
fill_ams_filament_ack_tray(*tray_it->second, jj);
} else {
BOOST_LOG_TRIVIAL(warning) << "ams_filament_setting, can not find in trayList, tray_id=" << tray_id;
}
@@ -5972,14 +5985,11 @@ void MachineObject::check_ams_filament_valid()
if (curr_tray->setting_id.size() == 8 && curr_tray->setting_id[0] == 'P' && filament_list.find(curr_tray->setting_id) == filament_list.end()) {
if (checked_filament.find(curr_tray->setting_id) != checked_filament.end()) {
need_checked_filament_id[nozzle_diameter_str].insert(curr_tray->setting_id);
wxColour color = *wxWHITE;
char col_buf[10];
sprintf(col_buf, "%02X%02X%02XFF", (int) color.Red(), (int) color.Green(), (int) color.Blue());
try {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " ams settings_id is not exist in filament_list and reset, ams_id: " << ams_id << " tray_id"
<< slot_id << "filament_id: " << curr_tray->setting_id;
command_ams_filament_settings(std::stoi(ams_id), std::stoi(slot_id), "", "", std::string(col_buf), "", 0, 0);
command_ams_filament_settings(std::stoi(ams_id), std::stoi(slot_id), "", "", "00000000", "", 0, 0);
continue;
} catch (...) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " stoi error and ams_id: " << ams_id << " tray_id" << slot_id;
@@ -6034,12 +6044,9 @@ void MachineObject::check_ams_filament_valid()
if (vt_tray.setting_id.size() == 8 && vt_tray.setting_id[0] == 'P' && filament_list.find(vt_tray.setting_id) == filament_list.end()) {
if (checked_filament.find(vt_tray.setting_id) != checked_filament.end()) {
need_checked_filament_id[nozzle_diameter_str].insert(vt_tray.setting_id);
wxColour color = *wxWHITE;
char col_buf[10];
sprintf(col_buf, "%02X%02X%02XFF", (int) color.Red(), (int) color.Green(), (int) color.Blue());
try {
BOOST_LOG_TRIVIAL(info) << "vt_tray.setting_id is not exist in filament_list and reset vt_tray and the filament_id is: " << vt_tray.setting_id;
command_ams_filament_settings(vt_id, 0, "", "", std::string(col_buf), "", 0, 0);
command_ams_filament_settings(vt_id, 0, "", "", "00000000", "", 0, 0);
continue;
} catch (...) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " stoi error and tray_id" << vt_tray.id;
+5
View File
@@ -170,6 +170,11 @@ public:
// Orca: an OPCP failure ack (result != "success"). Fills reason when present.
static bool ams_filament_ack_failed(const nlohmann::json& jj, std::string& reason);
static bool ams_filament_native_sync_pending(const nlohmann::json& jj);
static bool ams_filament_connector_only(const nlohmann::json& jj);
// Orca: apply one ams_filament_setting echo to a tray. A slim ack omits
// fields; an unguarded get here would throw and drop the whole push frame.
static void fill_ams_filament_ack_tray(DevAmsTray& tray, const nlohmann::json& jj);
// Orca: these DeviceCore module accessors are unwired on the read side — axis/chamber/status
// are fed every MQTT push but no GUI consumer reads them yet, and for calib/upgrade the inline
+6
View File
@@ -4616,6 +4616,12 @@ void StatusPanel::on_filament_edit(wxCommandEvent &event)
filament = tray->sub_brands;
}
}
else
{
// No tray state for this slot: never show the previous slot's.
m_filament_setting_dlg->set_empty_color(AMS_MATERIALS_SETTING_DEF_COLOUR);
m_filament_setting_dlg->ams_filament_id = "";
}
m_filament_setting_dlg->Move(wxPoint(current_position_x, current_position_y));
m_filament_setting_dlg->Popup(filament, sn_number, temp_min, temp_max, k_val, n_val);
+91 -5
View File
@@ -89,7 +89,8 @@ std::string map_filament_type_to_generic_id(const std::string& filament_type)
if (it == type_to_ofl_family.end())
return UNKNOWN_FILAMENT_ID;
if (auto* bundle = GUI::wxGetApp().preset_bundle) {
auto* bundle = wxTheApp != nullptr ? GUI::wxGetApp().preset_bundle : nullptr;
if (bundle) {
const Preset* preset = bundle->filaments.find_preset("Generic " + it->second + " @System");
if (preset != nullptr && preset->is_system && !preset->filament_id.empty())
return preset->filament_id;
@@ -270,7 +271,7 @@ LaneDataFetch read_moonraker_lane_data(const std::string& origin,
void resolve_tray_info_idx(std::vector<AmsTrayData>& trays)
{
auto* bundle = GUI::wxGetApp().preset_bundle;
auto* bundle = wxTheApp != nullptr ? GUI::wxGetApp().preset_bundle : nullptr;
for (auto& tray : trays) {
// Absent lanes render as placeholders; resolving an empty type is busy
// work at best and could bind a bogus id.
@@ -280,9 +281,15 @@ void resolve_tray_info_idx(std::vector<AmsTrayData>& trays)
// what is still empty so the generic fallback cannot overwrite it.
if (!tray.tray_info_idx.empty())
continue;
tray.tray_info_idx = bundle
? bundle->filaments.filament_id_by_type(tray.tray_type)
: map_filament_type_to_generic_id(tray.tray_type);
if (bundle) {
std::string filament_id;
if (bundle->filaments.filament_id_by_type(tray.tray_type, filament_id))
tray.tray_info_idx = std::move(filament_id);
} else {
const std::string filament_id = map_filament_type_to_generic_id(tray.tray_type);
if (filament_id != UNKNOWN_FILAMENT_ID)
tray.tray_info_idx = filament_id;
}
}
}
@@ -388,6 +395,7 @@ struct AmsDeviceCaps
bool ops_known = false;
bool has_ams = false;
bool filament_slots = false;
FilamentMetadataValues metadata;
};
static std::map<std::string, AmsDeviceCaps> g_ams_caps;
@@ -447,6 +455,84 @@ bool has_filament_slots(const std::string& dev_id)
return it != g_ams_caps.end() && it->second.filament_slots;
}
void register_filament_metadata(const std::string& dev_id, const FilamentMetadataValues& values)
{
if (dev_id.empty())
return;
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
g_ams_caps[dev_id].metadata = values;
}
FilamentMetadataValues filament_metadata_values(const std::string& dev_id)
{
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
auto it = g_ams_caps.find(dev_id);
if (it == g_ams_caps.end())
return {};
return it->second.metadata;
}
static bool material_prefix(const std::string& value, const char* family)
{
const std::string prefix(family);
if (value == prefix)
return true;
if (value.size() <= prefix.size() || value.compare(0, prefix.size(), prefix) != 0)
return false;
const char next = value[prefix.size()];
return next == ' ' || next == '+' || next == '-' || next == '/';
}
static bool qidi_pa_material(const std::string& value)
{
return material_prefix(value, "PA") || material_prefix(value, "PA12") ||
material_prefix(value, "PAHT") || material_prefix(value, "ULTRAPA");
}
// Keep distinct material families separate; only known variants share a stem.
std::string normalize_qidi_material_family(const std::string& value)
{
const std::string upper = boost::algorithm::to_upper_copy(boost::algorithm::trim_copy(value));
if (material_prefix(upper, "PLA-CF")) return "PLA-CF";
if (material_prefix(upper, "PPA-CF")) return "PPA-CF";
if (material_prefix(upper, "PC-ABS")) return "PC";
if (material_prefix(upper, "PCTG")) return "PCTG";
if (material_prefix(upper, "PPA")) return "PPA";
if (material_prefix(upper, "PLA")) return "PLA";
if (material_prefix(upper, "ABS")) return "ABS";
if (material_prefix(upper, "PETG")) return "PETG";
if (material_prefix(upper, "TPU")) return "TPU";
if (material_prefix(upper, "ASA")) return "ASA";
if (qidi_pa_material(upper) || material_prefix(upper, "NYLON")) return "PA";
if (material_prefix(upper, "PC")) return "PC";
if (material_prefix(upper, "PVA")) return "PVA";
return upper;
}
bool filament_material_compatible(const std::string& dev_id, const std::string& preset_type)
{
const FilamentMetadataValues values = filament_metadata_values(dev_id);
if (!values.constrained)
return true;
if (boost::algorithm::trim_copy(preset_type).empty())
return true; // a reply that never carried a type must not hide presets
std::vector<std::string> parts;
boost::algorithm::split(parts, preset_type, boost::algorithm::is_any_of(";"), boost::token_compress_on);
for (const std::string& part : parts) {
const std::string type = boost::algorithm::trim_copy(part);
if (type.empty())
continue;
const std::string category = normalize_qidi_material_family(type);
for (const std::string& declared : values.materials) {
if (boost::algorithm::iequals(boost::algorithm::trim_copy(declared), type))
return true;
if (normalize_qidi_material_family(declared) == category)
return true;
}
}
return false;
}
void clear_ams_caps(const std::string& dev_id)
{
if (dev_id.empty())
+20
View File
@@ -113,6 +113,26 @@ bool ams_caps_known(const std::string& dev_id);
void register_filament_slots(const std::string& dev_id, bool has_slots);
bool has_filament_slots(const std::string& dev_id);
// The values a fixed-vocabulary printer can hold for filament metadata, from
// protocol.filament_metadata.<driver>.values. Materials are the strings the
// status projection reports and colors are RRGGBBAA. `constrained` is true only
// when every writing driver declared values: one free-form driver keeps the
// full picker available.
struct FilamentMetadataValues
{
bool constrained = false;
std::vector<std::string> materials;
std::vector<std::string> colors;
};
void register_filament_metadata(const std::string& dev_id, const FilamentMetadataValues& values);
FilamentMetadataValues filament_metadata_values(const std::string& dev_id);
std::string normalize_qidi_material_family(const std::string& material);
// Whether a preset's filament type can be represented by the device's declared
// materials: an exact type, or the material category the two names share. An
// unconstrained or unanswered device accepts everything, and a type the reply
// never carried is kept rather than hidden.
bool filament_material_compatible(const std::string& dev_id, const std::string& preset_type);
// Forget a device's declared capabilities, so a reconnect starts from "no
// reply yet" instead of a stale declaration.
void clear_ams_caps(const std::string& dev_id);
+40 -3
View File
@@ -686,6 +686,43 @@ void OrcaPrinterAgent::register_ams_capabilities(const std::string& dev_id, cons
has_slots = slots_it->get<bool>();
}
register_filament_slots(dev_id, has_slots);
// filament_metadata.<driver>.values publishes what a fixed-vocabulary
// driver can hold. It constrains the picker only when every writing driver
// declared values: one free-form driver (Happy Hare, AFC) keeps the full
// color picker and preset list available. Registered on every reply so a
// later one without the key clears a stale palette.
FilamentMetadataValues metadata;
const auto meta_it = proto_it->find("filament_metadata");
if (meta_it != proto_it->end() && meta_it->is_object() && !meta_it->empty()) {
const auto collect = [](std::vector<std::string>& out, const nlohmann::json& values, const char* key) {
const auto arr = values.find(key);
if (arr == values.end() || !arr->is_array())
return;
for (const auto& v : *arr) {
if (!v.is_string())
continue;
const std::string entry = v.get<std::string>();
if (std::find(out.begin(), out.end(), entry) == out.end())
out.push_back(entry);
}
};
bool all_constrained = true;
for (auto driver = meta_it->begin(); driver != meta_it->end() && all_constrained; ++driver) {
const auto values_it = driver.value().find("values");
if (values_it == driver.value().end() || !values_it->is_object() || values_it->empty()) {
all_constrained = false;
break;
}
collect(metadata.materials, *values_it, "material");
collect(metadata.colors, *values_it, "color");
}
if (all_constrained && (!metadata.materials.empty() || !metadata.colors.empty()))
metadata.constrained = true;
else
metadata = FilamentMetadataValues{};
}
register_filament_metadata(dev_id, metadata);
}
void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local)
@@ -1299,9 +1336,9 @@ std::string OrcaPrinterAgent::canonicalize_ams_payload(const std::string& dev_id
if (cmd.empty() || (cmd.rfind("ams_", 0) != 0 && cmd != "auto_stop_ams_dry"))
return json_str;
// filament_setting is exempt from the ams_ops union: it persists
// connector state through the filament-slot model, so filament_slots
// alone advertises it (OrcaSonar OPCP §7.8).
// filament_setting is exempt from the ams_ops union: filament_slots
// advertises the canonical slot write, while native metadata support is
// reported separately by the driver (OrcaSonar OPCP §7.8).
auto op_allowed = [&dev_id](const std::string& o) {
if (o == "filament_setting")
return !ams_caps_known(dev_id) || has_filament_slots(dev_id);
+2 -20
View File
@@ -1,6 +1,7 @@
#include "QidiPrinterAgent.hpp"
#include "Http.hpp"
#include "IPrinterAgent.hpp"
#include "slic3r/Utils/AmsPayload.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/GUI_App.hpp"
@@ -507,26 +508,7 @@ std::string QidiPrinterAgent::infer_series_id(const std::string& model_id, const
std::string QidiPrinterAgent::normalize_filament_type(const std::string& filament_type)
{
const std::string upper = trim_and_upper(filament_type);
if (upper.find("PLA") != std::string::npos)
return "PLA";
if (upper.find("ABS") != std::string::npos)
return "ABS";
if (upper.find("PETG") != std::string::npos)
return "PETG";
if (upper.find("TPU") != std::string::npos)
return "TPU";
if (upper.find("ASA") != std::string::npos)
return "ASA";
if (upper.find("PA") != std::string::npos || upper.find("NYLON") != std::string::npos)
return "PA";
if (upper.find("PC") != std::string::npos)
return "PC";
if (upper.find("PVA") != std::string::npos)
return "PVA";
return upper;
return normalize_qidi_material_family(filament_type);
}
} // namespace Slic3r
+50
View File
@@ -285,6 +285,56 @@ TEST_CASE("AMS filament setting acks expose OPCP failures", "[DeviceManager]")
CHECK_FALSE(MachineObject::ams_filament_ack_failed(plain, reason));
}
TEST_CASE("AMS filament acknowledgements report native sync state", "[DeviceManager]")
{
CHECK(MachineObject::ams_filament_native_sync_pending(json::parse(R"({"native_sync":"pending"})")));
CHECK_FALSE(MachineObject::ams_filament_native_sync_pending(json::parse(R"({"native_sync":"applied"})")));
CHECK(MachineObject::ams_filament_connector_only(json::parse(R"({"native_sync":"connector_only"})")));
CHECK_FALSE(MachineObject::ams_filament_connector_only(json::parse(R"({"native_sync":"not_applicable"})")));
}
// Every tray field in the echo is optional: an unguarded get would throw
// type_error into parse_json's frame-wide catch and drop the rest of the push.
TEST_CASE("A slim AMS filament acknowledgement leaves the tray untouched", "[DeviceManager]")
{
DevAmsTray tray("0");
tray.color = "FF0000FF";
tray.nozzle_temp_min = "190";
tray.m_fila_type = "PLA";
CHECK_NOTHROW(MachineObject::fill_ams_filament_ack_tray(
tray, json::parse(R"({"command":"ams_filament_setting","result":"success","errno":0})")));
CHECK(tray.color == "FF0000FF");
CHECK(tray.nozzle_temp_min == "190");
CHECK(tray.m_fila_type == "PLA");
CHECK_NOTHROW(MachineObject::fill_ams_filament_ack_tray(
tray, json::parse(R"({"tray_color":"0000FFFF","tray_type":"ABS","tray_info_idx":"GFA01","nozzle_temp_min":210,"nozzle_temp_max":240})")));
CHECK(tray.color == "0000FFFF");
CHECK(tray.setting_id == "GFA01");
CHECK(tray.nozzle_temp_min == "210");
CHECK(tray.nozzle_temp_max == "240");
CHECK(tray.m_fila_type == "ABS");
}
// An echo that carries no tray fields at all must leave the frame alive: the
// frame-wide catch would silently swallow every later message in the push.
TEST_CASE("An AMS filament acknowledgement without tray fields does not abort the frame", "[DeviceManager]")
{
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
const json ams = json::parse(R"({"ams":{"tray_exist_bits":"1","ams":[
{ "id": "0", "info": "00000001", "tray": [ { "id": "0" } ] } ]}})");
DevFilaSystemParser::ParseV1_0(ams, &machine, machine.GetFilaSystem().get(), false);
REQUIRE(machine.GetFilaSystem()->GetAmsTray("0", "0") != nullptr);
CHECK_NOTHROW(machine.parse_json("lan", R"({"print":{"command":"ams_filament_setting","result":"success","errno":0,"ams_id":0,"tray_id":0}})", false));
const DevAmsTray* tray = machine.GetFilaSystem()->GetAmsTray("0", "0");
REQUIRE(tray != nullptr);
CHECK(tray->color.empty());
}
// An ack that targets a tray but omits tray_id must not abort the frame: it
// falls back to slot 0 instead of an unguarded get on a missing key.
TEST_CASE("An AMS filament ack without a tray_id targets slot 0", "[DeviceManager]")
@@ -351,6 +351,15 @@ TEST_CASE("OrcaPrinterAgent rewrites Bambu ams_* payloads onto the canonical Orc
CHECK(!out["print"].contains("tray_id"));
CHECK(out["print"]["tray_type"] == "PLA");
// tray_color is the one field this sync exists to carry: it must survive
// the funnel, both as a real color and as the empty-tray marker a reset
// sends.
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_filament_setting","ams_id":1,"slot_id":2,"tray_id":2,"tray_type":"PLA","tray_color":"00FF00FF","tray_info_idx":"GFL01"}})"));
CHECK(out["print"]["tray_color"] == "00FF00FF");
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_filament_setting","ams_id":1,"slot_id":2,"tray_id":2,"tray_type":"","tray_info_idx":"","tray_color":"00000000"}})"));
CHECK(out["print"]["tray_color"] == "00000000");
CHECK(out["print"]["tray_type"] == "");
// Wide-box dual form: both addressings resolve to one slot server-side.
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_filament_setting","ams_id":1,"slot_id":5,"tray_type":"PLA"}})"));
CHECK(out["print"]["ams_id"] == 1);
@@ -471,6 +480,73 @@ TEST_CASE("filament slot writes remain independent of FMS", "[OrcaPrinterAgent]"
CHECK(unsupported);
}
// A fixed-vocabulary printer publishes the values it can hold, so the dialog
// offers only those: an exact type and its variants pass the filter, a material
// the printer lacks does not. A free-form driver and an unanswered device stay
// unconstrained.
TEST_CASE("A printer publishes its filament metadata values", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
agent.deliver_to_sink("dev-qidi",
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{
"filament_metadata":{"qidi_box":{"read":["material","color"],"write":["material","color"],
"values":{"material":["PLA","PETG"],"color":["FF0000FF","00FF00FF"]}}}}}}})",
/*local=*/true);
const Slic3r::FilamentMetadataValues values = Slic3r::filament_metadata_values("dev-qidi");
CHECK(values.constrained);
CHECK(values.materials.size() == 2);
CHECK(values.colors.size() == 2);
CHECK(Slic3r::filament_material_compatible("dev-qidi", "PLA"));
CHECK(Slic3r::filament_material_compatible("dev-qidi", "PETG"));
CHECK(Slic3r::filament_material_compatible("dev-qidi", "PLA+"));
CHECK_FALSE(Slic3r::filament_material_compatible("dev-qidi", "HIPS"));
CHECK(Slic3r::normalize_qidi_material_family("PCTG") == "PCTG");
CHECK(Slic3r::normalize_qidi_material_family("PPA") == "PPA");
CHECK(Slic3r::normalize_qidi_material_family("PLA-CF") == "PLA-CF");
CHECK(Slic3r::normalize_qidi_material_family("PA12-CF") == "PA");
CHECK(Slic3r::normalize_qidi_material_family("PC-ABS-FR") == "PC");
agent.deliver_to_sink("dev-qidi-polymer",
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{
"filament_metadata":{"qidi_box":{"read":["material","color"],"write":["material","color"],
"values":{"material":["PC","PA"],"color":["FF0000FF"]}}}}}}})",
/*local=*/true);
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PC"));
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PA Nylon"));
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PA-CF"));
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PA12-CF"));
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PAHT-CF"));
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PAHT-GF"));
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PAHT-S"));
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "UltraPA"));
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "UltraPA-CF25"));
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PC-ABS-FR"));
CHECK_FALSE(Slic3r::filament_material_compatible("dev-qidi-polymer", "PCTG"));
CHECK_FALSE(Slic3r::filament_material_compatible("dev-qidi-polymer", "PPA"));
agent.deliver_to_sink("dev-happyhare",
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{
"filament_metadata":{"happy_hare":{"read":["material","color"],"write":["material","color"]}}}}}})",
/*local=*/true);
CHECK_FALSE(Slic3r::filament_metadata_values("dev-happyhare").constrained);
CHECK(Slic3r::filament_material_compatible("dev-happyhare", "HIPS"));
CHECK(Slic3r::filament_material_compatible("dev-unanswered", "HIPS"));
}
TEST_CASE("An unknown lane material does not bind to an arbitrary filament id", "[OrcaPrinterAgent]") {
AmsTrayData tray;
tray.has_filament = true;
tray.tray_type = "Unlisted polymer family";
std::vector<AmsTrayData> trays{tray};
Slic3r::resolve_tray_info_idx(trays);
CHECK(trays[0].tray_info_idx.empty());
}
// A box wider than 4 slots is shown as several 4-tray units, so the BBL tray id
// a panel reports must split into that unit's own (ams_id, slot_id). A flat lane
// would be the tray id, which is not the layout lane for a wide box