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

@@ -27,6 +27,7 @@ add_subdirectory(libigl)
add_subdirectory(libnest2d)
add_subdirectory(mcut)
add_subdirectory(md4c)
add_subdirectory(mdns)
add_subdirectory(miniz)
add_subdirectory(minilzo)
add_subdirectory(qhull)

View File

@@ -0,0 +1,19 @@
cmake_minimum_required(VERSION 3.13)
project(mdns)
# Static library wrapping mjansson/mdns (public domain) plus the cxmdns C++
# wrapper from CrealityPrint v7.1.1 (AGPL-3.0). See NOTICE.md for attribution.
add_library(mdns STATIC
mdns.h
mdns.c
cxmdns.h
cxmdns.cpp
)
target_include_directories(mdns SYSTEM PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
if (MSVC)
# mjansson/mdns uses GetAdaptersAddresses (Iphlpapi) and Winsock2 (Ws2_32).
target_link_libraries(mdns PUBLIC Iphlpapi Ws2_32)
endif()

39
deps_src/mdns/NOTICE.md Normal file
View File

@@ -0,0 +1,39 @@
# mDNS / DNS-SD library
The four files in this directory implement mDNS / DNS-SD lookup and are
vendored from third-party sources:
## mdns.h, mdns.c
mDNS / DNS-SD lookup library by Mattias Jansson. Originally released to
the public domain at https://github.com/mjansson/mdns.
The exact files here were taken from CrealityOfficial/CrealityPrint
v7.1.1, which split the upstream header-only library into separate
declaration (mdns.h) and implementation (mdns.c) files.
- Source: https://github.com/mjansson/mdns
- License: Public domain (no restrictions on use)
## cxmdns.h, cxmdns.cpp
Thin C++ wrapper over mdns.{h,c} that exposes a single function:
std::vector<machine_info> syncDiscoveryService(
const std::vector<std::string>& prefix);
It sends a DNS-SD meta-discovery query (`_services._dns-sd._udp.local.`),
listens for ~5 seconds, and returns `{ip, service_name}` for every
service announcement whose name contains any of the given prefixes.
OrcaSlicer uses this to find Creality K-series printers on the LAN
(service-name prefix "Creality"), since K-series firmware announces
each printer under a per-device-unique service type
`_Creality-<MAC-derived-hex>._udp.local.` that no fixed-name query can
target.
- Source: CrealityOfficial/CrealityPrint v7.1.1
`src/slic3r/GUI/print_manage/utils/cxmdns.{h,cpp}`
- License: GNU AGPL-3.0 (compatible with OrcaSlicer's AGPL-3.0; see
top-level LICENSE.txt)
- Imported: 2026-05-19

256
deps_src/mdns/cxmdns.cpp Normal file
View File

@@ -0,0 +1,256 @@
#include"cxmdns.h"
#include"mdns.h"
#ifdef _WIN32
#define _CRT_SECURE_NO_WARNINGS 1
#endif
#include <stdio.h>
#include<string.h>
#include <errno.h>
#include <signal.h>
#ifdef _WIN32
#include <winsock2.h>
#include <iphlpapi.h>
#define sleep(x) Sleep(x * 1000)
#else
#include <netdb.h>
#include <ifaddrs.h>
#include <net/if.h>
#endif
// Alias some things to simulate recieving data to fuzz library
#if defined(MDNS_FUZZING)
#define recvfrom(sock, buffer, capacity, flags, src_addr, addrlen) ((mdns_ssize_t)capacity)
#define printf
#endif
#include "mdns.h"
#if defined(MDNS_FUZZING)
#undef recvfrom
#endif
namespace cxnet
{
template <typename F>
mdns_record_callback_fn lambda2function(F lambda)
{
static auto lambdabak = lambda;
return [](int sock, const struct sockaddr* from, size_t addrlen,
mdns_entry_type_t entry, uint16_t query_id, uint16_t rtype,
uint16_t rclass, uint32_t ttl, const void* data, size_t size,
size_t name_offset, size_t name_length, size_t record_offset,
size_t record_length, void* user_data)->int {lambdabak(sock, from, addrlen, entry, query_id, rtype, rclass, ttl, data, size, name_offset, name_length, record_offset, record_length, user_data); return 0; };
}
volatile sig_atomic_t running = 1;
#ifdef _WIN32
BOOL console_handler(DWORD signal) {
if (signal == CTRL_C_EVENT) {
running = 0;
}
return TRUE;
}
#else
void signal_handler(int signal) {
running = 0;
}
#endif
void recvMachineInfoFromSocket(int sock, void* buffer, size_t capacity, const std::vector<std::string>& prefix, std::vector<machine_info>& retmachineInfos, int recIndex)
{
struct sockaddr_in6 addr;
struct sockaddr* saddr = (struct sockaddr*)&addr;
socklen_t addrlen = sizeof(addr);
memset(&addr, 0, sizeof(addr));
#ifdef __APPLE__
saddr->sa_len = sizeof(addr);
#endif
mdns_ssize_t ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen);
if (ret <= 0)
return;
size_t data_size = (size_t)ret;
//size_t records = 0;
const uint16_t* data = (uint16_t*)buffer;
uint16_t query_id = mdns_ntohs(data++);
uint16_t flags = mdns_ntohs(data++);
uint16_t questions = mdns_ntohs(data++);
uint16_t answer_rrs = mdns_ntohs(data++);
uint16_t authority_rrs = mdns_ntohs(data++);
uint16_t additional_rrs = mdns_ntohs(data++);
// According to RFC 6762 the query ID MUST match the sent query ID (which is 0 in our case)
if (query_id || (flags != 0x8400))
return; // Not a reply to our question
// It seems some implementations do not fill the correct questions field,
// so ignore this check for now and only validate answer string
// if (questions != 1)
// return 0;
int i;
for (i = 0; i < questions; ++i) {
size_t offset = MDNS_POINTER_DIFF(data, buffer);
size_t verify_offset = 12;
// Verify it's our question, _services._dns-sd._udp.local.
if (!mdns_string_equal(buffer, data_size, &offset, mdns_services_query,
sizeof(mdns_services_query), &verify_offset))
return;
data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, offset);
uint16_t rtype = mdns_ntohs(data++);
uint16_t rclass = mdns_ntohs(data++);
// Make sure we get a reply based on our PTR question for class IN
if ((rtype != MDNS_RECORDTYPE_PTR) || ((rclass & 0x7FFF) != MDNS_CLASS_IN))
return;
}
for (i = 0; i < answer_rrs; ++i) {
size_t offset = MDNS_POINTER_DIFF(data, buffer);
size_t verify_offset = 12;
// Verify it's an answer to our question, _services._dns-sd._udp.local.
size_t name_offset = offset;
int is_answer = mdns_string_equal(buffer, data_size, &offset, mdns_services_query,
sizeof(mdns_services_query), &verify_offset);
if (!is_answer && !mdns_string_skip(buffer, data_size, &offset))
break;
size_t name_length = offset - name_offset;
if ((offset + 10) > data_size)
return;
data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, offset);
uint16_t rtype = mdns_ntohs(data++);
uint16_t rclass = mdns_ntohs(data++);
uint32_t ttl = mdns_ntohl(data);
data += 2;
uint16_t length = mdns_ntohs(data++);
if (length > (data_size - offset))
return;
static char addrbuf[64];
static char entrybuf[256];
static char namebuf[256];
if (is_answer) {
offset = MDNS_POINTER_DIFF(data, buffer);
(void)sizeof(sock);
(void)sizeof(query_id);
(void)sizeof(name_length);
//(void)sizeof(0);
mdns_string_t fromaddrstr = ip_address_to_string(addrbuf, sizeof(addrbuf), saddr, addrlen);
mdns_string_t entrystr =
mdns_string_extract(buffer, data_size, &name_offset, entrybuf, sizeof(entrybuf));
if (rtype == MDNS_RECORDTYPE_PTR) {
mdns_string_t namestr = mdns_record_parse_ptr(buffer, data_size, offset, length,
namebuf, sizeof(namebuf));
if (!namestr.str || namestr.length == 0) {
return;
}
const std::string answer_name(namestr.str, namestr.length);
bool bFound = false;
for (const auto& item : prefix)
{
if (answer_name.find(item) != std::string::npos)
bFound = true;
}
if (!bFound)
{
return;
}
char ip[16] = { 0 };
sscanf(fromaddrstr.str, "%[^:]", ip);
retmachineInfos.push_back({ ip, answer_name });
}
}
}
}
std::vector<machine_info> syncDiscoveryService(const std::vector<std::string>& prefix)
{
std::vector<machine_info> retmachineInfos;
const char* hostname = "cxslice-host";
// Initialize network environment
#ifdef _WIN32
WORD versionWanted = MAKEWORD(1, 1);
WSADATA wsaData;
if (WSAStartup(versionWanted, &wsaData)) {
printf("Failed to initialize WinSock\n");
return retmachineInfos;
}
char hostname_buffer[256];
DWORD hostname_size = (DWORD)sizeof(hostname_buffer);
if (GetComputerNameA(hostname_buffer, &hostname_size))
hostname = hostname_buffer;
SetConsoleCtrlHandler(console_handler, TRUE);
#else
char hostname_buffer[256];
size_t hostname_size = sizeof(hostname_buffer);
if (gethostname(hostname_buffer, hostname_size) == 0)
hostname = hostname_buffer;
signal(SIGINT, signal_handler);
#endif
int sockets[32];
int num_sockets = open_client_sockets(sockets, sizeof(sockets) / sizeof(sockets[0]), 0);
if (num_sockets <= 0) {
printf("Failed to open any client sockets\n");
#ifdef _WIN32
WSACleanup();
#endif
return retmachineInfos;
}
printf("Opened %d socket%s for DNS-SD\n", num_sockets, num_sockets > 1 ? "s" : "");
printf("Sending DNS-SD discovery\n");
for (int isock = 0; isock < num_sockets; ++isock) {
if (mdns_discovery_send(sockets[isock]))
printf("Failed to send DNS-DS discovery: %s\n", strerror(errno));
}
size_t capacity = 2048;
void* buffer = malloc(capacity);
size_t recordNum = 0;
void* user_data = 0;
// This is a simple implementation that loops for 5 seconds or as long as we get replies
int res;
printf("Reading DNS-SD replies\n");
do {
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int nfds = 0;
fd_set readfs;
FD_ZERO(&readfs);
for (int isock = 0; isock < num_sockets; ++isock) {
if (sockets[isock] >= nfds)
nfds = sockets[isock] + 1;
FD_SET(sockets[isock], &readfs);
}
res = select(nfds, &readfs, 0, 0, &timeout);
if (res > 0) {
for (int isock = 0; isock < num_sockets; ++isock) {
if (FD_ISSET(sockets[isock], &readfs)) {
// records += mdns_discovery_recv(sockets[isock], buffer, capacity, query_callback,
// 0);
recvMachineInfoFromSocket(sockets[isock], buffer, capacity, prefix, retmachineInfos, isock);
recordNum++;
}
}
}
} while (res > 0);
free(buffer);
//
for (int isock = 0; isock < num_sockets; ++isock)
mdns_socket_close(sockets[isock]);
printf("Closed socket%s\n", num_sockets ? "s" : "");
#ifdef _WIN32
WSACleanup();
#endif
return std::move(retmachineInfos);
}
}

