mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-23 17:02:39 +00:00
Initial implementation of filament mapping on OrcaSonar side
This commit is contained in:
@@ -3446,7 +3446,7 @@ size_t PresetCollection::first_visible_idx() const
|
||||
return first_visible;
|
||||
}
|
||||
|
||||
size_t PresetCollection::first_visible_idx_by_type(const std::string& filament_type) const
|
||||
size_t PresetCollection::first_matching_filament_idx(const std::string& filament_type) const
|
||||
{
|
||||
size_t start = m_default_suppressed ? m_num_default_presets : 0;
|
||||
|
||||
@@ -3471,14 +3471,17 @@ size_t PresetCollection::first_visible_idx_by_type(const std::string& filament_t
|
||||
// e.g. "PLA High Speed" -> "PLA"
|
||||
// Dash-separated types like "PA-CF", "PET-CF" are distinct materials, not modifiers.
|
||||
auto sep = filament_type.find(' ');
|
||||
if (sep != std::string::npos) {
|
||||
idx = find_by_type(filament_type.substr(0, sep));
|
||||
if (idx != size_t(-1))
|
||||
return idx;
|
||||
}
|
||||
if (sep != std::string::npos)
|
||||
return find_by_type(filament_type.substr(0, sep));
|
||||
|
||||
return size_t(-1);
|
||||
}
|
||||
|
||||
size_t PresetCollection::first_visible_idx_by_type(const std::string& filament_type) const
|
||||
{
|
||||
size_t idx = first_matching_filament_idx(filament_type);
|
||||
// 3. Any visible preset
|
||||
return first_visible_idx();
|
||||
return idx != size_t(-1) ? idx : first_visible_idx();
|
||||
}
|
||||
|
||||
std::string PresetCollection::filament_id_by_type(const std::string& filament_type) const
|
||||
@@ -3486,6 +3489,15 @@ std::string PresetCollection::filament_id_by_type(const std::string& filament_ty
|
||||
return preset(first_visible_idx_by_type(filament_type)).filament_id;
|
||||
}
|
||||
|
||||
bool PresetCollection::filament_id_by_type(const std::string& filament_type, std::string& out) const
|
||||
{
|
||||
size_t idx = first_matching_filament_idx(filament_type);
|
||||
if (idx == size_t(-1))
|
||||
return false;
|
||||
out = preset(idx).filament_id;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> PresetCollection::diameters_of_selected_printer()
|
||||
{
|
||||
std::set<std::string> diameters;
|
||||
|
||||
@@ -756,6 +756,11 @@ public:
|
||||
size_t first_visible_idx_by_type(const std::string& filament_type) const;
|
||||
// Return the filament_id of the best-matching visible preset for the given filament type.
|
||||
std::string filament_id_by_type(const std::string& filament_type) const;
|
||||
// As above, but returns false (and leaves `out` untouched) when no preset
|
||||
// matches instead of falling back to first_visible_idx(). The fallback is a
|
||||
// plain first-visible/PLA preset, which would bind an unknown material to an
|
||||
// unrelated default.
|
||||
bool filament_id_by_type(const std::string& filament_type, std::string& out) const;
|
||||
// Return index of the first compatible preset. Certainly at least the '- default -' preset shall be compatible.
|
||||
// If one of the prefered_alternates is compatible, select it.
|
||||
template<typename PreferedCondition> size_t first_compatible_idx(PreferedCondition prefered_condition) const
|
||||
@@ -889,6 +894,10 @@ protected:
|
||||
void set_custom_preset_alias(Preset &preset);
|
||||
|
||||
private:
|
||||
// Index of the first visible, compatible, system base preset matching
|
||||
// filament_type (exact, then base type), or (size_t)-1 when none matches.
|
||||
size_t first_matching_filament_idx(const std::string& filament_type) const;
|
||||
|
||||
std::string canonical_preset_name(const std::string &name, const PresetOrigin &load_origin = PresetOrigin()) const;
|
||||
|
||||
// Comparator that sorts "Generic " prefixed presets before others, then alphabetically within each group.
|
||||
|
||||
@@ -162,8 +162,10 @@ void DevCalib::ExtrusionCalibSetParse(const json & jj){
|
||||
int slot_id = tray_ams_slot_map.find(tray_id) != tray_ams_slot_map.end() ? tray_ams_slot_map[tray_id].second : -1;
|
||||
|
||||
if(tray_id == VIRTUAL_TRAY_MAIN_ID) {
|
||||
GetOwner()->vt_slot[MAIN_EXTRUDER_ID].k = jj.value("k_value", GetOwner()->vt_slot[MAIN_EXTRUDER_ID].k);
|
||||
GetOwner()->vt_slot[MAIN_EXTRUDER_ID].n = jj.value("n_value", GetOwner()->vt_slot[MAIN_EXTRUDER_ID].n);
|
||||
if (GetOwner()->vt_slot.size() > MAIN_EXTRUDER_ID) {
|
||||
GetOwner()->vt_slot[MAIN_EXTRUDER_ID].k = jj.value("k_value", GetOwner()->vt_slot[MAIN_EXTRUDER_ID].k);
|
||||
GetOwner()->vt_slot[MAIN_EXTRUDER_ID].n = jj.value("n_value", GetOwner()->vt_slot[MAIN_EXTRUDER_ID].n);
|
||||
}
|
||||
}else{
|
||||
auto tray_item = GetOwner()->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id));
|
||||
if (tray_item) {
|
||||
|
||||
@@ -849,154 +849,4 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
}
|
||||
}
|
||||
|
||||
static DevAms::AmsType ams_type_from_string(const std::string& s)
|
||||
{
|
||||
if (s == "ams_lite" || s == "ams-lite") return DevAms::AMS_LITE;
|
||||
if (s == "n3f") return DevAms::N3F;
|
||||
if (s == "n3s") return DevAms::N3S;
|
||||
return DevAms::AMS; // default
|
||||
}
|
||||
|
||||
void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* obj, DevFilaSystem* system)
|
||||
{
|
||||
if (!system || !data.is_object())
|
||||
return;
|
||||
|
||||
// --- AMS units ---
|
||||
if (data.contains("units") && data["units"].is_array())
|
||||
{
|
||||
std::set<std::string> seen_units;
|
||||
|
||||
for (const auto& u : data["units"])
|
||||
{
|
||||
if (!u.is_object() || !u.contains("id"))
|
||||
continue;
|
||||
const std::string ams_id = u.value("id", std::string());
|
||||
if (ams_id.empty())
|
||||
continue;
|
||||
seen_units.insert(ams_id);
|
||||
|
||||
const int ext_id = u.value("extruder", MAIN_EXTRUDER_ID);
|
||||
const DevAms::AmsType type = ams_type_from_string(u.value("type", std::string("ams")));
|
||||
|
||||
DevAms* ams = nullptr;
|
||||
auto it = system->amsList.find(ams_id);
|
||||
if (it == system->amsList.end())
|
||||
{
|
||||
ams = new DevAms(ams_id, ext_id, type);
|
||||
system->amsList.insert(std::make_pair(ams_id, ams));
|
||||
}
|
||||
else
|
||||
{
|
||||
ams = it->second;
|
||||
ams->m_ext_id = ext_id;
|
||||
ams->SetAmsType(type);
|
||||
}
|
||||
|
||||
ams->m_exist = true;
|
||||
ams->m_current_temperature = u.value("temperature", (float) INVALID_AMS_TEMPERATURE);
|
||||
ams->m_humidity_percent = u.value("humidity_percent", -1);
|
||||
ams->m_left_dry_time = u.value("dry_time_min", 0);
|
||||
|
||||
// --- slots / trays ---
|
||||
std::set<std::string> seen_slots;
|
||||
if (u.contains("slots") && u["slots"].is_array())
|
||||
{
|
||||
for (const auto& s : u["slots"])
|
||||
{
|
||||
if (!s.is_object())
|
||||
continue;
|
||||
const std::string tray_id = std::to_string(s.value("index", -1));
|
||||
seen_slots.insert(tray_id);
|
||||
|
||||
DevAmsTray* tray = nullptr;
|
||||
auto tit = ams->m_trays.find(tray_id);
|
||||
if (tit == ams->m_trays.end())
|
||||
{
|
||||
tray = new DevAmsTray(tray_id);
|
||||
ams->m_trays.insert(std::make_pair(tray_id, tray));
|
||||
}
|
||||
else
|
||||
{
|
||||
tray = tit->second;
|
||||
}
|
||||
|
||||
tray->is_exists = s.value("loaded", false);
|
||||
tray->m_fila_type = s.value("material", std::string());
|
||||
tray->setting_id = s.value("preset_id", std::string());
|
||||
tray->UpdateColorFromStr(s.value("color", std::string()));
|
||||
tray->nozzle_temp_min = std::to_string(s.value("nozzle_temp_min", 0));
|
||||
tray->nozzle_temp_max = std::to_string(s.value("nozzle_temp_max", 0));
|
||||
tray->remain = s.value("remain_percent", -1);
|
||||
tray->k = s.value("k", 0.0f);
|
||||
if (s.contains("diameter_mm") && s["diameter_mm"].is_number())
|
||||
tray->diameter = std::to_string(s["diameter_mm"].get<double>());
|
||||
if (s.contains("weight_g") && s["weight_g"].is_number())
|
||||
tray->weight = std::to_string(s["weight_g"].get<int>());
|
||||
|
||||
tray->cols.clear();
|
||||
if (s.contains("colors") && s["colors"].is_array())
|
||||
{
|
||||
for (const auto& c : s["colors"])
|
||||
if (c.is_string())
|
||||
tray->cols.push_back(c.get<std::string>());
|
||||
}
|
||||
tray->ctype = tray->cols.size() > 1 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// prune trays no longer reported
|
||||
for (auto tit = ams->m_trays.begin(); tit != ams->m_trays.end();)
|
||||
{
|
||||
if (seen_slots.count(tit->first) == 0)
|
||||
{
|
||||
delete tit->second;
|
||||
tit = ams->m_trays.erase(tit);
|
||||
}
|
||||
else
|
||||
{
|
||||
++tit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prune units no longer reported
|
||||
for (auto it = system->amsList.begin(); it != system->amsList.end();)
|
||||
{
|
||||
if (seen_units.count(it->first) == 0)
|
||||
{
|
||||
delete it->second;
|
||||
it = system->amsList.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- external / direct spools -> obj->vt_slot ---
|
||||
// extruder 0 -> main virtual slot, extruder >0 -> deputy.
|
||||
if (obj && data.contains("external") && data["external"].is_array())
|
||||
{
|
||||
obj->vt_slot.clear();
|
||||
for (const auto& e : data["external"])
|
||||
{
|
||||
if (!e.is_object())
|
||||
continue;
|
||||
const int ext = e.value("extruder", MAIN_EXTRUDER_ID);
|
||||
const int vt_id = (ext == MAIN_EXTRUDER_ID) ? VIRTUAL_TRAY_MAIN_ID : VIRTUAL_TRAY_DEPUTY_ID;
|
||||
DevAmsTray tray(std::to_string(vt_id));
|
||||
tray.is_exists = e.value("loaded", false);
|
||||
tray.m_fila_type = e.value("material", std::string());
|
||||
tray.setting_id = e.value("preset_id", std::string());
|
||||
tray.UpdateColorFromStr(e.value("color", std::string()));
|
||||
tray.nozzle_temp_min = std::to_string(e.value("nozzle_temp_min", 0));
|
||||
tray.nozzle_temp_max = std::to_string(e.value("nozzle_temp_max", 0));
|
||||
tray.remain = e.value("remain_percent", -1);
|
||||
obj->vt_slot.push_back(tray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -415,8 +415,6 @@ class DevFilaSystemParser
|
||||
{
|
||||
public:
|
||||
static void ParseV1_0(const json& print_json, MachineObject* obj, DevFilaSystem* system, bool key_field_only);
|
||||
|
||||
static void ParseAgentFilament(const json& data, MachineObject* obj, DevFilaSystem* system);
|
||||
};
|
||||
|
||||
struct DevFilamentDryingPreset
|
||||
|
||||
@@ -536,6 +536,23 @@ PrinterSeries MachineObject::get_printer_series() const
|
||||
return PrinterSeries::SERIES_P1P;
|
||||
}
|
||||
|
||||
bool MachineObject::is_bbl_agent() const
|
||||
{
|
||||
// Empty id means "unattributed", treated as BBL to preserve the RFID lock.
|
||||
return printer_agent_id == BBL_PRINTER_AGENT_ID || printer_agent_id.empty();
|
||||
}
|
||||
|
||||
bool MachineObject::ams_filament_ack_failed(const nlohmann::json& jj, std::string& reason)
|
||||
{
|
||||
reason.clear();
|
||||
if (!jj.contains("result") || !jj["result"].is_string() ||
|
||||
jj["result"].get<std::string>() == "success")
|
||||
return false;
|
||||
if (jj.contains("reason") && jj["reason"].is_string())
|
||||
reason = jj["reason"].get<std::string>();
|
||||
return true;
|
||||
}
|
||||
|
||||
PrinterArch MachineObject::get_printer_arch() const
|
||||
{
|
||||
return DevPrinterConfigUtil::get_printer_arch(printer_type);
|
||||
@@ -619,8 +636,7 @@ MachineObject::MachineObject(DeviceManager* manager, NetworkAgent* agent, std::s
|
||||
has_ipcam = true; // default true
|
||||
|
||||
|
||||
auto vslot = DevAmsTray(std::to_string(VIRTUAL_TRAY_MAIN_ID));
|
||||
vt_slot.push_back(vslot);
|
||||
// vt_slot is seeded by reset() above; do not push a second copy here.
|
||||
|
||||
{
|
||||
m_lamp = new DevLamp(this);
|
||||
@@ -747,6 +763,7 @@ 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) {
|
||||
if (vt_slot.empty()) return nullptr;
|
||||
return &vt_slot[0];
|
||||
}
|
||||
|
||||
@@ -2568,13 +2585,11 @@ 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);
|
||||
}
|
||||
}
|
||||
// Restore the ctor seed rather than only resetting what is left: an
|
||||
// authoritative vir_slot:[] erases every tray, so reset must not leave
|
||||
// vt_slot permanently empty.
|
||||
vt_slot.clear();
|
||||
vt_slot.push_back(DevAmsTray(std::to_string(VIRTUAL_TRAY_MAIN_ID)));
|
||||
// why: reset reuses MachineObject, so release its lazy subtask
|
||||
// before dropping the pointer to prevent reconnect leaks.
|
||||
if (subtask_) {
|
||||
@@ -2749,6 +2764,7 @@ int MachineObject::local_publish_json(std::string json_str, int qos, int flag)
|
||||
std::string MachineObject::setting_id_to_type(std::string setting_id, std::string tray_type)
|
||||
{
|
||||
std::string type;
|
||||
if (wxTheApp == nullptr) return tray_type;
|
||||
PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle;
|
||||
if (preset_bundle) {
|
||||
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||
@@ -3968,8 +3984,12 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
catch (...) {
|
||||
;
|
||||
}
|
||||
update_printer_preset_name();
|
||||
update_filament_list();
|
||||
// Both read the GUI preset bundle; skip them in a headless
|
||||
// process (unit tests), where wxTheApp is null.
|
||||
if (wxTheApp != nullptr) {
|
||||
update_printer_preset_name();
|
||||
update_filament_list();
|
||||
}
|
||||
if (jj.contains("ams")) {
|
||||
DevFilaSystemParser::ParseV1_0(jj, this, m_fila_system.get(), key_field_only);
|
||||
}
|
||||
@@ -3979,6 +3999,19 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
try {
|
||||
if (jj.contains("vir_slot") && jj["vir_slot"].is_array()) {
|
||||
|
||||
if (jj["vir_slot"].empty()) {
|
||||
// Authoritative empty: OrcaSonar pushes [] when the
|
||||
// topology is known but has no slots; clear stale trays.
|
||||
vt_slot.clear();
|
||||
ams_support_virtual_tray = false;
|
||||
}
|
||||
else {
|
||||
// A keyed, populated vir_slot means virtual trays
|
||||
// are supported; without this a prior clear left
|
||||
// the flag false and the trays were ignored.
|
||||
ams_support_virtual_tray = true;
|
||||
}
|
||||
|
||||
for (auto it = jj["vir_slot"].begin(); it != jj["vir_slot"].end(); it++) {
|
||||
auto vslot = parse_vt_tray(it.value().get<json>());
|
||||
|
||||
@@ -3992,11 +4025,13 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
}
|
||||
}
|
||||
else if (vslot.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) {
|
||||
auto it = std::next(vt_slot.begin(), 1);
|
||||
if (it != vt_slot.end()) {
|
||||
// vt_slot[1] is the deputy. Only the main
|
||||
// branch creates index 0, so an orphan
|
||||
// deputy (no main) is dropped, not indexed.
|
||||
if (vt_slot.size() > 1) {
|
||||
vt_slot[1] = vslot;
|
||||
}
|
||||
else {
|
||||
else if (vt_slot.size() == 1) {
|
||||
vt_slot.push_back(vslot);
|
||||
}
|
||||
}
|
||||
@@ -4004,6 +4039,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
|
||||
}
|
||||
else if (jj.contains("vt_tray")) {
|
||||
ams_support_virtual_tray = true;
|
||||
auto main_slot = parse_vt_tray(jj["vt_tray"].get<json>());
|
||||
main_slot.id = std::to_string(VIRTUAL_TRAY_MAIN_ID);
|
||||
|
||||
@@ -4016,9 +4052,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
vt_slot.push_back(main_slot);
|
||||
}
|
||||
}
|
||||
else {
|
||||
ams_support_virtual_tray = false;
|
||||
}
|
||||
// No virtual-tray key at all: leave the flag as-is. An
|
||||
// authoritative clear is an explicit vir_slot: [], and
|
||||
// incremental frames must not hide existing trays.
|
||||
}
|
||||
catch (...) {
|
||||
;
|
||||
@@ -4057,13 +4093,28 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
// BBS trigger ams UI update
|
||||
ams_version = -1;
|
||||
|
||||
if (jj["ams_id"].is_number()) {
|
||||
// OPCP acks a rejected/failed write with result "fail" and an
|
||||
// errno (-19 validation, -4 persist). Without this the failure
|
||||
// looked like success and left the old spool on screen.
|
||||
std::string ack_reason;
|
||||
const bool ack_failed = ams_filament_ack_failed(jj, ack_reason);
|
||||
if (ack_failed) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "ams_filament_setting rejected: errno="
|
||||
<< (jj.contains("errno") ? jj["errno"].dump() : "?")
|
||||
<< ", reason=" << ack_reason;
|
||||
wxString text = _L("Failed to set AMS filament");
|
||||
if (!ack_reason.empty())
|
||||
text += wxString::FromUTF8(": ") + wxString::FromUTF8(ack_reason);
|
||||
GUI::wxGetApp().push_notification(this, text);
|
||||
}
|
||||
|
||||
if (!ack_failed && jj["ams_id"].is_number()) {
|
||||
int ams_id = jj["ams_id"].get<int>();
|
||||
int tray_id = 0;
|
||||
if (jj.contains("tray_id")) {
|
||||
tray_id = jj["tray_id"].get<int>();
|
||||
}
|
||||
if (ams_id == 255 && tray_id == VIRTUAL_TRAY_MAIN_ID) {
|
||||
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>());
|
||||
@@ -4076,7 +4127,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
} else {
|
||||
auto ams = m_fila_system->GetAmsById(std::to_string(ams_id));
|
||||
if (ams) {
|
||||
tray_id = jj["tray_id"].get<int>();
|
||||
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";
|
||||
@@ -4216,7 +4266,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
;
|
||||
}
|
||||
}
|
||||
if (tray_id == VIRTUAL_TRAY_MAIN_ID) {
|
||||
if (tray_id == VIRTUAL_TRAY_MAIN_ID && !vt_slot.empty()) {
|
||||
if (jj.contains("k_value"))
|
||||
vt_slot[0].k = jj["k_value"].get<float>();
|
||||
if (jj.contains("n_coef"))
|
||||
@@ -5630,7 +5680,9 @@ int MachineObject::get_flag_bits(int num, int start, int count, int base) const
|
||||
|
||||
void MachineObject::update_filament_list()
|
||||
{
|
||||
if (wxTheApp == nullptr) return;
|
||||
PresetBundle *preset_bundle = Slic3r::GUI::wxGetApp().preset_bundle;
|
||||
if (preset_bundle == nullptr) return;
|
||||
|
||||
// custom filament
|
||||
typedef std::map<std::string, std::pair<int, int>> map_pair;
|
||||
@@ -5700,6 +5752,7 @@ void MachineObject::update_filament_list()
|
||||
void MachineObject::update_printer_preset_name()
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << "start update preset_name";
|
||||
if (wxTheApp == nullptr) return;
|
||||
PresetBundle * preset_bundle = Slic3r::GUI::wxGetApp().preset_bundle;
|
||||
if (!preset_bundle) return;
|
||||
auto printer_model = DevPrinterConfigUtil::get_printer_display_name(this->printer_type);
|
||||
|
||||
@@ -161,6 +161,13 @@ public:
|
||||
void set_agent(NetworkAgent* agent) { m_agent = agent; }
|
||||
NetworkAgent* get_agent() const { return m_agent; } // Orca: needed by DeviceCore modules (DevAxisCtrl)
|
||||
|
||||
// Orca: true only for devices managed by Bambu's own agent, where a
|
||||
// non-zero tag_uid is an RFID lock. Other agents report it as metadata.
|
||||
bool is_bbl_agent() const;
|
||||
|
||||
// Orca: an OPCP failure ack (result != "success"). Fills reason when present.
|
||||
static bool ams_filament_ack_failed(const nlohmann::json& jj, std::string& reason);
|
||||
|
||||
// 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
|
||||
// parse in DeviceManager.cpp remains authoritative. Do not wire DevUpgrade naively: its
|
||||
|
||||
@@ -2325,8 +2325,13 @@ void GUI_App::init_networking_callbacks()
|
||||
|
||||
if (MachineObject* obj = m_device_manager->get_my_machine(dev_id)) {
|
||||
obj->parse_json("lan", msg);
|
||||
// Orca: skip it if it doesn't support subscription based filament sync
|
||||
if (this->m_device_manager->get_selected_machine() == obj &&
|
||||
// Orca: skip it if it doesn't support subscription based filament
|
||||
// sync, and skip frames that carry no filament state at all
|
||||
// (progress, temps, hms) so the AMS list is not rebuilt every push.
|
||||
const bool has_filament_state = msg.find("\"vir_slot\"") != std::string::npos ||
|
||||
msg.find("\"ams\":") != std::string::npos;
|
||||
if (has_filament_state &&
|
||||
this->m_device_manager->get_selected_machine() == obj &&
|
||||
m_agent->get_filament_sync_mode() == FilamentSyncMode::subscription) {
|
||||
GUI::wxGetApp().sidebar().load_ams_list(obj);
|
||||
}
|
||||
|
||||
@@ -5703,10 +5703,20 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
|
||||
}
|
||||
|
||||
auto build_tray_config = [](DevAmsTray const &tray, std::string const &name, std::string ams_id, std::string slot_id) {
|
||||
// Type-only agents (OrcaSonar/Qidi Box) report the material but not the
|
||||
// slicer's preset id. Resolve it from the type so AMS sync can match a
|
||||
// preset instead of skipping every tray as "unknown".
|
||||
std::string filament_id = tray.setting_id;
|
||||
if (filament_id.empty() && !tray.m_fila_type.empty() && tray.is_exists) {
|
||||
// Strict lookup: an unmatched material leaves filament_id empty so
|
||||
// the sync flow reports it as unknown instead of binding Generic PLA.
|
||||
if (auto *bundle = wxGetApp().preset_bundle)
|
||||
bundle->filaments.filament_id_by_type(tray.m_fila_type, filament_id);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("build_filament_ams_list: name %1% setting_id %2% type %3% color %4%")
|
||||
% name % tray.setting_id % tray.m_fila_type % tray.color;
|
||||
% name % filament_id % tray.m_fila_type % tray.color;
|
||||
DynamicPrintConfig tray_config;
|
||||
tray_config.set_key_value("filament_id", new ConfigOptionStrings{tray.setting_id});
|
||||
tray_config.set_key_value("filament_id", new ConfigOptionStrings{filament_id});
|
||||
tray_config.set_key_value("tag_uid", new ConfigOptionStrings{tray.tag_uid});
|
||||
tray_config.set_key_value("ams_id", new ConfigOptionStrings{ams_id});
|
||||
tray_config.set_key_value("slot_id", new ConfigOptionStrings{slot_id});
|
||||
@@ -5719,7 +5729,7 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
|
||||
tray_config.set_key_value("filament_slot_placeholder", new ConfigOptionBools{tray.is_slot_placeholder});
|
||||
std::optional<FilamentBaseInfo> info;
|
||||
if (wxGetApp().preset_bundle) {
|
||||
info = wxGetApp().preset_bundle->get_filament_by_filament_id(tray.setting_id);
|
||||
info = wxGetApp().preset_bundle->get_filament_by_filament_id(filament_id);
|
||||
}
|
||||
tray_config.set_key_value("filament_is_support", new ConfigOptionBools{ info.has_value() ? info->is_support : false});
|
||||
for (int i = 0; i < tray.cols.size(); ++i) {
|
||||
|
||||
@@ -4350,12 +4350,12 @@ void StatusPanel::on_ams_load_curr()
|
||||
vt_slot_idx = 1;
|
||||
}
|
||||
|
||||
if (vt_slot_idx < 0 || vt_slot_idx >= (int)obj->vt_slot.size()) return;
|
||||
|
||||
int old_temp = -1;
|
||||
int new_temp = -1;
|
||||
DevAmsTray* curr_tray = &obj->vt_slot[vt_slot_idx];
|
||||
|
||||
if (!curr_tray) return;
|
||||
|
||||
try {
|
||||
if (!curr_tray->nozzle_temp_max.empty() && !curr_tray->nozzle_temp_min.empty())
|
||||
old_temp = (atoi(curr_tray->nozzle_temp_min.c_str()) + atoi(curr_tray->nozzle_temp_max.c_str())) / 2;
|
||||
@@ -4602,7 +4602,11 @@ void StatusPanel::on_filament_edit(wxCommandEvent &event)
|
||||
m_filament_setting_dlg->set_colors(cols);
|
||||
}
|
||||
|
||||
m_filament_setting_dlg->m_is_third = !DevFilaSystem::IsBBL_Filament(tray->tag_uid);
|
||||
// A non-zero tag_uid is an RFID lock only on Bambu's own agent.
|
||||
// Agent-managed printers (Orca/Qidi/Snapmaker) send it as
|
||||
// metadata, and the spec says it must never be interpreted — so
|
||||
// the tag alone must not make their trays read-only.
|
||||
m_filament_setting_dlg->m_is_third = !obj->is_bbl_agent() || !DevFilaSystem::IsBBL_Filament(tray->tag_uid);
|
||||
if (!m_filament_setting_dlg->m_is_third)
|
||||
{
|
||||
sn_number = tray->uuid;
|
||||
@@ -4642,6 +4646,8 @@ void StatusPanel::on_ext_spool_edit(wxCommandEvent &event)
|
||||
m_filament_setting_dlg->slot_id = slot_id;
|
||||
int nozzle_index = ams_id == VIRTUAL_TRAY_MAIN_ID ? 0 : 1;
|
||||
|
||||
if (nozzle_index < 0 || nozzle_index >= (int)obj->vt_slot.size()) return;
|
||||
|
||||
try {
|
||||
std::string sn_number;
|
||||
std::string filament;
|
||||
@@ -4668,7 +4674,7 @@ void StatusPanel::on_ext_spool_edit(wxCommandEvent &event)
|
||||
m_filament_setting_dlg->set_colors(cols);
|
||||
}
|
||||
|
||||
m_filament_setting_dlg->m_is_third = !DevFilaSystem::IsBBL_Filament(obj->vt_slot[nozzle_index].tag_uid);
|
||||
m_filament_setting_dlg->m_is_third = !obj->is_bbl_agent() || !DevFilaSystem::IsBBL_Filament(obj->vt_slot[nozzle_index].tag_uid);
|
||||
if (!m_filament_setting_dlg->m_is_third) {
|
||||
sn_number = obj->vt_slot[nozzle_index].uuid;
|
||||
filament = obj->vt_slot[nozzle_index].sub_brands;
|
||||
|
||||
@@ -378,29 +378,19 @@ nlohmann::json build_bbl_ams_json(const std::vector<AmsTrayData>& trays,
|
||||
|
||||
// --- Removal/absence state and op capability ---------------------------------
|
||||
|
||||
// Last ams_count rendered per device: clear_ams_payload_for_device walks the
|
||||
// same unit set to mark them all absent.
|
||||
static std::mutex g_ams_state_mutex;
|
||||
static std::map<std::string, int> g_ams_last_count;
|
||||
|
||||
// One device's declaration from its get_capabilities reply. ops_known separates
|
||||
// "no reply yet" (never gate) from "answered without ops" (gate every write).
|
||||
struct AmsDeviceCaps
|
||||
{
|
||||
std::vector<std::string> ops;
|
||||
bool ops_known = false;
|
||||
bool has_ams = false;
|
||||
bool ops_known = false;
|
||||
bool has_ams = false;
|
||||
bool filament_slots = false;
|
||||
};
|
||||
static std::map<std::string, AmsDeviceCaps> g_ams_caps;
|
||||
|
||||
static void remember_ams_count(const std::string& dev_id, int ams_count)
|
||||
{
|
||||
if (dev_id.empty() || ams_count <= 0)
|
||||
return;
|
||||
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
|
||||
g_ams_last_count[dev_id] = std::max(g_ams_last_count[dev_id], ams_count);
|
||||
}
|
||||
|
||||
void register_ams_ops(const std::string& dev_id, const std::vector<std::string>& ops)
|
||||
{
|
||||
if (dev_id.empty())
|
||||
@@ -435,6 +425,29 @@ bool has_ams_capability(const std::string& dev_id)
|
||||
return it != g_ams_caps.end() && it->second.has_ams;
|
||||
}
|
||||
|
||||
void register_filament_slots(const std::string& dev_id, bool has_slots)
|
||||
{
|
||||
if (dev_id.empty())
|
||||
return;
|
||||
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
|
||||
g_ams_caps[dev_id].filament_slots = has_slots;
|
||||
}
|
||||
|
||||
bool has_filament_slots(const std::string& dev_id)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
|
||||
auto it = g_ams_caps.find(dev_id);
|
||||
return it != g_ams_caps.end() && it->second.filament_slots;
|
||||
}
|
||||
|
||||
void clear_ams_caps(const std::string& dev_id)
|
||||
{
|
||||
if (dev_id.empty())
|
||||
return;
|
||||
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
|
||||
g_ams_caps.erase(dev_id);
|
||||
}
|
||||
|
||||
void build_ams_payload_for_device(const std::string& dev_id,
|
||||
const std::optional<std::string>& printer_type,
|
||||
int ams_count,
|
||||
@@ -443,7 +456,6 @@ void build_ams_payload_for_device(const std::string& dev_id,
|
||||
const QueueOnMainFn& queue_fn,
|
||||
const TrayInfoResolver& vendor_resolver)
|
||||
{
|
||||
remember_ams_count(dev_id, ams_count);
|
||||
// A caller on the GUI thread must mutate DeviceManager inline: invoking
|
||||
// queue_fn (CallAfter) would defer the work until after the caller has
|
||||
// already read DevFilaSystem. Background callers route through queue_fn.
|
||||
@@ -511,58 +523,5 @@ void build_ams_payload_for_device(const std::string& dev_id,
|
||||
}
|
||||
}
|
||||
|
||||
void clear_ams_payload_for_device(const std::string& dev_id, const QueueOnMainFn& queue_fn)
|
||||
{
|
||||
int count = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
|
||||
auto it = g_ams_last_count.find(dev_id);
|
||||
if (it != g_ams_last_count.end())
|
||||
count = it->second;
|
||||
g_ams_last_count[dev_id] = 0;
|
||||
}
|
||||
if (count == 0)
|
||||
return; // nothing was ever rendered: nothing to clear
|
||||
|
||||
const bool on_main = wxIsMainThread();
|
||||
auto apply = [dev_id, count]() {
|
||||
auto* dev_manager = GUI::wxGetApp().getDeviceManager();
|
||||
if (!dev_manager)
|
||||
return;
|
||||
MachineObject* obj = dev_manager->get_my_machine(dev_id);
|
||||
if (!obj)
|
||||
return;
|
||||
|
||||
// All units present-but-empty: exist bits 0 marks them absent while
|
||||
// placeholder trays flush stale type/color data out of DevFilaSystem.
|
||||
nlohmann::json units = nlohmann::json::array();
|
||||
for (int ams_id = 0; ams_id < count; ++ams_id) {
|
||||
nlohmann::json trays = nlohmann::json::array();
|
||||
for (int slot_id = 0; slot_id < 4; ++slot_id) {
|
||||
trays.push_back(nlohmann::json{
|
||||
{"id", std::to_string(slot_id)},
|
||||
{"tag_uid", "0000000000000000"},
|
||||
{"tray_info_idx", ""},
|
||||
{"tray_type", ""},
|
||||
{"tray_color", "00000000"},
|
||||
{"tray_slot_placeholder", "1"},
|
||||
});
|
||||
}
|
||||
units.push_back(nlohmann::json{{"id", std::to_string(ams_id)}, {"info", "0002"}, {"tray", trays}});
|
||||
}
|
||||
nlohmann::json ams_json;
|
||||
ams_json["ams"] = units;
|
||||
ams_json["ams_exist_bits"] = "0";
|
||||
ams_json["tray_exist_bits"] = "0";
|
||||
nlohmann::json print_json;
|
||||
print_json["ams"] = ams_json;
|
||||
DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem().get(), false);
|
||||
BOOST_LOG_TRIVIAL(info) << "AmsPayload: cleared " << count << " AMS units for " << dev_id;
|
||||
};
|
||||
if (queue_fn && !on_main)
|
||||
queue_fn(apply);
|
||||
else
|
||||
apply();
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -90,11 +90,6 @@ void build_ams_payload_for_device(const std::string& dev_id,
|
||||
const QueueOnMainFn& queue_fn,
|
||||
const TrayInfoResolver& vendor_resolver = {});
|
||||
|
||||
// Clear the device's AMS view: render every previously-seen unit as absent
|
||||
// with placeholder trays so a removed/absent material system never leaves
|
||||
// stale filament data behind (lane_data read as authoritative empty).
|
||||
void clear_ams_payload_for_device(const std::string& dev_id, const QueueOnMainFn& queue_fn);
|
||||
|
||||
// Process-wide canonical AMS write capability (OrcaSonar REQ-STS-008), parsed
|
||||
// from the info.get_capabilities reply. A device with no record (no reply yet;
|
||||
// non-OrcaSonar agents never register) reports every op supported: gating only
|
||||
@@ -109,6 +104,17 @@ bool ams_op_supported(const std::string& dev_id, const std::string& op);
|
||||
void register_ams_capability(const std::string& dev_id, bool has_ams);
|
||||
bool has_ams_capability(const std::string& dev_id);
|
||||
|
||||
// Whether the device exposes the filament-slot model, from the
|
||||
// get_capabilities reply's protocol.features.filament_slots. The slot model is
|
||||
// connector state, independent of fms: a printer with no material hardware
|
||||
// still has slots, so this alone enables filament sync (REQ-FMS-001).
|
||||
void register_filament_slots(const std::string& dev_id, bool has_slots);
|
||||
bool has_filament_slots(const std::string& dev_id);
|
||||
|
||||
// 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);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
|
||||
@@ -658,10 +658,10 @@ int MoonrakerPrinterAgent::safe_array_int(const nlohmann::json& arr, int idx)
|
||||
// Fetch filament info from moonraker database
|
||||
bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index)
|
||||
{
|
||||
// Shared with OrcaPrinterAgent's lane_data read; only the synced outcome
|
||||
// matters here, since a missing namespace or an empty one both fall through
|
||||
// to the Happy Hare query. tray_info_idx is resolved later, on the main
|
||||
// thread, inside build_ams_payload_for_device.
|
||||
// Only the synced outcome matters here: a missing namespace, an empty one,
|
||||
// a transport error and an unreadable body all fall through to the Happy
|
||||
// Hare query. tray_info_idx is resolved later, on the main thread, inside
|
||||
// build_ams_payload_for_device.
|
||||
return read_moonraker_lane_data(device_info.base_url, device_info.api_key, trays, max_lane_index) ==
|
||||
LaneDataFetch::synced;
|
||||
}
|
||||
|
||||
@@ -444,15 +444,6 @@ OrcaPrinterAgent::~OrcaPrinterAgent()
|
||||
start_discovery(false, false);
|
||||
++m_lan_generation; // fence any late worker callback
|
||||
++m_cloud_generation;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
m_shutting_down = true; // workers stop arming new HTTP fetches
|
||||
}
|
||||
|
||||
// Drain the detached filament-refresh workers: the flag and generation bump
|
||||
// above end their loops, so this waits at most one in-flight HTTP fetch.
|
||||
while (m_filament_in_flight.load() > 0)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
|
||||
// Drop the cloud status callback before anything else: it holds `this`, and the
|
||||
// cloud agent outlives the printer agent (NetworkAgent::set_printer_agent swaps
|
||||
@@ -633,6 +624,9 @@ void OrcaPrinterAgent::register_ams_capabilities(const std::string& dev_id, cons
|
||||
if (info_it == envelope.end() || !info_it->is_object() || info_it->value("command", "") != "get_capabilities")
|
||||
return;
|
||||
const auto caps_it = info_it->find("capabilities");
|
||||
// A reply that is not a complete capabilities answer is ignored: treating a
|
||||
// transient malformed reply as "no capabilities" would gate every AMS write
|
||||
// until a good one arrives. Stale state is cleared on disconnect instead.
|
||||
if (caps_it == info_it->end() || !caps_it->is_object())
|
||||
return;
|
||||
const auto proto_it = caps_it->find("protocol");
|
||||
@@ -667,15 +661,20 @@ void OrcaPrinterAgent::register_ams_capabilities(const std::string& dev_id, cons
|
||||
if (!fms_known)
|
||||
has_ams = !ops.empty();
|
||||
register_ams_capability(dev_id, has_ams);
|
||||
|
||||
// features.filament_slots is the connector's slot model (REQ-FMS-001),
|
||||
// independent of fms: a printer with no material hardware still has slots.
|
||||
bool has_slots = false;
|
||||
if (features_it != proto_it->end() && features_it->is_object()) {
|
||||
const auto slots_it = features_it->find("filament_slots");
|
||||
if (slots_it != features_it->end() && slots_it->is_boolean())
|
||||
has_slots = slots_it->get<bool>();
|
||||
}
|
||||
register_filament_slots(dev_id, has_slots);
|
||||
}
|
||||
|
||||
void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local)
|
||||
{
|
||||
// Subscription doorbell, on the raw payload before the UI marshal so the
|
||||
// (possibly blocking) lane_data refresh never queues behind it.
|
||||
if (local && filament_doorbell_needed(dev_id, payload))
|
||||
request_filament_refresh(dev_id);
|
||||
|
||||
parse_ipcam_info(dev_id, payload);
|
||||
register_ams_capabilities(dev_id, payload);
|
||||
std::string merged_payload = merge_capabilities(dev_id, payload);
|
||||
@@ -715,19 +714,6 @@ void OrcaPrinterAgent::dispatch_local_connect(int state, const std::string& dev_
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection callback state=" << state << " dev_id=" << dev_id << " message=" << message
|
||||
<< " callback=" << (callback ? "set" : "null") << " queue_on_main=" << (queue ? "set" : "null");
|
||||
|
||||
// Eager filament sync on every (re)connect, like the Moonraker agents: the
|
||||
// cached DevFilaSystem may predate the drop. The doorbell cache resets too,
|
||||
// so the post-connect frame re-arms if the lane content moved meanwhile.
|
||||
// Runs before the callback check so it also fires when no GUI listener is
|
||||
// installed yet.
|
||||
if (state == ConnectStatusOk) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
m_material_hash.clear();
|
||||
}
|
||||
request_filament_refresh(dev_id);
|
||||
}
|
||||
|
||||
if (!callback)
|
||||
return;
|
||||
|
||||
@@ -803,9 +789,12 @@ int OrcaPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tr
|
||||
nlohmann::json j;
|
||||
j["print"]["command"] = "ams_change_filament";
|
||||
j["print"]["sequence_id"] = std::to_string(sequence_id);
|
||||
// tray_id here is the flat global lane (DevFilaSystem slot index).
|
||||
// tray_id is the BBL tray id (ams_id*4 + tray). Send the coordinates, not a
|
||||
// fabricated flat lane: the server's one resolver maps wide and sparse
|
||||
// boxes correctly (REQ-STS-008 §7.8).
|
||||
j["print"]["selector"] = "lane";
|
||||
j["print"]["lane"] = tray_number;
|
||||
j["print"]["ams_id"] = tray_number / 4;
|
||||
j["print"]["slot_id"] = tray_number % 4;
|
||||
return route_send(lan_mode, dev_id, j.dump());
|
||||
}
|
||||
|
||||
@@ -870,148 +859,12 @@ FilamentSyncMode OrcaPrinterAgent::get_filament_sync_mode() const
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
dev_id = (m_current_connection == LAN) ? m_lan_dev_id : selected_machine;
|
||||
}
|
||||
if (!dev_id.empty() && has_ams_capability(dev_id)) {
|
||||
if (!dev_id.empty() && (has_ams_capability(dev_id) || has_filament_slots(dev_id))) {
|
||||
return FilamentSyncMode::subscription;
|
||||
}
|
||||
return FilamentSyncMode::none;
|
||||
}
|
||||
|
||||
// Read the lane_data projection once and apply the REQ-STS-007 tri-state to
|
||||
// DevFilaSystem. Only LaneDataState::error arms the retry latch: 404 means
|
||||
// "not knowable yet" (pre-bootstrap or acknowledged-unknown topology) and {}
|
||||
// means "authoritatively no lanes" — both are settled states, and a printer
|
||||
// without a material system must not turn into a per-frame 404 poll.
|
||||
OrcaPrinterAgent::LaneDataState OrcaPrinterAgent::fetch_lane_data(const std::string& dev_id)
|
||||
{
|
||||
std::string origin;
|
||||
QueueOnMainFn queue_fn;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
if (m_shutting_down || m_current_connection != LAN || m_lan_dev_id != dev_id)
|
||||
return LaneDataState::unknown;
|
||||
origin = m_lan_http_origin;
|
||||
queue_fn = queue_on_main_fn;
|
||||
}
|
||||
if (origin.empty())
|
||||
return LaneDataState::unknown;
|
||||
const std::string api_key = lan_api_key(origin);
|
||||
|
||||
// OrcaSonar serves the canonical topology's lane projection on its
|
||||
// Moonraker-compatible façade. Called on the refresh worker (subscription
|
||||
// mode), so the payload mutation is marshalled onto the main thread through
|
||||
// queue_fn; a GUI-thread caller reads DevFilaSystem inline.
|
||||
std::vector<AmsTrayData> trays;
|
||||
int max_lane_index = 0;
|
||||
switch (read_moonraker_lane_data(origin, api_key, trays, max_lane_index)) {
|
||||
case LaneDataFetch::synced:
|
||||
break;
|
||||
case LaneDataFetch::none:
|
||||
// Authoritative empty: flush stale trays so a removed AMS does not
|
||||
// linger in the device panel.
|
||||
clear_ams_payload_for_device(dev_id, queue_fn);
|
||||
return LaneDataState::none;
|
||||
case LaneDataFetch::unknown:
|
||||
// 404: not knowable yet (pre-bootstrap or acknowledged-unknown topology).
|
||||
// Never latched; the next doorbell or reconnect retries.
|
||||
return LaneDataState::unknown;
|
||||
case LaneDataFetch::error:
|
||||
return LaneDataState::error;
|
||||
}
|
||||
|
||||
// printer_type stays unset: push_status already carries the OrcaSonar printer
|
||||
// type, and overwriting it here would clear it. build_ams_payload_for_device
|
||||
// marshals the DevFilaSystem mutation through queue_fn when set.
|
||||
build_ams_payload_for_device(dev_id, std::nullopt, ams_count_for_lanes(max_lane_index), max_lane_index, trays, queue_fn);
|
||||
return LaneDataState::synced;
|
||||
}
|
||||
|
||||
// Subscription scheduler for filament sync. A single detached worker drains
|
||||
// m_filament_wanted; extra requests arriving while it runs fold into its loop, so
|
||||
// a burst of topology_state doorbells costs one extra fetch that picks up the
|
||||
// trailing change. A failed fetch does not busy-retry: m_filament_failed makes
|
||||
// the next inbound LAN frame request again (an idle printer is silent, but it
|
||||
// cannot have changed lanes either), and a reconnect re-primes via the
|
||||
// ConnectStatusOk path in dispatch_local_connect().
|
||||
void OrcaPrinterAgent::request_filament_refresh(const std::string& dev_id)
|
||||
{
|
||||
uint64_t gen;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
if (m_current_connection != LAN || m_lan_dev_id != dev_id)
|
||||
return;
|
||||
gen = m_lan_generation.load();
|
||||
m_filament_wanted = true;
|
||||
if (m_filament_working)
|
||||
return; // the running worker will take the flag
|
||||
m_filament_working = true;
|
||||
}
|
||||
|
||||
m_filament_in_flight.fetch_add(1, std::memory_order_relaxed);
|
||||
std::thread([this, dev_id, gen] {
|
||||
struct InFlightGuard
|
||||
{
|
||||
std::atomic<int>& counter;
|
||||
~InFlightGuard() { counter.fetch_sub(1, std::memory_order_relaxed); }
|
||||
} guard{m_filament_in_flight};
|
||||
|
||||
for (;;) {
|
||||
bool needed;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
needed = !m_shutting_down && m_filament_wanted && m_lan_generation.load() == gen && m_current_connection == LAN &&
|
||||
m_lan_dev_id == dev_id;
|
||||
if (needed)
|
||||
m_filament_wanted = false;
|
||||
else
|
||||
m_filament_working = false; // same critical section that saw no work: no lost wake-up
|
||||
}
|
||||
if (!needed)
|
||||
return;
|
||||
const auto state = fetch_lane_data(dev_id);
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
m_filament_failed = state == LaneDataState::error; // latch read by filament_doorbell_needed()
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
// A LAN frame is a refresh trigger when it carries an OrcaSonar material
|
||||
// change (a new print.topology_state.material_hash, spec REQ-STS-007 §7.7) or
|
||||
// when the last fetch errored and this frame is the retry beat. The substring
|
||||
// guard keeps the JSON parse off the steady temp-tick cadence, and hash
|
||||
// equality absorbs the tick's topology_state re-emissions.
|
||||
bool OrcaPrinterAgent::filament_doorbell_needed(const std::string& dev_id, const std::string& payload)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
if (m_current_connection != LAN || m_lan_dev_id != dev_id)
|
||||
return false;
|
||||
if (m_filament_failed)
|
||||
return true;
|
||||
}
|
||||
// The cheap guard is the §7.7 doorbell token itself, so the steady
|
||||
// temperature-only tick never reaches the JSON parse.
|
||||
if (payload.find("material_hash") == std::string::npos)
|
||||
return false;
|
||||
auto json = nlohmann::json::parse(payload, nullptr, false);
|
||||
if (json.is_discarded() || !json.is_object())
|
||||
return false;
|
||||
const auto print_it = json.find("print");
|
||||
if (print_it == json.end() || !print_it->is_object())
|
||||
return false;
|
||||
const auto topo_it = print_it->find("topology_state");
|
||||
if (topo_it == print_it->end() || !topo_it->is_object())
|
||||
return false;
|
||||
const auto hash_it = topo_it->find("material_hash");
|
||||
if (hash_it == topo_it->end() || !hash_it->is_string())
|
||||
return false;
|
||||
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
if (hash_it->get<std::string>() == m_material_hash)
|
||||
return false; // content unchanged: not a doorbell
|
||||
m_material_hash = hash_it->get<std::string>();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Moonraker's client bootstrap: /access/api_key hands the façade key to a
|
||||
// trusted source (the default LAN ranges include the slicer). Cache it per
|
||||
// connection generation. If the request is refused — a hardened trusted_clients
|
||||
@@ -1251,7 +1104,6 @@ int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, st
|
||||
previous_connection = m_current_connection;
|
||||
m_lan_dev_id = dev_id;
|
||||
m_lan_url = cfg.url;
|
||||
m_lan_http_origin = http_origin_from_lan_ws(cfg.url);
|
||||
m_lan_password = password; // access code; the façade key is bootstrapped lazily
|
||||
m_lan_api_key.clear();
|
||||
m_lan_api_key_gen = gen;
|
||||
@@ -1328,10 +1180,7 @@ int OrcaPrinterAgent::disconnect_printer()
|
||||
doomed = std::move(lan_mqtt_connection);
|
||||
prev_dev = m_lan_dev_id;
|
||||
m_lan_dev_id.clear();
|
||||
m_lan_http_origin.clear();
|
||||
m_lan_api_key.clear();
|
||||
m_filament_wanted = false; // a stale worker self-exits on the generation mismatch
|
||||
m_filament_failed = false;
|
||||
if (m_current_connection == LAN) {
|
||||
m_current_connection = NONE;
|
||||
m_camera_stream_mode = CameraStreamMode::none;
|
||||
@@ -1344,6 +1193,10 @@ int OrcaPrinterAgent::disconnect_printer()
|
||||
<< " connected=" << (doomed && doomed->is_connected() ? "yes" : "no")
|
||||
<< " transport=" << connection_type_name(previous_connection) << "->"
|
||||
<< connection_type_name(current_connection);
|
||||
// Drop the device's declared capabilities so a reconnect starts from "no
|
||||
// reply yet" instead of a stale declaration from the previous session.
|
||||
if (!prev_dev.empty())
|
||||
clear_ams_caps(prev_dev);
|
||||
// Tell the printer to stop pushing and drop the report topic before the socket
|
||||
// goes away (§3.2/§5.4: deselect issues pushing.stop on both transports).
|
||||
if (doomed && !prev_dev.empty() && doomed->is_connected()) {
|
||||
@@ -1381,8 +1234,23 @@ std::string OrcaPrinterAgent::canonicalize_ams_payload(const std::string& dev_id
|
||||
const std::string cmd = print.value("command", std::string());
|
||||
if (cmd.empty() || (cmd.rfind("ams_", 0) != 0 && cmd != "auto_stop_ams_dry"))
|
||||
return json_str;
|
||||
if (cmd == "ams_change_filament" && print.contains("selector"))
|
||||
return json_str; // already canonical (e.g. command_ams_select_tray)
|
||||
|
||||
// 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).
|
||||
auto op_allowed = [&dev_id](const std::string& o) {
|
||||
return o.empty() || ams_op_supported(dev_id, o) || (o == "filament_setting" && has_filament_slots(dev_id));
|
||||
};
|
||||
|
||||
if (cmd == "ams_change_filament" && print.contains("selector")) {
|
||||
// Already canonical (e.g. command_ams_select_tray): gate the op,
|
||||
// but never rewrite the body.
|
||||
const std::string sel = print.value("selector", std::string());
|
||||
const std::string sel_op = (sel == "lane") ? "change_filament" : sel;
|
||||
if (!op_allowed(sel_op) && unsupported)
|
||||
*unsupported = true;
|
||||
return json_str;
|
||||
}
|
||||
|
||||
auto int_or = [&print](const char* key, int fallback) {
|
||||
const auto it = print.find(key);
|
||||
@@ -1394,34 +1262,45 @@ std::string OrcaPrinterAgent::canonicalize_ams_payload(const std::string& dev_id
|
||||
const int slot = int_or("slot_id", -1);
|
||||
const int ams = int_or("ams_id", -1);
|
||||
print.erase("target");
|
||||
print.erase("slot_id");
|
||||
print.erase("tray_id");
|
||||
print.erase("ams_id");
|
||||
if (target == 255 && slot == 255) {
|
||||
print.erase("slot_id");
|
||||
print.erase("ams_id");
|
||||
print["selector"] = "unload";
|
||||
op = "unload";
|
||||
} else if (target == 255 || ams == 254 || ams == 255) {
|
||||
print.erase("slot_id");
|
||||
print.erase("ams_id");
|
||||
print["selector"] = "external";
|
||||
op = "external";
|
||||
} else {
|
||||
const int lane = target >= 0 ? target : (ams >= 0 ? ams * 4 + slot : slot);
|
||||
// Box change: forward the wire coordinates unchanged. Erase
|
||||
// both first so a half-present coordinate pair from the client
|
||||
// cannot leak into the body (a lone lane alias is untouched). A
|
||||
// coordinate-less body is left for the server's own -19.
|
||||
print["selector"] = "lane";
|
||||
print["lane"] = lane;
|
||||
op = "change_filament";
|
||||
print.erase("ams_id");
|
||||
print.erase("slot_id");
|
||||
if (ams >= 0 && slot >= 0) {
|
||||
print["ams_id"] = ams;
|
||||
print["slot_id"] = slot;
|
||||
}
|
||||
op = "change_filament";
|
||||
}
|
||||
} else if (cmd == "ams_filament_setting") {
|
||||
op = "filament_setting";
|
||||
const int ams = int_or("ams_id", -1);
|
||||
const int slot = int_or("slot_id", -1);
|
||||
op = "filament_setting";
|
||||
if (ams >= 254) {
|
||||
if (unsupported)
|
||||
*unsupported = true; // no lane for a virtual tray
|
||||
return json_str;
|
||||
if (ams >= 0 && slot >= 0) {
|
||||
// Coordinates are the server resolver's own form (REQ-FMS-001
|
||||
// §6.2.13). Forward them unchanged for box and external spools;
|
||||
// never fabricate a flat lane (tray id != layout lane).
|
||||
print["ams_id"] = ams;
|
||||
print["slot_id"] = slot;
|
||||
print.erase("tray_id");
|
||||
}
|
||||
print["lane"] = ams * 4 + slot;
|
||||
print.erase("slot_id");
|
||||
print.erase("tray_id");
|
||||
print.erase("ams_id");
|
||||
// A lane-only or coordinate-less body is left as sent so the server
|
||||
// applies the dual-form rule and its own -19, never -5.
|
||||
} else if (cmd == "ams_control") {
|
||||
std::string action = print.value("action", print.value("param", std::string()));
|
||||
op = action; // only "pause" is ever declared; the rest gate out
|
||||
@@ -1441,10 +1320,8 @@ std::string OrcaPrinterAgent::canonicalize_ams_payload(const std::string& dev_id
|
||||
} else if (cmd == "auto_stop_ams_dry") {
|
||||
op = "stop_dry";
|
||||
}
|
||||
if (!op.empty() && !ams_op_supported(dev_id, op)) {
|
||||
if (unsupported)
|
||||
*unsupported = true;
|
||||
}
|
||||
if (!op_allowed(op) && unsupported)
|
||||
*unsupported = true;
|
||||
return envelope.dump();
|
||||
} catch (const std::exception&) {
|
||||
return json_str;
|
||||
|
||||
@@ -94,10 +94,10 @@ public:
|
||||
bool lan_mode) override;
|
||||
|
||||
// Filament sync (subscription): `subscription` when the selected printer
|
||||
// reports a material system (get_capabilities protocol.features.fms),
|
||||
// `none` otherwise. The mode is transport-agnostic; the lane_data fetch and
|
||||
// topology_state doorbell that keep DevFilaSystem fresh are LAN-only, and
|
||||
// cloud printers get their AMS view from the mirrored push_status.
|
||||
// reports a material system (get_capabilities protocol.features.fms) or the
|
||||
// filament-slot model (protocol.features.filament_slots), `none` otherwise.
|
||||
// The AMS view comes from the pushed `ams`/`vir_slot` in push_status, so
|
||||
// there is no fetch to keep fresh and the mode is transport-agnostic.
|
||||
FilamentSyncMode get_filament_sync_mode() const override;
|
||||
|
||||
// Test-only: drive emit_connect_sequence directly (no socket).
|
||||
@@ -109,15 +109,6 @@ public:
|
||||
// Test-only: advance the LAN connection epoch without a connect/disconnect cycle.
|
||||
void bump_lan_generation_for_test() { ++m_lan_generation; }
|
||||
|
||||
// Test-only: observe the subscription doorbell policy without spawning the refresh worker.
|
||||
bool filament_doorbell_needed_for_test(const std::string& dev_id, const std::string& payload)
|
||||
{ return filament_doorbell_needed(dev_id, payload); }
|
||||
// Test-only: arm the fetch-failure latch (retry-on-next-LAN-frame rule).
|
||||
void set_filament_failed_for_test(bool v)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
m_filament_failed = v;
|
||||
}
|
||||
// Test-only: Bambu ams_* wire JSON -> canonical OrcaSonar bodies (§7.8).
|
||||
// Returns the (possibly rewritten) payload; *unsupported is set when the
|
||||
// device's declared ams_ops exclude the operation (CAP_NOT_AVAILABLE in
|
||||
@@ -210,7 +201,6 @@ private:
|
||||
|
||||
std::string m_lan_dev_id; // guarded by state_mutex
|
||||
std::string m_lan_url; // guarded by state_mutex — the Config.url of the live LAN session
|
||||
std::string m_lan_http_origin; // guarded by state_mutex — http(s) origin of the LAN façade
|
||||
std::string m_lan_password; // guarded by state_mutex — MQTT access code; X-Api-Key fallback
|
||||
std::string m_lan_api_key; // guarded by state_mutex — cached Moonraker-façade key, "" = unresolved
|
||||
uint64_t m_lan_api_key_gen = 0; // m_lan_generation the cached key belongs to
|
||||
@@ -220,34 +210,9 @@ private:
|
||||
// The Moonraker-façade X-Api-Key, bootstrapped from /access/api_key (trusted
|
||||
// clients only) and cached per connection generation; falls back to the
|
||||
// access code when the endpoint is unavailable (untrusted/hardened config).
|
||||
// Used by the LAN upload path.
|
||||
std::string lan_api_key(const std::string& origin);
|
||||
|
||||
// Subscription-mode filament sync. One detached worker per burst drains
|
||||
// m_filament_wanted by re-reading lane_data; doorbell frames (a pushed
|
||||
// print.topology_state change) and the failure latch are the only triggers —
|
||||
// no timer: an idle OrcaSonar emits no frames, and cannot change lanes either.
|
||||
void request_filament_refresh(const std::string& dev_id);
|
||||
// OPCP §7.7: the doorbell is a CHANGE in print.topology_state.material_hash
|
||||
// (lane content only — tool temperature churn inside topology_state must not
|
||||
// re-fetch); consuming a new value updates m_material_hash.
|
||||
bool filament_doorbell_needed(const std::string& dev_id, const std::string& payload);
|
||||
|
||||
// Tri-state lane_data fetch outcome (REQ-STS-007 §7.7 read semantics):
|
||||
// synced — 200 with lane entries, payload built;
|
||||
// none — 200 with an empty value: authoritative "no lanes", AMS view cleared;
|
||||
// unknown — 404: topology not bootstrapped yet or material_units unknown;
|
||||
// never latched, the next doorbell or reconnect retries;
|
||||
// error — transport/5xx: latched, retried on the next inbound frame.
|
||||
enum class LaneDataState { synced, none, unknown, error };
|
||||
LaneDataState fetch_lane_data(const std::string& dev_id);
|
||||
|
||||
bool m_filament_wanted = false; // guarded by state_mutex; a fetch is pending
|
||||
bool m_filament_working = false; // guarded by state_mutex; a worker owns the queue
|
||||
bool m_filament_failed = false; // guarded by state_mutex; retry-on-next-LAN-frame rule
|
||||
bool m_shutting_down = false; // guarded by state_mutex; workers stop taking fetches
|
||||
std::string m_material_hash; // guarded by state_mutex; last doorbell value seen
|
||||
std::atomic<int> m_filament_in_flight{0}; // detached workers; drained by the destructor
|
||||
|
||||
OrcaCloudServiceAgent* get_orca_cloud_agent();
|
||||
|
||||
OrcaMqttConnection* get_appropriate_mqtt_connection(bool is_lan = true);
|
||||
|
||||
@@ -5482,3 +5482,35 @@ TEST_CASE("Config import confines zip entries, preset names and bundle ids to th
|
||||
CHECK_FALSE(any_filename_contains(temp_dir.path(), "bundle-escape"));
|
||||
}
|
||||
}
|
||||
|
||||
// OrcaSonar and the Qidi Box report a material type but no slicer preset id.
|
||||
// The Orca agent's AMS sync path fills one with filament_id_by_type, whose
|
||||
// modifier stripping ("PETG Basic" -> the PETG family) this locks in.
|
||||
TEST_CASE("filament_id_by_type resolves a modifier type to its generic family", "[Preset][Filament]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "Generic PLA @System");
|
||||
pla.is_system = true;
|
||||
pla.is_visible = true;
|
||||
pla.is_compatible = true;
|
||||
pla.filament_id = "GFL99";
|
||||
pla.config.option<ConfigOptionString>("filament_type")->value = "PLA";
|
||||
|
||||
Preset &petg = add_inmemory_preset(bundle.filaments, "Generic PETG @System");
|
||||
petg.is_system = true;
|
||||
petg.is_visible = true;
|
||||
petg.is_compatible = true;
|
||||
petg.filament_id = "GFG99";
|
||||
petg.config.option<ConfigOptionString>("filament_type")->value = "PETG";
|
||||
|
||||
CHECK(bundle.filaments.filament_id_by_type("PETG Basic") == "GFG99");
|
||||
CHECK(bundle.filaments.filament_id_by_type("PLA") == "GFL99");
|
||||
|
||||
// The strict overload reports no match instead of binding a fallback.
|
||||
std::string out = "unchanged";
|
||||
CHECK_FALSE(bundle.filaments.filament_id_by_type("Kevlar", out));
|
||||
CHECK(out == "unchanged");
|
||||
CHECK(bundle.filaments.filament_id_by_type("PETG Basic", out));
|
||||
CHECK(out == "GFG99");
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_dev_mapping.cpp
|
||||
test_filament_bitmap_utils.cpp
|
||||
test_device_progress.cpp
|
||||
test_device_manager.cpp
|
||||
test_network_versions.cpp
|
||||
test_action_source.cpp
|
||||
test_plugin_host_api.cpp
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
// why: match the GUI include order to avoid rpcndr.h byte/std::byte
|
||||
// ambiguity in the Windows COM headers.
|
||||
// why: wx/timer.h must precede DeviceManager.hpp because
|
||||
// DeviceErrorDialog.hpp uses wxTimerEvent.
|
||||
#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 <stdexcept>
|
||||
|
||||
#include <wx/timer.h>
|
||||
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
#include "slic3r/GUI/DeviceCore/DevFilaSystem.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
using namespace Slic3r;
|
||||
|
||||
// DeviceManager's push_status contract for the OrcaSonar virtual tray: an
|
||||
// authoritative empty clears, a populated key re-enables, and a frame that
|
||||
// omits the key leaves both the trays and the support flag alone.
|
||||
|
||||
// Contract: an authoritative empty vir_slot ([] = "known, no virtual slots")
|
||||
// clears the seeded virtual trays. The consumers were guarded so an empty
|
||||
// vector is safe.
|
||||
TEST_CASE("An empty vir_slot clears the virtual trays", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.vt_slot.size() == 1);
|
||||
REQUIRE(machine.vt_slot[0].id == "255");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[]}})", false);
|
||||
|
||||
CHECK(machine.vt_slot.empty());
|
||||
CHECK_FALSE(machine.ams_support_virtual_tray);
|
||||
}
|
||||
|
||||
// A frame that omits vir_slot must leave the seeded virtual tray untouched.
|
||||
TEST_CASE("A missing vir_slot keeps the virtual trays", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.vt_slot.size() == 1);
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status"}})", false);
|
||||
|
||||
CHECK(machine.vt_slot.size() == 1);
|
||||
CHECK(machine.ams_support_virtual_tray);
|
||||
}
|
||||
|
||||
// Repopulating after a clear must not rely on the constructor's seed, and must
|
||||
// re-enable virtual-tray support even though the clear turned the flag off.
|
||||
TEST_CASE("Virtual trays repopulate after an authoritative clear", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[]}})", false);
|
||||
REQUIRE(machine.vt_slot.empty());
|
||||
REQUIRE_FALSE(machine.ams_support_virtual_tray);
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[{"id":"255"},{"id":"254"}]}})", false);
|
||||
|
||||
REQUIRE(machine.vt_slot.size() == 2);
|
||||
CHECK(machine.vt_slot[0].id == "255");
|
||||
CHECK(machine.vt_slot[1].id == "254");
|
||||
CHECK(machine.ams_support_virtual_tray);
|
||||
}
|
||||
|
||||
// A deputy with no main is not an index-1 write into an empty vector.
|
||||
TEST_CASE("An orphan deputy virtual tray is dropped, not indexed", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[]}})", false);
|
||||
REQUIRE(machine.vt_slot.empty());
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[{"id":"254"}]}})", false);
|
||||
|
||||
CHECK(machine.vt_slot.empty());
|
||||
}
|
||||
|
||||
// acks that target the virtual tray must survive an emptied vt_slot.
|
||||
TEST_CASE("Virtual tray acks are safe with no virtual tray", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[]}})", false);
|
||||
REQUIRE(machine.vt_slot.empty());
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"ams_filament_setting","ams_id":255,"tray_id":255}})", false);
|
||||
machine.parse_json("lan", R"({"print":{"command":"extrusion_cali_set","tray_id":255,"k_value":0.02}})", false);
|
||||
|
||||
CHECK(machine.vt_slot.empty());
|
||||
}
|
||||
|
||||
// Only Bambu's own agent treats a non-zero tag_uid as an RFID lock; agent-managed
|
||||
// printers send it as metadata and must keep their trays editable.
|
||||
TEST_CASE("Only the BBL agent is RFID-locking", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.printer_agent_id = "bbl";
|
||||
CHECK(machine.is_bbl_agent());
|
||||
|
||||
machine.printer_agent_id = "orca";
|
||||
CHECK_FALSE(machine.is_bbl_agent());
|
||||
|
||||
machine.printer_agent_id = "";
|
||||
CHECK(machine.is_bbl_agent());
|
||||
}
|
||||
|
||||
// An OPCP error ack carries result "fail" and a reason; a success ack (or one
|
||||
// without result) must not be mistaken for a failure.
|
||||
TEST_CASE("AMS filament setting acks expose OPCP failures", "[DeviceManager]")
|
||||
{
|
||||
std::string reason;
|
||||
|
||||
const json failure = json::parse(R"({"result":"fail","errno":-19,"reason":"unknown slot"})");
|
||||
CHECK(MachineObject::ams_filament_ack_failed(failure, reason));
|
||||
CHECK(reason == "unknown slot");
|
||||
|
||||
reason = "stale";
|
||||
const json success = json::parse(R"({"result":"success","errno":0})");
|
||||
CHECK_FALSE(MachineObject::ams_filament_ack_failed(success, reason));
|
||||
CHECK(reason.empty());
|
||||
|
||||
const json plain = json::parse(R"({"ams_id":1,"tray_id":2})");
|
||||
CHECK_FALSE(MachineObject::ams_filament_ack_failed(plain, reason));
|
||||
}
|
||||
|
||||
// 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]")
|
||||
{
|
||||
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","ams_id":0,"tray_color":"FF0000FF","tray_type":"PLA","tray_info_idx":"GFA00","nozzle_temp_min":190,"nozzle_temp_max":230}})", false));
|
||||
|
||||
const DevAmsTray* tray = machine.GetFilaSystem()->GetAmsTray("0", "0");
|
||||
REQUIRE(tray != nullptr);
|
||||
CHECK(tray->color == "FF0000FF");
|
||||
}
|
||||
@@ -139,3 +139,5 @@ TEST_CASE("Zero progress replaces active shared progress", "[DeviceManager][Prog
|
||||
REQUIRE(machine.mc_print_percent == 0);
|
||||
CHECK(current_subtask->task_progress == 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -127,12 +127,28 @@ TEST_CASE("filament sync follows the printer's AMS capability", "[OrcaPrinterAge
|
||||
/*local=*/true);
|
||||
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::none);
|
||||
|
||||
// filament_slots alone enables sync: a standalone printer with no material
|
||||
// system still has slots (REQ-FMS-001).
|
||||
agent.deliver_to_sink("dev-ams-1",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"features":{"fms":false,"filament_slots":true}}}}})",
|
||||
/*local=*/true);
|
||||
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::subscription);
|
||||
|
||||
// Older payloads without features.fms fall back to a non-empty ams_ops.
|
||||
agent.deliver_to_sink("dev-ams-1",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"ams_ops":["change_filament"]}}}})",
|
||||
/*local=*/true);
|
||||
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::subscription);
|
||||
|
||||
// A reply without a protocol block is not a capabilities answer: it is
|
||||
// ignored, so a transient malformed reply cannot gate every write. The
|
||||
// ams_ops fallback above still holds.
|
||||
agent.deliver_to_sink("dev-ams-1",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{}}})",
|
||||
/*local=*/true);
|
||||
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::subscription);
|
||||
|
||||
// Disconnecting forgets the declaration rather than letting it go stale.
|
||||
agent.disconnect_printer();
|
||||
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::none);
|
||||
}
|
||||
@@ -155,46 +171,21 @@ TEST_CASE("a capability reply without ams_ops gates AMS writes", "[OrcaPrinterAg
|
||||
CHECK(unsupported);
|
||||
}
|
||||
|
||||
// The subscription refresh is self-triggered: a pushed frame whose print block
|
||||
// carries a CHANGED topology_state.material_hash (spec REQ-STS-007 §7.7) is
|
||||
// the doorbell; repeat hashes, temperature-only frames and hash-less blocks
|
||||
// are not (no per-frame polling regression).
|
||||
TEST_CASE("a material_hash change is the filament-sync doorbell", "[OrcaPrinterAgent][.integration]") {
|
||||
// A malformed get_capabilities reply must be ignored, not read as "no
|
||||
// capabilities": doing so would set ops_known with an empty set and gate every
|
||||
// AMS write until a good reply arrives.
|
||||
TEST_CASE("a malformed capability reply does not gate AMS writes", "[OrcaPrinterAgent]") {
|
||||
Probe agent("/tmp");
|
||||
REQUIRE(agent.connect_printer("dev-1", "10.255.255.1", "orcasonar", "code", false) == BAMBU_NETWORK_SUCCESS);
|
||||
agent.deliver_to_sink("dev-malformed",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"ams_ops":["change_filament"]}}}})",
|
||||
/*local=*/true);
|
||||
CHECK(Slic3r::ams_op_supported("dev-malformed", "change_filament"));
|
||||
|
||||
const std::string frame_a = R"({"print":{"command":"push_status","topology_state":{"material_hash":"sha256:aaaaaaaaaaaaaaaa","units":[]}}})";
|
||||
CHECK(agent.filament_doorbell_needed_for_test("dev-1", frame_a));
|
||||
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", frame_a));
|
||||
CHECK(agent.filament_doorbell_needed_for_test("dev-1",
|
||||
R"({"print":{"command":"push_status","topology_state":{"material_hash":"sha256:bbbbbbbbbbbbbbbb","units":[]}}})"));
|
||||
// Topology block without the doorbell token (older/foreign payload): stays quiet.
|
||||
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1",
|
||||
R"({"print":{"command":"push_status","topology_state":{"units":[]}}})"));
|
||||
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", R"({"print":{"command":"push_status","mc_percent":10}})"));
|
||||
// Not the active LAN device: never a doorbell.
|
||||
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-2", frame_a));
|
||||
|
||||
agent.disconnect_printer();
|
||||
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", frame_a));
|
||||
}
|
||||
|
||||
// After a failed lane_data fetch there is no retry timer: any next LAN frame
|
||||
// re-arms the refresh, because a changing printer keeps pushing.
|
||||
TEST_CASE("a failed filament fetch retries on the next LAN frame", "[OrcaPrinterAgent][.integration]") {
|
||||
Probe agent("/tmp");
|
||||
REQUIRE(agent.connect_printer("dev-1", "10.255.255.1", "orcasonar", "code", false) == BAMBU_NETWORK_SUCCESS);
|
||||
|
||||
const std::string telemetry = R"({"print":{"command":"push_status","mc_percent":10}})";
|
||||
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", telemetry));
|
||||
agent.set_filament_failed_for_test(true);
|
||||
CHECK(agent.filament_doorbell_needed_for_test("dev-1", telemetry));
|
||||
|
||||
agent.disconnect_printer();
|
||||
// disconnect clears the latch; the next connect eager-fetches instead.
|
||||
REQUIRE(agent.connect_printer("dev-1", "10.255.255.1", "orcasonar", "code", false) == BAMBU_NETWORK_SUCCESS);
|
||||
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", telemetry));
|
||||
agent.disconnect_printer();
|
||||
agent.deliver_to_sink("dev-malformed",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{}}})",
|
||||
/*local=*/true);
|
||||
// The malformed reply changed nothing, rather than clearing the op set.
|
||||
CHECK(Slic3r::ams_op_supported("dev-malformed", "change_filament"));
|
||||
}
|
||||
|
||||
TEST_CASE("post-connect sequence is subscribe then 4 requests in order", "[OrcaPrinterAgent]") {
|
||||
@@ -320,10 +311,11 @@ TEST_CASE("OrcaPrinterAgent rewrites Bambu ams_* payloads onto the canonical Orc
|
||||
auto out = nlohmann::json::parse(canon("dev-c1",
|
||||
R"({"print":{"command":"ams_change_filament","sequence_id":"1","target":5,"slot_id":1,"ams_id":1,"curr_temp":210,"tar_temp":220}})"));
|
||||
CHECK(out["print"]["selector"] == "lane");
|
||||
CHECK(out["print"]["lane"] == 5);
|
||||
// Coordinates are the resolver's own form; target (a BBL tray id) is dropped.
|
||||
CHECK(out["print"]["ams_id"] == 1);
|
||||
CHECK(out["print"]["slot_id"] == 1);
|
||||
CHECK(!out["print"].contains("lane"));
|
||||
CHECK(!out["print"].contains("target"));
|
||||
CHECK(!out["print"].contains("slot_id"));
|
||||
CHECK(!out["print"].contains("ams_id"));
|
||||
CHECK(out["print"]["tar_temp"] == 220);
|
||||
|
||||
// External-spool selection ("254" arrives hacked to 255 with slot_id=0):
|
||||
@@ -331,17 +323,48 @@ TEST_CASE("OrcaPrinterAgent rewrites Bambu ams_* payloads onto the canonical Orc
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_change_filament","ams_id":255,"target":255,"slot_id":0}})"));
|
||||
CHECK(out["print"]["selector"] == "external");
|
||||
CHECK(!out["print"].contains("lane"));
|
||||
CHECK(!out["print"].contains("ams_id"));
|
||||
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_change_filament","ams_id":0,"target":255,"slot_id":255}})"));
|
||||
CHECK(out["print"]["selector"] == "unload");
|
||||
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_change_filament","ams_id":1,"slot_id":2}})"));
|
||||
CHECK(out["print"]["lane"] == 6);
|
||||
// A coordinate-less body must not fabricate a flat lane from a BBL tray id;
|
||||
// the server validates the address and answers -19.
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_change_filament","target":6}})"));
|
||||
CHECK(!out["print"].contains("lane"));
|
||||
CHECK(!out["print"].contains("target"));
|
||||
CHECK(out["print"]["selector"] == "lane");
|
||||
|
||||
// Box coordinates are forwarded unchanged: a fabricated flat lane would be
|
||||
// the BBL tray id, which is not the layout lane for wide/sparse boxes.
|
||||
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"}})"));
|
||||
CHECK(out["print"]["lane"] == 6);
|
||||
CHECK(out["print"]["ams_id"] == 1);
|
||||
CHECK(out["print"]["slot_id"] == 2);
|
||||
CHECK(!out["print"].contains("lane"));
|
||||
CHECK(!out["print"].contains("tray_id"));
|
||||
CHECK(out["print"]["tray_type"] == "PLA");
|
||||
|
||||
// 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);
|
||||
CHECK(out["print"]["slot_id"] == 5);
|
||||
CHECK(!out["print"].contains("lane"));
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_filament_setting","ams_id":2,"slot_id":1,"tray_type":"PLA"}})"));
|
||||
CHECK(out["print"]["ams_id"] == 2);
|
||||
CHECK(out["print"]["slot_id"] == 1);
|
||||
|
||||
// A lane-only body must pass through untouched, never become lane = -5.
|
||||
const std::string lane_only = R"({"print":{"command":"ams_filament_setting","lane":5,"tray_type":"PLA"}})";
|
||||
CHECK(canon("dev-c1", lane_only) == lane_only);
|
||||
|
||||
// External/direct spool (Bambu 254/255): forwarded as the canonical
|
||||
// ams_id address, never a fabricated lane (REQ-FMS-001).
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_filament_setting","ams_id":255,"slot_id":0,"tray_id":0,"tray_type":"PLA"}})"));
|
||||
CHECK(out["print"]["ams_id"] == 255);
|
||||
CHECK(out["print"]["slot_id"] == 0);
|
||||
CHECK(!out["print"].contains("lane"));
|
||||
CHECK(!out["print"].contains("tray_id"));
|
||||
|
||||
// Legacy RFID call shape (ams_id+slot_id, no tray_id) flattens to tray_id.
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_get_rfid","ams_id":1,"slot_id":2}})"));
|
||||
CHECK(out["print"]["tray_id"] == 6);
|
||||
@@ -363,9 +386,40 @@ TEST_CASE("OrcaPrinterAgent rewrites Bambu ams_* payloads onto the canonical Orc
|
||||
unsupported = false;
|
||||
canon("dev-c2", R"({"print":{"command":"ams_change_filament","target":1}})", &unsupported);
|
||||
CHECK(!unsupported);
|
||||
// A selector-carrying canonical body is gated on its selector's op too.
|
||||
unsupported = false;
|
||||
canon("dev-c2", R"({"print":{"command":"ams_change_filament","selector":"lane","lane":1}})", &unsupported);
|
||||
CHECK(!unsupported);
|
||||
unsupported = false;
|
||||
canon("dev-c2", R"({"print":{"command":"ams_change_filament","selector":"external"}})", &unsupported);
|
||||
CHECK(unsupported);
|
||||
|
||||
// A device with no capabilities record is never gated (server backstops).
|
||||
unsupported = false;
|
||||
canon("dev-c3", R"({"print":{"command":"ams_user_setting","ams_id":0}})", &unsupported);
|
||||
CHECK(!unsupported);
|
||||
}
|
||||
|
||||
// filament_setting is advertised by filament_slots alone (OPCP §7.8), so a
|
||||
// standalone printer with no ams_ops can still write slots, while the material
|
||||
// writes stay gated.
|
||||
TEST_CASE("a filament_slots reply admits ams_filament_setting without ams_ops", "[OrcaPrinterAgent]") {
|
||||
Probe agent("/tmp");
|
||||
agent.deliver_to_sink("dev-slots",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"features":{"fms":false,"filament_slots":true}}}}})",
|
||||
/*local=*/true);
|
||||
|
||||
bool unsupported = false;
|
||||
OrcaPrinterAgent::canonicalize_ams_payload(
|
||||
"dev-slots",
|
||||
R"({"print":{"command":"ams_filament_setting","ams_id":255,"slot_id":0,"tray_id":0,"tray_type":"PLA"}})",
|
||||
&unsupported);
|
||||
CHECK_FALSE(unsupported);
|
||||
|
||||
unsupported = false;
|
||||
OrcaPrinterAgent::canonicalize_ams_payload(
|
||||
"dev-slots",
|
||||
R"({"print":{"command":"ams_change_filament","target":1,"slot_id":1,"ams_id":0}})",
|
||||
&unsupported);
|
||||
CHECK(unsupported);
|
||||
}
|
||||
|
||||
@@ -209,6 +209,14 @@ TEST_CASE("unit: AMS capability registry reports only declared material systems"
|
||||
// Later replies overwrite: a removed material system must clear the flag.
|
||||
register_ams_capability("cap-true", false);
|
||||
CHECK_FALSE(has_ams_capability("cap-true"));
|
||||
|
||||
// filament_slots is a separate connector flag: no record reads false, and
|
||||
// a later reply clears it.
|
||||
CHECK_FALSE(has_filament_slots("cap-slot-none"));
|
||||
register_filament_slots("cap-slots", true);
|
||||
CHECK(has_filament_slots("cap-slots"));
|
||||
register_filament_slots("cap-slots", false);
|
||||
CHECK_FALSE(has_filament_slots("cap-slots"));
|
||||
}
|
||||
|
||||
// why: these builders preserve the Bambu firmware dialect byte-for-byte, including its trailing space.
|
||||
|
||||
Reference in New Issue
Block a user