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** — `Browse...` flow on a `host_type=crealityprint` printer. Click → ~5–10 s LAN scan → K2 found with model + hostname + IP. | |  | **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** — 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:
@@ -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)
|
||||
|
||||
19
deps_src/mdns/CMakeLists.txt
Normal file
19
deps_src/mdns/CMakeLists.txt
Normal 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
39
deps_src/mdns/NOTICE.md
Normal 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
256
deps_src/mdns/cxmdns.cpp
Normal 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
16
deps_src/mdns/cxmdns.h
Normal 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
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
1641
deps_src/mdns/mdns.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
118
src/slic3r/GUI/CrealityDiscoveryDialog.cpp
Normal file
118
src/slic3r/GUI/CrealityDiscoveryDialog.cpp
Normal 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
|
||||
49
src/slic3r/GUI/CrealityDiscoveryDialog.hpp
Normal file
49
src/slic3r/GUI/CrealityDiscoveryDialog.hpp
Normal 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
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}}
|
||||
|
||||
@@ -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:
|
||||
|
||||
128
src/slic3r/Utils/CrealityHostDiscovery.cpp
Normal file
128
src/slic3r/Utils/CrealityHostDiscovery.cpp
Normal 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
|
||||
45
src/slic3r/Utils/CrealityHostDiscovery.hpp
Normal file
45
src/slic3r/Utils/CrealityHostDiscovery.hpp
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
334
src/slic3r/Utils/CrealityPrintAgent.cpp
Normal file
334
src/slic3r/Utils/CrealityPrintAgent.cpp
Normal 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
|
||||
67
src/slic3r/Utils/CrealityPrintAgent.hpp
Normal file
67
src/slic3r/Utils/CrealityPrintAgent.hpp
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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("
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user