16
deps_src/mdns/cxmdns.h Normal file
View File

@@ -0,0 +1,16 @@
#ifndef _CX_MDNS_H
#define _CX_MDNS_H
#include<string>
#include<vector>
namespace cxnet
{
struct machine_info
{
std::string machineIp;
std::string answer;
};
std::vector<machine_info> syncDiscoveryService(const std::vector<std::string>& prefix);
}
#endif

1263
deps_src/mdns/mdns.c Normal file

File diff suppressed because it is too large Load Diff

1641
deps_src/mdns/mdns.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,8 @@
"fan_speedup_overhangs": "1",
"fan_speedup_time": "0",
"gcode_flavor": "klipper",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"high_current_on_filament_swap": "0",
"machine_end_gcode": "END_PRINT",
"machine_load_filament_time": "0",

View File

@@ -188,7 +188,8 @@
"creality_flush_time": "86",
"disable_m73": "0",
"enable_long_retraction_when_cut": "2",
"host_type": "octoprint",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"machine_LED_light_exist": "1",
"machine_min_extruding_rate": "0,0",
"machine_min_travel_rate": "0,0",

View File

@@ -189,7 +189,8 @@
"creality_flush_time": "86",
"disable_m73": "0",
"enable_long_retraction_when_cut": "0",
"host_type": "octoprint",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"machine_LED_light_exist": "1",
"machine_min_extruding_rate": "0,0",
"machine_min_travel_rate": "0,0",

View File

@@ -189,7 +189,8 @@
"creality_flush_time": "86",
"disable_m73": "0",
"enable_long_retraction_when_cut": "0",
"host_type": "octoprint",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"machine_LED_light_exist": "1",
"machine_min_extruding_rate": "0,0",
"machine_min_travel_rate": "0,0",

View File

