diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b767529a54..5325c4a199 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -65,6 +65,8 @@ #include "libslic3r/SLAPrint.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/PresetBundle.hpp" +#include "slic3r/Utils/CrealityPrint.hpp" +#include "slic3r/Utils/CrealityPrintAgent.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ObjColorUtils.hpp" // For stl export @@ -3479,6 +3481,18 @@ void Sidebar::load_ams_list(MachineObject* obj) void Sidebar::sync_ams_list(bool is_from_big_sync_btn) { + // K-series Creality CFS hosts don't bind a MachineObject (that's a BBL + // cloud-connected-printer concept), so the dispatch below would early-return. + // Route to the explicit-pull CFS sync instead. + if (auto* preset_bundle = wxGetApp().preset_bundle) { + const auto* opt = preset_bundle->printers.get_edited_preset().config + .option>("host_type"); + if (opt && opt->value == htCrealityPrint) { + sync_filaments_from_creality_cfs(); + return; + } + } + wxBusyCursor cursor; // Force load ams list auto obj = wxGetApp().getDeviceManager()->get_selected_machine(); @@ -3719,6 +3733,205 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "finish pop_finsish_sync_ams_dialog"; } +// Explicit-pull filament sync for Creality K-series printers with CFS. +// +// We can't reuse the BBL path (sync_ams_list above) because it requires a +// MachineObject bound to the device, which is a cloud-connected-printer concept +// that doesn't apply to LAN Moonraker-style hosts like the K2. Instead, we query +// the CFS over the K2's port-9999 WebSocket directly (using the CrealityPrint +// helper added in upstream PR #13291), then route the slot data through the +// existing PresetBundle::sync_ams_list machinery so the filament combo widgets +// get the same correct rebuild as BBL printers. +// +// PresetBundle::sync_ams_list resolves each tray's filament_id only against +// system *base* presets (get_preset_base(f) == &f), so user-edited copies of +// system presets (the typical K-series workflow — copy "Creality Generic PLA +// @K2-all" and tune PA/temps for one's specific spool) are collapsed back to +// the system base. We replay the user-copy assignment after sync_ams_list +// returns to preserve the matcher's preference. +void Sidebar::sync_filaments_from_creality_cfs() +{ + auto* preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle) + return; + + const auto& printer_cfg = preset_bundle->printers.get_edited_preset().config; + const auto* host_type_opt = printer_cfg.option>("host_type"); + if (!host_type_opt || host_type_opt->value != htCrealityPrint) { + BOOST_LOG_TRIVIAL(warning) + << "sync_filaments_from_creality_cfs: host_type is not crealityprint"; + return; + } + + wxBusyCursor cursor; + + // Build a minimal config for the CrealityPrint helper. We can't pass + // printer_cfg directly because its option set is wider than CrealityPrint's + // constructor expects. + DynamicPrintConfig cfg; + cfg.set_key_value("print_host", new ConfigOptionString(printer_cfg.opt_string("print_host"))); + cfg.set_key_value("print_host_webui", new ConfigOptionString(printer_cfg.opt_string("print_host_webui"))); + cfg.set_key_value("printhost_cafile", new ConfigOptionString(printer_cfg.opt_string("printhost_cafile"))); + cfg.set_key_value("printhost_port", new ConfigOptionString(printer_cfg.opt_string("printhost_port"))); + cfg.set_key_value("printhost_apikey", new ConfigOptionString(printer_cfg.opt_string("printhost_apikey"))); + cfg.set_key_value("printhost_ssl_ignore_revoke", new ConfigOptionBool(printer_cfg.opt_bool("printhost_ssl_ignore_revoke"))); + + CrealityPrint host(&cfg); + if (!host.supports_multi_color_print()) { + BOOST_LOG_TRIVIAL(warning) + << "sync_filaments_from_creality_cfs: " << host.model_name() + << " is not CFS-capable"; + MessageDialog dlg(this, + _L("This printer is not a CFS-capable K-series board, or could not be reached at its configured IP."), + _L("Sync filaments from CFS"), wxOK); + dlg.ShowModal(); + return; + } + + BOOST_LOG_TRIVIAL(warning) + << "sync_filaments_from_creality_cfs: querying CFS slots on " << host.model_name(); + + const std::string response = host.query_boxes_info(); + + std::vector slots; + int box_count = 0; + std::string parse_err; + if (!CrealityPrintAgent::parse_cfs_response(response, slots, box_count, parse_err)) { + BOOST_LOG_TRIVIAL(warning) + << "sync_filaments_from_creality_cfs: CFS query failed (" << parse_err << ")"; + MessageDialog dlg(this, + _L("Could not read CFS slot information from the printer.") + "\n" + parse_err, + _L("Sync filaments from CFS"), wxOK); + dlg.ShowModal(); + return; + } + + if (slots.empty()) { + MessageDialog dlg(this, + _L("No loaded filament slots detected on the CFS."), + _L("Sync filaments from CFS"), wxOK); + dlg.ShowModal(); + return; + } + + BOOST_LOG_TRIVIAL(warning) + << "sync_filaments_from_creality_cfs: " << box_count << " CFS box(es), " + << slots.size() << " loaded slot(s)"; + + auto& filaments = preset_bundle->filaments; + + // Build filament_ams_list — same shape as Sidebar::build_filament_ams_list produces + // for BBL printers, consumed by PresetBundle::sync_ams_list. + // Map key encodes (extruder, ams_id, slot_id) — main extruder uses the 0x10000 prefix. + constexpr int kMainExtruder = 0x10000; + std::map new_filament_ams_list; + + // Side table of user-copy preset names per filament index. PresetBundle::sync_ams_list + // collapses filament_id back to the SYSTEM base preset name; we replay the user copy + // assignment after it returns to preserve the matcher's preference. + std::map user_preset_overrides; + + for (const auto& s : slots) { + const std::string normalized_type = CrealityPrintAgent::normalize_filament_type(s.filament_type); + const std::string matched_id = CrealityPrintAgent::match_filament_preset( + filaments, s.vendor, s.brand_name, normalized_type); + + // Find the best user copy with the matched filament_id, if any. The matcher + // already biases toward user copies for scoring; this re-finds the best user + // copy by filament_id so we can override after sync_ams_list. + const Preset* best_user = nullptr; + if (!matched_id.empty()) { + for (const auto& p : filaments.get_presets()) { + if (!p.is_visible || !p.is_compatible) continue; + if (p.filament_id != matched_id) continue; + if (p.is_system || p.is_default) continue; + if (!best_user) { + best_user = &p; + break; // first user copy wins; matcher's scoring tiebreak already ran + } + } + } + + DynamicPrintConfig tray_config; + tray_config.set_key_value("filament_id", new ConfigOptionStrings{matched_id}); + tray_config.set_key_value("tag_uid", new ConfigOptionStrings{std::string()}); + tray_config.set_key_value("ams_id", new ConfigOptionStrings{std::to_string(s.box_id)}); + tray_config.set_key_value("slot_id", new ConfigOptionStrings{std::to_string(s.slot_id)}); + tray_config.set_key_value("filament_type", new ConfigOptionStrings{normalized_type}); + const std::string tray_name = std::string(1, char('A' + s.box_id)) + std::to_string(s.slot_id + 1); + tray_config.set_key_value("tray_name", new ConfigOptionStrings{tray_name}); + tray_config.set_key_value("filament_colour", new ConfigOptionStrings{s.color_hex.empty() ? std::string("#FFFFFF") : s.color_hex}); + tray_config.set_key_value("filament_multi_colour", new ConfigOptionStrings{}); + tray_config.set_key_value("filament_colour_type", new ConfigOptionStrings{std::string("0")}); + tray_config.set_key_value("filament_exist", new ConfigOptionBools{true}); + tray_config.set_key_value("filament_slot_placeholder", new ConfigOptionBools{false}); + tray_config.set_key_value("filament_is_support", new ConfigOptionBools{false}); + + const int slot_in_filament_array = s.box_id * 4 + s.slot_id; + const int map_key = kMainExtruder + slot_in_filament_array; + new_filament_ams_list.emplace(map_key, std::move(tray_config)); + + if (best_user) { + user_preset_overrides[slot_in_filament_array] = best_user->name; + BOOST_LOG_TRIVIAL(warning) + << "sync_filaments_from_creality_cfs: slot " << slot_in_filament_array + << " spool {" << s.vendor << " " << s.brand_name << " (" << normalized_type + << ")} -> filament_id=\"" << matched_id + << "\" user-override=\"" << best_user->name << "\""; + } else { + BOOST_LOG_TRIVIAL(warning) + << "sync_filaments_from_creality_cfs: slot " << slot_in_filament_array + << " spool {" << s.vendor << " " << s.brand_name << " (" << normalized_type + << ")} -> filament_id=\"" << matched_id + << "\" (no user copy; system base will be used)"; + } + } + + preset_bundle->filament_ams_list = new_filament_ams_list; + + std::vector> unknowns; + std::map empty_maps; + MergeFilamentInfo merge_info; + auto n = preset_bundle->sync_ams_list(unknowns, false /*use_map*/, empty_maps, + false /*enable_append*/, merge_info, false /*color_only*/); + + BOOST_LOG_TRIVIAL(warning) + << "sync_filaments_from_creality_cfs: PresetBundle::sync_ams_list returned " << n; + + if (n == 0) { + MessageDialog dlg(this, + _L("CFS slots were read but no compatible filament presets resolved. " + "Enable the relevant filament profiles via the Filaments tickbox in printer settings."), + _L("Sync filaments from CFS"), wxOK); + dlg.ShowModal(); + return; + } + + // Override system-base names with the user copies the matcher preferred. + auto& filament_presets = preset_bundle->filament_presets; + for (const auto& [slot_idx, user_name] : user_preset_overrides) { + if (slot_idx >= 0 && slot_idx < int(filament_presets.size())) { + BOOST_LOG_TRIVIAL(warning) + << "sync_filaments_from_creality_cfs: overriding slot " << slot_idx + << " from \"" << filament_presets[slot_idx] << "\" to \"" << user_name << "\""; + filament_presets[slot_idx] = user_name; + } + } + + // Mirror the BBL post-sync refresh sequence. + wxGetApp().plater()->on_filament_count_change(n); + for (auto& c : p->combos_filament) + c->update(); + update_filaments_area_height(); + Layout(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(filament_presets[0]); + preset_bundle->export_selections(*wxGetApp().app_config); + update_dynamic_filament_list(); + + BOOST_LOG_TRIVIAL(warning) + << "sync_filaments_from_creality_cfs: applied " << n << " slot(s)"; +} + bool Sidebar::should_show_SEMM_buttons() { PresetBundle &preset_bundle = *wxGetApp().preset_bundle; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index f2089c1c4f..5d31ff9b08 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -193,6 +193,11 @@ public: void load_ams_list(MachineObject* obj); std::map build_filament_ams_list(MachineObject* obj); void sync_ams_list(bool is_from_big_sync_btn = false); + // Explicit-pull filament sync for Creality K-series hosts (host_type=crealityprint). + // The BBL-style sync_ams_list path requires a bound MachineObject which is never + // created for LAN Moonraker hosts. sync_ams_list dispatches here when host_type + // is crealityprint, but the function is also usable on its own. + void sync_filaments_from_creality_cfs(); bool sync_extruder_list(); bool need_auto_sync_extruder_list_after_connect_priner(const MachineObject* obj); void update_sync_status(const MachineObject* obj); diff --git a/src/slic3r/Utils/CrealityPrintAgent.cpp b/src/slic3r/Utils/CrealityPrintAgent.cpp index 39f83f73d0..f6f873832d 100644 --- a/src/slic3r/Utils/CrealityPrintAgent.cpp +++ b/src/slic3r/Utils/CrealityPrintAgent.cpp @@ -27,6 +27,8 @@ bool has_visible_base_preset(const PresetCollection& filaments, const std::strin return false; } +} // namespace + // Score visible compatible filament presets against the CFS spool metadata and // return the best-matching filament_id. Scoring: // +20 preset name contains brand_name as a substring @@ -38,10 +40,10 @@ bool has_visible_base_preset(const PresetCollection& filaments, const std::strin // Requires the preset's declared filament_type to equal the spool's base type // (PLA/PETG/ABS/...) so we never auto-pick a PETG preset for a PLA spool. // Falls back to filaments.filament_id_by_type(base_type) when nothing scores. -std::string match_filament_preset(const PresetCollection& filaments, - const std::string& vendor, - const std::string& brand_name, - const std::string& base_type) +std::string CrealityPrintAgent::match_filament_preset(const PresetCollection& filaments, + const std::string& vendor, + const std::string& brand_name, + const std::string& base_type) { auto to_lower = [](std::string s) { for (auto& c : s) c = static_cast(std::tolower(static_cast(c))); @@ -112,8 +114,6 @@ std::string match_filament_preset(const PresetCollection& filaments, return matches.front().preset->filament_id; } -} // namespace - CrealityPrintAgent::CrealityPrintAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) { @@ -131,8 +131,6 @@ AgentInfo CrealityPrintAgent::get_agent_info_static() std::string CrealityPrintAgent::normalize_filament_type(const std::string& filament_type) { - // Strip subtype suffixes ("PLA Silk", "PLA+", "ABS Pro") to base type so the - // preset_bundle->filaments.filament_id_by_type() lookup succeeds. static const std::vector bases = { "PETG", "PET", "PLA", "ABS", "ASA", "TPU", "PC", "PA", "PVA", "HIPS" }; diff --git a/src/slic3r/Utils/CrealityPrintAgent.hpp b/src/slic3r/Utils/CrealityPrintAgent.hpp index 348f742891..288aa8a9d8 100644 --- a/src/slic3r/Utils/CrealityPrintAgent.hpp +++ b/src/slic3r/Utils/CrealityPrintAgent.hpp @@ -8,6 +8,8 @@ namespace Slic3r { +class PresetCollection; + // Filament sync for Creality K-series printers with CFS. // // Inherits MoonrakerPrinterAgent for all communication / certificates / discovery / @@ -19,19 +21,15 @@ namespace Slic3r { // Model detection delegated to CrealityPrint::supports_multi_color_print() (PR #13291). // For non-CFS K-series boards or when the WS query fails, falls back to the base // MoonrakerPrinterAgent behaviour. +// +// The CFS-slot parser and preset matcher are also exposed as public statics so the +// Sidebar's manual "Sync from CFS" path can reuse them without going through the +// agent dispatch (which only fires when a MachineObject is bound — a BBL concept that +// doesn't apply to Moonraker-style hosts). class CrealityPrintAgent final : public MoonrakerPrinterAgent { public: - explicit CrealityPrintAgent(std::string log_dir); - ~CrealityPrintAgent() override = default; - - static AgentInfo get_agent_info_static(); - AgentInfo get_agent_info() override { return get_agent_info_static(); } - - bool fetch_filament_info(std::string dev_id) override; - -private: struct CFSSlot { int box_id = 0; // CFS unit index (0 for first box, 1 for chained second box) @@ -42,12 +40,31 @@ private: std::string vendor; // "Creality", "eSUN", or "" if unknown }; + explicit CrealityPrintAgent(std::string log_dir); + ~CrealityPrintAgent() override = default; + + static AgentInfo get_agent_info_static(); + AgentInfo get_agent_info() override { return get_agent_info_static(); } + + bool fetch_filament_info(std::string dev_id) override; + + // Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info() into + // a flat list of loaded slots, plus the count of CFS boxes the printer reports. static bool parse_cfs_response(const std::string& response, std::vector& slots, int& box_count, std::string& error); + // Strip PLA/PETG/... subtype suffixes ("PLA Silk", "PLA+", "ABS Pro") to base + // type so the preset_bundle->filaments.filament_id_by_type() lookup succeeds. static std::string normalize_filament_type(const std::string& filament_type); + + // Score visible compatible filament presets against the CFS spool metadata and + // return the best-matching filament_id. See implementation for scoring details. + static std::string match_filament_preset(const PresetCollection& filaments, + const std::string& vendor, + const std::string& brand_name, + const std::string& base_type); }; } // namespace Slic3r