From 23f6771dcb7385f8afd10bbe555bccb76115aa72 Mon Sep 17 00:00:00 2001 From: peachismomo Date: Sun, 11 Oct 2026 20:11:47 +0800 Subject: [PATCH] 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);