@@ -7,6 +7,8 @@
"instantiation": "true",
"printer_model": "Creality K2 Plus",
"gcode_flavor": "klipper",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"default_print_profile": "0.14mm Optimal @Creality K2 Plus 0.2 nozzle",
"nozzle_diameter": [
"0.2"

View File

@@ -7,6 +7,8 @@
"instantiation": "true",
"printer_model": "Creality K2 Plus",
"gcode_flavor": "klipper",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"default_print_profile": "0.16mm Optimal @Creality K2 Plus 0.4 nozzle",
"nozzle_diameter": [
"0.4"

View File

@@ -7,6 +7,8 @@
"instantiation": "true",
"printer_model": "Creality K2 Plus",
"gcode_flavor": "klipper",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"default_print_profile": "0.24mm Optimal @Creality K2 Plus 0.6 nozzle",
"nozzle_diameter": [
"0.6"

View File

@@ -7,6 +7,8 @@
"instantiation": "true",
"printer_model": "Creality K2 Plus",
"gcode_flavor": "klipper",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"default_print_profile": "0.32mm Optimal @Creality K2 Plus 0.8 nozzle",
"nozzle_diameter": [
"0.8"

View File

@@ -7,6 +7,8 @@
"instantiation": "true",
"printer_model": "Creality K2 Pro",
"gcode_flavor": "klipper",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"default_print_profile": "0.16mm Optimal @Creality K2 Pro 0.2 nozzle",
"nozzle_diameter": [
"0.2"

View File

@@ -162,7 +162,8 @@
"fan_speedup_overhangs": "1",
"fan_speedup_time": "0",
"high_current_on_filament_swap": "0",
"host_type": "octoprint",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"machine_LED_light_exist": "1",
"machine_load_filament_time": "86",
"machine_min_extruding_rate": "0,0",

View File

@@ -162,7 +162,8 @@
"fan_speedup_overhangs": "1",
"fan_speedup_time": "0",
"high_current_on_filament_swap": "0",
"host_type": "octoprint",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"machine_LED_light_exist": "1",
"machine_load_filament_time": "86",
"machine_min_extruding_rate": "0,0",

View File

@@ -162,7 +162,8 @@
"fan_speedup_overhangs": "1",
"fan_speedup_time": "0",
"high_current_on_filament_swap": "0",
"host_type": "octoprint",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"machine_LED_light_exist": "1",
"machine_load_filament_time": "86",
"machine_min_extruding_rate": "0,0",

View File

@@ -32,7 +32,8 @@
"file_start_gcode": "; multicolor_method = 1\n",
"gcode_flavor": "klipper",
"high_current_on_filament_swap": "0",
"host_type": "octoprint",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"machine_LED_light_exist": "1",
"machine_end_gcode": "G1 E-0.8 F2400 ; retract\nG2 Z{position[2]+3} I0.86 J0.86 P1 F10000 ; spiral lift a little from second lift\nG92 E0 ; zero the extruder\n{if print_sequence == \"by object\"}\nG0 Z{max_layer_z + 1} F1800\n{endif}\nG1 X0 Y180 F8000\nEND_PRINT",
"machine_load_filament_time": "30",
@@ -69,8 +70,6 @@
"pellet_modded_printer": "0",
"preferred_orientation": "0",
"prime_tower_position_type": "Middle Upper",
"print_host": "http://10.10.1.39:4408/",
"print_host_webui": "http://10.10.1.39:4408/",
"printable_area": "0x0,260x0,260x260,0x260",
"printable_height": "255",
"printer_technology": "FFF",

View File

@@ -32,7 +32,8 @@
"file_start_gcode": "; multicolor_method = 1\n",
"gcode_flavor": "klipper",
"high_current_on_filament_swap": "0",
"host_type": "octoprint",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"machine_LED_light_exist": "1",
"machine_end_gcode": "G1 E-0.8 F2400 ; retract\nG2 Z{position[2]+3} I0.86 J0.86 P1 F10000 ; spiral lift a little from second lift\nG92 E0 ; zero the extruder\n{if print_sequence == \"by object\"}\nG0 Z{max_layer_z + 1} F1800\n{endif}\nG1 X0 Y180 F8000\nEND_PRINT",
"machine_load_filament_time": "30",
@@ -69,8 +70,6 @@
"pellet_modded_printer": "0",
"preferred_orientation": "0",
"prime_tower_position_type": "Middle Upper",
"print_host": "http://10.10.1.39:4408/",
"print_host_webui": "http://10.10.1.39:4408/",
"printable_area": "0x0,260x0,260x260,0x260",
"printable_height": "255",
"printer_technology": "FFF",

View File

@@ -32,7 +32,8 @@
"file_start_gcode": "; multicolor_method = 1\n",
"gcode_flavor": "klipper",
"high_current_on_filament_swap": "0",
"host_type": "octoprint",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"machine_LED_light_exist": "1",
"machine_end_gcode": "G1 E-0.8 F2400 ; retract\nG2 Z{position[2]+3} I0.86 J0.86 P1 F10000 ; spiral lift a little from second lift\nG92 E0 ; zero the extruder\n{if print_sequence == \"by object\"}\nG0 Z{max_layer_z + 1} F1800\n{endif}\nG1 X0 Y180 F8000\nEND_PRINT",
"machine_load_filament_time": "30",
@@ -69,8 +70,6 @@
"pellet_modded_printer": "0",
"preferred_orientation": "0",
"prime_tower_position_type": "Middle Upper",
"print_host": "http://10.10.1.39:4408/",
"print_host_webui": "http://10.10.1.39:4408/",
"printable_area": "0x0,260x0,260x260,0x260",
"printable_height": "255",
"printer_technology": "FFF",

View File

@@ -32,7 +32,8 @@
"file_start_gcode": "; multicolor_method = 1\n",
"gcode_flavor": "klipper",
"high_current_on_filament_swap": "0",
"host_type": "octoprint",
"host_type": "crealityprint",
"printer_agent": "crealityprint",
"machine_LED_light_exist": "1",
"machine_end_gcode": "G1 E-0.8 F2400 ; retract\nG2 Z{position[2]+3} I0.86 J0.86 P1 F10000 ; spiral lift a little from second lift\nG92 E0 ; zero the extruder\n{if print_sequence == \"by object\"}\nG0 Z{max_layer_z + 1} F1800\n{endif}\nG1 X0 Y180 F8000\nEND_PRINT",
"machine_load_filament_time": "30",
@@ -69,8 +70,6 @@
"pellet_modded_printer": "0",
"preferred_orientation": "0",
"prime_tower_position_type": "Middle Upper",
"print_host": "http://10.10.1.39:4408/",
"print_host_webui": "http://10.10.1.39:4408/",
"printable_area": "0x0,260x0,260x260,0x260",
"printable_height": "255",
"printer_technology": "FFF",

View File

@@ -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
@@ -582,6 +584,10 @@ set(SLIC3R_GUI_SOURCES
Utils/ColorSpaceConvert.hpp
Utils/CrealityPrint.cpp
Utils/CrealityPrint.hpp
Utils/CrealityPrintAgent.cpp
Utils/CrealityPrintAgent.hpp
Utils/CrealityHostDiscovery.cpp
Utils/CrealityHostDiscovery.hpp
Utils/Duet.cpp
Utils/Duet.hpp
Utils/ElegooLink.cpp
@@ -758,7 +764,7 @@ else()
set(_opengl_link_lib OpenGL::GL)
endif()
target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto noise::noise)
target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi mdns ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto noise::noise)
if (MSVC)

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:

View File

@@ -0,0 +1,128 @@
#include "CrealityHostDiscovery.hpp"
#include "cxmdns.h"
#include "Http.hpp"
#include <boost/log/trivial.hpp>
#include <nlohmann/json.hpp>
#include <algorithm>
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://<ip>/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<std::string>();
if (j.contains("mac") && j["mac"].is_string())
host.mac = j["mac"].get<std::string>();
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<CrealityHost> CrealityHostDiscovery::scan(bool probe)
{
const std::vector<std::string> 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<CrealityHost> 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<std::string> 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

View File

@@ -0,0 +1,45 @@
#ifndef slic3r_CrealityHostDiscovery_hpp_
#define slic3r_CrealityHostDiscovery_hpp_
#include <string>
#include <vector>
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-<MAC-derived-hex>._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://<ip>/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<CrealityHost> scan(bool probe_info = true);
};
} // namespace Slic3r
#endif

View File

@@ -1,6 +1,7 @@
#include "CrealityPrint.hpp"
#include <algorithm>
#include <map>
#include <sstream>
#include <exception>
#include <boost/format.hpp>
@@ -88,7 +89,8 @@ bool CrealityPrint::test(wxString& msg) const
// Here we do not have to add custom "Host" header - the url contains host filled by user and libCurl will set the header by itself.
auto http = Http::get(std::move(url));
set_auth(http);
http.on_error([&](std::string body, std::string error, unsigned status) {
http.timeout_max(5)
.on_error([&](std::string body, std::string error, unsigned status) {
BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting version: %2%, HTTP %3%, body: `%4%`") % name % error % status %
body;
res = false;
@@ -96,6 +98,15 @@ bool CrealityPrint::test(wxString& msg) const
})
.on_complete([&, this](std::string body, unsigned) {
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got version: %2%") % name % body;
try {
auto info = json::parse(body);
if (info.contains("model")) {
m_model = info["model"].get<std::string>();
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Detected model: %2%") % name % m_model;
}
} catch (const json::exception& e) {
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: Failed to parse /info response: %2%") % name % e.what();
}
})
#ifdef WIN32
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
@@ -130,14 +141,19 @@ bool CrealityPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn,
auto http = Http::post(url); // std::move(url));
set_auth(http);
http.form_add("path", upload_parent_path.string())
.form_add_file("file", upload_data.source_path.string(), upload_filename.string())
if (!supports_multi_color_print())
http.form_add("path", upload_parent_path.string());
http.form_add_file("file", upload_data.source_path.string(), upload_filename.string())
.on_complete([&](std::string body, unsigned status) {
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: File uploaded: HTTP %2%: %3%") % name % status % body;
if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) {
start_print(safe_filename(upload_filename.string()));
wxString errormsg;
if (!start_print(errormsg, safe_filename(upload_filename.string()), upload_data.extended_info)) {
error_fn(std::move(errormsg));
res = false;
}
}
})
.on_error([&](std::string body, std::string error, unsigned status) {
@@ -182,53 +198,231 @@ std::string CrealityPrint::safe_filename(const std::string &filename) const
return safe_filename;
}
void CrealityPrint::start_print(const std::string &filename) const
static void ws_connect(net::io_context& ioc, websocket::stream<beast::tcp_stream>& ws,
const std::string& host_url, const std::string& port)
{
std::string host = Http::get_host_from_url(host_url);
tcp::resolver resolver{ioc};
beast::get_lowest_layer(ws).expires_after(std::chrono::seconds(5));
auto const results = resolver.resolve(host, port);
beast::get_lowest_layer(ws).connect(results);
host += ':' + std::to_string(beast::get_lowest_layer(ws).socket().remote_endpoint().port());
ws.set_option(websocket::stream_base::decorator(
[](websocket::request_type& req) {
req.set(http::field::user_agent,
std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
}));
ws.handshake(host, "/");
#ifdef _WIN32
DWORD recv_timeout = 3000;
#else
struct timeval recv_timeout = {3, 0};
#endif
setsockopt(beast::get_lowest_layer(ws).socket().native_handle(),
SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&recv_timeout), sizeof(recv_timeout));
}
static std::string ws_send_and_read(websocket::stream<beast::tcp_stream>& ws, const json& cmd, const std::string& expected_key, int max_reads = 20)
{
ws.write(net::buffer(to_string(cmd)));
for (int i = 0; i < max_reads; i++) {
beast::flat_buffer buf;
beast::error_code ec;
ws.read(buf, ec);
if (ec == net::error::would_block)
break;
if (ec)
throw beast::system_error{ec};
std::string msg = beast::buffers_to_string(buf.data());
if (msg.find(expected_key) != std::string::npos)
return msg;
}
BOOST_LOG_TRIVIAL(warning) << "CrealityPrint: No '" << expected_key << "' response after " << max_reads << " messages";
return {};
}
void CrealityPrint::query_model() const
{
if (!m_model.empty())
return;
wxString msg;
test(msg);
}
bool CrealityPrint::supports_multi_color_print() const
{
query_model();
// K2-platform printers with CFS support
return m_model == "F008" // K2 Plus
|| m_model == "F012" // K2 Pro
|| m_model == "F021" // K2
|| m_model == "F022"; // SPARKX i7
}
std::string CrealityPrint::model_name() const
{
static const std::map<std::string, std::string> names = {
{"F008", "K2 Plus"},
{"F012", "K2 Pro"},
{"F021", "K2"},
{"F022", "SPARKX i7"},
};
query_model();
if (m_model.empty())
return "unreachable";
auto it = names.find(m_model);
return it != names.end() ? it->second : "unknown (" + m_model + ")";
}
std::string CrealityPrint::query_boxes_info() const
{
try {
std::string host = m_host;
auto const port = "9999";
net::io_context ioc;
websocket::stream<beast::tcp_stream> ws{ioc};
ws_connect(ioc, ws, m_host, "9999");
json j2 = {
{ "method", "set" },
{
"params", {
{ "opGcodeFile", "printprt:/usr/data/printer_data/gcodes/" + filename }
}
}
};
json boxs_query = {{"method", "get"}, {"params", {{"boxsInfo", 1}}}};
std::string result = ws_send_and_read(ws, boxs_query, "boxsInfo");
ws.close(websocket::close_code::normal);
return result;
} catch (std::exception const& e) {
BOOST_LOG_TRIVIAL(error) << "CrealityPrint: Failed to query boxsInfo: " << e.what();
return {};
}
}
std::string CrealityPrint::get_print_host_webui(DynamicPrintConfig* config)
{
// K-series printers (K2 / K2 Plus / K2 Pro) ship with Mainsail on port 4408.
// Port 80 hosts only the Creality control / upload API, which returns 404
// for unknown paths and therefore renders as a blank/404 page in Orca's
// Device WebView. Default to the Mainsail URL when the user hasn't
// explicitly set print_host_webui.
if (config == nullptr)
return {};
std::string explicit_url = config->opt_string("print_host_webui");
if (!explicit_url.empty())
return explicit_url;
std::string host = config->opt_string("print_host");
if (host.empty())
return {};
if (boost::algorithm::istarts_with(host, "http://"))
host = host.substr(7);
else if (boost::algorithm::istarts_with(host, "https://"))
host = host.substr(8);
if (auto slash = host.find('/'); slash != std::string::npos)
host = host.substr(0, slash);
if (auto colon = host.find(':'); colon != std::string::npos)
host = host.substr(0, colon);
return "http://" + host + ":4408/";
}
bool CrealityPrint::start_print(wxString &msg, const std::string &filename, const std::map<std::string, std::string>& extended_info) const
{
try {
const std::string gcode_path = "/mnt/UDISK/printer_data/gcodes/" + filename;
net::io_context ioc;
websocket::stream<beast::tcp_stream> ws{ioc};
ws_connect(ioc, ws, m_host, "9999");
tcp::resolver resolver{ioc};
websocket::stream<tcp::socket> ws{ioc};
if (supports_multi_color_print()) {
// Build colorMatch list from the mapping provided by the dialog
bool use_spool_holder = false;
json color_list = json::array();
for (int i = 0; ; i++) {
auto it = extended_info.find("colorMatch_" + std::to_string(i));
if (it == extended_info.end())
break;
// Value format: "toolId\ttype\tcolor\tboxId\tmaterialId"
auto val = it->second;
std::vector<std::string> parts;
std::istringstream iss(val);
std::string part;
while (std::getline(iss, part, '\t'))
parts.push_back(part);
if (parts.size() >= 5) {
int box_id = std::stoi(parts[3]);
if (box_id == 0)
use_spool_holder = true;
color_list.push_back({
{"id", parts[0]},
{"type", parts[1]},
{"color", parts[2]},
{"boxId", box_id},
{"materialId", std::stoi(parts[4])}
});
}
}
auto const results = resolver.resolve(host, port);
auto ep = net::connect(ws.next_layer(), results);
host += ':' + std::to_string(ep.port());
ws.set_option(websocket::stream_base::decorator(
[](websocket::request_type& req)
int enable_self_test = 0;
{
req.set(http::field::user_agent,
std::string(BOOST_BEAST_VERSION_STRING) +
" websocket-client-coro");
}));
auto it = extended_info.find("enableSelfTest");
if (it != extended_info.end())
enable_self_test = std::stoi(it->second);
}
ws.handshake(host, "/");
ws.write(net::buffer(to_string(j2)));
if (use_spool_holder) {
json cmd = {
{"method", "set"},
{"params", {
{"opGcodeFile", "printprt:" + gcode_path},
{"enableSelfTest", enable_self_test}
}}
};
ws.write(net::buffer(to_string(cmd)));
} else {
json color_match = {
{"method", "set"},
{"params", {
{"colorMatch", {
{"path", gcode_path},
{"list", color_list}
}}
}}
};
ws.write(net::buffer(to_string(color_match)));
beast::flat_buffer buffer;
json multi_color_print = {
{"method", "set"},
{"params", {
{"multiColorPrint", {
{"gcode", gcode_path},
{"enableSelfTest", enable_self_test}
}}
}}
};
ws.write(net::buffer(to_string(multi_color_print)));
}
} else {
json cmd = {
{"method", "set"},
{"params", {
{"opGcodeFile", "printprt:/usr/data/printer_data/gcodes/" + filename}
}}
};
ws.write(net::buffer(to_string(cmd)));
ws.read(buffer);
beast::flat_buffer buffer;
ws.read(buffer);
}
ws.close(websocket::close_code::normal);
return true;
} catch(std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
BOOST_LOG_TRIVIAL(error) << "CrealityPrint: Error starting print: " << e.what();
msg = wxString::FromUTF8(e.what());
return false;
}
}
}

View File

@@ -1,6 +1,7 @@
#ifndef slic3r_CrealityPrint_hpp_
#define slic3r_CrealityPrint_hpp_
#include <map>
#include <string>
#include <wx/string.h>
#include <boost/optional.hpp>
@@ -29,6 +30,13 @@ public:
virtual bool test(wxString& curl_msg) const override;
PrintHostPostUploadActions get_post_upload_actions() const;
bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override;
bool supports_multi_color_print() const;
std::string query_boxes_info() const;
std::string model_name() const;
// Mainsail on K-series printers listens on port 4408. Use that as the
// default Device-tab WebView URL when the user has not set print_host_webui.
static std::string get_print_host_webui(DynamicPrintConfig *config);
protected:
virtual void set_auth(Http& http) const;
@@ -39,10 +47,12 @@ private:
std::string m_cafile;
std::string m_web_ui;
bool m_ssl_revoke_best_effort;
mutable std::string m_model;
std::string make_url(const std::string& path) const;
void start_print(const std::string& path) const;
bool start_print(wxString& msg, const std::string& filename, const std::map<std::string, std::string>& extended_info) const;
std::string safe_filename(const std::string& filename) const;
void query_model() const;
};
} // namespace Slic3r

View File

@@ -0,0 +1,334 @@
#include "CrealityPrintAgent.hpp"
#include "CrealityPrint.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include <boost/log/trivial.hpp>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <map>
namespace Slic3r {
namespace {
constexpr const char* CrealityPrintAgent_VERSION = "0.1.0";
bool has_visible_base_preset(const PresetCollection& filaments, const std::string& filament_id)
{
for (const auto& p : filaments.get_presets()) {
if (p.is_visible && p.is_compatible
&& filaments.get_preset_base(p) == &p
&& p.filament_id == filament_id)
return true;
}
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
// (e.g. "Hyper PLA" in "Hyper PLA @Creality K2 0.4 nozzle")
// +10 preset name contains the vendor substring (e.g. "Creality")
// Tiebreak: prefer the SYSTEM (shipped) preset over user copies. Brand-
// specific system presets carry their own filament_id; user copies of
// generic presets inherit a generic filament_id from their parent, so
// preferring the user copy can collapse a brand-specific match back to
// "Generic PLA" via the inherited id. Plus: this code targets upstream
// OrcaSlicer where shipping the user's local tuning would be wrong.
// 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 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<char>(std::tolower(static_cast<unsigned char>(c)));
return s;
};
const std::string vendor_lower = to_lower(vendor);
const std::string brand_lower = to_lower(brand_name);
const std::string type_lower = to_lower(base_type);
struct Match {
const Preset* preset;
int score;
bool is_user;
};
std::vector<Match> matches;
int considered = 0;
for (const auto& p : filaments.get_presets()) {
if (!p.is_visible || !p.is_compatible) continue;
// Note: we deliberately do NOT filter on get_preset_base(p) == &p.
// K2 owners frequently keep tweaked copies of system presets
// (e.g. "Creality Hyper PLA @K2 (Harky)" with their per-spool PA),
// which are derived presets — filtering to bases-only would skip
// exactly the presets users care about most.
++considered;
std::string preset_type;
if (const auto* ft = p.config.option<ConfigOptionStrings>("filament_type"))
if (!ft->values.empty()) preset_type = ft->values.front();
if (to_lower(preset_type) != type_lower) continue;
const std::string name_lower = to_lower(p.name);
int score = 0;
if (!brand_lower.empty() && name_lower.find(brand_lower) != std::string::npos)
score += 20;
if (!vendor_lower.empty() && name_lower.find(vendor_lower) != std::string::npos)
score += 10;
if (score > 0)
matches.push_back({&p, score, !p.is_system && !p.is_default});
}
if (matches.empty()) {
const std::string fallback = filaments.filament_id_by_type(base_type);
const bool fallback_ok = has_visible_base_preset(filaments, fallback);
BOOST_LOG_TRIVIAL(info)
<< "CrealityPrintAgent: no preset scored for spool {" << vendor << " "
<< brand_name << " (" << base_type << ")} after considering " << considered
<< " presets; falling back to generic preset id \"" << fallback << "\""
<< (fallback_ok ? "" : " (NOT visible — returning empty)");
return fallback_ok ? fallback : std::string();
}
std::sort(matches.begin(), matches.end(),
[](const Match& a, const Match& b) {
if (a.score != b.score) return a.score > b.score;
if (a.is_user != b.is_user) return !a.is_user; // prefer system over user
return false;
});
BOOST_LOG_TRIVIAL(info)
<< "CrealityPrintAgent: matched spool {" << vendor << " " << brand_name
<< " (" << base_type << ")} -> preset \"" << matches.front().preset->name
<< "\" (score=" << matches.front().score
<< ", " << matches.size() << " candidate(s) of " << considered << " considered)";
return matches.front().preset->filament_id;
}
CrealityPrintAgent::CrealityPrintAgent(std::string log_dir)
: MoonrakerPrinterAgent(std::move(log_dir))
{
}
AgentInfo CrealityPrintAgent::get_agent_info_static()
{
return AgentInfo{
"crealityprint",
"CrealityPrint",
CrealityPrintAgent_VERSION,
"Creality K-series printer agent (CFS-aware filament sync)"
};
}
std::string CrealityPrintAgent::normalize_filament_type(const std::string& filament_type)
{
static const std::vector<std::string> bases = {
"PETG", "PET", "PLA", "ABS", "ASA", "TPU", "PC", "PA", "PVA", "HIPS"
};
for (const auto& base : bases) {
if (filament_type.rfind(base, 0) == 0) return base;
}
return filament_type;
}
// Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info().
// Schema (verified 2026-05-06 against K2 Combo F021 firmware v1.1.260206):
// { "boxsInfo": { "materialBoxs": [
// { "id": int, "state": int, "type": int, // type 0 = CFS, 1 = single-spool external
// "materials": [
// { "id": int, "state": int, // state 1 = loaded
// "vendor": str, "type": str, "name": str,
// "color": "#0RRGGBB" }, ...
// ]}, ...
// ]}}
bool CrealityPrintAgent::parse_cfs_response(const std::string& response,
std::vector<CFSSlot>& slots,
int& box_count,
std::string& error)
{
using nlohmann::json;
slots.clear();
box_count = 0;
if (response.empty()) {
error = "empty response";
return false;
}
json resp;
try {
resp = json::parse(response);
} catch (const std::exception& e) {
error = std::string("JSON parse error: ") + e.what();
return false;
}
if (!resp.contains("boxsInfo") || !resp["boxsInfo"].contains("materialBoxs")) {
error = "invalid schema (missing boxsInfo.materialBoxs)";
return false;
}
// Sequential AMS-style index for accepted CFS boxes. The K2's raw box.id has
// gaps (id 0 is the external spool holder, type=1, skipped) — using the raw id
// would publish phantom slots for the gap. Renumber accepted boxes 0,1,2,...
int cfs_count = 0;
for (const auto& box : resp["boxsInfo"]["materialBoxs"]) {
const int box_st = box.value("state", 0);
const int box_type = box.value("type", 0);
if (box_st != 1) continue; // inactive boxes
if (box_type != 0) continue; // non-CFS (external spool holder, handled separately by upload dialog)
const int cfs_index = cfs_count++;
if (!box.contains("materials") || !box["materials"].is_array())
continue;
for (const auto& mat : box["materials"]) {
// CFS slot state encoding observed across K2 family firmwares:
// * K2 (base) / K2 Pro : 0 = empty, 1 = loaded.
// * K2 Plus (1.1.5.5/CFS 1.4.2 onwards): 0 = empty,
// 1 = loaded AND currently
// selected as the active
// spool for printing,
// 2 = loaded but not selected.
// We treat anything non-zero as loaded. Belt-and-braces: also skip
// entries that look blank (no vendor and no type) regardless of state.
const int s_state = mat.value("state", 0);
const std::string s_vendor = mat.value("vendor", std::string());
const std::string s_type = mat.value("type", std::string());
if (s_state == 0) continue; // explicitly empty
if (s_vendor.empty() && s_type.empty()) continue; // blank entry — likely empty under a different state encoding
CFSSlot s;
s.box_id = cfs_index;
s.slot_id = mat.value("id", 0);
s.vendor = s_vendor;
s.brand_name = mat.value("name", "");
s.filament_type = s_type;
s.color_hex = mat.value("color", "#FFFFFF");
// Creality reports colour as "#0RRGGBB" (8 chars with a leading zero
// after '#'). Normalise to standard "#RRGGBB".
if (s.color_hex.size() == 8 && s.color_hex[0] == '#')
s.color_hex = "#" + s.color_hex.substr(2);
slots.push_back(std::move(s));
}
}
box_count = cfs_count;
return true;
}
bool CrealityPrintAgent::fetch_filament_info(std::string dev_id)
{
if (device_info.dev_ip.empty()) {
BOOST_LOG_TRIVIAL(warning)
<< "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent";
return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id));
}
// Build a CrealityPrint helper so we can use its model detection + WS helpers
// (added in upstream PR #13291).
DynamicPrintConfig cfg;
cfg.set_key_value("print_host", new ConfigOptionString("http://" + device_info.dev_ip));
cfg.set_key_value("print_host_webui", new ConfigOptionString(""));
cfg.set_key_value("printhost_cafile", new ConfigOptionString(""));
cfg.set_key_value("printhost_port", new ConfigOptionString(""));
cfg.set_key_value("printhost_apikey", new ConfigOptionString(device_info.api_key));
cfg.set_key_value("printhost_ssl_ignore_revoke", new ConfigOptionBool(false));
CrealityPrint host(&cfg);
// Defer to base if this isn't a K-series board with CFS firmware support.
if (!host.supports_multi_color_print()) {
BOOST_LOG_TRIVIAL(info)
<< "CrealityPrintAgent: " << host.model_name()
<< " is not CFS-capable, deferring to base Moonraker agent";
return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id));
}
BOOST_LOG_TRIVIAL(info)
<< "CrealityPrintAgent: querying CFS slots on " << host.model_name();
const std::string response = host.query_boxes_info();
std::vector<CFSSlot> slots;
int box_count = 0;
std::string parse_err;
if (!parse_cfs_response(response, slots, box_count, parse_err)) {
BOOST_LOG_TRIVIAL(warning)
<< "CrealityPrintAgent: CFS query failed (" << parse_err << "), "
<< "falling back to base agent";
return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id));
}
if (box_count == 0) {
// No active CFS boxes attached — printer is in direct-spool mode. Let the
// base agent take over so the user still gets whatever filament info
// Moonraker exposes.
BOOST_LOG_TRIVIAL(info)
<< "CrealityPrintAgent: no active CFS boxes, deferring to base agent";
return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id));
}
BOOST_LOG_TRIVIAL(info)
<< "CrealityPrintAgent: " << box_count << " CFS box(es), "
<< slots.size() << " loaded slot(s)";
// Index loaded slots by (box, slot) for O(1) lookup as we walk the full
// box_count * 4 grid, emitting an AmsTrayData entry for each physical slot.
std::map<std::pair<int, int>, const CFSSlot*> by_position;
for (const auto& s : slots)
by_position[{s.box_id, s.slot_id}] = &s;
auto* bundle = GUI::wxGetApp().preset_bundle;
const int max_slots = box_count * 4;
std::vector<AmsTrayData> trays;
trays.reserve(max_slots);
for (int box = 0; box < box_count; ++box) {
for (int idx = 0; idx < 4; ++idx) {
AmsTrayData tray;
tray.slot_index = box * 4 + idx;
auto it = by_position.find({box, idx});
if (it == by_position.end()) {
tray.has_filament = false;
trays.push_back(std::move(tray));
continue;
}
const CFSSlot& s = *it->second;
tray.has_filament = true;
tray.tray_type = normalize_filament_type(s.filament_type);
tray.tray_color = s.color_hex;
if (bundle) {
tray.tray_info_idx = match_filament_preset(
bundle->filaments, s.vendor, s.brand_name, tray.tray_type);
}
trays.push_back(std::move(tray));
}
}
build_ams_payload(box_count, max_slots - 1, trays);
return true;
}
} // namespace Slic3r

