From 598df9ff0b53ec1dba243350d448179e058a8fa5 Mon Sep 17 00:00:00 2001 From: grant0013 Date: Tue, 19 May 2026 16:28:41 +0000 Subject: [PATCH] Add Creality K-series LAN discovery via DNS-SD mDNS When a user adds a Creality K-series printer (host_type=crealityprint) and clicks the existing "Browse" button in the Physical Printer dialog, dispatch to a new CrealityDiscoveryDialog that finds K2 / K2 Plus / K2 Pro printers on the LAN automatically. For other host types the button keeps its existing BonjourDialog behaviour. CrealityHostDiscovery (src/slic3r/Utils/CrealityHostDiscovery.{hpp,cpp}) Wraps the vendored cxmdns wrapper from the previous commit: static std::vector scan(bool probe_info = true); Calls cxnet::syncDiscoveryService({"Creality", "creality"}) to find K-series printers via DNS-SD, dedupes by IP, then optionally HTTP GETs http:///info on each match to fetch the model code (F008 = K2 Plus, F012 = K2 Pro, F021 = K2) and MAC. Returns enriched {ip, hostname, model_code, model_name, mac, cfs_capable} entries. CrealityDiscoveryDialog (src/slic3r/GUI/CrealityDiscoveryDialog.{hpp,cpp}) Modal dialog with a wxListView showing Model / Hostname / IP per discovered host. Runs CrealityHostDiscovery::scan() synchronously with wxBusyCursor + wxWindowDisabler (5-10s total wait). User picks one, dialog returns the IP via selected_ip(). PhysicalPrinterDialog (src/slic3r/GUI/PhysicalPrinterDialog.cpp) The "Browse" button's click handler now reads host_type from the edited config. If htCrealityPrint, opens CrealityDiscoveryDialog and writes "http://" into the print_host field. Otherwise the existing BonjourDialog path runs unchanged -- no behaviour change for OctoPrint / Moonraker / Klipper users. No new UI surface: one existing button now does the right thing per host_type, mirroring how Creality Print discovers its own printers. --- src/slic3r/CMakeLists.txt | 8 ++ src/slic3r/GUI/CrealityDiscoveryDialog.cpp | 122 ++++++++++++++++++++ src/slic3r/GUI/CrealityDiscoveryDialog.hpp | 49 ++++++++ src/slic3r/GUI/PhysicalPrinterDialog.cpp | 21 +++- src/slic3r/Utils/CrealityHostDiscovery.cpp | 128 +++++++++++++++++++++ src/slic3r/Utils/CrealityHostDiscovery.hpp | 45 ++++++++ 6 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 src/slic3r/GUI/CrealityDiscoveryDialog.cpp create mode 100644 src/slic3r/GUI/CrealityDiscoveryDialog.hpp create mode 100644 src/slic3r/Utils/CrealityHostDiscovery.cpp create mode 100644 src/slic3r/Utils/CrealityHostDiscovery.hpp diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index aed941f783..57a1ab04c3 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -56,6 +56,8 @@ set(SLIC3R_GUI_SOURCES GUI/BitmapComboBox.hpp GUI/BonjourDialog.cpp GUI/BonjourDialog.hpp + GUI/CrealityDiscoveryDialog.cpp + GUI/CrealityDiscoveryDialog.hpp GUI/calib_dlg.cpp GUI/calib_dlg.hpp GUI/Calibration.cpp @@ -578,6 +580,8 @@ set(SLIC3R_GUI_SOURCES Utils/CrealityPrint.hpp Utils/CrealityPrintAgent.cpp Utils/CrealityPrintAgent.hpp + Utils/CrealityHostDiscovery.cpp + Utils/CrealityHostDiscovery.hpp Utils/Duet.cpp Utils/Duet.hpp Utils/ElegooLink.cpp @@ -661,6 +665,10 @@ set(SLIC3R_GUI_SOURCES Utils/WxFontUtils.hpp Utils/FileTransferUtils.cpp Utils/FileTransferUtils.hpp + Utils/mdns/mdns.h + Utils/mdns/mdns.c + Utils/mdns/cxmdns.h + Utils/mdns/cxmdns.cpp ) add_subdirectory(GUI/DeviceCore) diff --git a/src/slic3r/GUI/CrealityDiscoveryDialog.cpp b/src/slic3r/GUI/CrealityDiscoveryDialog.cpp new file mode 100644 index 0000000000..be46182e5f --- /dev/null +++ b/src/slic3r/GUI/CrealityDiscoveryDialog.cpp @@ -0,0 +1,122 @@ +#include "CrealityDiscoveryDialog.hpp" +#include "slic3r/Utils/CrealityHostDiscovery.hpp" +#include "GUI_App.hpp" +#include "I18N.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace Slic3r { +namespace GUI { + +CrealityDiscoveryDialog::CrealityDiscoveryDialog(wxWindow* parent) + : wxDialog(parent, wxID_ANY, _L("Detect Creality K-series printer"), + wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +{ + const int em = wxGetApp().em_unit(); + + m_status = new wxStaticText(this, wxID_ANY, _L("Click Scan to look for K-series printers on your network.")); + + m_list = new wxListView(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, + wxLC_REPORT | wxSIMPLE_BORDER | wxLC_SINGLE_SEL); + m_list->SetMinSize(wxSize(50 * em, 18 * em)); + m_list->AppendColumn(_L("Model"), wxLIST_FORMAT_LEFT, 8 * em); + m_list->AppendColumn(_L("Hostname"), wxLIST_FORMAT_LEFT, 14 * em); + m_list->AppendColumn(_L("IP"), wxLIST_FORMAT_LEFT, 14 * em); + + auto* scan_btn = new wxButton(this, wxID_ANY, _L("Scan")); + auto* ok_btn = new wxButton(this, wxID_OK, _L("Use Selected")); + auto* cancel_btn = new wxButton(this, wxID_CANCEL, _L("Cancel")); + ok_btn->Disable(); + + auto* button_sizer = new wxBoxSizer(wxHORIZONTAL); + button_sizer->Add(scan_btn, 0, wxALL, em); + button_sizer->AddStretchSpacer(1); + button_sizer->Add(ok_btn, 0, wxALL, em); + button_sizer->Add(cancel_btn, 0, wxALL, em); + + auto* vsizer = new wxBoxSizer(wxVERTICAL); + vsizer->Add(m_status, 0, wxEXPAND | wxALL, em); + vsizer->Add(m_list, 1, wxEXPAND | wxALL, em); + vsizer->Add(button_sizer, 0, wxEXPAND); + SetSizerAndFit(vsizer); + + scan_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { run_discovery(); }); + + m_list->Bind(wxEVT_LIST_ITEM_SELECTED, [ok_btn](wxListEvent&) { ok_btn->Enable(); }); + m_list->Bind(wxEVT_LIST_ITEM_DESELECTED, [ok_btn](wxListEvent&) { ok_btn->Disable(); }); + m_list->Bind(wxEVT_LIST_ITEM_ACTIVATED, [this](wxListEvent&) { on_ok(); EndModal(wxID_OK); }); + + ok_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_ok(); EndModal(wxID_OK); }); + + wxGetApp().UpdateDlgDarkUI(this); + + // Run discovery synchronously from the ctor so results are ready by the + // time the dialog is shown. Posting an async CallAfter from a ShowModal + // override risked the event firing after the modal loop had exited -- the + // captured `this` would then be a dangling stack pointer and subsequent + // UI access could fault. + run_discovery(); +} + +void CrealityDiscoveryDialog::run_discovery() +{ + m_status->SetLabel(_L("Scanning the LAN for K-series printers... this takes a few seconds.")); + m_list->DeleteAllItems(); + m_rows.clear(); + Layout(); + Update(); + + std::vector hosts; + { + wxBusyCursor cursor; + wxWindowDisabler disabler(this); + hosts = CrealityHostDiscovery::scan(/*probe_info=*/true); + } + + for (const auto& h : hosts) { + Row row; + row.ip = h.ip; + row.hostname = h.hostname; + if (!h.model_name.empty()) + row.model = h.model_name; + else if (h.cfs_capable) + row.model = "(unknown K-series)"; + else + row.model = "Creality"; + m_rows.push_back(std::move(row)); + } + + for (size_t i = 0; i < m_rows.size(); ++i) { + long idx = m_list->InsertItem(i, wxString::FromUTF8(m_rows[i].model)); + m_list->SetItem(idx, 1, wxString::FromUTF8(m_rows[i].hostname)); + m_list->SetItem(idx, 2, wxString::FromUTF8(m_rows[i].ip)); + } + + if (m_rows.empty()) { + m_status->SetLabel(_L( + "No K-series printers found. Make sure the printer is on the same " + "network and not blocked by Wi-Fi client isolation, then click Scan again.")); + } else { + m_status->SetLabel(wxString::Format( + _L("Found %zu Creality printer(s). Select one and click Use Selected."), + m_rows.size())); + m_list->Select(0); + } +} + +void CrealityDiscoveryDialog::on_ok() +{ + auto sel = m_list->GetFirstSelected(); + if (sel >= 0 && sel < int(m_rows.size())) { + m_selected_ip = m_rows[sel].ip; + } +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/CrealityDiscoveryDialog.hpp b/src/slic3r/GUI/CrealityDiscoveryDialog.hpp new file mode 100644 index 0000000000..2aa347a456 --- /dev/null +++ b/src/slic3r/GUI/CrealityDiscoveryDialog.hpp @@ -0,0 +1,49 @@ +#ifndef slic3r_CrealityDiscoveryDialog_hpp_ +#define slic3r_CrealityDiscoveryDialog_hpp_ + +#include + +#include +#include + +class wxListView; +class wxStaticText; + +namespace Slic3r { +namespace GUI { + +// Modal dialog that finds Creality K-series printers on the LAN via DNS-SD +// mDNS (vendored mjansson/mdns + cxmdns wrapper) and lets the user pick one. +// +// Discovery is synchronous (~5 second mDNS listen + ~2-4 sec /info probe per +// host) and runs during ShowModal() with a busy cursor. The user-facing busy +// time is bounded by the mDNS listener's 5-second deadline plus any in-flight +// probes; in practice 5-10 seconds total for a typical LAN with one K2. +// +// After ShowModal() returns wxID_OK, selected_ip() yields the chosen +// printer's IPv4 address. Returns the empty string on Cancel or if no match +// was found. +class CrealityDiscoveryDialog : public wxDialog +{ +public: + CrealityDiscoveryDialog(wxWindow* parent); + ~CrealityDiscoveryDialog() override = default; + + std::string selected_ip() const { return m_selected_ip; } + +private: + void run_discovery(); + void on_ok(); + + struct Row { std::string ip; std::string model; std::string hostname; }; + + wxListView* m_list = nullptr; + wxStaticText* m_status = nullptr; + std::vector m_rows; + std::string m_selected_ip; +}; + +} // namespace GUI +} // namespace Slic3r + +#endif diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 667919aca3..78847e73ed 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -35,6 +35,7 @@ #include "RemovableDriveManager.hpp" #include "BitmapCache.hpp" #include "BonjourDialog.hpp" +#include "CrealityDiscoveryDialog.hpp" #include "MsgDialog.hpp" #include "OAuthDialog.hpp" #include "SimplyPrint.hpp" @@ -200,10 +201,28 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr return sizer; }; - auto printhost_browse = [=](wxWindow* parent) + auto printhost_browse = [=](wxWindow* parent) { auto sizer = create_sizer_with_btn(parent, &m_printhost_browse_btn, "printer_host_browser", _L("Browse") + " " + dots); m_printhost_browse_btn->Bind(wxEVT_BUTTON, [=](wxCommandEvent& e) { + // Creality K-series printers announce themselves via DNS-SD under a + // per-device-unique service type _Creality-._udp, so the + // standard fixed-service-name Bonjour browser does not find them. + // Dispatch to the Creality-specific scanner instead. + const auto* host_type_opt = m_config->option>("host_type"); + const auto host_type = host_type_opt ? host_type_opt->value : htOctoPrint; + if (host_type == htCrealityPrint) { + CrealityDiscoveryDialog dialog(this); + if (dialog.ShowModal() == wxID_OK && !dialog.selected_ip().empty()) { + // set_value expects the value wrapped as wxString -- TextCtrl::set_value + // any_casts to wxString, so a raw std::string throws bad_any_cast. + wxString new_url = wxString::FromUTF8("http://" + dialog.selected_ip()); + m_optgroup->set_value("print_host", new_url, true); + m_optgroup->get_field("print_host")->field_changed(); + } + return; + } + BonjourDialog dialog(this, Preset::printer_technology(*m_config)); if (dialog.show_and_lookup()) { m_optgroup->set_value("print_host", dialog.get_selected(), true); diff --git a/src/slic3r/Utils/CrealityHostDiscovery.cpp b/src/slic3r/Utils/CrealityHostDiscovery.cpp new file mode 100644 index 0000000000..bab570c116 --- /dev/null +++ b/src/slic3r/Utils/CrealityHostDiscovery.cpp @@ -0,0 +1,128 @@ +#include "CrealityHostDiscovery.hpp" +#include "mdns/cxmdns.h" +#include "Http.hpp" + +#include +#include + +#include + +namespace Slic3r { + +namespace { + +struct ModelEntry { const char* code; const char* name; }; +constexpr ModelEntry kCfsCapableModels[] = { + {"F008", "K2 Plus"}, + {"F012", "K2 Pro"}, + {"F021", "K2"}, +}; + +bool is_cfs_capable(const std::string& code) +{ + for (const auto& m : kCfsCapableModels) + if (code == m.code) return true; + return false; +} + +std::string model_name_for(const std::string& code) +{ + for (const auto& m : kCfsCapableModels) + if (code == m.code) return m.name; + return {}; +} + +// Extract the device suffix from a service name like +// "_Creality-543324280CDB19._udp.local." and synthesise a hostname-ish label +// (e.g. "K2-DB19" using the last 4 hex of the MAC-derived suffix). +std::string hostname_from_service(const std::string& service_name) +{ + auto dash = service_name.find_last_of('-'); + if (dash == std::string::npos) return {}; + auto dot = service_name.find('.', dash); + if (dot == std::string::npos) return {}; + std::string suffix = service_name.substr(dash + 1, dot - dash - 1); + if (suffix.size() >= 4) { + return "K2-" + suffix.substr(suffix.size() - 4); + } + return suffix.empty() ? std::string{} : "K2-" + suffix; +} + +// Synchronously probe http:///info for {model, mac}. Short timeout -- +// we don't want one slow host to drag down discovery. +void probe_info(CrealityHost& host) +{ + const std::string url = "http://" + host.ip + "/info"; + auto http = Http::get(url); + http.timeout_connect(2) + .timeout_max(4) + .on_complete([&host](std::string body, unsigned /*status*/) { + try { + auto j = nlohmann::json::parse(body); + if (j.contains("model") && j["model"].is_string()) + host.model_code = j["model"].get(); + if (j.contains("mac") && j["mac"].is_string()) + host.mac = j["mac"].get(); + if (is_cfs_capable(host.model_code)) { + host.cfs_capable = true; + host.model_name = model_name_for(host.model_code); + } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) + << "CrealityHostDiscovery: /info parse failed for " + << host.ip << ": " << e.what(); + } + }) + .on_error([&host](std::string /*body*/, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(info) + << "CrealityHostDiscovery: /info GET failed for " + << host.ip << ": " << error << " (HTTP " << status << ")"; + }) + .perform_sync(); +} + +} // namespace + +std::vector CrealityHostDiscovery::scan(bool probe) +{ + const std::vector prefixes{ "Creality", "creality" }; + + BOOST_LOG_TRIVIAL(info) + << "CrealityHostDiscovery: starting DNS-SD discovery (prefixes: Creality, creality)"; + + auto raw = cxnet::syncDiscoveryService(prefixes); + + BOOST_LOG_TRIVIAL(info) + << "CrealityHostDiscovery: mDNS returned " << raw.size() << " match(es)"; + + std::vector hosts; + hosts.reserve(raw.size()); + + // Dedupe by IP -- one printer may announce twice if multi-homed or if + // we capture both IPv4/IPv6 replies. + std::vector seen_ips; + for (const auto& m : raw) { + if (m.machineIp.empty()) continue; + if (std::find(seen_ips.begin(), seen_ips.end(), m.machineIp) != seen_ips.end()) + continue; + seen_ips.push_back(m.machineIp); + + CrealityHost h; + h.ip = m.machineIp; + h.service_name = m.answer; + h.hostname = hostname_from_service(m.answer); + + if (probe) { + probe_info(h); + } + + hosts.push_back(std::move(h)); + } + + BOOST_LOG_TRIVIAL(info) + << "CrealityHostDiscovery: " << hosts.size() << " unique host(s) after dedup"; + + return hosts; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/CrealityHostDiscovery.hpp b/src/slic3r/Utils/CrealityHostDiscovery.hpp new file mode 100644 index 0000000000..be9b85faa1 --- /dev/null +++ b/src/slic3r/Utils/CrealityHostDiscovery.hpp @@ -0,0 +1,45 @@ +#ifndef slic3r_CrealityHostDiscovery_hpp_ +#define slic3r_CrealityHostDiscovery_hpp_ + +#include +#include + +namespace Slic3r { + +// One discovered Creality K-series host on the LAN. +struct CrealityHost +{ + std::string ip; // dotted-quad IPv4 + std::string service_name; // raw mDNS service type, e.g. "_Creality-543324280CDB19._udp.local." + std::string hostname; // e.g. "K2-DB19" (derived from service-name suffix) + std::string model_code; // "F008" / "F012" / "F021" (empty if /info probe failed) + std::string model_name; // "K2 Plus" / "K2 Pro" / "K2" (empty if model not in our table) + std::string mac; // from /info if probed + bool cfs_capable = false; // true when model_code is in the K2 family +}; + +// Synchronous LAN discovery for Creality K-series printers via DNS-SD mDNS. +// +// Sends a meta-discovery query (_services._dns-sd._udp.local.) and listens +// for ~5 seconds for service announcements whose type-name contains the +// "Creality" / "creality" substring. K-series firmware announces each +// printer under a per-device-unique type _Creality-._udp.local, +// so a fixed-name query does not work -- the meta-discovery is the only +// reliable way to find them. +// +// When probe_info is true, each discovered host is followed up with an HTTP +// GET http:///info call to fetch the printer's model code (F008/F012/F021) +// and MAC. The probe step adds ~2-4 seconds per host but yields enriched +// results that let the UI display "K2" / "K2 Plus" / "K2 Pro" instead of +// just an IP. +// +// Call from a background thread -- the function blocks for at least 5 seconds. +class CrealityHostDiscovery +{ +public: + static std::vector scan(bool probe_info = true); +}; + +} // namespace Slic3r + +#endif