Creality K-series support: LAN discovery + CFS filament sync + filament profiles (#13752)

## Summary

Adds end-to-end Creality K-series (K2 / K2 Plus / K2 Pro) host support
to OrcaSlicer in a single bundle, per [@SoftFever's request to
consolidate](https://github.com/OrcaSlicer/OrcaSlicer/pull/13752#issuecomment-4560837450)
the previously stacked PRs. Three logically separable features, all
gated on `host_type=crealityprint`:

1. **LAN auto-discovery** — `Browse...` in the Physical Printer dialog
now finds K-series printers on the local network via a DNS-SD
meta-browser (per-device-unique service names
`_Creality-<MAC>._udp.local.`). Other host types unchanged.
2. **CFS filament sync** — `CrealityPrintAgent` (inheriting
`MoonrakerPrinterAgent`) queries the K-series WebSocket on `:9999` for
`boxsInfo`, maps loaded CFS slots to Orca filament presets, and
populates the Sidebar via the standard `fetch_filament_info` →
`build_ams_payload` path. Matches the shape of `MoonrakerPrinterAgent` /
`QidiPrinterAgent` / `SnapmakerPrinterAgent` per [the earlier review
feedback](https://github.com/OrcaSlicer/OrcaSlicer/pull/13752#discussion_r3278574545).
3. **K-series filament profiles** — system profiles for CR-PLA / CR-PETG
/ CR-ABS / CR-Silk / CR-TPU / CR-Nylon / CR-Wood / Hyper PLA / etc. on
K2 / K2 Plus / K2 Pro nozzle sizes (imported from CrealityPrint v7.1.0+,
normalised to OrcaSlicer profile conventions).

The previous stack base (#13291, *CrealityPrint as host type*, by
@imammedo) is **also bundled into this PR** since it's currently
conflicting with main and not moving. Happy to extract it back out if
@imammedo's PR is preferred to land first for attribution — let me know.

## What this PR is *not*

- **No new UI surfaces.** All three features hook into existing UI
(Browse button, Sidebar sync icon, filament dropdowns).
- **No phone-home / telemetry.** No Hark Tech endpoints, no licence
checks, no opt-in dialogs. Pure upstream feature work.
- **No K-series-specific Device tab.** Embedded WebView falls back to
Fluidd/Mainsail on `:4408`, same shape as the existing Moonraker
integration.

## Screenshots

Captured against a K2 Combo (F021, firmware v1.1.260206) on the v4 test
build:

| | |
|---|---|
| ![Discovery
dialog](https://harktech.co.uk/tools/orca-k2/screenshots/orca-k2-discovery-dialog.png)
| **Discovery dialog** — `Browse...` flow on a `host_type=crealityprint`
printer. Click → ~5–10 s LAN scan → K2 found with model + hostname + IP.
|
| ![Filament sidebar populated from
CFS](https://harktech.co.uk/tools/orca-k2/screenshots/orca-k2-cfs-sync-filaments.png)
| **CFS filament sync** — Sidebar after clicking the sync icon: 4 slots
populate with the real loaded CFS spools (3× Hyper PLA + 1× CR-Silk). |
| ![Device tab showing
Mainsail](https://harktech.co.uk/tools/orca-k2/screenshots/orca-k2-device-tab-mainsail.png)
| **Device tab** — Mainsail loaded into the embedded WebView for
`host_type=crealityprint`, mid-print state visible. |

## What's added

### LAN discovery
- **`deps_src/mdns/`** — vendors
[mjansson/mdns](https://github.com/mjansson/mdns) (public domain) plus
Creality's `cxmdns` C++ wrapper from CrealityPrint v7.1.1 (AGPL-3.0,
compatible with OrcaSlicer's AGPL-3.0). Attribution in
`deps_src/mdns/NOTICE.md`.
- **`Utils/CrealityHostDiscovery.{hpp,cpp}`** — synchronous DNS-SD scan
+ per-host `GET /info` probe. Maps model codes `F008` / `F012` / `F021`
→ K2 Plus / K2 Pro / K2.
- **`GUI/CrealityDiscoveryDialog.{hpp,cpp}`** — modal `wxDialog` showing
Model / Hostname / IP for each discovered host.
- **`src/slic3r/CMakeLists.txt`** — adds `Iphlpapi.lib` and `Ws2_32.lib`
to `libslic3r_gui`'s MSVC link line (needed by `GetAdaptersAddresses` +
Winsock2 calls in vendored `mdns.c`).

### CFS filament sync
- **`Utils/CrealityPrintAgent.{hpp,cpp}`** — inherits
`MoonrakerPrinterAgent`, overrides `fetch_filament_info()` to query the
K-series WS protocol on `:9999`, build `AmsTrayData`, and call inherited
`build_ams_payload()`. No printer-specific code lives outside the agent.
- K2 Plus slot-state parser handles the three documented slot states
(`0` empty / `1` manually entered / `2` RFID-tagged) per [DaviBe92's
reverse-engineering docs](https://github.com/DaviBe92/k2-websocket-re).

### K-series filament profiles
- ~110 profile JSONs under `resources/profiles/Creality/filament/`
covering K2 / K2 Plus / K2 Pro × 0.2 / 0.4 / 0.6 / 0.8 nozzle combos ×
CR-PLA / CR-PETG / CR-ABS / CR-Silk / CR-TPU / CR-Nylon / CR-Wood /
Hyper PLA / Hyper PETG-GF / Hyper PLA-CF / etc.
- Imported from CrealityPrint v7.1.0; normalised to OrcaSlicer profile
conventions (tabs not spaces, no `{if !multicolor_method}` wrappers,
`filament_vendor: ["Creality"]` on Creality Generic profiles).

## Tester confirmations on the v4 test build

| Printer | Firmware | Result | Reporter |
|---|---|---|---|
| K2 Pro | v1.1.5.5 / CFS v1.4.2 |  LAN discovery on #13752 test build
|
[@Requiem-MH](https://github.com/OrcaSlicer/OrcaSlicer/pull/13752#issuecomment-4495235225)
|
| K2 Pro | v1.1.5.5 / CFS v1.4.2 |  CFS sync across 1-CFS, 2-CFS,
partial, full configurations |
[@Requiem-MH](https://github.com/OrcaSlicer/OrcaSlicer/pull/13744#issuecomment-4495230061)
|
| K2 Plus | v1.1.5.2 / CFS v1.2.2 |  Slot-state fix resolves the
partial-sync regression |
[@DaviBe92](https://github.com/OrcaSlicer/OrcaSlicer/pull/13744#issuecomment-4499425852)
|
| K2 Plus | v1.1.5.5 / CFS v1.4.2 |  All slots syncing correctly after
fix |
[@swilsonnc](https://github.com/OrcaSlicer/OrcaSlicer/pull/13744#issuecomment-4503273127)
|
| K2 Plus | (Reddit u/TrainAss) |  Both PLA + PETG slots populated
correctly |
[@TrainAss](https://github.com/OrcaSlicer/OrcaSlicer/pull/13744#issuecomment-4503401664)
|
| K1C | (latest stock) |  `boxsInfo` payload format compatible (4 slots
of generic PETG) |
[@JoveYu](https://github.com/OrcaSlicer/OrcaSlicer/pull/13744#issuecomment-4519036448)
|

## Known follow-ups (out of scope)

- **Snapmaker U1 regression**
([@TrainAss](https://github.com/OrcaSlicer/OrcaSlicer/pull/13744#issuecomment-4529350262)):
the v3 build also happened to sync filament from his U1; v4 regressed
this. The refactor only touches `htCrealityPrint`-gated code so this is
likely incidental — needs his config + logs to diagnose. Will follow up
in a separate issue once this lands.
- **Native Device tab for K-series**: deferred. Current Mainsail WebView
shim covers the common case.
- **#13581 (@hamham999) profile overlap**: confirmed minimal code
conflict (zero), profile-file overlap of ~204 files. Whichever PR lands
second rebases off the other.

## Test plan

- [x] Linux build clean on commit `<UPDATED AFTER BUILD>` (LXC 104, GCC
12, cmake)
- [x] MSVC link clean (manual VS 2026 / MSVC 14.51 build)
- [x] End-to-end on real hardware: K2 Combo, K2 Pro, K2 Plus, K1C
- [x] `host_type ≠ htCrealityPrint` paths unchanged — Bonjour fires for
OctoPrint, Flashforge picker fires for Flashforge, Moonraker / Qidi /
Snapmaker agents unchanged
- [x] Profile-validation CI green (was a separate Elegoo test-fixture
failure on main, not introduced by this PR)

Signed-off-by: Igor Mammedov <niallain@gmail.com>
Co-authored-by: Igor Mammedov <niallain@gmail.com>
Co-authored-by: grant0013 <grant@harktech.co.uk>
Co-authored-by: SoftFever <softfeverever@gmail.com>
Co-authored-by: hamham999 <hamham999@users.noreply.github.com>
Co-authored-by: Alys Andreollo <3528187+alysandreollo@users.noreply.github.com>
This commit is contained in:
SoftFever
2026-06-07 13:16:50 +08:00
committed by GitHub
42 changed files with 4652 additions and 200 deletions

View File

@@ -0,0 +1,118 @@
#include "CrealityDiscoveryDialog.hpp"
#include "slic3r/Utils/CrealityHostDiscovery.hpp"
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "Widgets/DialogButtons.hpp"
#include <wx/sizer.h>
#include <wx/button.h>
#include <wx/listctrl.h>
#include <wx/stattext.h>
#include <wx/utils.h>
#include <boost/log/trivial.hpp>
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* dlg_btns = new DialogButtons(this, {"Scan", "OK", "Cancel"}, "", 1 /*left_aligned*/);
auto* ok_btn = dlg_btns->GetOK();
ok_btn->SetLabel(_L("Use Selected"));
ok_btn->Disable();
auto* vsizer = new wxBoxSizer(wxVERTICAL);
vsizer->Add(m_status, 0, wxEXPAND | wxALL, em);
vsizer->Add(m_list, 1, wxEXPAND | wxALL, em);
vsizer->Add(dlg_btns, 0, wxEXPAND);
SetSizerAndFit(vsizer);
dlg_btns->GetFIRST()->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); });
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
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<CrealityHost> 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

View File

@@ -0,0 +1,49 @@
#ifndef slic3r_CrealityDiscoveryDialog_hpp_
#define slic3r_CrealityDiscoveryDialog_hpp_
#include <wx/dialog.h>
#include <string>
#include <vector>
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<Row> m_rows;
std::string m_selected_ip;
};
} // namespace GUI
} // namespace Slic3r
#endif

View File

@@ -37,6 +37,7 @@
#include "RemovableDriveManager.hpp"
#include "BitmapCache.hpp"
#include "BonjourDialog.hpp"
#include "CrealityDiscoveryDialog.hpp"
#include "MsgDialog.hpp"
#include "OAuthDialog.hpp"
#include "SimplyPrint.hpp"
@@ -203,11 +204,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) {
const auto host_type = m_config->opt_enum<PrintHostType>("host_type");
// Creality K-series printers announce themselves via DNS-SD under a
// per-device-unique service type _Creality-<MAC-hex>._udp, so the
// standard fixed-service-name Bonjour browser does not find them.
// Dispatch to the Creality-specific scanner instead.
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;
}
if (host_type == htFlashforge) {
wxBusyCursor wait;
std::vector<FlashforgeDiscoveredPrinter> printers;
@@ -232,12 +250,13 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
update_printhost_buttons();
}
}
} else {
BonjourDialog dialog(this, Preset::printer_technology(*m_config));
if (dialog.show_and_lookup()) {
m_optgroup->set_value("print_host", dialog.get_selected(), 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);
m_optgroup->get_field("print_host")->field_changed();
}
});

View File

@@ -65,6 +65,7 @@
#include "libslic3r/SLAPrint.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/Utils/CrealityPrint.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ObjColorUtils.hpp"
// For stl export
@@ -3673,6 +3674,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "finish pop_finsish_sync_ams_dialog";
}
bool Sidebar::should_show_SEMM_buttons()
{
PresetBundle &preset_bundle = *wxGetApp().preset_bundle;
@@ -16110,6 +16112,11 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us
pDlg = std::make_unique<ElegooPrintHostSendDialog>(default_output_file, upload_job.printhost->get_post_upload_actions(), groups,
storage_paths, storage_names,
config->get_bool("open_device_tab_post_upload"));
} else if (host_type == htCrealityPrint) {
pDlg = std::make_unique<CrealityPrintHostSendDialog>(default_output_file, upload_job.printhost->get_post_upload_actions(), groups,
storage_paths, storage_names,
config->get_bool("open_device_tab_post_upload"),
upload_job.printhost.get());
} else if (flashforge_local_api) {
auto* flashforge_host = dynamic_cast<Flashforge*>(upload_job.printhost.get());
if (flashforge_host == nullptr) {

View File

@@ -35,6 +35,11 @@
#include "NotificationManager.hpp"
#include "ExtraRenderers.hpp"
#include "format.hpp"
#include "../Utils/CrealityPrint.hpp"
#include "BitmapComboBox.hpp"
#include "wxExtensions.hpp"
#include <nlohmann/json.hpp>
namespace fs = boost::filesystem;
using json = nlohmann::json;
@@ -1854,4 +1859,251 @@ void ElegooPrintHostSendDialog::refresh()
this->Fit();
}
CrealityPrintHostSendDialog::CrealityPrintHostSendDialog(const fs::path& path,
PrintHostPostUploadActions post_actions,
const wxArrayString& groups,
const wxArrayString& storage_paths,
const wxArrayString& storage_names,
bool switch_to_device_tab,
PrintHost* printhost)
: PrintHostSendDialog(path, post_actions, groups, storage_paths, storage_names, switch_to_device_tab)
, m_enableSelfTest(false)
, m_printhost(printhost)
{}
void CrealityPrintHostSendDialog::init()
{
PrintHostSendDialog::init();
auto* creality_host = static_cast<CrealityPrint*>(m_printhost);
bool multi_color;
std::string printer_name;
{
wxBusyCursor wait;
multi_color = creality_host->supports_multi_color_print();
if (multi_color)
printer_name = creality_host->model_name();
}
if (!multi_color)
return;
auto* group_box = new wxStaticBox(this, wxID_ANY,
wxString::Format(_L("Printer: %s"), printer_name));
auto* group_sizer = new wxStaticBoxSizer(group_box, wxVERTICAL);
content_sizer->Add(group_sizer, 0, wxEXPAND);
const AppConfig* app_config = wxGetApp().app_config;
std::string saved = app_config->get("recent", CONFIG_KEY_ENABLESELFTEST);
if (!saved.empty()) {
try { m_enableSelfTest = std::stoi(saved) != 0; } catch (...) {}
}
// Calibration checkbox
{
auto checkbox_sizer = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new ::CheckBox(this);
checkbox->SetValue(m_enableSelfTest);
checkbox->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) {
m_enableSelfTest = e.IsChecked();
AppConfig* ac = wxGetApp().app_config;
ac->set("recent", CONFIG_KEY_ENABLESELFTEST, m_enableSelfTest ? "1" : "0");
e.Skip();
});
checkbox_sizer->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Calibrate before printing"), wxDefaultPosition, wxDefaultSize, 0);
checkbox_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
checkbox_text->SetFont(::Label::Body_13);
checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
group_sizer->Add(checkbox_sizer);
group_sizer->AddSpacer(VERT_SPACING);
}
// --- Color mapping UI ---
// Get gcode filament info from slicer
auto preset_bundle = wxGetApp().preset_bundle;
auto full_config = preset_bundle->full_config();
auto* filament_colors = full_config.option<ConfigOptionStrings>("filament_colour");
auto* filament_types = full_config.option<ConfigOptionStrings>("filament_type");
int gcode_filament_count = filament_colors ? (int)filament_colors->values.size() : 0;
// Query printer for loaded materials
{
wxBusyCursor wait;
std::string boxes_json = creality_host->query_boxes_info();
if (!boxes_json.empty()) {
try {
auto resp = nlohmann::json::parse(boxes_json);
if (resp.contains("boxsInfo") && resp["boxsInfo"].contains("materialBoxs")) {
for (auto& box : resp["boxsInfo"]["materialBoxs"]) {
int box_id = box["id"].get<int>();
int box_type = box.value("type", 0);
// Skip inactive CFS boxes (type 0 with state != 1)
// Spool holder (type 1) is always available
if (box_type == 0 && box.value("state", 0) != 1)
continue;
for (auto& mat : box["materials"]) {
int slot_id = mat["id"].get<int>();
std::string tool_id = "T" + std::to_string(box_id) + std::string(1, 'A' + slot_id);
// Creality uses "#0RRGGBB" (7 hex digits), normalize to "#RRGGBB"
std::string color = mat.value("color", "#FFFFFF");
if (color.size() == 8 && color[0] == '#')
color = "#" + color.substr(2);
m_printer_slots.push_back({
tool_id,
mat.value("type", ""),
color,
box_id,
slot_id
});
}
}
}
} catch (const nlohmann::json::exception& e) {
BOOST_LOG_TRIVIAL(error) << "CrealityPrint dialog: Failed to parse boxsInfo: " << e.what();
}
}
}
if (gcode_filament_count > 0 && !m_printer_slots.empty()) {
auto* label = new wxStaticText(this, wxID_ANY, _L("Filament Mapping:"));
label->SetFont(::Label::Body_13);
label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
group_sizer->Add(label);
group_sizer->AddSpacer(4);
for (int i = 0; i < gcode_filament_count; i++) {
auto* row_sizer = new wxBoxSizer(wxHORIZONTAL);
// Left side: gcode filament color swatch + type
std::string gc_color = (filament_colors && i < (int)filament_colors->values.size())
? filament_colors->values[i] : "#FFFFFF";
std::string gc_type = (filament_types && i < (int)filament_types->values.size())
? filament_types->values[i] : "?";
// Color indicator panel
auto* color_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16)));
color_panel->SetBackgroundColour(wxColour(gc_color));
color_panel->SetMinSize(wxSize(FromDIP(16), FromDIP(16)));
row_sizer->Add(color_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
auto* type_label = new wxStaticText(this, wxID_ANY,
wxString::Format("%d (%s)", i + 1, gc_type.c_str()));
type_label->SetFont(::Label::Body_13);
type_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
type_label->SetMinSize(wxSize(FromDIP(80), -1));
row_sizer->Add(type_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8));
// Arrow
auto* arrow_label = new wxStaticText(this, wxID_ANY, wxString::FromUTF8("\xe2\x86\x92"));
arrow_label->SetFont(::Label::Body_13);
row_sizer->Add(arrow_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8));
// Right side: dropdown with color icons per slot
int icon_sz = FromDIP(16);
auto* combo = new BitmapComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
for (auto& slot : m_printer_slots) {
wxBitmap* bmp = get_extruder_color_icon(slot.color, "", icon_sz, icon_sz);
wxString label_str;
if (slot.box_id == 0)
label_str = wxString::Format("Ext - %s", slot.type.c_str());
else
label_str = wxString::Format("%s - %s", slot.tool_id.substr(1).c_str(), slot.type.c_str());
combo->Append(label_str, bmp ? *bmp : wxNullBitmap);
}
// Find best default: CFS exact color+type, CFS type-only,
// Ext exact, Ext type-only, else positional
int default_sel = (i < (int)m_printer_slots.size()) ? i : 0;
bool matched = false;
for (int pass = 0; pass < 4 && !matched; pass++) {
for (int s = 0; s < (int)m_printer_slots.size(); s++) {
bool is_ext = (m_printer_slots[s].box_id == 0);
bool type_match = (m_printer_slots[s].type == gc_type);
bool color_match = (wxColour(m_printer_slots[s].color) == wxColour(gc_color));
bool hit = false;
switch (pass) {
case 0: hit = !is_ext && type_match && color_match; break;
case 1: hit = !is_ext && type_match; break;
case 2: hit = is_ext && type_match && color_match; break;
case 3: hit = is_ext && type_match; break;
}
if (hit) {
default_sel = s;
matched = true;
break;
}
}
}
combo->SetSelection(default_sel);
row_sizer->Add(combo, 0, wxALIGN_CENTER_VERTICAL);
group_sizer->Add(row_sizer);
group_sizer->AddSpacer(4);
m_slot_combos.push_back(combo);
}
int ext_slot_idx = -1;
for (int s = 0; s < (int)m_printer_slots.size(); s++) {
if (m_printer_slots[s].box_id == 0) {
ext_slot_idx = s;
break;
}
}
if (ext_slot_idx >= 0) {
for (int ci = 0; ci < (int)m_slot_combos.size(); ci++) {
int sel = m_slot_combos[ci]->GetSelection();
if (sel >= 0 && sel < (int)m_printer_slots.size() &&
m_printer_slots[sel].box_id == 0) {
for (int cj = 0; cj < (int)m_slot_combos.size(); cj++) {
if (cj != ci)
m_slot_combos[cj]->Enable(false);
}
break;
}
}
for (auto* c : m_slot_combos) {
c->Bind(wxEVT_COMBOBOX, [this, ext_slot_idx](wxCommandEvent& e) {
int sel = e.GetSelection();
if (sel >= 0 && sel < (int)m_printer_slots.size() &&
m_printer_slots[sel].box_id == 0) {
for (auto* c2 : m_slot_combos) {
if (c2 != e.GetEventObject())
c2->Enable(false);
}
} else {
for (auto* c2 : m_slot_combos)
c2->Enable(true);
}
e.Skip();
});
}
}
}
this->Layout();
this->Fit();
}
std::map<std::string, std::string> CrealityPrintHostSendDialog::extendedInfo() const
{
std::map<std::string, std::string> info;
info["enableSelfTest"] = m_enableSelfTest ? "1" : "0";
// Color mapping: colorMatch_0, colorMatch_1, ... tab-delimited
for (int i = 0; i < (int)m_slot_combos.size(); i++) {
int sel = m_slot_combos[i]->GetSelection();
if (sel >= 0 && sel < (int)m_printer_slots.size()) {
auto& slot = m_printer_slots[sel];
// id = gcode tool index (T1A for first filament, T1B for second, ...),
// not the destination CFS slot — firmware matches by gcode tool.
std::string gcode_tool = "T1" + std::string(1, 'A' + i);
info["colorMatch_" + std::to_string(i)] =
gcode_tool + "\t" + slot.type + "\t" + slot.color + "\t" +
std::to_string(slot.box_id) + "\t" + std::to_string(slot.material_id);
}
}
return info;
}
}}