View File

@@ -0,0 +1,67 @@
#ifndef __CREALITY_PRINT_AGENT_HPP__
#define __CREALITY_PRINT_AGENT_HPP__
#include "MoonrakerPrinterAgent.hpp"
#include <string>
#include <vector>
namespace Slic3r {
class PresetCollection;
// Filament sync for Creality K-series printers with CFS.
//
// Inherits MoonrakerPrinterAgent for all communication / certificates / discovery /
// binding / print-job operations. Overrides fetch_filament_info() to query the
// K-series CFS over its port-9999 WebSocket, convert each loaded slot to an
// AmsTrayData entry, and publish via the base-class build_ams_payload() — the
// same shape used by QidiPrinterAgent and SnapmakerPrinterAgent.
//
// 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.
class CrealityPrintAgent final : public MoonrakerPrinterAgent
{
public:
struct CFSSlot
{
int box_id = 0; // CFS unit index (0 for first box, 1 for chained second box)
int slot_id = 0; // Slot index within the box (0..3)
std::string color_hex; // "#RRGGBB"
std::string filament_type; // "PLA", "ABS", "PETG", ...
std::string brand_name; // "Hyper PLA", ...
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<CFSSlot>& 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
#endif

View File

@@ -170,77 +170,12 @@ namespace Slic3r {
}
}
std::string get_host_from_url(const std::string& url_in)
{
std::string url = url_in;
// add http:// if there is no scheme
size_t double_slash = url.find("//");
if (double_slash == std::string::npos)
url = "http://" + url;
std::string out = url;
CURLU* hurl = curl_url();
if (hurl) {
// Parse the input URL.
CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, url.c_str(), 0);
if (rc == CURLUE_OK) {
// Replace the address.
char* host;
rc = curl_url_get(hurl, CURLUPART_HOST, &host, 0);
if (rc == CURLUE_OK) {
char* port;
rc = curl_url_get(hurl, CURLUPART_PORT, &port, 0);
if (rc == CURLUE_OK && port != nullptr) {
out = std::string(host) + ":" + port;
curl_free(port);
} else {
out = host;
curl_free(host);
}
}
else
BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to get host form URL " << url;
}
else
BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to parse URL " << url;
curl_url_cleanup(hurl);
}
else
BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to allocate curl_url";
return out;
}
std::string get_host_from_url_no_port(const std::string& url_in)
{
std::string url = url_in;
// add http:// if there is no scheme
size_t double_slash = url.find("//");
if (double_slash == std::string::npos)
url = "http://" + url;
std::string out = url;
CURLU* hurl = curl_url();
if (hurl) {
// Parse the input URL.
CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, url.c_str(), 0);
if (rc == CURLUE_OK) {
// Replace the address.
char* host;
rc = curl_url_get(hurl, CURLUPART_HOST, &host, 0);
if (rc == CURLUE_OK) {
out = host;
curl_free(host);
}
else
BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to get host form URL " << url;
}
else
BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to parse URL " << url;
curl_url_cleanup(hurl);
}
else
BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to allocate curl_url";
return out;
}
// NOTE (merge): host parsing was moved into Http::get_host_from_url /
// Http::get_host_header_value by the K2 discovery refactor on this branch, so the
// former ElegooLink-local get_host_from_url/get_host_from_url_no_port helpers are gone.
// main only added the CC2 serial-number lookup below; it is kept here and routed through
// Http::get_host_header_value, which has the same host:port semantics the SN cache key
// relies on.
std::string lookup_cc2_serial_impl(const std::string& printer_model,
const std::string& print_host,
const std::string& apikey)
@@ -248,7 +183,7 @@ namespace Slic3r {
if (classify_printer_model(printer_model) != ElegooPrinterType::CC2)
return {};
const std::string host_ip = get_host_from_url(print_host);
const std::string host_ip = Http::get_host_header_value(print_host);
const std::string token = get_cc2_token(apikey);
std::string sn = lookup_sn(host_ip, token);
if (sn.empty())
@@ -408,9 +343,8 @@ namespace Slic3r {
std::string web_path = resources_dir() + "/web/elegoolink/lan_service_web/index.html";
std::replace(web_path.begin(), web_path.end(), '\\', '/');
web_path = "file://" + web_path;
const std::string token = get_cc2_token(config->opt_string("printhost_apikey"));
const std::string host_ip = get_host_from_url(host);
const std::string host_ip = Http::get_host_header_value(host);
// Pass sn= so the panel can subscribe to the correct MQTT topics.
std::string sn = lookup_cc2_serial(config);
@@ -551,7 +485,7 @@ namespace Slic3r {
msg = format_error(body, error_message.empty() ? "CC2 device not detected" : error_message, status);
return;
}
persist_sn(get_host_from_url(m_host), token, serial_number);
persist_sn(Http::get_host_header_value(m_host), token, serial_number);
res = true;
})
#ifdef WIN32
@@ -574,7 +508,6 @@ namespace Slic3r {
// Msg contains ip string.
auto url = substitute_host(make_url(""), GUI::into_u8(msg));
msg.Clear();
std::string host = get_host_from_url(m_host);
auto http = Http::get(url); // std::move(url));
// "Host" header is necessary here. We have resolved IP address and subsituted it into "url" variable.
// And when creating Http object above, libcurl automatically includes "Host" header from address it got.
@@ -582,7 +515,7 @@ namespace Slic3r {
// Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse
// proxy is used (issue #9734). Also when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays.
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
http.header("Host", host);
http.header("Host", Http::get_host_header_value(m_host));
set_auth(http);
http.on_error([&](std::string body, std::string error, unsigned status) {
BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting version at %2% : %3%, HTTP %4%, body: `%5%`") % name % url %
@@ -626,11 +559,10 @@ namespace Slic3r {
bool res = true;
const auto token = cc2_token();
auto url = substitute_host(make_cc2_info_url(), GUI::into_u8(msg));
std::string host_header = get_host_from_url(m_host);
auto http = Http::get(url);
msg.Clear();
http.header("Host", host_header);
http.header("Host", Http::get_host_header_value(m_host));
http.header("X-Token", token);
http.header("Accept", "application/json");
http.on_error([&](std::string body, std::string error, unsigned status) {
@@ -689,7 +621,7 @@ namespace Slic3r {
std::string url = substitute_host(make_cc2_upload_url(), resolved_addr.to_string());
info_fn(L"resolve", boost::nowide::widen(url));
return loopUploadCC2(url, get_host_from_url(m_host), std::move(upload_data), prorgess_fn, error_fn, info_fn);
return loopUploadCC2(url, Http::get_host_header_value(m_host), std::move(upload_data), prorgess_fn, error_fn, info_fn);
}
wxString legacy_msg = GUI::from_u8(resolved_addr.to_string());
@@ -735,7 +667,7 @@ namespace Slic3r {
}
#endif // _WIN32
return loopUploadCC2(url, get_host_from_url(m_host), std::move(upload_data), prorgess_fn, error_fn, info_fn);
return loopUploadCC2(url, Http::get_host_header_value(m_host), std::move(upload_data), prorgess_fn, error_fn, info_fn);
}
wxString legacy_msg;
@@ -874,8 +806,7 @@ namespace Slic3r {
// on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734). Also
// when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays.
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
std::string host = get_host_from_url(m_host);
http.header("Host", host);
http.header("Host", Http::get_host_header_value(m_host));
http.header("Accept", "application/json, text/plain, */*");
#endif // _WIN32
set_auth(http);
@@ -906,7 +837,7 @@ namespace Slic3r {
if (res) {
if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) {
// connect to websocket, since the upload is successful, the file will be printed
std::string wsUrl = get_host_from_url_no_port(m_host);
std::string wsUrl = Http::get_host_from_url(m_host);
WebSocketClient client;
try {
client.connect(wsUrl, "3030", "/websocket");
@@ -1087,7 +1018,7 @@ namespace Slic3r {
#ifndef WIN32
return upload_inner_with_host(std::move(upload_data), prorgess_fn, error_fn, info_fn);
#else
std::string host = get_host_from_url(m_host);
std::string host = Http::get_host_from_url(m_host);
// decide what to do based on m_host - resolve hostname or upload to ip
std::vector<boost::asio::ip::address> resolved_addr;

View File

@@ -978,6 +978,51 @@ std::string Http::get_filename_from_url(const std::string &url)
return path_url.substr(start_pos + 1, path_url.length() - start_pos - 1);
}
std::string Http::get_host_from_url(const std::string &url_in, std::string *port)
{
std::string url = url_in;
if (url.find("//") == std::string::npos)
url = "http://" + url;
if (port)
port->clear();
std::string out = url_in;
CURLU *hurl = curl_url();
if (hurl) {
CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, url.c_str(), 0);
if (rc == CURLUE_OK) {
char *host;
rc = curl_url_get(hurl, CURLUPART_HOST, &host, 0);
if (rc == CURLUE_OK) {
out = host;
curl_free(host);
if (port) {
char *pstr;
rc = curl_url_get(hurl, CURLUPART_PORT, &pstr, 0);
if (rc == CURLUE_OK && pstr) {
*port = pstr;
curl_free(pstr);
}
}
} else
BOOST_LOG_TRIVIAL(error) << "Http::get_host_from_url: failed to get host from URL " << url;
} else
BOOST_LOG_TRIVIAL(error) << "Http::get_host_from_url: failed to parse URL " << url;
curl_url_cleanup(hurl);
} else
BOOST_LOG_TRIVIAL(error) << "Http::get_host_from_url: failed to allocate curl_url";
return out;
}
std::string Http::get_host_header_value(const std::string &url)
{
std::string port;
std::string host = get_host_from_url(url, &port);
if (!port.empty())
host += ":" + port;
return host;
}
std::ostream& operator<<(std::ostream &os, const Http::Progress &progress)
{
os << "Http::Progress("

View File

@@ -204,6 +204,8 @@ public:
static std::string url_decode(const std::string &str);
static std::string get_filename_from_url(const std::string &url);
static std::string get_host_from_url(const std::string &url, std::string *port = nullptr);
static std::string get_host_header_value(const std::string &url);
private:
Http(const std::string &url);

View File

@@ -6,6 +6,7 @@
#include "QidiPrinterAgent.hpp"
#include "SnapmakerPrinterAgent.hpp"
#include "MoonrakerPrinterAgent.hpp"
#include "CrealityPrintAgent.hpp"
#include <boost/log/trivial.hpp>
#include <map>
#include <mutex>
@@ -133,6 +134,9 @@ void NetworkAgentFactory::register_all_agents()
register_agent<OrcaPrinterAgent>();
register_agent<QidiPrinterAgent>();
register_agent<SnapmakerPrinterAgent>();
register_agent<CrealityPrintAgent>(); // Must come BEFORE MoonrakerPrinterAgent —
// CrealityPrintAgent extends Moonraker behaviour
// for K-series boards with CFS support.
register_agent<MoonrakerPrinterAgent>();
// BBLPrinterAgent takes no constructor args, so register manually

View File

@@ -33,45 +33,6 @@ namespace Slic3r {
namespace {
#ifdef WIN32
std::string get_host_from_url(const std::string& url_in)
{
std::string url = url_in;
// add http:// if there is no scheme
size_t double_slash = url.find("//");
if (double_slash == std::string::npos)
url = "http://" + url;
std::string out = url;
CURLU* hurl = curl_url();
if (hurl) {
// Parse the input URL.
CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, url.c_str(), 0);
if (rc == CURLUE_OK) {
// Replace the address.
char* host;
rc = curl_url_get(hurl, CURLUPART_HOST, &host, 0);
if (rc == CURLUE_OK) {
char* port;
rc = curl_url_get(hurl, CURLUPART_PORT, &port, 0);
if (rc == CURLUE_OK && port != nullptr) {
out = std::string(host) + ":" + port;
curl_free(port);
} else {
out = host;
curl_free(host);
}
}
else
BOOST_LOG_TRIVIAL(error) << "OctoPrint get_host_from_url: failed to get host form URL " << url;
}
else
BOOST_LOG_TRIVIAL(error) << "OctoPrint get_host_from_url: failed to parse URL " << url;
curl_url_cleanup(hurl);
}
else
BOOST_LOG_TRIVIAL(error) << "OctoPrint get_host_from_url: failed to allocate curl_url";
return out;
}
// Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail.
std::string substitute_host(const std::string& orig_addr, std::string sub_addr)
{
@@ -186,7 +147,6 @@ bool OctoPrint::test_with_resolved_ip(wxString &msg) const
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get version at: %2%") % name % url;
std::string host = get_host_from_url(m_host);
auto http = Http::get(url);//std::move(url));
// "Host" header is necessary here. We have resolved IP address and subsituted it into "url" variable.
// And when creating Http object above, libcurl automatically includes "Host" header from address it got.
@@ -194,7 +154,7 @@ bool OctoPrint::test_with_resolved_ip(wxString &msg) const
// Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734).
// Also when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays.
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
http.header("Host", host);
http.header("Host", Http::get_host_header_value(m_host));
set_auth(http);
http
.on_error([&](std::string body, std::string error, unsigned status) {
@@ -306,7 +266,7 @@ bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Erro
#ifndef WIN32
return upload_inner_with_host(std::move(upload_data), prorgess_fn, error_fn, info_fn);
#else
std::string host = get_host_from_url(m_host);
std::string host = Http::get_host_from_url(m_host);
// decide what to do based on m_host - resolve hostname or upload to ip
std::vector<boost::asio::ip::address> resolved_addr;
@@ -393,14 +353,13 @@ bool OctoPrint::upload_inner_with_resolved_ip(PrintHostUpload upload_data, Progr
% upload_parent_path.string()
% (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false");
std::string host = get_host_from_url(m_host);
auto http = Http::post(url);//std::move(url));
// "Host" header is necessary here. We have resolved IP address and subsituted it into "url" variable.
// And when creating Http object above, libcurl automatically includes "Host" header from address it got.
// Thus "Host" is set to the resolved IP instead of host filled by user. We need to change it back.
// Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734).
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
http.header("Host", host);
http.header("Host", Http::get_host_header_value(m_host));
set_auth(http);
http.form_add("print", upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false")
.form_add("path", upload_parent_path.string()) // XXX: slashes on windows ???
@@ -486,8 +445,7 @@ bool OctoPrint::upload_inner_with_host(PrintHostUpload upload_data, ProgressFn p
// Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734).
// Also when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays.
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
std::string host = get_host_from_url(m_host);
http.header("Host", host);
http.header("Host", Http::get_host_header_value(m_host));
#endif // _WIN32
set_auth(http);
http.form_add("print", upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false")
@@ -884,7 +842,6 @@ bool PrusaLink::test_with_resolved_ip_and_method_check(wxString& msg, bool& use_
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get version at: %2%") % name % url;
std::string host = get_host_from_url(m_host);
auto http = Http::get(url);//std::move(url));
// "Host" header is necessary here. We have resolved IP address and subsituted it into "url" variable.
// And when creating Http object above, libcurl automatically includes "Host" header from address it got.
@@ -892,7 +849,7 @@ bool PrusaLink::test_with_resolved_ip_and_method_check(wxString& msg, bool& use_
// Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734).
// Also when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays.
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
http.header("Host", host);
http.header("Host", Http::get_host_header_value(m_host));
set_auth(http);
http
.on_error([&](std::string body, std::string error, unsigned status) {
@@ -1053,8 +1010,7 @@ bool PrusaLink::put_inner(PrintHostUpload upload_data, std::string url, const st
// Thus "Host" is set to the resolved IP instead of host filled by user. We need to change it back.
// Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734).
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
std::string host = get_host_from_url(m_host);
http.header("Host", host);
http.header("Host", Http::get_host_header_value(m_host));
#endif // _WIN32
set_auth(http);
// This is ugly, but works. There was an error at PrusaLink side that accepts any string at Print-After-Upload as true, thus False was also triggering print after upload.
@@ -1103,8 +1059,7 @@ bool PrusaLink::post_inner(PrintHostUpload upload_data, std::string url, const s
// Thus "Host" is set to the resolved IP instead of host filled by user. We need to change it back.
// Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734).
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
std::string host = get_host_from_url(m_host);
http.header("Host", host);
http.header("Host", Http::get_host_header_value(m_host));
#endif // _WIN32
set_auth(http);
set_http_post_header_args(http, upload_data.post_action);

View File

@@ -92,6 +92,10 @@ std::string PrintHost::get_print_host_webui(DynamicPrintConfig* config)
webui_url = ElegooLink::get_print_host_webui(config);
break;
}
case htCrealityPrint: {
webui_url = CrealityPrint::get_print_host_webui(config);
break;
}
default: break;
}