From 3d627696646fb55e73976aee53d3d4812090dee0 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 8 Oct 2026 17:46:01 +0800 Subject: [PATCH 1/5] feat: expand filament parsing in MoonrakerPrinterAgent --- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 201 +++++++++++++++------ src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 14 ++ 2 files changed, 160 insertions(+), 55 deletions(-) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 96570b656b..280802ede2 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -441,19 +441,7 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays) { - - // Look up MachineObject via DeviceManager - auto* dev_manager = GUI::wxGetApp().getDeviceManager(); - if (!dev_manager) { - return; - } - MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id); - if (!obj) { - return; - } - // Build BBL-format JSON for DevFilaSystemParser::ParseV1_0 - nlohmann::json ams_json = nlohmann::json::object(); nlohmann::json ams_array = nlohmann::json::array(); // Calculate ams_exist_bits and tray_exist_bits @@ -461,11 +449,9 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, unsigned long tray_exist_bits = 0; for (int ams_id = 0; ams_id < ams_count; ++ams_id) { - ams_exist_bits |= (1 << ams_id); - - nlohmann::json ams_unit = nlohmann::json::object(); - ams_unit["id"] = std::to_string(ams_id); - ams_unit["info"] = "0002"; // treat as AMS_LITE + if (ams_id < 32) { + ams_exist_bits |= (1UL << ams_id); + } nlohmann::json tray_array = nlohmann::json::array(); int max_slot_in_this_ams = std::min(3, max_lane_index - ams_id * 4); @@ -481,43 +467,127 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, } } - nlohmann::json tray_json = nlohmann::json::object(); - tray_json["id"] = std::to_string(slot_id); - tray_json["tag_uid"] = "0000000000000000"; - - if (tray && tray->has_filament) { - tray_exist_bits |= (1 << slot_index); - - tray_json["tray_info_idx"] = tray->tray_info_idx; - tray_json["tray_type"] = tray->tray_type; - tray_json["tray_color"] = normalize_color_value(tray->tray_color); - - // Add temperature data if provided - if (tray->bed_temp > 0) { - tray_json["bed_temp"] = std::to_string(tray->bed_temp); - } - if (tray->nozzle_temp > 0) { - tray_json["nozzle_temp_max"] = std::to_string(tray->nozzle_temp); - } - } else { - tray_json["tray_info_idx"] = ""; - tray_json["tray_type"] = ""; - tray_json["tray_color"] = "00000000"; - tray_json["tray_slot_placeholder"] = "1"; + if (tray && tray->has_filament && slot_index < 32) { + tray_exist_bits |= (1UL << slot_index); } - tray_array.push_back(tray_json); + static const AmsTrayData empty_tray; + tray_array.push_back(make_ams_tray_json(std::to_string(slot_id), tray ? *tray : empty_tray)); } + + nlohmann::json ams_unit = nlohmann::json::object(); + ams_unit["id"] = std::to_string(ams_id); + ams_unit["info"] = "0002"; // treat as AMS_LITE ams_unit["tray"] = tray_array; ams_array.push_back(ams_unit); } + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays"; + submit_ams_payload(ams_array, ams_exist_bits, tray_exist_bits); +} + +void MoonrakerPrinterAgent::build_ams_payload_grouped(const std::vector& trays) +{ + // One AMS box per extruder, best effort. Lane-data changers (AFC, recent Happy Hare) report an + // extruder per lane but no physical box layout, so a box here is a logical feed group, not a + // discovered unit. Lanes without an extruder fall back to the main extruder. + std::map> boxes; + for (const auto& tray : trays) { + boxes[tray.extruder_id].push_back(&tray); + } + + nlohmann::json ams_array = nlohmann::json::array(); + unsigned long ams_exist_bits = 0; + unsigned long tray_exist_bits = 0; + + int ams_id = 0; + for (auto& [extruder_id, lanes] : boxes) { + if (ams_id < 32) { + ams_exist_bits |= (1UL << ams_id); + } + + // Keep a deterministic slot order within the box. + std::stable_sort(lanes.begin(), lanes.end(), + [](const AmsTrayData* a, const AmsTrayData* b) { return a->slot_index < b->slot_index; }); + + nlohmann::json tray_array = nlohmann::json::array(); + for (size_t slot = 0; slot < lanes.size(); ++slot) { + const AmsTrayData& tray = *lanes[slot]; + int bit = ams_id * 4 + static_cast(slot); + if (tray.has_filament && bit < 32) { + tray_exist_bits |= (1UL << bit); + } + tray_array.push_back(make_ams_tray_json(std::to_string(slot), tray)); + } + + nlohmann::json ams_unit = nlohmann::json::object(); + ams_unit["id"] = std::to_string(ams_id); + // info: bits 0-3 = unit type (2 = AMS_LITE), bits 8-11 = bound extruder id. + const int info_val = 2 | ((extruder_id & 0x0F) << 8); + const char* hex_digits = "0123456789ABCDEF"; + std::string info = "0000"; + info[3] = hex_digits[info_val & 0x0F]; + info[2] = hex_digits[(info_val >> 4) & 0x0F]; + info[1] = hex_digits[(info_val >> 8) & 0x0F]; + info[0] = hex_digits[(info_val >> 12) & 0x0F]; + ams_unit["info"] = info; + ams_unit["tray"] = tray_array; + ams_array.push_back(ams_unit); + ++ams_id; + } + + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload_grouped: Parsed " << trays.size() + << " trays in " << boxes.size() << " extruder box(es)"; + submit_ams_payload(ams_array, ams_exist_bits, tray_exist_bits); +} + +nlohmann::json MoonrakerPrinterAgent::make_ams_tray_json(const std::string& tray_id, const AmsTrayData& tray) +{ + nlohmann::json tray_json = nlohmann::json::object(); + tray_json["id"] = tray_id; + tray_json["tag_uid"] = "0000000000000000"; + + if (tray.has_filament) { + tray_json["tray_info_idx"] = tray.tray_info_idx; + tray_json["tray_type"] = tray.tray_type; + tray_json["tray_color"] = normalize_color_value(tray.tray_color); + + // Add temperature data if provided + if (tray.bed_temp > 0) { + tray_json["bed_temp"] = std::to_string(tray.bed_temp); + } + if (tray.nozzle_temp > 0) { + tray_json["nozzle_temp_max"] = std::to_string(tray.nozzle_temp); + } + } else { + tray_json["tray_info_idx"] = ""; + tray_json["tray_type"] = ""; + tray_json["tray_color"] = "00000000"; + tray_json["tray_slot_placeholder"] = "1"; + } + + return tray_json; +} + +void MoonrakerPrinterAgent::submit_ams_payload(const nlohmann::json& ams_array, unsigned long ams_exist_bits, unsigned long tray_exist_bits) +{ + // Look up MachineObject via DeviceManager + auto* dev_manager = GUI::wxGetApp().getDeviceManager(); + if (!dev_manager) { + return; + } + MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id); + if (!obj) { + return; + } + // Format as hex strings (matching BBL protocol) std::ostringstream ams_exist_ss; ams_exist_ss << std::hex << std::uppercase << ams_exist_bits; std::ostringstream tray_exist_ss; tray_exist_ss << std::hex << std::uppercase << tray_exist_bits; + nlohmann::json ams_json = nlohmann::json::object(); ams_json["ams"] = ams_array; ams_json["ams_exist_bits"] = ams_exist_ss.str(); ams_json["tray_exist_bits"] = tray_exist_ss.str(); @@ -528,7 +598,6 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, // Call the parser to populate DevFilaSystem DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem().get(), false); - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays"; // Set printer_type so update_sync_status() can match it against the preset's printer type. // Without this, the comparison fails and all sync badges are cleared. @@ -568,8 +637,7 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync if (fetch_moonraker_filament_data(trays, max_lane_index)) { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Moonraker filament system with " << (max_lane_index + 1) << " lanes"; - int ams_count = (max_lane_index + 4) / 4; - build_ams_payload(ams_count, max_lane_index, trays); + build_ams_payload_grouped(trays); return true; } @@ -684,6 +752,30 @@ int MoonrakerPrinterAgent::safe_json_int(const nlohmann::json& obj, const char* return 0; } +int MoonrakerPrinterAgent::safe_json_int_flexible(const nlohmann::json& obj, const char* key, int default_value) +{ + auto it = obj.find(key); + if (it == obj.end()) + return default_value; + if (it->is_number_integer()) + return it->get(); + if (it->is_string()) { + const std::string s = it->get(); + if (s.empty()) + return default_value; + try { + std::size_t consumed = 0; + const int value = std::stoi(s, &consumed); + if (consumed != s.size()) // reject partial parses such as "2oops" + return default_value; + return value; + } catch (...) { + return default_value; + } + } + return default_value; +} + std::string MoonrakerPrinterAgent::safe_array_string(const nlohmann::json& arr, int idx) { if (arr.is_array() && idx >= 0 && idx < static_cast(arr.size()) && arr[idx].is_string()) @@ -792,23 +884,22 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector& trays); + // Build ams JSON with one box per extruder (best effort, for lane_data-based filament + // changers such as AFC) and call the parser. Boxes are ordered by extruder id. + void build_ams_payload_grouped(const std::vector& trays); + // Methods that derived classes may need to override or access virtual bool init_device_info(const std::string& dev_id, const std::string& dev_ip, const std::string& username, const std::string& password, bool use_ssl, const std::string& port); virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; @@ -164,9 +169,18 @@ private: bool fetch_hh_filament_info(std::vector& trays, int& max_lane_index); bool fetch_moonraker_filament_data(std::vector& trays, int& max_lane_index); + // Build one BBL tray JSON object (placeholder when the tray has no filament). + static nlohmann::json make_ams_tray_json(const std::string& tray_id, const AmsTrayData& tray); + + // Wrap the AMS boxes into the payload ParseV1_0 understands and apply the pull-mode + // readiness state (printer_type, push counters, storage, module versions). + void submit_ams_payload(const nlohmann::json& ams_array, unsigned long ams_exist_bits, unsigned long tray_exist_bits); + // JSON helper methods static std::string safe_json_string(const nlohmann::json& obj, const char* key); static int safe_json_int(const nlohmann::json& obj, const char* key); + // Accepts either a JSON number or a numeric string (AFC reports some fields as strings). + static int safe_json_int_flexible(const nlohmann::json& obj, const char* key, int default_value); static std::string safe_array_string(const nlohmann::json& arr, int idx); static int safe_array_int(const nlohmann::json& arr, int idx); static std::string normalize_color_value(const std::string& color); From 6b366ad8a99e1e9171325c2a32f192f54b1b8cd7 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 8 Oct 2026 20:42:11 +0800 Subject: [PATCH 2/5] fix: sync_ams_list should drop placeholder filaments --- src/libslic3r/PresetBundle.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 0b3d30d5a5..0522e8dcaa 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -3894,6 +3894,16 @@ unsigned int PresetBundle::sync_ams_list(std::vector{ams_filament_colors[i]}); + } else if (i < ams_infos.size() && ams_infos[i].is_placeholder) { + // Orca: an empty AMS slot is a placeholder, not a filament, so drop it instead of + // keeping or filling a slot for it. The device AMS panel still shows the empty tray. + continue; } else if (i < exist_presets.size()) { // Empty tray or beyond tray count: keep existing filament result_colors.push_back(exist_colors[i]); @@ -4295,8 +4303,8 @@ unsigned int PresetBundle::sync_ams_list(std::vectorvalues = result_color_types; this->filament_presets = result_presets; ams_multi_color_filment = result_multi_colors; - filament_map->values.resize(total, 1); - filament_volume_map->values.resize(total, static_cast(NozzleVolumeType::nvtStandard)); + filament_map->values.resize(result_presets.size(), 1); + filament_volume_map->values.resize(result_presets.size(), static_cast(NozzleVolumeType::nvtStandard)); } else { // BBL: existing wholesale replace filament_color->values = ams_filament_colors; From 6f0ec60d21f2b18a46fd560db9b9feae7aeb389c Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 8 Oct 2026 21:26:00 +0800 Subject: [PATCH 3/5] feat: update happy hare parsing --- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 50 +++++++++------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 280802ede2..d5f7390454 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -641,12 +641,11 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync return true; } - // Attempt Happy Hare first (more widely adopted, supports more filament changers) + // Fall back to the Happy Hare `mmu` object for HH builds that do not publish lane_data if (fetch_hh_filament_info(trays, max_lane_index)) { BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Happy Hare MMU with " << (max_lane_index + 1) << " gates"; - int ams_count = (max_lane_index + 4) / 4; - build_ams_payload(ams_count, max_lane_index, trays); + build_ams_payload_grouped(trays); return true; } @@ -1004,48 +1003,41 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector& tra return false; } - // Parse gate data + // Parse gate data. Every gate becomes a lane so empty/unknown gates surface + // as empty AMS slots rather than vanishing. HH is single-extruder, so all + // lanes belong to feed group 0. trays.clear(); - max_lane_index = 0; for (int gate_idx = 0; gate_idx < num_gates; ++gate_idx) { // Check gate_status: -1 = unknown, 0 = empty, 1 or 2 = available int status = safe_array_int(gate_status, gate_idx); - if (status <= 0) { - continue; // Skip unknown or empty gates - } - - // Extract gate data std::string material = safe_array_string(gate_material, gate_idx); std::string color = safe_array_string(gate_color, gate_idx); int nozzle_temp = safe_array_int(gate_temperature, gate_idx); - // Skip if no material type (empty gate) - if (material.empty()) { - continue; - } - AmsTrayData tray; tray.slot_index = gate_idx; - tray.tray_type = material; - tray.tray_color = color; - tray.nozzle_temp = nozzle_temp; - tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays - tray.has_filament = true; + tray.extruder_id = 0; - auto* bundle = GUI::wxGetApp().preset_bundle; - tray.tray_info_idx = bundle - ? bundle->filaments.filament_id_by_type(tray.tray_type) - : map_filament_type_to_generic_id(tray.tray_type); + if (status > 0 && !material.empty()) { + tray.tray_type = material; + tray.tray_color = color; + tray.nozzle_temp = nozzle_temp; + tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays + tray.has_filament = true; + + auto* bundle = GUI::wxGetApp().preset_bundle; + tray.tray_info_idx = bundle + ? bundle->filaments.filament_id_by_type(tray.tray_type) + : map_filament_type_to_generic_id(tray.tray_type); + } else { + tray.has_filament = false; + } - max_lane_index = std::max(max_lane_index, gate_idx); trays.push_back(tray); } - if (trays.empty()) { - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_hh_filament_info: No valid HH gates found"; - return false; - } + max_lane_index = num_gates - 1; return true; } From aebc0a297985289d87c957fa50e8bd049107222f Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Fri, 9 Oct 2026 18:27:14 +0800 Subject: [PATCH 4/5] fix: change ams type when parsing AFC mmu --- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index d5f7390454..d515d7329a 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -477,7 +477,9 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, nlohmann::json ams_unit = nlohmann::json::object(); ams_unit["id"] = std::to_string(ams_id); - ams_unit["info"] = "0002"; // treat as AMS_LITE + // Generic unit: type 1 (AMS) maps to AMSModel::GENERIC_AMS, which drives the + // variable-lane rendering. This is not a real Bambu AMS-Lite. + ams_unit["info"] = "0001"; ams_unit["tray"] = tray_array; ams_array.push_back(ams_unit); } @@ -522,8 +524,9 @@ void MoonrakerPrinterAgent::build_ams_payload_grouped(const std::vector Date: Sun, 11 Oct 2026 20:11:47 +0800 Subject: [PATCH 5/5] fix: parse AFC lane data and non-bambu ams tray indices correctly --- src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp | 136 ++++++++++++++++--- src/slic3r/GUI/Plater.cpp | 17 ++- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 143 +++++++++++++++++++- src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 4 + 4 files changed, 270 insertions(+), 30 deletions(-) diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp index 14d78fba1f..0aa273c53c 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,36 @@ using namespace nlohmann; namespace Slic3r { + +bool devPrinterUtil::IsVirtualSlot(int ams_id) +{ + // When the app or preset bundle is unavailable we cannot know the vendor's + // extruder count, so fall back deterministically to the fixed Bambu pair + // {255, 254} rather than guessing from an unknown generic layout. + if (wxTheApp == nullptr || GUI::wxGetApp().preset_bundle == nullptr || GUI::wxGetApp().preset_bundle->is_bbl_vendor()) + return ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID; + + const int extruder_count = GUI::wxGetApp().preset_bundle->get_printer_extruder_count(); + return ams_id >= VIRTUAL_TRAY_MAIN_ID - extruder_count + 1 && ams_id <= VIRTUAL_TRAY_MAIN_ID; +} + +bool devPrinterUtil::IsVirtualSlot(const std::string& ams_id) +{ + // Strict: only a fully numeric string is a valid slot id. std::stoi alone + // would accept trailing garbage (e.g. "255abc" -> 255). + if (ams_id.empty()) + return false; + for (char c : ams_id) { + if (c < '0' || c > '9') + return false; + } + try { + return IsVirtualSlot(std::stoi(ams_id)); + } catch (...) { + return false; + } +} + static int _hex_digit_to_int(const char c) { return (c >= '0' && c <= '9') ? c - '0' : (c >= 'A' && c <= 'F') ? c - 'A' + 10 : (c >= 'a' && c <= 'f') ? c - 'a' + 10 : -1; } wxColour DevAmsTray::decode_color(const std::string &color) @@ -200,18 +231,22 @@ wxString DevAms::GetDisplayName() const int DevAms::GetSlotCount() const { - // GetAmsType() maps AMS_LITE_MIXED -> AMS_LITE, so N9 reports 4 slots like AMS-Lite. - auto ams_type = GetAmsType(); - if (ams_type == AMS || ams_type == AMS_LITE || ams_type == N3F) - { - return 4; - } - else if (ams_type == N3S) - { + // N3S is a 1-slot Bambu contract, so report 1 regardless of whether the + // preset bundle is momentarily unavailable (m_trays.size() can be 0). + if (GetAmsType() == N3S) return 1; + + if (wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && + GUI::wxGetApp().preset_bundle->is_bbl_vendor()) { + // GetAmsType() maps AMS_LITE_MIXED -> AMS_LITE, so N9 reports 4 slots like AMS-Lite. + auto ams_type = GetAmsType(); + if (ams_type == AMS || ams_type == AMS_LITE || ams_type == N3F) + { + return 4; + } } - return 1; + return static_cast(m_trays.size()); } DevAmsTray* DevAms::GetTray(const std::string& tray_id) const @@ -302,13 +337,21 @@ int DevFilaSystem::GetExtruderIdByAmsId(const std::string& ams_id) const { return it->second->GetExtruderId(); } - else if (stoi(ams_id) == VIRTUAL_TRAY_MAIN_ID) - { - return MAIN_EXTRUDER_ID; + + const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor(); + if (!is_bbl_vendor && GetOwner()) { + for (const auto& tray : GetOwner()->vt_slot) { + if (tray.id == ams_id) + return VIRTUAL_TRAY_MAIN_ID - std::stoi(ams_id); + } } - else if (stoi(ams_id) == VIRTUAL_TRAY_DEPUTY_ID) - { + + if (is_bbl_vendor && ams_id == VIRTUAL_AMS_MAIN_ID_STR) + return MAIN_EXTRUDER_ID; + if (is_bbl_vendor && ams_id == VIRTUAL_AMS_DEPUTY_ID_STR) return DEPUTY_EXTRUDER_ID; + if (!is_bbl_vendor && devPrinterUtil::IsVirtualSlot(ams_id)) { + return VIRTUAL_TRAY_MAIN_ID - std::stoi(ams_id); } assert(false && __FUNCTION__); @@ -325,9 +368,22 @@ std::string DevFilaSystem::GetNozzleFlowStringByAmsId(const std::string& ams_id) std::map DevFilaSystem::GetTrayIndexMap() { std::map tray_id_map; - tray_id_map[VIRTUAL_TRAY_MAIN_ID] = DevAmsSlotId{VIRTUAL_TRAY_MAIN_ID, 0}; - tray_id_map[VIRTUAL_TRAY_DEPUTY_ID] = DevAmsSlotId{VIRTUAL_TRAY_DEPUTY_ID, 0}; + const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor(); + if (is_bbl_vendor) { + tray_id_map[VIRTUAL_TRAY_MAIN_ID] = DevAmsSlotId{VIRTUAL_TRAY_MAIN_ID, 0}; + tray_id_map[VIRTUAL_TRAY_DEPUTY_ID] = DevAmsSlotId{VIRTUAL_TRAY_DEPUTY_ID, 0}; + } else if (GetOwner()) { + for (const auto& tray : GetOwner()->vt_slot) { + try { + const int tray_id = std::stoi(tray.id); + tray_id_map[tray_id] = DevAmsSlotId{tray_id, 0}; + } catch (...) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " invalid virtual tray id: " << tray.id; + } + } + } + int generic_tray_index = 0; for (auto& [ams_id, ams_item] : GetAmsList()) { for (auto &[slot_id, slot_item] : ams_item->GetTrays()) { if (ams_item && slot_item) { @@ -335,9 +391,11 @@ std::map DevFilaSystem::GetTrayIndexMap() int ams_id_int = stoi(ams_id); int slot_id_int = stoi(slot_id); int tray_index = -1; - if (ams_item->GetAmsType() == DevAms::N3S) { + if (!is_bbl_vendor) { + tray_index = generic_tray_index + slot_id_int; + } else if (ams_item->GetAmsType() == DevAms::N3S) { tray_index = ams_id_int; - } else if(ams_item->GetAmsType() == DevAms::AMS_LITE && ams_item->IsAmsLiteMixed()) { + } else if (ams_item->GetAmsType() == DevAms::AMS_LITE && ams_item->IsAmsLiteMixed()) { tray_index = 24 + slot_id_int; } else { tray_index = (ams_id_int * 4 + slot_id_int); @@ -348,6 +406,8 @@ std::map DevFilaSystem::GetTrayIndexMap() } } } + if (!is_bbl_vendor) + generic_tray_index += static_cast(ams_item->GetTrays().size()); } return tray_id_map; @@ -456,6 +516,13 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS ams_id_set.insert(it->first); } + // Bambu trays index their exist bit as ams_id*4+tray_id; a generic (non-Bambu) unit + // reports its own lane count, so its trays carry a cumulative lane index instead. + // Same index DevFilaSystem::GetTrayIndexMap() builds and the agent sets bits with. + const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && + GUI::wxGetApp().preset_bundle->is_bbl_vendor(); + int generic_tray_base = 0; + for (auto it = j_ams.begin(); it != j_ams.end(); it++) { if (!it->contains("id")) continue; @@ -795,7 +862,18 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS if (type_id < 4) { - curr_tray->is_exists = (obj->tray_exist_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0 ? true : false; + if (is_bbl_vendor) + { + curr_tray->is_exists = (obj->tray_exist_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0 ? true : false; + } + else + { + // A generic unit is not four slots wide, so its trays carry a + // cumulative lane index across units instead of ams_id*4+tray_id. + const int tray_bit = generic_tray_base + tray_id_int; + curr_tray->is_exists = tray_bit >= 0 && tray_bit < 32 && + (obj->tray_exist_bits & (1UL << tray_bit)) != 0; + } } else if (type_id == DevAms::AMS_LITE_MIXED) { @@ -851,6 +929,9 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS } } } + if (!is_bbl_vendor && it->contains("tray") && (*it)["tray"].is_array()) { + generic_tray_base += static_cast((*it)["tray"].size()); + } } // remove not in amsList for (auto it = ams_id_set.begin(); it != ams_id_set.end(); it++) @@ -913,6 +994,8 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob } ams->m_exist = true; + ams->m_binded_extruder_set = {ext_id}; + ams->m_binded_switcher_pos.reset(); 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); @@ -995,16 +1078,22 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob } // --- external / direct spools -> obj->vt_slot --- - // extruder 0 -> main virtual slot, extruder >0 -> deputy. + // Keep one virtual slot per extruder. The legacy two-extruder mapping uses + // 255 for extruder 0 and 254 for extruder 1; continue that sequence for + // additional extruders (253, 252, ...). if (obj && data.contains("external") && data["external"].is_array()) { + std::map external_slots; 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; + if (ext < MAIN_EXTRUDER_ID || ext >= VIRTUAL_TRAY_MAIN_ID) + continue; + + const int vt_id = VIRTUAL_TRAY_MAIN_ID - ext; DevAmsTray tray(std::to_string(vt_id)); tray.is_exists = e.value("loaded", false); tray.m_fila_type = e.value("material", std::string()); @@ -1013,8 +1102,11 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob 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); + external_slots.insert_or_assign(ext, std::move(tray)); } + + for (auto& entry : external_slots) + obj->vt_slot.push_back(std::move(entry.second)); } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 76ccac2838..0af089ea76 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6025,6 +6025,8 @@ void Sidebar::on_bed_type_change(BedType bed_type) * - Map key encoding: * - Virtual trays: 0x10000 + vt_tray.id (first/main extruder), or just vt_tray.id (secondary) * - AMS trays: 0x10000 + (ams_id * 4 + slot_id) (main extruder), or (ams_id * 4 + slot_id) (secondary) + * - Generic (non-Bambu) units are not four slots wide: their trays index by a cumulative lane + * count across units instead of ams_id * 4 + slot_id * - The 0x10000 flag indicates the main/right extruder * - Map value: DynamicPrintConfig with filament properties (id, type, color, etc.) * @@ -6090,13 +6092,26 @@ std::map Sidebar::build_filament_ams_list(MachineObject return std::string(); }; + const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor(); + auto list = obj->GetFilaSystem()->GetAmsList(); for (auto ams : list) { int ams_id = std::stoi(ams.first); int extruder = ams.second->GetExtruderId() ? 0 : 0x10000; // Main (first) extruder at right + // Bambu units are four slots wide; a generic unit reports its own lane count and its trays + // carry a cumulative lane index. The AMS map is keyed by string id, so count preceding units + // numerically to keep this the same index the parser and the agent use. + int generic_tray_base = 0; + if (!is_bbl_vendor) { + for (const auto& other : list) { + if (std::stoi(other.first) < ams_id) + generic_tray_base += static_cast(other.second->GetTrays().size()); + } + } for (auto tray : ams.second->GetTrays()) { int slot_id = std::stoi(tray.first); - filament_ams_list.emplace(extruder + (ams_id * 4 + slot_id), + const int tray_index = is_bbl_vendor ? ams_id * 4 + slot_id : generic_tray_base + slot_id; + filament_ams_list.emplace(extruder + tray_index, build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), std::to_string(ams_id), std::to_string(slot_id))); } } diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index d515d7329a..e9c6551d58 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -59,6 +59,42 @@ namespace websocket = beast::websocket; namespace net = boost::asio; using tcp = net::ip::tcp; +// Decimal value of a string that is nothing but digits, or -1 when it is empty or not a number. +int decimal_number(const std::string& digits) +{ + if (digits.empty() || !std::all_of(digits.begin(), digits.end(), [](unsigned char c) { return std::isdigit(c) != 0; })) + return -1; + try { + return std::stoi(digits); + } catch (...) { + return -1; + } +} + +// Lane database keys address a lane either as "T" (0-based tool index) or as "lane" +// (1-based, the AFC/Happy Hare style). Returns -1 when the key carries no usable lane number. +int lane_index_from_key(const std::string& lane_key) +{ + if (lane_key.size() > 1 && (lane_key[0] == 'T' || lane_key[0] == 't')) + return decimal_number(lane_key.substr(1)); + if (lane_key.size() > 4 && lane_key.compare(0, 4, "lane") == 0) { + const int lane_number = decimal_number(lane_key.substr(4)); + return lane_number > 0 ? lane_number - 1 : -1; + } + return -1; +} + +// AFC names toolheads "extruder", "extruder1", ... and derives the toolhead index from the name +// itself (AFC_lane.lane_extruder_index): no suffix is 0, otherwise the suffix is the index. +int extruder_index_from_name(const std::string& toolhead) +{ + if (toolhead == "extruder") + return 0; + if (toolhead.size() > 8 && toolhead.compare(0, 8, "extruder") == 0) + return decimal_number(toolhead.substr(8)); + return -1; +} + struct WsEndpoint { std::string host; @@ -503,6 +539,9 @@ void MoonrakerPrinterAgent::build_ams_payload_grouped(const std::vector(slot); + // A logical feed group is not a four-slot Bambu AMS: the bit is the lane's cumulative + // index, not ams_id*4+slot. DevFilaSystemParser::ParseV1_0 reads the same index. + const int bit = lane_index++; if (tray.has_filament && bit < 32) { tray_exist_bits |= (1UL << bit); } @@ -881,26 +922,38 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector toolhead binding from the + // AFC printer objects so lanes on different toolheads end up in different AMS boxes. + std::map lane_extruder; + fetch_afc_lane_extruders(lane_extruder); + for (const auto& [lane_key, lane_obj] : value.items()) { if (!lane_obj.is_object()) { continue; } // Lane tool number (0-based). AFC reports it as a string in the documented sample but it - // can also be a number; accept either and skip lanes without a usable index. - const int lane_index = safe_json_int_flexible(lane_obj, "lane", -1); + // can also be a number; accept either and skip lanes without a usable index. Databases + // written without a "lane" field fall back to the key, which names the lane either as + // "T" (0-based) or as "lane" (1-based). + int lane_index = safe_json_int_flexible(lane_obj, "lane", -1); if (lane_index < 0) { - continue; + lane_index = lane_index_from_key(lane_key); + if (lane_index < 0) { + continue; + } } AmsTrayData tray; tray.slot_index = lane_index; // AFC exposes the lane's extruder/toolhead as "extruder_index"; tolerate "extruder_id" as - // an alias. Unknown or negative falls back to the main extruder (best effort). + // an alias. lane_data has neither, so fall back to the AFC lane objects and only then to + // the main extruder (best effort). tray.extruder_id = safe_json_int_flexible(lane_obj, "extruder_index", - safe_json_int_flexible(lane_obj, "extruder_id", 0)); + safe_json_int_flexible(lane_obj, "extruder_id", -1)); if (tray.extruder_id < 0) { - tray.extruder_id = 0; + const auto it = lane_extruder.find(lane_key); + tray.extruder_id = it != lane_extruder.end() ? it->second : 0; } tray.tray_color = safe_json_string(lane_obj, "color"); tray.tray_type = safe_json_string(lane_obj, "material"); @@ -1045,6 +1098,82 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector& tra return true; } +// AFC writes its filament data into the lane_data database namespace, and the records of the +// fixture/agent write path carry no extruder field, so the lane -> toolhead binding has to be read +// from the printer objects: each AFC_lane .extruder names the toolhead its lane feeds, and +// the toolhead's name carries its own index. Best effort: on failure every lane keeps the fallback +// of the main extruder. +bool MoonrakerPrinterAgent::fetch_afc_lane_extruders(std::map& lane_extruder) +{ + auto query_objects = [this](const std::string& query_string, nlohmann::json& out) { + auto http = Http::get(join_url(device_info.base_url, "/printer/objects/query?" + query_string)); + if (!device_info.api_key.empty()) { + http.header("X-Api-Key", device_info.api_key); + } + std::string body; + unsigned status_code = 0; + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string response, unsigned status) { + body = std::move(response); + status_code = status; + }) + .perform_sync(); + if (status_code != 200) { + return false; + } + out = nlohmann::json::parse(body, nullptr, false, true); + return !out.is_discarded(); + }; + + nlohmann::json afc_json; + if (!query_objects("AFC", afc_json) || !afc_json.contains("result") || + !afc_json["result"].contains("status") || !afc_json["result"]["status"].contains("AFC")) { + return false; + } + + const auto& afc = afc_json["result"]["status"]["AFC"]; + const auto& lanes = afc.contains("lanes") ? afc["lanes"] : nlohmann::json::array(); + if (!lanes.is_array() || lanes.empty()) { + return false; + } + + // Query every lane object in one request; the object names are "AFC_lane ". + std::string query_string; + for (const auto& lane : lanes) { + if (!lane.is_string()) { + return false; + } + if (!query_string.empty()) { + query_string += "&"; + } + query_string += "AFC_lane%20" + lane.get(); + } + + nlohmann::json lanes_json; + if (!query_objects(query_string, lanes_json) || !lanes_json.contains("result") || + !lanes_json["result"].contains("status")) { + return false; + } + + const auto& status = lanes_json["result"]["status"]; + for (const auto& lane : lanes) { + const std::string name = lane.get(); + const std::string key = "AFC_lane " + name; + if (!status.contains(key) || !status[key].is_object() || !status[key].contains("extruder") || + !status[key]["extruder"].is_string()) { + continue; + } + const std::string toolhead = status[key]["extruder"].get(); + const int toolhead_index = extruder_index_from_name(toolhead); + if (toolhead_index >= 0) { + lane_extruder[name] = toolhead_index; + } + } + + return !lane_extruder.empty(); +} + int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) { auto json = nlohmann::json::parse(json_str, nullptr, false); diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index a4a18449b6..7773ac247c 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -168,6 +169,9 @@ private: // System-specific filament fetch methods bool fetch_hh_filament_info(std::vector& trays, int& max_lane_index); bool fetch_moonraker_filament_data(std::vector& trays, int& max_lane_index); + // Lane name -> toolhead index, read from the AFC printer objects (the lane_data database has no + // extruder field). Returns false when AFC is not present or the objects cannot be read. + bool fetch_afc_lane_extruders(std::map& lane_extruder); // Build one BBL tray JSON object (placeholder when the tray has no filament). static nlohmann::json make_ams_tray_json(const std::string& tray_id, const AmsTrayData& tray);