View File

@@ -25,6 +25,8 @@ class wxStaticText;
class wxWrapSizer;
class CheckBox;
namespace Slic3r { namespace GUI { class BitmapComboBox; } }
namespace Slic3r {
namespace GUI {
@@ -186,6 +188,37 @@ private:
BedType m_BedType;
};
class CrealityPrintHostSendDialog : public PrintHostSendDialog
{
public:
CrealityPrintHostSendDialog(const boost::filesystem::path& path,
PrintHostPostUploadActions post_actions,
const wxArrayString& groups,
const wxArrayString& storage_paths,
const wxArrayString& storage_names,
bool switch_to_device_tab,
PrintHost* printhost);
virtual void init() override;
virtual std::map<std::string, std::string> extendedInfo() const;
private:
static constexpr const char* CONFIG_KEY_ENABLESELFTEST = "crealityprint_enable_self_test";
bool m_enableSelfTest;
PrintHost* m_printhost;
struct SlotInfo {
std::string tool_id; // e.g. "T1A"
std::string type; // e.g. "PLA"
std::string color; // e.g. "#ffffff"
int box_id;
int material_id;
};
std::vector<SlotInfo> m_printer_slots;
std::vector<BitmapComboBox*> m_slot_combos; // one per gcode filament
};
class FlashforgePrintHostSendDialog : public PrintHostSendDialog
{
public: