diff --git a/resources/images/add.svg b/resources/images/add.svg
new file mode 100644
index 0000000000..37050d7481
--- /dev/null
+++ b/resources/images/add.svg
@@ -0,0 +1,22 @@
+
+
+
diff --git a/resources/images/add_copies.svg b/resources/images/add_copies.svg
new file mode 100644
index 0000000000..45b1d27cf9
--- /dev/null
+++ b/resources/images/add_copies.svg
@@ -0,0 +1,19 @@
+
+
+
diff --git a/resources/images/browse.svg b/resources/images/browse.svg
new file mode 100644
index 0000000000..c4297c41da
--- /dev/null
+++ b/resources/images/browse.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp
index 1e8297b669..006b644e8a 100644
--- a/src/libslic3r/GCode.cpp
+++ b/src/libslic3r/GCode.cpp
@@ -14,7 +14,7 @@
#include "libslic3r.h"
#include "LocalesUtils.hpp"
#include "libslic3r/format.hpp"
-
+#include "Time.hpp"
#include
#include
#include
@@ -1352,9 +1352,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
}
//BBS: add plate id into thumbnail render logic
- //DoExport::export_thumbnails_to_file(thumbnail_cb, print.get_plate_index(), THUMBNAIL_SIZE,
- // [&file](const char* sz) { file.write(sz); },
- // [&print]() { print.throw_if_canceled(); });
+ file.write_format("; hack-fix: write fake slicer info here so that Moonraker will extract thumbs.\n");
+ file.write_format("; %s\n\n",std::string(std::string("generated by PrusaSlicer " SLIC3R_VERSION " on " ) + Slic3r::Utils::utc_timestamp()).c_str());
+ DoExport::export_thumbnails_to_file(thumbnail_cb, print.get_plate_index(), { Vec2d(300, 300) },
+ [&file](const char* sz) { file.write(sz); },
+ [&print]() { print.throw_if_canceled(); });
// Write some terse information on the slicing parameters.
diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp
index 6407aab34e..ce10743df1 100644
--- a/src/libslic3r/Preset.cpp
+++ b/src/libslic3r/Preset.cpp
@@ -724,7 +724,13 @@ static std::vector s_Preset_printer_options {
"silent_mode",
// BBS
"scan_first_layer", "machine_load_filament_time", "machine_unload_filament_time", "machine_pause_gcode",
- "nozzle_type", "auxiliary_fan", "nozzle_volume"
+ "nozzle_type", "auxiliary_fan", "nozzle_volume",
+ //SoftFever
+ "connection_moonraker_url","connection_port", "host_type", "print_host", "printhost_apikey",
+ "printhost_cafile","printhost_port","printhost_authorization_type",
+ "printhost_user",
+ "printhost_password",
+ "printhost_ssl_ignore_revoke"
};
static std::vector s_Preset_sla_print_options {
@@ -2189,6 +2195,21 @@ void add_correct_opts_to_diff(const std::string &opt_key, t_config_option_keys&
}
}
+// template
+// void add_correct_opt_to_diff(const std::string &opt_key, t_config_option_keys& vec, const ConfigBase &other, const ConfigBase &this_c)
+// {
+// const T* opt_init = static_cast(other.option(opt_key));
+// const T* opt_cur = static_cast(this_c.option(opt_key));
+// int opt_init_max_id = opt_init->values.size() - 1;
+// for (int i = 0; i < int(opt_cur->values.size()); i++)
+// {
+// int init_id = i <= opt_init_max_id ? i : 0;
+// if (opt_cur->values[i] != opt_init->values[init_id])
+// vec.emplace_back(opt_key + "#" + std::to_string(i));
+// }
+// }
+
+
// Use deep_diff to correct return of changed options, considering individual options for each extruder.
inline t_config_option_keys deep_diff(const ConfigBase &config_this, const ConfigBase &config_other)
{
@@ -2212,6 +2233,7 @@ inline t_config_option_keys deep_diff(const ConfigBase &config_this, const Confi
case coBools: add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break;
case coFloats: add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break;
case coStrings: add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break;
+ // case coString: add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break;
case coPercents:add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break;
case coPoints: add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break;
// BBS
@@ -2519,6 +2541,16 @@ static std::vector s_PhysicalPrinter_opts {
"preset_name", // temporary option to compatibility with older Slicer
"preset_names",
"printer_technology",
+ "host_type",
+ "print_host",
+ "printhost_apikey",
+ "printhost_cafile",
+ "printhost_port",
+ "printhost_authorization_type",
+ // HTTP digest authentization (RFC 2617)
+ "printhost_user",
+ "printhost_password",
+ "printhost_ssl_ignore_revoke"
};
const std::vector& PhysicalPrinter::printer_options()
@@ -2682,6 +2714,8 @@ void PhysicalPrinterCollection::load_printers(
// see https://github.com/prusa3d/PrusaSlicer/issues/732
boost::filesystem::path dir = boost::filesystem::absolute(boost::filesystem::path(dir_path) / subdir).make_preferred();
m_dir_path = dir.string();
+ if(!boost::filesystem::exists(dir))
+ return;
std::string errors_cummulative;
// Store the loaded printers into a new vector, otherwise the binary search for already existing presets would be broken.
std::deque printers_loaded;
diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp
index 145535eb0c..394a3fa65d 100644
--- a/src/libslic3r/PresetBundle.cpp
+++ b/src/libslic3r/PresetBundle.cpp
@@ -1245,8 +1245,10 @@ DynamicPrintConfig PresetBundle::full_config() const
DynamicPrintConfig PresetBundle::full_config_secure() const
{
DynamicPrintConfig config = this->full_config();
- //BBS example: config.erase("print_host");
- return config;
+ //FIXME legacy, the keys should not be there after conversion to a Physical Printer profile.
+ config.erase("print_host");
+ config.erase("printhost_apikey");
+ config.erase("printhost_cafile"); return config;
}
const std::set ignore_settings_list ={
@@ -3231,4 +3233,4 @@ void PresetBundle::set_default_suppressed(bool default_suppressed)
printers.set_default_suppressed(default_suppressed);
}
-} // namespace Slic3r
+} // namespace Slic3r
\ No newline at end of file
diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp
index 9241cc2ebe..5a3da2a51f 100644
--- a/src/libslic3r/PrintConfig.cpp
+++ b/src/libslic3r/PrintConfig.cpp
@@ -43,6 +43,23 @@ static t_config_enum_values s_keys_map_PrinterTechnology {
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrinterTechnology)
+static t_config_enum_values s_keys_map_PrintHostType {
+ { "prusalink", htPrusaLink },
+ { "octoprint", htOctoPrint },
+ { "duet", htDuet },
+ { "flashair", htFlashAir },
+ { "astrobox", htAstroBox },
+ { "repetier", htRepetier },
+ { "mks", htMKS }
+};
+CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintHostType)
+
+static t_config_enum_values s_keys_map_AuthorizationType {
+ { "key", atKeyPassword },
+ { "user", atUserPassword }
+};
+CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(AuthorizationType)
+
static t_config_enum_values s_keys_map_GCodeFlavor {
{ "marlin", gcfMarlinLegacy },
{ "reprap", gcfRepRapSprinter },
@@ -257,6 +274,7 @@ void PrintConfigDef::init_common_params()
def = this->add("printable_area", coPoints);
def->label = L("Printable area");
+
//BBS
def->mode = comDevelop;
def->set_default_value(new ConfigOptionPoints{ Vec2d(0, 0), Vec2d(200, 0), Vec2d(200, 200), Vec2d(0, 200) });
@@ -304,6 +322,98 @@ void PrintConfigDef::init_common_params()
def->mode = comDevelop;
def->set_default_value(new ConfigOptionStrings());
+ //SoftFever
+ def = this->add("connection_moonraker_url", coString);
+ def->label = L("Moonraker URL");
+ //def->tooltip = L("Names of presets related to the physical printer");
+ def->mode = comAdvanced;
+ def->set_default_value(new ConfigOptionString("http://"));
+
+ def = this->add("connection_port", coString);
+ def->label = L("Connection port");
+ //def->tooltip = L("Names of presets related to the physical printer");
+ def->mode = comAdvanced;
+ def->set_default_value(new ConfigOptionString("7125"));
+
+
+
+ def = this->add("print_host", coString);
+ def->label = L("Hostname, IP or URL");
+ def->tooltip = L("Slic3r can upload G-code files to a printer host. This field should contain "
+ "the hostname, IP address or URL of the printer host instance. "
+ "Print host behind HAProxy with basic auth enabled can be accessed by putting the user name and password into the URL "
+ "in the following format: https://username:password@your-octopi-address/");
+ def->mode = comAdvanced;
+ def->cli = ConfigOptionDef::nocli;
+ def->set_default_value(new ConfigOptionString(""));
+
+ def = this->add("printhost_apikey", coString);
+ def->label = L("API Key / Password");
+ def->tooltip = L("Slic3r can upload G-code files to a printer host. This field should contain "
+ "the API Key or the password required for authentication.");
+ def->mode = comAdvanced;
+ def->cli = ConfigOptionDef::nocli;
+ def->set_default_value(new ConfigOptionString(""));
+
+ def = this->add("printhost_port", coString);
+ def->label = L("Printer");
+ def->tooltip = L("Name of the printer");
+ def->gui_type = ConfigOptionDef::GUIType::select_open;
+ def->mode = comAdvanced;
+ def->cli = ConfigOptionDef::nocli;
+ def->set_default_value(new ConfigOptionString(""));
+
+ def = this->add("printhost_cafile", coString);
+ def->label = L("HTTPS CA File");
+ def->tooltip = L("Custom CA certificate file can be specified for HTTPS OctoPrint connections, in crt/pem format. "
+ "If left blank, the default OS CA certificate repository is used.");
+ def->mode = comAdvanced;
+ def->cli = ConfigOptionDef::nocli;
+ def->set_default_value(new ConfigOptionString(""));
+
+ // Options used by physical printers
+
+ def = this->add("printhost_user", coString);
+ def->label = L("User");
+// def->tooltip = L("");
+ def->mode = comAdvanced;
+ def->cli = ConfigOptionDef::nocli;
+ def->set_default_value(new ConfigOptionString(""));
+
+ def = this->add("printhost_password", coString);
+ def->label = L("Password");
+// def->tooltip = L("");
+ def->mode = comAdvanced;
+ def->cli = ConfigOptionDef::nocli;
+ def->set_default_value(new ConfigOptionString(""));
+
+ // Only available on Windows.
+ def = this->add("printhost_ssl_ignore_revoke", coBool);
+ def->label = L("Ignore HTTPS certificate revocation checks");
+ def->tooltip = L("Ignore HTTPS certificate revocation checks in case of missing or offline distribution points. "
+ "One may want to enable this option for self signed certificates if connection fails.");
+ def->mode = comAdvanced;
+ def->cli = ConfigOptionDef::nocli;
+ def->set_default_value(new ConfigOptionBool(false));
+
+ def = this->add("preset_names", coStrings);
+ def->label = L("Printer preset names");
+ def->tooltip = L("Names of presets related to the physical printer");
+ def->mode = comAdvanced;
+ def->set_default_value(new ConfigOptionStrings());
+
+ def = this->add("printhost_authorization_type", coEnum);
+ def->label = L("Authorization Type");
+// def->tooltip = L("");
+ def->enum_keys_map = &ConfigOptionEnum::get_enum_values();
+ def->enum_values.push_back("key");
+ def->enum_values.push_back("user");
+ def->enum_labels.push_back(L("API key"));
+ def->enum_labels.push_back(L("HTTP digest"));
+ def->mode = comAdvanced;
+ def->cli = ConfigOptionDef::nocli;
+ def->set_default_value(new ConfigOptionEnum(atKeyPassword));
+
// temporary workaround for compatibility with older Slicer
{
def = this->add("preset_name", coString);
@@ -1624,6 +1734,30 @@ void PrintConfigDef::init_fff_params()
def->mode = comDevelop;
def->set_default_value(new ConfigOptionFloats { 0.4 });
+ def = this->add("host_type", coEnum);
+ def->label = L("Host Type");
+ def->tooltip = L("Slic3r can upload G-code files to a printer host. This field must contain "
+ "the kind of the host.");
+ def->enum_keys_map = &ConfigOptionEnum::get_enum_values();
+ def->enum_values.push_back("prusalink");
+ def->enum_values.push_back("octoprint");
+ def->enum_values.push_back("duet");
+ def->enum_values.push_back("flashair");
+ def->enum_values.push_back("astrobox");
+ def->enum_values.push_back("repetier");
+ def->enum_values.push_back("mks");
+ def->enum_labels.push_back("PrusaLink");
+ def->enum_labels.push_back("OctoPrint");
+ def->enum_labels.push_back("Duet");
+ def->enum_labels.push_back("FlashAir");
+ def->enum_labels.push_back("AstroBox");
+ def->enum_labels.push_back("Repetier");
+ def->enum_labels.push_back("MKS");
+ def->mode = comAdvanced;
+ def->cli = ConfigOptionDef::nocli;
+ def->set_default_value(new ConfigOptionEnum(htOctoPrint));
+
+
def = this->add("nozzle_volume", coFloat);
def->label = L("Nozzle volume");
def->tooltip = L("Volume of nozzle between the cutter and the end of nozzle");
diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp
index f08702299a..05d35fe063 100644
--- a/src/libslic3r/PrintConfig.hpp
+++ b/src/libslic3r/PrintConfig.hpp
@@ -42,6 +42,14 @@ enum class FuzzySkinType {
All,
};
+enum PrintHostType {
+ htPrusaLink, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS
+};
+
+enum AuthorizationType {
+ atKeyPassword, atUserPassword
+};
+
#define HAS_LIGHTNING_INFILL 0
enum InfillPattern : int {
@@ -236,6 +244,9 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BedType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(DraftShield)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ForwardCompatibilitySubstitutionRule)
+CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PrintHostType)
+CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AuthorizationType)
+
#undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS
// Defines each and every confiuration option of Slic3r, including the properties of the GUI dialogs.
@@ -755,6 +766,10 @@ PRINT_CONFIG_CLASS_DEFINE(
//BBS
((ConfigOptionEnum, nozzle_type))
((ConfigOptionBool, auxiliary_fan))
+ //SoftFever
+ ((ConfigOptionString, connection_moonraker_url))
+ ((ConfigOptionString, connection_port))
+
)
// This object is mapped to Perl as Slic3r::Config::Print.
diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp
index 67b873cc46..6cf8b1265a 100644
--- a/src/libslic3r/utils.cpp
+++ b/src/libslic3r/utils.cpp
@@ -1106,7 +1106,7 @@ std::string string_printf(const char *format, ...)
std::string header_slic3r_generated()
{
- return std::string(SLIC3R_APP_NAME " " SLIC3R_VERSION);
+ return std::string(SLIC3R_APP_NAME "-SoftFever" " " SLIC3R_VERSION);
}
std::string header_gcodeviewer_generated()
diff --git a/src/minilzo/CMakeLists.txt b/src/minilzo/CMakeLists.txt
index c5122ccf0f..d23e871472 100644
--- a/src/minilzo/CMakeLists.txt
+++ b/src/minilzo/CMakeLists.txt
@@ -1,5 +1,5 @@
project(minilzo)
-cmake_minimum_required(VERSION 2.6)
+cmake_minimum_required(VERSION 3.0)
add_library(minilzo INTERFACE)
diff --git a/src/qhull/CMakeLists.txt b/src/qhull/CMakeLists.txt
index ab9aba9afa..6f0e090dc7 100644
--- a/src/qhull/CMakeLists.txt
+++ b/src/qhull/CMakeLists.txt
@@ -28,7 +28,7 @@ endif()
else(Qhull_FOUND)
project(qhull)
-cmake_minimum_required(VERSION 2.6)
+cmake_minimum_required(VERSION 3.1)
# Define qhull_VERSION in CMakeLists.txt, Makefile, qhull-exports.def, qhull_p-exports.def, qhull_r-exports.def, qhull-warn.pri
set(qhull_VERSION2 "2015.2 2016/01/18") # not used, See global.c, global_r.c, rbox.c, rbox_r.c
diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt
index d85be22309..f4e122e00a 100644
--- a/src/slic3r/CMakeLists.txt
+++ b/src/slic3r/CMakeLists.txt
@@ -177,6 +177,10 @@ set(SLIC3R_GUI_SOURCES
GUI/SavePresetDialog.cpp
GUI/GUI_Colors.hpp
GUI/GUI_Colors.cpp
+ GUI/PhysicalPrinterDialog.hpp
+ GUI/PhysicalPrinterDialog.cpp
+ GUI/PrintHostDialogs.cpp
+ GUI/PrintHostDialogs.hpp
GUI/GUI_Factories.cpp
GUI/GUI_Factories.hpp
GUI/GUI_ObjectList.cpp
@@ -348,6 +352,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Calibration.cpp
GUI/PrintOptionsDialog.hpp
GUI/PrintOptionsDialog.cpp
+ GUI/BonjourDialog.hpp
+ GUI/BonjourDialog.cpp
Utils/json_diff.hpp
Utils/json_diff.cpp
GUI/KBShortcutsDialog.hpp
@@ -375,6 +381,22 @@ set(SLIC3R_GUI_SOURCES
Utils/ColorSpaceConvert.cpp
Utils/NetworkAgent.cpp
Utils/NetworkAgent.hpp
+ Utils/OctoPrint.cpp
+ Utils/OctoPrint.hpp
+ Utils/PrintHost.cpp
+ Utils/PrintHost.hpp
+ Utils/Serial.cpp
+ Utils/Serial.hpp
+ Utils/MKS.hpp
+ Utils/MKS.cpp
+ Utils/Duet.cpp
+ Utils/Duet.hpp
+ Utils/FlashAir.cpp
+ Utils/FlashAir.hpp
+ Utils/AstroBox.cpp
+ Utils/AstroBox.hpp
+ Utils/Repetier.cpp
+ Utils/Repetier.hpp
)
if (APPLE)
diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp
index 4ef4a0e074..d906dd17d9 100644
--- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp
+++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp
@@ -227,6 +227,9 @@ void BackgroundSlicingProcess::process_fff()
if (! m_export_path.empty()) {
wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_began_id));
finalize_gcode();
+ } else if (! m_upload_job.empty()) {
+ wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_began_id));
+ prepare_upload();
} else {
m_print->set_status(100, _utf8(L("Slicing complete")));
}
@@ -684,6 +687,19 @@ void BackgroundSlicingProcess::schedule_export(const std::string &path, bool exp
m_export_path_on_removable_media = export_path_on_removable_media;
}
+void BackgroundSlicingProcess::schedule_upload(Slic3r::PrintHostJob upload_job)
+{
+ assert(m_export_path.empty());
+ if (! m_export_path.empty())
+ return;
+
+ // Guard against entering the export step before changing the export path.
+ std::scoped_lock lock(m_print->state_mutex());
+ this->invalidate_step(bspsGCodeFinalize);
+ m_export_path.clear();
+ m_upload_job = std::move(upload_job);
+}
+
void BackgroundSlicingProcess::reset_export()
{
assert(! this->running());
@@ -800,6 +816,45 @@ void BackgroundSlicingProcess::finalize_gcode()
m_print->set_status(100, (boost::format(_utf8(L("Succeed to export G-code to %1%"))) % export_path).str());
}
+// A print host upload job has been scheduled, enqueue it to the printhost job queue
+void BackgroundSlicingProcess::prepare_upload()
+{
+ // Generate a unique temp path to which the gcode/zip file is copied/exported
+ boost::filesystem::path source_path = boost::filesystem::temp_directory_path()
+ / boost::filesystem::unique_path("." SLIC3R_APP_KEY ".upload.%%%%-%%%%-%%%%-%%%%");
+
+ if (m_print == m_fff_print) {
+ m_print->set_status(95, _utf8(L("Running post-processing scripts")));
+ std::string error_message;
+ if (copy_file(m_temp_output_path, source_path.string(), error_message) != SUCCESS)
+ throw Slic3r::RuntimeError(_utf8(L("Copying of the temporary G-code to the output G-code failed")));
+ m_upload_job.upload_data.upload_path = m_fff_print->print_statistics().finalize_output_path(m_upload_job.upload_data.upload_path.string());
+ // Make a copy of the source path, as run_post_process_scripts() is allowed to change it when making a copy of the source file
+ // (not here, but when the final target is a file).
+ std::string source_path_str = source_path.string();
+ std::string output_name_str = m_upload_job.upload_data.upload_path.string();
+ if (run_post_process_scripts(source_path_str, false, m_upload_job.printhost->get_name(), output_name_str, m_fff_print->full_print_config()))
+ m_upload_job.upload_data.upload_path = output_name_str;
+ } else {
+ m_upload_job.upload_data.upload_path = m_sla_print->print_statistics().finalize_output_path(m_upload_job.upload_data.upload_path.string());
+
+ ThumbnailsList thumbnails = this->render_thumbnails(
+ ThumbnailsParams{current_print()->full_print_config().option("thumbnails")->values, true, true, true, true});
+ // true, false, true, true); // renders also supports and pad
+ Zipper zipper{source_path.string()};
+ m_sla_archive.export_print(zipper, *m_sla_print, m_upload_job.upload_data.upload_path.string());
+ for (const ThumbnailData& data : thumbnails)
+ if (data.is_valid())
+ write_thumbnail(zipper, data);
+ zipper.finalize();
+ }
+
+ m_print->set_status(100, (boost::format(_utf8(L("Scheduling upload to `%1%`. See Window -> Print Host Upload Queue"))) % m_upload_job.printhost->get_host()).str());
+
+ m_upload_job.upload_data.source_path = std::move(source_path);
+
+ GUI::wxGetApp().printhost_job_queue().enqueue(std::move(m_upload_job));
+}
// Executed by the background thread, to start a task on the UI thread.
ThumbnailsList BackgroundSlicingProcess::render_thumbnails(const ThumbnailsParams ¶ms)
{
diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.hpp b/src/slic3r/GUI/BackgroundSlicingProcess.hpp
index 9a28cdab93..d369b010bb 100644
--- a/src/slic3r/GUI/BackgroundSlicingProcess.hpp
+++ b/src/slic3r/GUI/BackgroundSlicingProcess.hpp
@@ -12,6 +12,7 @@
#include "libslic3r/PrintBase.hpp"
#include "libslic3r/GCode/ThumbnailData.hpp"
#include "libslic3r/Format/SL1.hpp"
+#include "slic3r/Utils/PrintHost.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "PartPlate.hpp"
@@ -148,10 +149,14 @@ public:
// Set the export path of the G-code.
// Once the path is set, the G-code
void schedule_export(const std::string &path, bool export_path_on_removable_media);
+ // Set print host upload job data to be enqueued to the PrintHostJobQueue
+ // after current print slicing is complete
+ void schedule_upload(Slic3r::PrintHostJob upload_job);
// Clear m_export_path.
void reset_export();
// Once the G-code export is scheduled, the apply() methods will do nothing.
bool is_export_scheduled() const { return ! m_export_path.empty(); }
+ bool is_upload_scheduled() const { return ! m_upload_job.empty(); }
enum State {
// m_thread is not running yet, or it did not reach the STATE_IDLE yet (it does not wait on the condition yet).
@@ -238,6 +243,9 @@ private:
// but once set, it cannot be re-set.
std::string m_export_path;
bool m_export_path_on_removable_media = false;
+ // Print host upload job to schedule after slicing is complete, used by schedule_upload(),
+ // empty by default (ie. no upload to schedule)
+ PrintHostJob m_upload_job;
// Thread, on which the background processing is executed. The thread will always be present
// and ready to execute the slicing process.
boost::thread m_thread;
@@ -276,6 +284,7 @@ private:
// If the background processing stop was requested, throw CanceledException.
void throw_if_canceled() const { if (m_print->canceled()) throw CanceledException(); }
void finalize_gcode();
+ void prepare_upload();
// To be executed at the background thread.
ThumbnailsList render_thumbnails(const ThumbnailsParams ¶ms);
// Execute task from background thread on the UI thread synchronously. Returns true if processed, false if cancelled before executing the task.
diff --git a/src/slic3r/GUI/BonjourDialog.cpp b/src/slic3r/GUI/BonjourDialog.cpp
new file mode 100644
index 0000000000..516b1ab4a5
--- /dev/null
+++ b/src/slic3r/GUI/BonjourDialog.cpp
@@ -0,0 +1,239 @@
+#include "slic3r/Utils/Bonjour.hpp" // On Windows, boost needs to be included before wxWidgets headers
+
+#include "BonjourDialog.hpp"
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "slic3r/GUI/GUI.hpp"
+#include "slic3r/GUI/GUI_App.hpp"
+#include "slic3r/GUI/I18N.hpp"
+#include "slic3r/Utils/Bonjour.hpp"
+
+namespace Slic3r {
+
+
+class BonjourReplyEvent : public wxEvent
+{
+public:
+ BonjourReply reply;
+
+ BonjourReplyEvent(wxEventType eventType, int winid, BonjourReply &&reply) :
+ wxEvent(winid, eventType),
+ reply(std::move(reply))
+ {}
+
+ virtual wxEvent *Clone() const
+ {
+ return new BonjourReplyEvent(*this);
+ }
+};
+
+wxDEFINE_EVENT(EVT_BONJOUR_REPLY, BonjourReplyEvent);
+
+wxDECLARE_EVENT(EVT_BONJOUR_COMPLETE, wxCommandEvent);
+wxDEFINE_EVENT(EVT_BONJOUR_COMPLETE, wxCommandEvent);
+
+class ReplySet: public std::set {};
+
+struct LifetimeGuard
+{
+ std::mutex mutex;
+ BonjourDialog *dialog;
+
+ LifetimeGuard(BonjourDialog *dialog) : dialog(dialog) {}
+};
+
+BonjourDialog::BonjourDialog(wxWindow *parent, Slic3r::PrinterTechnology tech)
+ : wxDialog(parent, wxID_ANY, _(L("Network lookup")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER)
+ , list(new wxListView(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT|wxSIMPLE_BORDER))
+ , replies(new ReplySet)
+ , label(new wxStaticText(this, wxID_ANY, ""))
+ , timer(new wxTimer())
+ , timer_state(0)
+ , tech(tech)
+{
+ const int em = GUI::wxGetApp().em_unit();
+ list->SetMinSize(wxSize(80 * em, 30 * em));
+
+ wxBoxSizer *vsizer = new wxBoxSizer(wxVERTICAL);
+
+ vsizer->Add(label, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, em);
+
+ list->SetSingleStyle(wxLC_SINGLE_SEL);
+ list->SetSingleStyle(wxLC_SORT_DESCENDING);
+ list->AppendColumn(_(L("Address")), wxLIST_FORMAT_LEFT, 5 * em);
+ list->AppendColumn(_(L("Hostname")), wxLIST_FORMAT_LEFT, 10 * em);
+ list->AppendColumn(_(L("Service name")), wxLIST_FORMAT_LEFT, 20 * em);
+ if (tech == ptFFF) {
+ list->AppendColumn(_(L("OctoPrint version")), wxLIST_FORMAT_LEFT, 5 * em);
+ }
+
+ vsizer->Add(list, 1, wxEXPAND | wxALL, em);
+
+ wxBoxSizer *button_sizer = new wxBoxSizer(wxHORIZONTAL);
+ button_sizer->Add(new wxButton(this, wxID_OK, "OK"), 0, wxALL, em);
+ button_sizer->Add(new wxButton(this, wxID_CANCEL, "Cancel"), 0, wxALL, em);
+ // ^ Note: The Ok/Cancel labels are translated by wxWidgets
+
+ vsizer->Add(button_sizer, 0, wxALIGN_CENTER);
+ SetSizerAndFit(vsizer);
+
+ Bind(EVT_BONJOUR_REPLY, &BonjourDialog::on_reply, this);
+
+ Bind(EVT_BONJOUR_COMPLETE, [this](wxCommandEvent &) {
+ this->timer_state = 0;
+ });
+
+ Bind(wxEVT_TIMER, &BonjourDialog::on_timer, this);
+ GUI::wxGetApp().UpdateDlgDarkUI(this);
+}
+
+BonjourDialog::~BonjourDialog()
+{
+ // Needed bacuse of forward defs
+}
+
+bool BonjourDialog::show_and_lookup()
+{
+ Show(); // Because we need GetId() to work before ShowModal()
+
+ timer->Stop();
+ timer->SetOwner(this);
+ timer_state = 1;
+ timer->Start(1000);
+ on_timer_process();
+
+ // The background thread needs to queue messages for this dialog
+ // and for that it needs a valid pointer to it (mandated by the wxWidgets API).
+ // Here we put the pointer under a shared_ptr and protect it by a mutex,
+ // so that both threads can access it safely.
+ auto dguard = std::make_shared(this);
+
+ // Note: More can be done here when we support discovery of hosts other than Octoprint and SL1
+ Bonjour::TxtKeys txt_keys { "version", "model" };
+
+ bonjour = Bonjour("octoprint")
+ .set_txt_keys(std::move(txt_keys))
+ .set_retries(3)
+ .set_timeout(4)
+ .on_reply([dguard](BonjourReply &&reply) {
+ std::lock_guard lock_guard(dguard->mutex);
+ auto dialog = dguard->dialog;
+ if (dialog != nullptr) {
+ auto evt = new BonjourReplyEvent(EVT_BONJOUR_REPLY, dialog->GetId(), std::move(reply));
+ wxQueueEvent(dialog, evt);
+ }
+ })
+ .on_complete([dguard]() {
+ std::lock_guard lock_guard(dguard->mutex);
+ auto dialog = dguard->dialog;
+ if (dialog != nullptr) {
+ auto evt = new wxCommandEvent(EVT_BONJOUR_COMPLETE, dialog->GetId());
+ wxQueueEvent(dialog, evt);
+ }
+ })
+ .lookup();
+
+ bool res = ShowModal() == wxID_OK && list->GetFirstSelected() >= 0;
+ {
+ // Tell the background thread the dialog is going away...
+ std::lock_guard lock_guard(dguard->mutex);
+ dguard->dialog = nullptr;
+ }
+ return res;
+}
+
+wxString BonjourDialog::get_selected() const
+{
+ auto sel = list->GetFirstSelected();
+ return sel >= 0 ? list->GetItemText(sel) : wxString();
+}
+
+
+// Private
+
+void BonjourDialog::on_reply(BonjourReplyEvent &e)
+{
+ if (replies->find(e.reply) != replies->end()) {
+ // We already have this reply
+ return;
+ }
+
+ // Filter replies based on selected technology
+ const auto model = e.reply.txt_data.find("model");
+ const bool sl1 = model != e.reply.txt_data.end() && model->second == "SL1";
+ if ((tech == ptFFF && sl1) || (tech == ptSLA && !sl1)) {
+ return;
+ }
+
+ replies->insert(std::move(e.reply));
+
+ auto selected = get_selected();
+
+ wxWindowUpdateLocker freeze_guard(this);
+ (void)freeze_guard;
+
+ list->DeleteAllItems();
+
+ // The whole list is recreated so that we benefit from it already being sorted in the set.
+ // (And also because wxListView's sorting API is bananas.)
+ for (const auto &reply : *replies) {
+ auto item = list->InsertItem(0, reply.full_address);
+ list->SetItem(item, 1, reply.hostname);
+ list->SetItem(item, 2, reply.service_name);
+
+ if (tech == ptFFF) {
+ const auto it = reply.txt_data.find("version");
+ if (it != reply.txt_data.end()) {
+ list->SetItem(item, 3, GUI::from_u8(it->second));
+ }
+ }
+ }
+
+ const int em = GUI::wxGetApp().em_unit();
+
+ for (int i = 0; i < list->GetColumnCount(); i++) {
+ list->SetColumnWidth(i, wxLIST_AUTOSIZE);
+ if (list->GetColumnWidth(i) < 10 * em) { list->SetColumnWidth(i, 10 * em); }
+ }
+
+ if (!selected.IsEmpty()) {
+ // Attempt to preserve selection
+ auto hit = list->FindItem(-1, selected);
+ if (hit >= 0) { list->SetItemState(hit, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); }
+ }
+}
+
+void BonjourDialog::on_timer(wxTimerEvent &)
+{
+ on_timer_process();
+}
+
+// This is here so the function can be bound to wxEVT_TIMER and also called
+// explicitly (wxTimerEvent should not be created by user code).
+void BonjourDialog::on_timer_process()
+{
+ const auto search_str = _utf8(L("Searching for devices"));
+
+ if (timer_state > 0) {
+ const std::string dots(timer_state, '.');
+ label->SetLabel(GUI::from_u8((boost::format("%1% %2%") % search_str % dots).str()));
+ timer_state = (timer_state) % 3 + 1;
+ } else {
+ label->SetLabel(GUI::from_u8((boost::format("%1%: %2%") % search_str % (_utf8(L("Finished"))+".")).str()));
+ timer->Stop();
+ }
+}
+
+
+
+
+}
diff --git a/src/slic3r/GUI/BonjourDialog.hpp b/src/slic3r/GUI/BonjourDialog.hpp
new file mode 100644
index 0000000000..def0838d7e
--- /dev/null
+++ b/src/slic3r/GUI/BonjourDialog.hpp
@@ -0,0 +1,53 @@
+#ifndef slic3r_BonjourDialog_hpp_
+#define slic3r_BonjourDialog_hpp_
+
+#include
+
+#include
+
+#include "libslic3r/PrintConfig.hpp"
+
+class wxListView;
+class wxStaticText;
+class wxTimer;
+class wxTimerEvent;
+
+
+namespace Slic3r {
+
+class Bonjour;
+class BonjourReplyEvent;
+class ReplySet;
+
+
+class BonjourDialog: public wxDialog
+{
+public:
+ BonjourDialog(wxWindow *parent, Slic3r::PrinterTechnology);
+ BonjourDialog(BonjourDialog &&) = delete;
+ BonjourDialog(const BonjourDialog &) = delete;
+ BonjourDialog &operator=(BonjourDialog &&) = delete;
+ BonjourDialog &operator=(const BonjourDialog &) = delete;
+ ~BonjourDialog();
+
+ bool show_and_lookup();
+ wxString get_selected() const;
+private:
+ wxListView *list;
+ std::unique_ptr replies;
+ wxStaticText *label;
+ std::shared_ptr bonjour;
+ std::unique_ptr timer;
+ unsigned timer_state;
+ Slic3r::PrinterTechnology tech;
+
+ void on_reply(BonjourReplyEvent &);
+ void on_timer(wxTimerEvent &);
+ void on_timer_process();
+};
+
+
+
+}
+
+#endif
diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp
index 90d97ee43f..d2b45f6d0e 100644
--- a/src/slic3r/GUI/Field.cpp
+++ b/src/slic3r/GUI/Field.cpp
@@ -1225,7 +1225,9 @@ void Choice::set_value(const boost::any& value, bool change_event)
// BBS
case coEnums: {
int val = boost::any_cast(value);
-
+ if (m_opt_id.compare("host_type") == 0 && val != 0 &&
+ m_opt.enum_values.size() > field->GetCount()) // for case, when PrusaLink isn't used as a HostType
+ val--;
if (m_opt_id == "top_surface_pattern" || m_opt_id == "bottom_surface_pattern" || m_opt_id == "sparse_infill_pattern")
{
std::string key;
@@ -1305,7 +1307,11 @@ boost::any& Choice::get_value()
// BBS
if (m_opt.type == coEnum || m_opt.type == coEnums)
{
- if (m_opt_id == "top_surface_pattern" || m_opt_id == "bottom_surface_pattern" || m_opt_id == "sparse_infill_pattern") {
+ if (m_opt_id.compare("host_type") == 0 && m_opt.enum_values.size() > field->GetCount()) {
+ // for case, when PrusaLink isn't used as a HostType
+ m_value = field->GetSelection()+1;
+ }
+ else if (m_opt_id == "top_surface_pattern" || m_opt_id == "bottom_surface_pattern" || m_opt_id == "sparse_infill_pattern") {
const std::string& key = m_opt.enum_values[field->GetSelection()];
m_value = int(ConfigOptionEnum::get_enum_values().at(key));
}
diff --git a/src/slic3r/GUI/GLToolbar.cpp b/src/slic3r/GUI/GLToolbar.cpp
index c7a4699002..a2e0ce2055 100644
--- a/src/slic3r/GUI/GLToolbar.cpp
+++ b/src/slic3r/GUI/GLToolbar.cpp
@@ -24,6 +24,7 @@ wxDEFINE_EVENT(EVT_GLTOOLBAR_SLICE_PLATE, SimpleEvent);
wxDEFINE_EVENT(EVT_GLTOOLBAR_PRINT_ALL, SimpleEvent);
wxDEFINE_EVENT(EVT_GLTOOLBAR_PRINT_PLATE, SimpleEvent);
wxDEFINE_EVENT(EVT_GLTOOLBAR_EXPORT_GCODE, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLTOOLBAR_SEND_GCODE, SimpleEvent);
wxDEFINE_EVENT(EVT_GLTOOLBAR_EXPORT_SLICED_FILE, SimpleEvent);
wxDEFINE_EVENT(EVT_GLTOOLBAR_PRINT_SELECT, SimpleEvent);
diff --git a/src/slic3r/GUI/GLToolbar.hpp b/src/slic3r/GUI/GLToolbar.hpp
index c1b47b0fb0..9ca3868b7b 100644
--- a/src/slic3r/GUI/GLToolbar.hpp
+++ b/src/slic3r/GUI/GLToolbar.hpp
@@ -24,6 +24,7 @@ wxDECLARE_EVENT(EVT_GLTOOLBAR_SLICE_PLATE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLTOOLBAR_PRINT_ALL, SimpleEvent);
wxDECLARE_EVENT(EVT_GLTOOLBAR_PRINT_PLATE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLTOOLBAR_EXPORT_GCODE, SimpleEvent);
+wxDECLARE_EVENT(EVT_GLTOOLBAR_SEND_GCODE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLTOOLBAR_EXPORT_SLICED_FILE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLTOOLBAR_PRINT_SELECT, SimpleEvent);
diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp
index 588bda8565..a258ac6315 100644
--- a/src/slic3r/GUI/GUI_App.cpp
+++ b/src/slic3r/GUI/GUI_App.cpp
@@ -56,6 +56,7 @@
#include "GLCanvas3D.hpp"
#include "../Utils/PresetUpdater.hpp"
+#include "../Utils/PrintHost.hpp"
#include "../Utils/Process.hpp"
#include "../Utils/MacDarkMode.hpp"
#include "../Utils/Http.hpp"
@@ -70,6 +71,7 @@
#include "NotificationManager.hpp"
#include "UnsavedChangesDialog.hpp"
#include "SavePresetDialog.hpp"
+#include "PrintHostDialogs.hpp"
#include "DesktopIntegrationDialog.hpp"
#include "SendSystemInfoDialog.hpp"
#include "ParamsDialog.hpp"
@@ -2081,6 +2083,8 @@ bool GUI_App::on_init_inner()
plater_->init_notification_manager();
+ m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg()));
+
if (is_gcode_viewer()) {
mainframe->update_layout();
if (plater_ != nullptr)
@@ -2596,6 +2600,7 @@ void GUI_App::recreate_GUI(const wxString& msg_name)
old_main_frame->Destroy();
dlg.Update(80, _L("Loading current presets") + dots);
+ m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg()));
load_current_presets();
mainframe->Show(true);
//mainframe->refresh_plugin_tips();
@@ -4301,6 +4306,34 @@ bool GUI_App::can_load_project()
return true;
}
+bool GUI_App::check_print_host_queue()
+{
+ wxString dirty;
+ std::vector> jobs;
+ // Get ongoing jobs from dialog
+ mainframe->m_printhost_queue_dlg->get_active_jobs(jobs);
+ if (jobs.empty())
+ return true;
+ // Show dialog
+ wxString job_string = wxString();
+ for (const auto& job : jobs) {
+ job_string += format_wxstr(" %1% : %2% \n", job.first, job.second);
+ }
+ wxString message;
+ message += _(L("The uploads are still ongoing")) + ":\n\n" + job_string +"\n" + _(L("Stop them and continue anyway?"));
+ //wxMessageDialog dialog(mainframe,
+ MessageDialog dialog(mainframe,
+ message,
+ wxString(SLIC3R_APP_NAME) + " - " + _(L("Ongoing uploads")),
+ wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT);
+ if (dialog.ShowModal() == wxID_YES)
+ return true;
+
+ // TODO: If already shown, bring forward
+ mainframe->m_printhost_queue_dlg->Show();
+ return false;
+}
+
bool GUI_App::checked_tab(Tab* tab)
{
bool ret = true;
diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp
index d88e1053fc..d02b0fe941 100644
--- a/src/slic3r/GUI/GUI_App.hpp
+++ b/src/slic3r/GUI/GUI_App.hpp
@@ -7,11 +7,13 @@
#include "ConfigWizard.hpp"
#include "OpenGLManager.hpp"
#include "libslic3r/Preset.hpp"
+#include "wxExtensions.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/DeviceManager.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/GUI/WebViewDialog.hpp"
#include "slic3r/GUI/Jobs/UpgradeNetworkJob.hpp"
+#include "../Utils/PrintHost.hpp"
#include
#include
@@ -42,6 +44,7 @@ class AppConfig;
class PresetBundle;
class PresetUpdater;
class ModelObject;
+// class PrintHostJobQueue;
class Model;
class DeviceManager;
class NetworkAgent;
@@ -244,6 +247,7 @@ private:
//std::unique_ptr m_removable_drive_manager;
std::unique_ptr m_imgui;
+ std::unique_ptr m_printhost_job_queue;
//std::unique_ptr m_other_instance_message_handler;
//std::unique_ptr m_single_instance_checker;
//std::string m_instance_hash_string;
@@ -416,6 +420,7 @@ public:
void apply_keeped_preset_modifications();
bool check_and_keep_current_preset_changes(const wxString& caption, const wxString& header, int action_buttons, bool* postponed_apply_of_keeped_changes = nullptr);
bool can_load_project();
+ bool check_print_host_queue();
bool checked_tab(Tab* tab);
//BBS: add preset combox re-active logic
void load_current_presets(bool active_preset_combox = false, bool check_printer_presets = true);
@@ -483,6 +488,8 @@ public:
ImGuiWrapper* imgui() { return m_imgui.get(); }
+ PrintHostJobQueue& printhost_job_queue() { return *m_printhost_job_queue.get(); }
+
void open_web_page_localized(const std::string &http_address);
bool may_switch_to_SLA_preset(const wxString& caption);
bool run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page = ConfigWizard::SP_WELCOME);
diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp
index fc0df06bce..c726e729a3 100644
--- a/src/slic3r/GUI/MainFrame.cpp
+++ b/src/slic3r/GUI/MainFrame.cpp
@@ -25,6 +25,7 @@
#include "ProgressStatusBar.hpp"
#include "3DScene.hpp"
#include "ParamsDialog.hpp"
+#include "PrintHostDialogs.hpp"
#include "wxExtensions.hpp"
#include "GUI_ObjectList.hpp"
#include "Mouse3DController.hpp"
@@ -149,6 +150,7 @@ wxDEFINE_EVENT(EVT_SYNC_CLOUD_PRESET, SimpleEvent);
MainFrame::MainFrame() :
DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_STYLE, "mainframe")
+ , m_printhost_queue_dlg(new PrintHostQueueDialog(this))
// BBS
, m_recent_projects(9)
, m_settings_dialog(this)
@@ -399,6 +401,10 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< "cancelled by close_with_confirm selection";
return;
}
+ if (event.CanVeto() && !wxGetApp().check_print_host_queue()) {
+ event.Veto();
+ return;
+ }
#if 0 // BBS
//if (m_plater != nullptr) {
@@ -1105,6 +1111,17 @@ bool MainFrame::can_export_gcode() const
return true;
}
+bool MainFrame::can_send_gcode() const
+{
+ if (m_plater && !m_plater->model().objects.empty())
+ {
+ auto cfg = wxGetApp().preset_bundle->printers.get_selected_preset().config;
+ if (const auto *print_host_opt = cfg.option("print_host"); print_host_opt)
+ return !print_host_opt->value.empty();
+ }
+ return true;
+}
+
/*bool MainFrame::can_export_gcode_sd() const
{
if (m_plater == nullptr)
@@ -1226,6 +1243,8 @@ wxBoxSizer* MainFrame::create_side_tools()
}
else if (m_print_select == eExportGcode)
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_EXPORT_GCODE));
+ else if (m_print_select == eSendGcode)
+ wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SEND_GCODE));
else if (m_print_select == eExportSlicedFile)
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_EXPORT_SLICED_FILE));
});
@@ -1313,6 +1332,17 @@ wxBoxSizer* MainFrame::create_side_tools()
this->Layout();
p->Dismiss();
});
+ SideButton* send_gcode_btn = new SideButton(p, _L("Send sliced file (.gcode)"), "");
+ send_gcode_btn->SetCornerRadius(0);
+ send_gcode_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) {
+ m_print_btn->SetLabel(_L("Send Sliced File (.gcode)"));
+ m_print_select = eSendGcode;
+ if (m_print_enable)
+ m_print_enable = get_enable_print_status() && can_send_gcode();
+ m_print_btn->Enable(m_print_enable);
+ this->Layout();
+ p->Dismiss();
+ });
#if ENABEL_PRINT_ALL
p->append_button(print_all_btn);
@@ -1320,6 +1350,7 @@ wxBoxSizer* MainFrame::create_side_tools()
p->append_button(print_plate_btn);
p->append_button(export_sliced_file_3mf_btn);
p->append_button(export_sliced_file_gcode_btn);
+ p->append_button(send_gcode_btn);
p->Popup(m_print_btn);
}
);
diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp
index 17ff07465d..1b80eb9234 100644
--- a/src/slic3r/GUI/MainFrame.hpp
+++ b/src/slic3r/GUI/MainFrame.hpp
@@ -40,6 +40,7 @@ namespace GUI
{
class Tab;
+class PrintHostQueueDialog;
class Plater;
class MainFrame;
class ParamsDialog;
@@ -110,6 +111,7 @@ class MainFrame : public DPIFrame
bool can_export_toolpaths() const;
bool can_export_supports() const;
bool can_export_gcode() const;
+ bool can_send_gcode() const;
//bool can_export_gcode_sd() const;
//bool can_eject() const;
bool can_slice() const;
@@ -171,6 +173,7 @@ class MainFrame : public DPIFrame
ePrintPlate = 1,
eExportSlicedFile = 2,
eExportGcode = 3,
+ eSendGcode = 4,
};
@@ -305,6 +308,7 @@ public:
// BBS. Replace title bar and menu bar with top bar.
BBLTopbar* m_topbar{ nullptr };
+ PrintHostQueueDialog* printhost_queue_dlg() { return m_printhost_queue_dlg; }
Plater* m_plater { nullptr };
//BBS: GUI refactor
MonitorPanel* m_monitor{ nullptr };
@@ -321,6 +325,7 @@ public:
SettingsDialog m_settings_dialog;
DiffPresetDialog diff_dialog;
wxWindow* m_plater_page{ nullptr };
+ PrintHostQueueDialog* m_printhost_queue_dlg;
// BBS
mutable int m_print_select{ ePrintAll };
diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp
index 7cee03e5d1..4a8ef590ea 100644
--- a/src/slic3r/GUI/MsgDialog.cpp
+++ b/src/slic3r/GUI/MsgDialog.cpp
@@ -114,9 +114,13 @@ Button* MsgDialog::add_button(wxWindowID btn_id, bool set_focus /*= false*/, con
else if (label.length() >= 5 && label.length() < 8) {
type = ButtonSizeMiddle;
btn->SetMinSize(MSG_DIALOG_MIDDLE_BUTTON_SIZE);
+ }
+ else if (label.length() >= 8 && label.length() < 12) {
+ type = ButtonSizeMiddle;
+ btn->SetMinSize(MSG_DIALOG_LONG_BUTTON_SIZE);
} else {
type = ButtonSizeLong;
- btn->SetMinSize(MSG_DIALOG_LONG_BUTTON_SIZE);
+ btn->SetMinSize(MSG_DIALOG_LONGER_BUTTON_SIZE);
}
btn->SetCornerRadius(12);
diff --git a/src/slic3r/GUI/MsgDialog.hpp b/src/slic3r/GUI/MsgDialog.hpp
index e00ce5689b..c6dc0b5637 100644
--- a/src/slic3r/GUI/MsgDialog.hpp
+++ b/src/slic3r/GUI/MsgDialog.hpp
@@ -29,6 +29,7 @@ enum ButtonSizeType{
#define MSG_DIALOG_BUTTON_SIZE wxSize(FromDIP(58), FromDIP(24))
#define MSG_DIALOG_MIDDLE_BUTTON_SIZE wxSize(FromDIP(76), FromDIP(24))
#define MSG_DIALOG_LONG_BUTTON_SIZE wxSize(FromDIP(90), FromDIP(24))
+#define MSG_DIALOG_LONGER_BUTTON_SIZE wxSize(FromDIP(120), FromDIP(24))
namespace Slic3r {
diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp
index d2683e4ccf..5a921c0aef 100644
--- a/src/slic3r/GUI/NotificationManager.cpp
+++ b/src/slic3r/GUI/NotificationManager.cpp
@@ -983,6 +983,149 @@ void NotificationManager::UpdatedItemsInfoNotification::add_type(InfoItemType ty
update(data);
}
+//------PrintHostUploadNotification----------------
+void NotificationManager::PrintHostUploadNotification::init()
+{
+ ProgressBarNotification::init();
+ if (m_state == EState::NotFading && m_uj_state == UploadJobState::PB_COMPLETED)
+ m_state = EState::Shown;
+}
+void NotificationManager::PrintHostUploadNotification::count_spaces()
+{
+ //determine line width
+ m_line_height = ImGui::CalcTextSize("A").y;
+
+ m_left_indentation = m_line_height;
+ if (m_uj_state == UploadJobState::PB_ERROR) {
+ std::string text;
+ text = (m_data.level == NotificationLevel::ErrorNotificationLevel ? ImGui::ErrorMarker : ImGui::WarningMarker);
+ float picture_width = ImGui::CalcTextSize(text.c_str()).x;
+ m_left_indentation = picture_width + m_line_height / 2;
+ }
+ m_window_width_offset = m_line_height * 6; //(m_has_cancel_button ? 6 : 4);
+ m_window_width = m_line_height * 25;
+}
+bool NotificationManager::PrintHostUploadNotification::push_background_color()
+{
+
+ if (m_uj_state == UploadJobState::PB_ERROR) {
+ ImVec4 backcolor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg);
+ backcolor.x += 0.3f;
+ push_style_color(ImGuiCol_WindowBg, backcolor, m_state == EState::FadingOut, m_current_fade_opacity);
+ return true;
+ }
+ return false;
+}
+void NotificationManager::PrintHostUploadNotification::set_percentage(float percent)
+{
+ m_percentage = percent;
+ if (percent >= 1.0f) {
+ m_uj_state = UploadJobState::PB_COMPLETED;
+ m_has_cancel_button = false;
+ init();
+ } else if (percent < 0.0f) {
+ error();
+ } else {
+ m_uj_state = UploadJobState::PB_PROGRESS;
+ m_has_cancel_button = true;
+ }
+}
+void NotificationManager::PrintHostUploadNotification::render_bar(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
+{
+ std::string text;
+ switch (m_uj_state) {
+ case Slic3r::GUI::NotificationManager::PrintHostUploadNotification::UploadJobState::PB_PROGRESS:
+ {
+ ProgressBarNotification::render_bar(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y);
+ float uploaded = m_file_size * m_percentage;
+ std::stringstream stream;
+ stream << std::fixed << std::setprecision(2) << (int)(m_percentage * 100) << "% - " << uploaded << " of " << m_file_size << "MB uploaded";
+ text = stream.str();
+ ImGui::SetCursorPosX(m_left_indentation);
+ ImGui::SetCursorPosY(win_size_y / 2 + win_size_y / 6 - (m_multiline ? 0 : m_line_height / 4));
+ break;
+ }
+ case Slic3r::GUI::NotificationManager::PrintHostUploadNotification::UploadJobState::PB_ERROR:
+ text = _u8L("ERROR");
+ ImGui::SetCursorPosX(m_left_indentation);
+ ImGui::SetCursorPosY(win_size_y / 2 + win_size_y / 6 - (m_multiline ? m_line_height / 4 : m_line_height / 2));
+ break;
+ case Slic3r::GUI::NotificationManager::PrintHostUploadNotification::UploadJobState::PB_CANCELLED:
+ text = _u8L("CANCELED");
+ ImGui::SetCursorPosX(m_left_indentation);
+ ImGui::SetCursorPosY(win_size_y / 2 + win_size_y / 6 - (m_multiline ? m_line_height / 4 : m_line_height / 2));
+ break;
+ case Slic3r::GUI::NotificationManager::PrintHostUploadNotification::UploadJobState::PB_COMPLETED:
+ text = _u8L("COMPLETED");
+ ImGui::SetCursorPosX(m_left_indentation);
+ ImGui::SetCursorPosY(win_size_y / 2 + win_size_y / 6 - (m_multiline ? m_line_height / 4 : m_line_height / 2));
+ break;
+ }
+
+ imgui.text(text.c_str());
+
+}
+void NotificationManager::PrintHostUploadNotification::render_left_sign(ImGuiWrapper& imgui)
+{
+ if (m_uj_state == UploadJobState::PB_ERROR) {
+ std::string text;
+ text = ImGui::ErrorMarker;
+ ImGui::SetCursorPosX(m_line_height / 3);
+ ImGui::SetCursorPosY(m_window_height / 2 - m_line_height);
+ imgui.text(text.c_str());
+ }
+}
+void NotificationManager::PrintHostUploadNotification::render_cancel_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
+{
+ ImVec2 win_size(win_size_x, win_size_y);
+ ImVec2 win_pos(win_pos_x, win_pos_y);
+ ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f));
+ ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f));
+ push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
+ push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
+ ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f));
+
+ std::string button_text;
+ button_text = ImGui::CancelButton;
+
+ if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - m_line_height * 5.f, win_pos.y),
+ ImVec2(win_pos.x - m_line_height * 2.5f, win_pos.y + win_size.y),
+ true))
+ {
+ button_text = ImGui::CancelHoverButton;
+ // tooltip
+ long time_now = wxGetLocalTime();
+ if (m_hover_time > 0 && m_hover_time < time_now) {
+ ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BACKGROUND);
+ ImGui::BeginTooltip();
+ imgui.text(_u8L("Cancel upload") + " " + GUI::shortkey_ctrl_prefix() + "T");
+ ImGui::EndTooltip();
+ ImGui::PopStyleColor();
+ }
+ if (m_hover_time == 0)
+ m_hover_time = time_now;
+ }
+ else
+ m_hover_time = 0;
+
+ ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str());
+ ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f);
+ ImGui::SetCursorPosX(win_size.x - m_line_height * 5.0f);
+ ImGui::SetCursorPosY(win_size.y / 2 - button_size.y);
+ if (imgui.button(button_text.c_str(), button_size.x, button_size.y))
+ {
+ wxGetApp().printhost_job_queue().cancel(m_job_id - 1);
+ }
+
+ //invisible large button
+ ImGui::SetCursorPosX(win_size.x - m_line_height * 4.625f);
+ ImGui::SetCursorPosY(0);
+ if (imgui.button(" ", m_line_height * 2.f, win_size.y))
+ {
+ wxGetApp().printhost_job_queue().cancel(m_job_id - 1);
+ }
+ ImGui::PopStyleColor(5);
+}
//------SlicingProgressNotification
void NotificationManager::SlicingProgressNotification::init()
{
@@ -1596,6 +1739,59 @@ void NotificationManager::push_exporting_finished_notification(const std::string
set_slicing_progress_hidden();
}
+void NotificationManager::push_upload_job_notification(int id, float filesize, const std::string& filename, const std::string& host, float percentage)
+{
+ // find if upload with same id was not already in notification
+ // done by compare_jon_id not compare_text thus has to be performed here
+ for (std::unique_ptr& notification : m_pop_notifications) {
+ if (notification->get_type() == NotificationType::PrintHostUpload && dynamic_cast(notification.get())->compare_job_id(id)) {
+ return;
+ }
+ }
+ std::string text = PrintHostUploadNotification::get_upload_job_text(id, filename, host);
+ NotificationData data{ NotificationType::PrintHostUpload, NotificationLevel::ProgressBarNotificationLevel, 10, text };
+ push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, 0, id, filesize), 0);
+}
+void NotificationManager::set_upload_job_notification_percentage(int id, const std::string& filename, const std::string& host, float percentage)
+{
+ for (std::unique_ptr& notification : m_pop_notifications) {
+ if (notification->get_type() == NotificationType::PrintHostUpload) {
+ PrintHostUploadNotification* phun = dynamic_cast(notification.get());
+ if (phun->compare_job_id(id)) {
+ phun->set_percentage(percentage);
+ wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0);
+ break;
+ }
+ }
+ }
+}
+void NotificationManager::upload_job_notification_show_canceled(int id, const std::string& filename, const std::string& host)
+{
+ for (std::unique_ptr& notification : m_pop_notifications) {
+ if (notification->get_type() == NotificationType::PrintHostUpload) {
+ PrintHostUploadNotification* phun = dynamic_cast(notification.get());
+ if (phun->compare_job_id(id)) {
+ phun->cancel();
+ wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0);
+ break;
+ }
+ }
+ }
+}
+void NotificationManager::upload_job_notification_show_error(int id, const std::string& filename, const std::string& host)
+{
+ for (std::unique_ptr& notification : m_pop_notifications) {
+ if (notification->get_type() == NotificationType::PrintHostUpload) {
+ PrintHostUploadNotification* phun = dynamic_cast(notification.get());
+ if(phun->compare_job_id(id)) {
+ phun->error();
+ wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0);
+ break;
+ }
+ }
+ }
+}
+
void NotificationManager::init_slicing_progress_notification(std::function cancel_callback)
{
for (std::unique_ptr& notification : m_pop_notifications) {
diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp
index d758dbe061..d1947a80dd 100644
--- a/src/slic3r/GUI/NotificationManager.hpp
+++ b/src/slic3r/GUI/NotificationManager.hpp
@@ -182,6 +182,11 @@ public:
void stop_delayed_notifications_of_type(const NotificationType type);
// Creates Validate Error notification with a custom text and no fade out.
void push_validate_error_notification(StringObjectException const & error);
+ // print host upload
+ void push_upload_job_notification(int id, float filesize, const std::string& filename, const std::string& host, float percentage = 0);
+ void set_upload_job_notification_percentage(int id, const std::string& filename, const std::string& host, float percentage);
+ void upload_job_notification_show_canceled(int id, const std::string& filename, const std::string& host);
+ void upload_job_notification_show_error(int id, const std::string& filename, const std::string& host);
// Creates Slicing Error notification with a custom text and no fade out.
void push_slicing_error_notification(const std::string& text);
// Creates Slicing Warning notification with a custom text and no fade out.
@@ -557,6 +562,48 @@ private:
};
+ class PrintHostUploadNotification : public ProgressBarNotification
+ {
+ public:
+ enum class UploadJobState
+ {
+ PB_PROGRESS,
+ PB_ERROR,
+ PB_CANCELLED,
+ PB_COMPLETED
+ };
+ PrintHostUploadNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler, float percentage, int job_id, float filesize)
+ :ProgressBarNotification(n, id_provider, evt_handler)
+ , m_job_id(job_id)
+ , m_file_size(filesize)
+ {
+ m_has_cancel_button = true;
+ set_percentage(percentage);
+ }
+ static std::string get_upload_job_text(int id, const std::string& filename, const std::string& host) { return /*"[" + std::to_string(id) + "] " + */filename + " -> " + host; }
+ void set_percentage(float percent) override;
+ void cancel() { m_uj_state = UploadJobState::PB_CANCELLED; m_has_cancel_button = false; }
+ void error() { m_uj_state = UploadJobState::PB_ERROR; m_has_cancel_button = false; init(); }
+ bool compare_job_id(const int other_id) const { return m_job_id == other_id; }
+ bool compare_text(const std::string& text) const override { return false; }
+ protected:
+ void init() override;
+ void count_spaces() override;
+ bool push_background_color() override;
+ void render_bar(ImGuiWrapper& imgui,
+ const float win_size_x, const float win_size_y,
+ const float win_pos_x, const float win_pos_y) override;
+ void render_cancel_button(ImGuiWrapper& imgui,
+ const float win_size_x, const float win_size_y,
+ const float win_pos_x, const float win_pos_y) override;
+ void render_left_sign(ImGuiWrapper& imgui) override;
+ // Identifies job in cancel callback
+ int m_job_id;
+ // Size of uploaded size to be displayed in MB
+ float m_file_size;
+ long m_hover_time{ 0 };
+ UploadJobState m_uj_state{ UploadJobState::PB_PROGRESS };
+ };
class SlicingProgressNotification : public ProgressBarNotification
{
public:
diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp
new file mode 100644
index 0000000000..a487aa0379
--- /dev/null
+++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp
@@ -0,0 +1,401 @@
+#include "PhysicalPrinterDialog.hpp"
+#include "PresetComboBoxes.hpp"
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "libslic3r/libslic3r.h"
+#include "libslic3r/PrintConfig.hpp"
+#include "libslic3r/PresetBundle.hpp"
+
+#include "GUI.hpp"
+#include "GUI_App.hpp"
+#include "MainFrame.hpp"
+#include "format.hpp"
+#include "Tab.hpp"
+#include "wxExtensions.hpp"
+#include "PrintHostDialogs.hpp"
+#include "../Utils/ASCIIFolding.hpp"
+#include "../Utils/PrintHost.hpp"
+#include "../Utils/FixModelByWin10.hpp"
+#include "../Utils/UndoRedo.hpp"
+#include "RemovableDriveManager.hpp"
+#include "BitmapCache.hpp"
+#include "BonjourDialog.hpp"
+#include "MsgDialog.hpp"
+
+namespace Slic3r {
+namespace GUI {
+
+#define BORDER_W 10
+
+//------------------------------------------
+// PhysicalPrinterDialog
+//------------------------------------------
+
+PhysicalPrinterDialog::PhysicalPrinterDialog(wxWindow* parent) :
+ DPIDialog(parent, wxID_ANY, _L("Physical Printer"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), -1), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
+{
+ SetFont(wxGetApp().normal_font());
+#ifndef _WIN32
+ SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
+#endif
+
+ m_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
+ m_optgroup = new ConfigOptionsGroup(this, _L("Print Host upload"), m_config);
+ build_printhost_settings(m_optgroup);
+
+ wxStdDialogButtonSizer* btns = this->CreateStdDialogButtonSizer(wxOK | wxCANCEL);
+ wxButton* btnOK = static_cast(this->FindWindowById(wxID_OK, this));
+ wxGetApp().UpdateDarkUI(btnOK);
+ btnOK->Bind(wxEVT_BUTTON, &PhysicalPrinterDialog::OnOK, this);
+
+ wxGetApp().UpdateDarkUI(static_cast(this->FindWindowById(wxID_CANCEL, this)));
+
+
+ wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL);
+
+ // topSizer->Add(label_top , 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, BORDER_W);
+ topSizer->Add(m_optgroup->sizer , 1, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, BORDER_W);
+ topSizer->Add(btns , 0, wxEXPAND | wxALL, BORDER_W);
+
+ SetSizer(topSizer);
+ topSizer->SetSizeHints(this);
+ this->CenterOnScreen();
+}
+
+PhysicalPrinterDialog::~PhysicalPrinterDialog()
+{
+}
+
+void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgroup)
+{
+ m_optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value) {
+ if (opt_key == "host_type" || opt_key == "printhost_authorization_type")
+ this->update();
+ if (opt_key == "print_host")
+ this->update_printhost_buttons();
+ };
+
+ m_optgroup->append_single_option_line("host_type");
+
+ auto create_sizer_with_btn = [](wxWindow* parent, ScalableButton** btn, const std::string& icon_name, const wxString& label) {
+ *btn = new ScalableButton(parent, wxID_ANY, icon_name, label, wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT);
+ (*btn)->SetFont(wxGetApp().normal_font());
+
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(*btn);
+ return sizer;
+ };
+
+ auto printhost_browse = [=](wxWindow* parent)
+ {
+ auto sizer = create_sizer_with_btn(parent, &m_printhost_browse_btn, "browse", _L("Browse") + " " + dots);
+ m_printhost_browse_btn->Bind(wxEVT_BUTTON, [=](wxCommandEvent& e) {
+ 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 sizer;
+ };
+
+ auto print_host_test = [=](wxWindow* parent) {
+ auto sizer = create_sizer_with_btn(parent, &m_printhost_test_btn, "test", _L("Test"));
+
+ m_printhost_test_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) {
+ std::unique_ptr host(PrintHost::get_print_host(m_config));
+ if (!host) {
+ const wxString text = _L("Could not get a valid Printer Host reference");
+ show_error(this, text);
+ return;
+ }
+ wxString msg;
+ bool result;
+ {
+ // Show a wait cursor during the connection test, as it is blocking UI.
+ wxBusyCursor wait;
+ result = host->test(msg);
+ }
+ if (result)
+ show_info(this, host->get_test_ok_msg(), _L("Success!"));
+ else
+ show_error(this, host->get_test_failed_msg(msg));
+ });
+
+ return sizer;
+ };
+
+ auto print_host_printers = [this, create_sizer_with_btn](wxWindow* parent) {
+ //add_scaled_button(parent, &m_printhost_port_browse_btn, "browse", _(L("Refresh Printers")), wxBU_LEFT | wxBU_EXACTFIT);
+ auto sizer = create_sizer_with_btn(parent, &m_printhost_port_browse_btn, "browse", _(L("Refresh Printers")));
+ ScalableButton* btn = m_printhost_port_browse_btn;
+ btn->SetFont(Slic3r::GUI::wxGetApp().normal_font());
+ btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent e) { update_printers(); });
+ return sizer;
+ };
+
+ // Set a wider width for a better alignment
+ Option option = m_optgroup->get_option("print_host");
+ option.opt.width = Field::def_width_wider();
+ Line host_line = m_optgroup->create_single_option_line(option);
+ host_line.append_widget(printhost_browse);
+ host_line.append_widget(print_host_test);
+ m_optgroup->append_line(host_line);
+
+ m_optgroup->append_single_option_line("printhost_authorization_type");
+
+ option = m_optgroup->get_option("printhost_apikey");
+ option.opt.width = Field::def_width_wider();
+ m_optgroup->append_single_option_line(option);
+
+ option = m_optgroup->get_option("printhost_port");
+ option.opt.width = Field::def_width_wider();
+ Line port_line = m_optgroup->create_single_option_line(option);
+ port_line.append_widget(print_host_printers);
+ m_optgroup->append_line(port_line);
+
+ const auto ca_file_hint = _u8L("HTTPS CA file is optional. It is only needed if you use HTTPS with a self-signed certificate.");
+
+ if (Http::ca_file_supported()) {
+ option = m_optgroup->get_option("printhost_cafile");
+ option.opt.width = Field::def_width_wider();
+ Line cafile_line = m_optgroup->create_single_option_line(option);
+
+ auto printhost_cafile_browse = [=](wxWindow* parent) {
+ auto sizer = create_sizer_with_btn(parent, &m_printhost_cafile_browse_btn, "browse", _L("Browse") + " " + dots);
+ m_printhost_cafile_browse_btn->Bind(wxEVT_BUTTON, [this, m_optgroup](wxCommandEvent e) {
+ static const auto filemasks = _L("Certificate files (*.crt, *.pem)|*.crt;*.pem|All files|*.*");
+ wxFileDialog openFileDialog(this, _L("Open CA certificate file"), "", "", filemasks, wxFD_OPEN | wxFD_FILE_MUST_EXIST);
+ if (openFileDialog.ShowModal() != wxID_CANCEL) {
+ m_optgroup->set_value("printhost_cafile", openFileDialog.GetPath(), true);
+ m_optgroup->get_field("printhost_cafile")->field_changed();
+ }
+ });
+
+ return sizer;
+ };
+
+ cafile_line.append_widget(printhost_cafile_browse);
+ m_optgroup->append_line(cafile_line);
+
+ Line cafile_hint{ "", "" };
+ cafile_hint.full_width = 1;
+ cafile_hint.widget = [ca_file_hint](wxWindow* parent) {
+ auto txt = new wxStaticText(parent, wxID_ANY, ca_file_hint);
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(txt);
+ return sizer;
+ };
+ m_optgroup->append_line(cafile_hint);
+ }
+ else {
+
+ Line line{ "", "" };
+ line.full_width = 1;
+
+ line.widget = [ca_file_hint](wxWindow* parent) {
+ std::string info = _u8L("HTTPS CA File") + ":\n\t" +
+ (boost::format(_u8L("On this system, %s uses HTTPS certificates from the system Certificate Store or Keychain.")) % SLIC3R_APP_NAME).str() +
+ "\n\t" + _u8L("To use a custom CA file, please import your CA file into Certificate Store / Keychain.");
+
+ //auto txt = new wxStaticText(parent, wxID_ANY, from_u8((boost::format("%1%\n\n\t%2%") % info % ca_file_hint).str()));
+ auto txt = new wxStaticText(parent, wxID_ANY, from_u8((boost::format("%1%\n\t%2%") % info % ca_file_hint).str()));
+ txt->SetFont(wxGetApp().normal_font());
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(txt, 1, wxEXPAND);
+ return sizer;
+ };
+ m_optgroup->append_line(line);
+ }
+
+ for (const std::string& opt_key : std::vector{ "printhost_user", "printhost_password" }) {
+ option = m_optgroup->get_option(opt_key);
+ option.opt.width = Field::def_width_wider();
+ m_optgroup->append_single_option_line(option);
+ }
+
+#ifdef WIN32
+ option = m_optgroup->get_option("printhost_ssl_ignore_revoke");
+ option.opt.width = Field::def_width_wider();
+ m_optgroup->append_single_option_line(option);
+#endif
+
+ m_optgroup->activate();
+
+ Field* printhost_field = m_optgroup->get_field("print_host");
+ if (printhost_field)
+ {
+ wxTextCtrl* temp = dynamic_cast(printhost_field->getWindow());
+ if (temp)
+ temp->Bind(wxEVT_TEXT, ([printhost_field, temp](wxEvent& e)
+ {
+#ifndef __WXGTK__
+ e.Skip();
+ temp->GetToolTip()->Enable(true);
+#endif // __WXGTK__
+ // Remove all leading and trailing spaces from the input
+ std::string trimed_str, str = trimed_str = temp->GetValue().ToStdString();
+ boost::trim(trimed_str);
+ if (trimed_str != str)
+ temp->SetValue(trimed_str);
+
+ TextCtrl* field = dynamic_cast(printhost_field);
+ if (field)
+ field->propagate_value();
+ }), temp->GetId());
+ }
+
+ // Always fill in the "printhost_port" combo box from the config and select it.
+ {
+ Choice* choice = dynamic_cast(m_optgroup->get_field("printhost_port"));
+ choice->set_values({ m_config->opt_string("printhost_port") });
+ choice->set_selection();
+ }
+
+ update();
+}
+
+void PhysicalPrinterDialog::update_printhost_buttons()
+{
+ std::unique_ptr host(PrintHost::get_print_host(m_config));
+ m_printhost_test_btn->Enable(!m_config->opt_string("print_host").empty() && host->can_test());
+ m_printhost_browse_btn->Enable(host->has_auto_discovery());
+}
+
+void PhysicalPrinterDialog::update(bool printer_change)
+{
+ m_optgroup->reload_config();
+
+ const PrinterTechnology tech = Preset::printer_technology(*m_config);
+ // Only offer the host type selection for FFF, for SLA it's always the SL1 printer (at the moment)
+ bool supports_multiple_printers = false;
+ if (tech == ptFFF) {
+ update_host_type(printer_change);
+ const auto opt = m_config->option>("host_type");
+ m_optgroup->show_field("host_type");
+ if (opt->value == htPrusaLink)
+ {
+ m_optgroup->show_field("printhost_authorization_type");
+ AuthorizationType auth_type = m_config->option>("printhost_authorization_type")->value;
+ m_optgroup->show_field("printhost_apikey", auth_type == AuthorizationType::atKeyPassword);
+ for (const char* opt_key : { "printhost_user", "printhost_password" })
+ m_optgroup->show_field(opt_key, auth_type == AuthorizationType::atUserPassword);
+ } else {
+ m_optgroup->hide_field("printhost_authorization_type");
+ m_optgroup->show_field("printhost_apikey", true);
+ for (const std::string& opt_key : std::vector{ "printhost_user", "printhost_password" })
+ m_optgroup->hide_field(opt_key);
+ supports_multiple_printers = opt && opt->value == htRepetier;
+ }
+
+ }
+ else {
+ m_optgroup->set_value("host_type", int(PrintHostType::htOctoPrint), false);
+ m_optgroup->hide_field("host_type");
+
+ m_optgroup->show_field("printhost_authorization_type");
+
+ AuthorizationType auth_type = m_config->option>("printhost_authorization_type")->value;
+ m_optgroup->show_field("printhost_apikey", auth_type == AuthorizationType::atKeyPassword);
+
+ for (const char *opt_key : { "printhost_user", "printhost_password" })
+ m_optgroup->show_field(opt_key, auth_type == AuthorizationType::atUserPassword);
+ }
+
+ m_optgroup->show_field("printhost_port", supports_multiple_printers);
+ m_printhost_port_browse_btn->Show(supports_multiple_printers);
+
+ update_printhost_buttons();
+
+ this->SetSize(this->GetBestSize());
+ this->Layout();
+}
+
+void PhysicalPrinterDialog::update_host_type(bool printer_change)
+{
+ if (m_config == nullptr)
+ return;
+ bool all_presets_are_from_mk3_family = false;
+ Field* ht = m_optgroup->get_field("host_type");
+
+ wxArrayString types;
+ // Append localized enum_labels
+ assert(ht->m_opt.enum_labels.size() == ht->m_opt.enum_values.size());
+ for (size_t i = 0; i < ht->m_opt.enum_labels.size(); i++) {
+ if (ht->m_opt.enum_values[i] == "prusalink" && !all_presets_are_from_mk3_family)
+ continue;
+ types.Add(_(ht->m_opt.enum_labels[i]));
+ }
+
+ Choice* choice = dynamic_cast(ht);
+ choice->set_values(types);
+ auto set_to_choice_and_config = [this, choice](PrintHostType type) {
+ choice->set_value(static_cast(type));
+ m_config->set_key_value("host_type", new ConfigOptionEnum(type));
+ };
+ if ((printer_change && all_presets_are_from_mk3_family) || all_presets_are_from_mk3_family)
+ set_to_choice_and_config(htPrusaLink);
+ else if ((printer_change && !all_presets_are_from_mk3_family) || (!all_presets_are_from_mk3_family && m_config->option>("host_type")->value == htPrusaLink))
+ set_to_choice_and_config(htOctoPrint);
+ else
+ choice->set_value(m_config->option("host_type")->getInt());
+}
+
+void PhysicalPrinterDialog::update_printers()
+{
+ wxBusyCursor wait;
+
+ std::unique_ptr host(PrintHost::get_print_host(m_config));
+
+ wxArrayString printers;
+ Field *rs = m_optgroup->get_field("printhost_port");
+ try {
+ if (! host->get_printers(printers))
+ printers.clear();
+ } catch (const HostNetworkError &err) {
+ printers.clear();
+ show_error(this, _L("Connection to printers connected via the print host failed.") + "\n\n" + from_u8(err.what()));
+ }
+ Choice *choice = dynamic_cast(rs);
+ choice->set_values(printers);
+ printers.empty() ? rs->disable() : rs->enable();
+}
+
+void PhysicalPrinterDialog::on_dpi_changed(const wxRect& suggested_rect)
+{
+ const int& em = em_unit();
+
+ m_printhost_browse_btn->msw_rescale();
+ m_printhost_test_btn->msw_rescale();
+ if (m_printhost_cafile_browse_btn)
+ m_printhost_cafile_browse_btn->msw_rescale();
+
+ m_optgroup->msw_rescale();
+
+ msw_buttons_rescale(this, em, { wxID_OK, wxID_CANCEL });
+
+ const wxSize& size = wxSize(45 * em, 35 * em);
+ SetMinSize(size);
+
+ Fit();
+ Refresh();
+}
+
+void PhysicalPrinterDialog::OnOK(wxEvent& event)
+{
+ event.Skip();
+}
+
+}} // namespace Slic3r::GUI
diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.hpp b/src/slic3r/GUI/PhysicalPrinterDialog.hpp
new file mode 100644
index 0000000000..e6d2015e63
--- /dev/null
+++ b/src/slic3r/GUI/PhysicalPrinterDialog.hpp
@@ -0,0 +1,59 @@
+#ifndef slic3r_PhysicalPrinterDialog_hpp_
+#define slic3r_PhysicalPrinterDialog_hpp_
+
+#include
+
+#include
+
+#include "libslic3r/Preset.hpp"
+#include "GUI_Utils.hpp"
+
+class wxString;
+class wxTextCtrl;
+class wxStaticText;
+class ScalableButton;
+class wxBoxSizer;
+
+namespace Slic3r {
+
+namespace GUI {
+
+//------------------------------------------
+// PhysicalPrinterDialog
+//------------------------------------------
+
+class ConfigOptionsGroup;
+class PhysicalPrinterDialog : public DPIDialog
+{
+ DynamicPrintConfig* m_config { nullptr };
+ ConfigOptionsGroup* m_optgroup { nullptr };
+
+ ScalableButton* m_printhost_browse_btn {nullptr};
+ ScalableButton* m_printhost_test_btn {nullptr};
+ ScalableButton* m_printhost_cafile_browse_btn {nullptr};
+ ScalableButton* m_printhost_client_cert_browse_btn {nullptr};
+ ScalableButton* m_printhost_port_browse_btn {nullptr};
+
+
+ void build_printhost_settings(ConfigOptionsGroup* optgroup);
+ void OnOK(wxEvent& event);
+
+public:
+ PhysicalPrinterDialog(wxWindow* parent);
+ ~PhysicalPrinterDialog();
+
+ void update(bool printer_change = false);
+ void update_host_type(bool printer_change);
+ void update_printhost_buttons();
+ void update_printers();
+
+protected:
+ void on_dpi_changed(const wxRect& suggested_rect) override;
+ void on_sys_color_changed() override {};
+};
+
+
+} // namespace GUI
+} // namespace Slic3r
+
+#endif
diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp
index 06d49d9bef..6fc7fba619 100644
--- a/src/slic3r/GUI/Plater.cpp
+++ b/src/slic3r/GUI/Plater.cpp
@@ -124,6 +124,9 @@
#include "libslic3r/Platform.hpp"
#include "nlohmann/json.hpp"
+#include "PhysicalPrinterDialog.hpp"
+#include "PrintHostDialogs.hpp"
+
using boost::optional;
namespace fs = boost::filesystem;
using Slic3r::_3DScene;
@@ -503,12 +506,23 @@ Sidebar::Sidebar(Plater *parent)
combo_printer->edit_btn = edit_btn;
p->combo_printer = combo_printer;
- wxBoxSizer* vsizer_printer = new wxBoxSizer(wxVERTICAL);
- wxBoxSizer* hsizer_printer = new wxBoxSizer(wxHORIZONTAL);
+ ScalableButton* connection_btn = new ScalableButton(p->m_panel_printer_content, wxID_ANY, "monitor_signal_strong");
+ connection_btn->SetBackgroundColour(wxColour(255, 255, 255));
+ connection_btn->SetToolTip(_L("Connection"));
+ connection_btn->Bind(wxEVT_BUTTON, [this, combo_printer](wxCommandEvent)
+ {
+ PhysicalPrinterDialog dlg(this->GetParent());
+ dlg.ShowModal();
+ });
+
+ wxBoxSizer *vsizer_printer = new wxBoxSizer(wxVERTICAL);
+ wxBoxSizer *hsizer_printer = new wxBoxSizer(wxHORIZONTAL);
hsizer_printer->Add(combo_printer, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(3));
hsizer_printer->Add(edit_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(3));
hsizer_printer->Add(FromDIP(8), 0, 0, 0, 0);
+ hsizer_printer->Add(connection_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(3));
+ hsizer_printer->Add(FromDIP(8), 0, 0, 0, 0);
vsizer_printer->Add(hsizer_printer, 0, wxEXPAND, 0);
// Bed type selection
@@ -1718,6 +1732,8 @@ struct Plater::priv
}
}
void export_gcode(fs::path output_path, bool output_path_on_removable_media);
+ void export_gcode(fs::path output_path, bool output_path_on_removable_media, PrintHostJob upload_job);
+
void reload_from_disk();
bool replace_volume_with_stl(int object_idx, int volume_idx, const fs::path& new_path, const std::string& snapshot = "");
void replace_with_stl();
@@ -1774,6 +1790,7 @@ struct Plater::priv
void on_action_print_plate(SimpleEvent&);
void on_action_print_all(SimpleEvent&);
void on_action_export_gcode(SimpleEvent&);
+ void on_action_send_gcode(SimpleEvent&);
void on_action_export_sliced_file(SimpleEvent&);
void on_action_select_sliced_plate(wxCommandEvent& evt);
@@ -2162,6 +2179,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
q->Bind(EVT_GLTOOLBAR_SELECT_SLICED_PLATE, &priv::on_action_select_sliced_plate, this);
q->Bind(EVT_GLTOOLBAR_PRINT_ALL, &priv::on_action_print_all, this);
q->Bind(EVT_GLTOOLBAR_EXPORT_GCODE, &priv::on_action_export_gcode, this);
+ q->Bind(EVT_GLTOOLBAR_SEND_GCODE, &priv::on_action_send_gcode, this);
q->Bind(EVT_GLTOOLBAR_EXPORT_SLICED_FILE, &priv::on_action_export_sliced_file, this);
q->Bind(EVT_GLCANVAS_PLATE_SELECT, &priv::on_plate_selected, this);
q->Bind(EVT_DOWNLOAD_PROJECT, &priv::on_action_download_project, this);
@@ -3994,7 +4012,38 @@ void Plater::priv::export_gcode(fs::path output_path, bool output_path_on_remova
this->background_process.set_task(PrintBase::TaskParams());
this->restart_background_process(priv::UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT);
}
+void Plater::priv::export_gcode(fs::path output_path, bool output_path_on_removable_media, PrintHostJob upload_job)
+{
+ wxCHECK_RET(!(output_path.empty() && upload_job.empty()), "export_gcode: output_path and upload_job empty");
+ if (model.objects.empty())
+ return;
+
+ if (background_process.is_export_scheduled()) {
+ GUI::show_error(q, _L("Another export job is currently running."));
+ return;
+ }
+
+ // bitmask of UpdateBackgroundProcessReturnState
+ unsigned int state = update_background_process(true);
+ if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE)
+ view3D->reload_scene(false);
+
+ if ((state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) != 0)
+ return;
+
+ show_warning_dialog = true;
+ if (! output_path.empty()) {
+ background_process.schedule_export(output_path.string(), output_path_on_removable_media);
+ notification_manager->push_delayed_notification(NotificationType::ExportOngoing, []() {return true; }, 1000, 0);
+ } else {
+ background_process.schedule_upload(std::move(upload_job));
+ }
+
+ // If the SLA processing of just a single object's supports is running, restart slicing for the whole object.
+ this->background_process.set_task(PrintBase::TaskParams());
+ this->restart_background_process(priv::UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT);
+}
unsigned int Plater::priv::update_restart_background_process(bool force_update_scene, bool force_update_preview)
{
bool switch_print = true;
@@ -5301,6 +5350,14 @@ void Plater::priv::on_action_export_gcode(SimpleEvent&)
}
}
+void Plater::priv::on_action_send_gcode(SimpleEvent&)
+{
+ if (q != nullptr) {
+ BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export gcode event\n" ;
+ q->send_gcode_legacy();
+ }
+}
+
void Plater::priv::on_action_export_sliced_file(SimpleEvent&)
{
if (q != nullptr) {
@@ -8482,7 +8539,53 @@ void Plater::reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &
// and let the background processing start.
this->p->restart_background_process(state | priv::UPDATE_BACKGROUND_PROCESS_FORCE_RESTART);
}
+void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn)
+{
+ // if physical_printer is selected, send gcode for this printer
+ // DynamicPrintConfig* physical_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config();
+ DynamicPrintConfig* physical_printer_config = &Slic3r::GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config;
+ if (! physical_printer_config || p->model.objects.empty())
+ return;
+ PrintHostJob upload_job(physical_printer_config);
+ if (upload_job.empty())
+ return;
+
+ // Obtain default output path
+ fs::path default_output_file;
+ try {
+ // Update the background processing, so that the placeholder parser will get the correct values for the ouput file template.
+ // Also if there is something wrong with the current configuration, a pop-up dialog will be shown and the export will not be performed.
+ unsigned int state = this->p->update_restart_background_process(false, false);
+ if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID)
+ return;
+ default_output_file = this->p->background_process.output_filepath_for_project(into_path(get_project_filename(".3mf")));
+ } catch (const Slic3r::PlaceholderParserError& ex) {
+ // Show the error with monospaced font.
+ show_error(this, ex.what(), true);
+ return;
+ } catch (const std::exception& ex) {
+ show_error(this, ex.what(), false);
+ return;
+ }
+ default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string()));
+
+ // Repetier specific: Query the server for the list of file groups.
+ wxArrayString groups;
+ {
+ wxBusyCursor wait;
+ upload_job.printhost->get_groups(groups);
+ }
+
+ PrintHostSendDialog dlg(default_output_file, upload_job.printhost->get_post_upload_actions(), groups);
+ if (dlg.ShowModal() == wxID_OK) {
+ upload_job.upload_data.upload_path = dlg.filename();
+ upload_job.upload_data.post_action = dlg.post_action();
+ upload_job.upload_data.group = dlg.group();
+
+ p->export_gcode(fs::path(), false, std::move(upload_job));
+ }
+}
int Plater::send_gcode(int plate_idx, Export3mfProgressFn proFn)
{
int result = 0;
diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp
index 5fb45dfa3c..98741587c9 100644
--- a/src/slic3r/GUI/Plater.hpp
+++ b/src/slic3r/GUI/Plater.hpp
@@ -328,6 +328,7 @@ public:
/* -1: send current gcode if not specified
* -2: send all gcode to target machine */
int send_gcode(int plate_idx = -1, Export3mfProgressFn proFn = nullptr);
+ void send_gcode_legacy(int plate_idx = -1, Export3mfProgressFn proFn = nullptr);
int export_config_3mf(int plate_idx = -1, Export3mfProgressFn proFn = nullptr);
//BBS jump to nonitor after print job finished
void print_job_finished(wxCommandEvent &evt);
diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp
new file mode 100644
index 0000000000..f3569298cc
--- /dev/null
+++ b/src/slic3r/GUI/PrintHostDialogs.cpp
@@ -0,0 +1,518 @@
+#include "PrintHostDialogs.hpp"
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include "GUI.hpp"
+#include "GUI_App.hpp"
+#include "MsgDialog.hpp"
+#include "I18N.hpp"
+#include "MainFrame.hpp"
+#include "libslic3r/AppConfig.hpp"
+#include "NotificationManager.hpp"
+#include "ExtraRenderers.hpp"
+
+namespace fs = boost::filesystem;
+
+namespace Slic3r {
+namespace GUI {
+
+static const char *CONFIG_KEY_PATH = "printhost_path";
+static const char *CONFIG_KEY_GROUP = "printhost_group";
+
+PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUploadActions post_actions, const wxArrayString &groups)
+ : MsgDialog(static_cast(wxGetApp().mainframe), _L("Send G-Code to printer host"), _L("Upload to Printer Host with the following filename:"),0)
+ , txt_filename(new wxTextCtrl(this, wxID_ANY))
+ , combo_groups(!groups.IsEmpty() ? new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, groups, wxCB_READONLY) : nullptr)
+ , post_upload_action(PrintHostPostUploadAction::None)
+{
+#ifdef __APPLE__
+ txt_filename->OSXDisableAllSmartSubstitutions();
+#endif
+ const AppConfig *app_config = wxGetApp().app_config;
+
+ auto *label_dir_hint = new wxStaticText(this, wxID_ANY, _L("Use forward slashes ( / ) as a directory separator if needed."));
+ label_dir_hint->Wrap(CONTENT_WIDTH * wxGetApp().em_unit());
+
+ content_sizer->Add(txt_filename, 0, wxEXPAND);
+ content_sizer->Add(label_dir_hint);
+ content_sizer->AddSpacer(VERT_SPACING);
+
+ if (combo_groups != nullptr) {
+ // Repetier specific: Show a selection of file groups.
+ auto *label_group = new wxStaticText(this, wxID_ANY, _L("Group"));
+ content_sizer->Add(label_group);
+ content_sizer->Add(combo_groups, 0, wxBOTTOM, 2*VERT_SPACING);
+ wxString recent_group = from_u8(app_config->get("recent", CONFIG_KEY_GROUP));
+ if (! recent_group.empty())
+ combo_groups->SetValue(recent_group);
+ }
+
+ wxString recent_path = from_u8(app_config->get("recent", CONFIG_KEY_PATH));
+ if (recent_path.Length() > 0 && recent_path[recent_path.Length() - 1] != '/') {
+ recent_path += '/';
+ }
+ const auto recent_path_len = recent_path.Length();
+ recent_path += path.filename().wstring();
+ wxString stem(path.stem().wstring());
+ const auto stem_len = stem.Length();
+
+ txt_filename->SetValue(recent_path);
+ txt_filename->SetFocus();
+
+ m_valid_suffix = recent_path.substr(recent_path.find_last_of('.'));
+ // .gcode suffix control
+ auto validate_path = [this](const wxString &path) -> bool {
+ if (! path.Lower().EndsWith(m_valid_suffix.Lower())) {
+ MessageDialog msg_wingow(this, wxString::Format(_L("Upload filename doesn't end with \"%s\". Do you wish to continue?"), m_valid_suffix), wxString(SLIC3R_APP_NAME), wxYES | wxNO);
+ if (msg_wingow.ShowModal() == wxID_NO)
+ return false;
+ }
+ return true;
+ };
+
+ auto* btn_upload = add_button(wxID_YES, false, _L("Upload"));
+ btn_upload->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) {
+ if (validate_path(txt_filename->GetValue())) {
+ post_upload_action = PrintHostPostUploadAction::None;
+ EndDialog(wxID_OK);
+ }
+ });
+
+ if (post_actions.has(PrintHostPostUploadAction::StartPrint)) {
+ auto* btn_print = add_button(wxID_YES, false, _L("Upload and Print"));
+ btn_print->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) {
+ if (validate_path(txt_filename->GetValue())) {
+ post_upload_action = PrintHostPostUploadAction::StartPrint;
+ EndDialog(wxID_OK);
+ }
+ });
+ }
+
+ if (post_actions.has(PrintHostPostUploadAction::StartSimulation)) {
+ // Using wxID_MORE as a button identifier to be different from the other buttons, wxID_MORE has no other meaning here.
+ auto* btn_simulate = add_button(wxID_MORE, false, _L("Upload and Simulate"));
+ btn_simulate->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) {
+ if (validate_path(txt_filename->GetValue())) {
+ post_upload_action = PrintHostPostUploadAction::StartSimulation;
+ EndDialog(wxID_OK);
+ }
+ });
+ }
+
+ add_button(wxID_CANCEL,false,"Cancel");
+ finalize();
+
+#ifdef __linux__
+ // On Linux with GTK2 when text control lose the focus then selection (colored background) disappears but text color stay white
+ // and as a result the text is invisible with light mode
+ // see https://github.com/prusa3d/PrusaSlicer/issues/4532
+ // Workaround: Unselect text selection explicitly on kill focus
+ txt_filename->Bind(wxEVT_KILL_FOCUS, [this](wxEvent& e) {
+ e.Skip();
+ txt_filename->SetInsertionPoint(txt_filename->GetLastPosition());
+ }, txt_filename->GetId());
+#endif /* __linux__ */
+
+ Bind(wxEVT_SHOW, [=](const wxShowEvent &) {
+ // Another similar case where the function only works with EVT_SHOW + CallAfter,
+ // this time on Mac.
+ CallAfter([=]() {
+ txt_filename->SetSelection(recent_path_len, recent_path_len + stem_len);
+ });
+ });
+}
+
+fs::path PrintHostSendDialog::filename() const
+{
+ return into_path(txt_filename->GetValue());
+}
+
+PrintHostPostUploadAction PrintHostSendDialog::post_action() const
+{
+ return post_upload_action;
+}
+
+std::string PrintHostSendDialog::group() const
+{
+ if (combo_groups == nullptr) {
+ return "";
+ } else {
+ wxString group = combo_groups->GetValue();
+ return into_u8(group);
+ }
+}
+
+void PrintHostSendDialog::EndModal(int ret)
+{
+ if (ret == wxID_OK) {
+ // Persist path and print settings
+ wxString path = txt_filename->GetValue();
+ int last_slash = path.Find('/', true);
+ if (last_slash == wxNOT_FOUND)
+ path.clear();
+ else
+ path = path.SubString(0, last_slash);
+
+ AppConfig *app_config = wxGetApp().app_config;
+ app_config->set("recent", CONFIG_KEY_PATH, into_u8(path));
+
+ if (combo_groups != nullptr) {
+ wxString group = combo_groups->GetValue();
+ app_config->set("recent", CONFIG_KEY_GROUP, into_u8(group));
+ }
+ }
+
+ MsgDialog::EndModal(ret);
+}
+
+
+
+wxDEFINE_EVENT(EVT_PRINTHOST_PROGRESS, PrintHostQueueDialog::Event);
+wxDEFINE_EVENT(EVT_PRINTHOST_ERROR, PrintHostQueueDialog::Event);
+wxDEFINE_EVENT(EVT_PRINTHOST_CANCEL, PrintHostQueueDialog::Event);
+
+PrintHostQueueDialog::Event::Event(wxEventType eventType, int winid, size_t job_id)
+ : wxEvent(winid, eventType)
+ , job_id(job_id)
+{}
+
+PrintHostQueueDialog::Event::Event(wxEventType eventType, int winid, size_t job_id, int progress)
+ : wxEvent(winid, eventType)
+ , job_id(job_id)
+ , progress(progress)
+{}
+
+PrintHostQueueDialog::Event::Event(wxEventType eventType, int winid, size_t job_id, wxString error)
+ : wxEvent(winid, eventType)
+ , job_id(job_id)
+ , error(std::move(error))
+{}
+
+wxEvent *PrintHostQueueDialog::Event::Clone() const
+{
+ return new Event(*this);
+}
+
+PrintHostQueueDialog::PrintHostQueueDialog(wxWindow *parent)
+ : DPIDialog(parent, wxID_ANY, _L("Print host upload queue"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
+ , on_progress_evt(this, EVT_PRINTHOST_PROGRESS, &PrintHostQueueDialog::on_progress, this)
+ , on_error_evt(this, EVT_PRINTHOST_ERROR, &PrintHostQueueDialog::on_error, this)
+ , on_cancel_evt(this, EVT_PRINTHOST_CANCEL, &PrintHostQueueDialog::on_cancel, this)
+{
+ const auto em = GetTextExtent("m").x;
+
+ auto *topsizer = new wxBoxSizer(wxVERTICAL);
+
+ std::vector widths;
+ widths.reserve(6);
+ if (!load_user_data(UDT_COLS, widths)) {
+ widths.clear();
+ for (size_t i = 0; i < 6; i++)
+ widths.push_back(-1);
+ }
+
+ job_list = new wxDataViewListCtrl(this, wxID_ANY);
+
+ // MSW DarkMode: workaround for the selected item in the list
+ auto append_text_column = [this](const wxString& label, int width, wxAlignment align = wxALIGN_LEFT,
+ int flags = wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE) {
+#ifdef _WIN32
+ job_list->AppendColumn(new wxDataViewColumn(label, new TextRenderer(), job_list->GetColumnCount(), width, align, flags));
+#else
+ job_list->AppendTextColumn(label, wxDATAVIEW_CELL_INERT, width, align, flags);
+#endif
+ };
+
+ // Note: Keep these in sync with Column
+ append_text_column(_L("ID"), widths[0]);
+ job_list->AppendProgressColumn(_L("Progress"), wxDATAVIEW_CELL_INERT, widths[1], wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE);
+ append_text_column(_L("Status"),widths[2]);
+ append_text_column(_L("Host"), widths[3]);
+ append_text_column(_CTX_utf8(L_CONTEXT("Size", "OfFile"), "OfFile"), widths[4]);
+ append_text_column(_L("Filename"), widths[5]);
+ append_text_column(_L("Error Message"), -1, wxALIGN_CENTER, wxDATAVIEW_COL_HIDDEN);
+
+ auto *btnsizer = new wxBoxSizer(wxHORIZONTAL);
+ btn_cancel = new wxButton(this, wxID_DELETE, _L("Cancel selected"));
+ btn_cancel->Disable();
+ btn_error = new wxButton(this, wxID_ANY, _L("Show error message"));
+ btn_error->Disable();
+ // Note: The label needs to be present, otherwise we get accelerator bugs on Mac
+ auto *btn_close = new wxButton(this, wxID_CANCEL, _L("Close"));
+ btnsizer->Add(btn_cancel, 0, wxRIGHT, SPACING);
+ btnsizer->Add(btn_error, 0);
+ btnsizer->AddStretchSpacer();
+ btnsizer->Add(btn_close);
+
+ topsizer->Add(job_list, 1, wxEXPAND | wxBOTTOM, SPACING);
+ topsizer->Add(btnsizer, 0, wxEXPAND);
+ SetSizer(topsizer);
+
+ wxGetApp().UpdateDlgDarkUI(this);
+ wxGetApp().UpdateDVCDarkUI(job_list);
+
+ std::vector size;
+ SetSize(load_user_data(UDT_SIZE, size) ? wxSize(size[0] * em, size[1] * em) : wxSize(HEIGHT * em, WIDTH * em));
+
+ Bind(wxEVT_SIZE, [this](wxSizeEvent& evt) {
+ OnSize(evt);
+ save_user_data(UDT_SIZE | UDT_POSITION | UDT_COLS);
+ });
+
+ std::vector pos;
+ if (load_user_data(UDT_POSITION, pos))
+ SetPosition(wxPoint(pos[0], pos[1]));
+
+ Bind(wxEVT_MOVE, [this](wxMoveEvent& evt) {
+ save_user_data(UDT_SIZE | UDT_POSITION | UDT_COLS);
+ });
+
+ job_list->Bind(wxEVT_DATAVIEW_SELECTION_CHANGED, [this](wxDataViewEvent&) { on_list_select(); });
+
+ btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
+ int selected = job_list->GetSelectedRow();
+ if (selected == wxNOT_FOUND) { return; }
+
+ const JobState state = get_state(selected);
+ if (state < ST_ERROR) {
+ GUI::wxGetApp().printhost_job_queue().cancel(selected);
+ }
+ });
+
+ btn_error->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
+ int selected = job_list->GetSelectedRow();
+ if (selected == wxNOT_FOUND) { return; }
+ GUI::show_error(nullptr, job_list->GetTextValue(selected, COL_ERRORMSG));
+ });
+}
+
+void PrintHostQueueDialog::append_job(const PrintHostJob &job)
+{
+ wxCHECK_RET(!job.empty(), "PrintHostQueueDialog: Attempt to append an empty job");
+
+ wxVector fields;
+ fields.push_back(wxVariant(wxString::Format("%d", job_list->GetItemCount() + 1)));
+ fields.push_back(wxVariant(0));
+ fields.push_back(wxVariant(_L("Enqueued")));
+ fields.push_back(wxVariant(job.printhost->get_host()));
+ boost::system::error_code ec;
+ boost::uintmax_t size_i = boost::filesystem::file_size(job.upload_data.source_path, ec);
+ std::stringstream stream;
+ if (ec) {
+ stream << "unknown";
+ size_i = 0;
+ BOOST_LOG_TRIVIAL(error) << ec.message();
+ } else
+ stream << std::fixed << std::setprecision(2) << ((float)size_i / 1024 / 1024) << "MB";
+ fields.push_back(wxVariant(stream.str()));
+ fields.push_back(wxVariant(job.upload_data.upload_path.string()));
+ fields.push_back(wxVariant(""));
+ job_list->AppendItem(fields, static_cast(ST_NEW));
+ // Both strings are UTF-8 encoded.
+ upload_names.emplace_back(job.printhost->get_host(), job.upload_data.upload_path.string());
+
+ wxGetApp().notification_manager()->push_upload_job_notification(job_list->GetItemCount(), (float)size_i / 1024 / 1024, job.upload_data.upload_path.string(), job.printhost->get_host());
+}
+
+void PrintHostQueueDialog::on_dpi_changed(const wxRect &suggested_rect)
+{
+ const int& em = em_unit();
+
+ msw_buttons_rescale(this, em, { wxID_DELETE, wxID_CANCEL, btn_error->GetId() });
+
+ SetMinSize(wxSize(HEIGHT * em, WIDTH * em));
+
+ Fit();
+ Refresh();
+
+ save_user_data(UDT_SIZE | UDT_POSITION | UDT_COLS);
+}
+
+void PrintHostQueueDialog::on_sys_color_changed()
+{
+#ifdef _WIN32
+ wxGetApp().UpdateDlgDarkUI(this);
+ wxGetApp().UpdateDVCDarkUI(job_list);
+#endif
+}
+
+PrintHostQueueDialog::JobState PrintHostQueueDialog::get_state(int idx)
+{
+ wxCHECK_MSG(idx >= 0 && idx < job_list->GetItemCount(), ST_ERROR, "Out of bounds access to job list");
+ return static_cast(job_list->GetItemData(job_list->RowToItem(idx)));
+}
+
+void PrintHostQueueDialog::set_state(int idx, JobState state)
+{
+ wxCHECK_RET(idx >= 0 && idx < job_list->GetItemCount(), "Out of bounds access to job list");
+ job_list->SetItemData(job_list->RowToItem(idx), static_cast(state));
+
+ switch (state) {
+ case ST_NEW: job_list->SetValue(_L("Enqueued"), idx, COL_STATUS); break;
+ case ST_PROGRESS: job_list->SetValue(_L("Uploading"), idx, COL_STATUS); break;
+ case ST_ERROR: job_list->SetValue(_L("Error"), idx, COL_STATUS); break;
+ case ST_CANCELLING: job_list->SetValue(_L("Cancelling"), idx, COL_STATUS); break;
+ case ST_CANCELLED: job_list->SetValue(_L("Cancelled"), idx, COL_STATUS); break;
+ case ST_COMPLETED: job_list->SetValue(_L("Completed"), idx, COL_STATUS); break;
+ }
+ // This might be ambigous call, but user data needs to be saved time to time
+ save_user_data(UDT_SIZE | UDT_POSITION | UDT_COLS);
+}
+
+void PrintHostQueueDialog::on_list_select()
+{
+ int selected = job_list->GetSelectedRow();
+ if (selected != wxNOT_FOUND) {
+ const JobState state = get_state(selected);
+ btn_cancel->Enable(state < ST_ERROR);
+ btn_error->Enable(state == ST_ERROR);
+ Layout();
+ } else {
+ btn_cancel->Disable();
+ }
+}
+
+void PrintHostQueueDialog::on_progress(Event &evt)
+{
+ wxCHECK_RET(evt.job_id < (size_t)job_list->GetItemCount(), "Out of bounds access to job list");
+
+ if (evt.progress < 100) {
+ set_state(evt.job_id, ST_PROGRESS);
+ job_list->SetValue(wxVariant(evt.progress), evt.job_id, COL_PROGRESS);
+ } else {
+ set_state(evt.job_id, ST_COMPLETED);
+ job_list->SetValue(wxVariant(100), evt.job_id, COL_PROGRESS);
+ }
+
+ on_list_select();
+
+ if (evt.progress > 0)
+ {
+ wxVariant nm, hst;
+ job_list->GetValue(nm, evt.job_id, COL_FILENAME);
+ job_list->GetValue(hst, evt.job_id, COL_HOST);
+ wxGetApp().notification_manager()->set_upload_job_notification_percentage(evt.job_id + 1, boost::nowide::narrow(nm.GetString()), boost::nowide::narrow(hst.GetString()), evt.progress / 100.f);
+ }
+}
+
+void PrintHostQueueDialog::on_error(Event &evt)
+{
+ wxCHECK_RET(evt.job_id < (size_t)job_list->GetItemCount(), "Out of bounds access to job list");
+
+ set_state(evt.job_id, ST_ERROR);
+
+ auto errormsg = from_u8((boost::format("%1%\n%2%") % _utf8(L("Error uploading to print host:")) % std::string(evt.error.ToUTF8())).str());
+ job_list->SetValue(wxVariant(0), evt.job_id, COL_PROGRESS);
+ job_list->SetValue(wxVariant(errormsg), evt.job_id, COL_ERRORMSG); // Stashes the error message into a hidden column for later
+
+ on_list_select();
+
+ GUI::show_error(nullptr, errormsg);
+
+ wxVariant nm, hst;
+ job_list->GetValue(nm, evt.job_id, COL_FILENAME);
+ job_list->GetValue(hst, evt.job_id, COL_HOST);
+ wxGetApp().notification_manager()->upload_job_notification_show_error(evt.job_id + 1, boost::nowide::narrow(nm.GetString()), boost::nowide::narrow(hst.GetString()));
+}
+
+void PrintHostQueueDialog::on_cancel(Event &evt)
+{
+ wxCHECK_RET(evt.job_id < (size_t)job_list->GetItemCount(), "Out of bounds access to job list");
+
+ set_state(evt.job_id, ST_CANCELLED);
+ job_list->SetValue(wxVariant(0), evt.job_id, COL_PROGRESS);
+
+ on_list_select();
+
+ wxVariant nm, hst;
+ job_list->GetValue(nm, evt.job_id, COL_FILENAME);
+ job_list->GetValue(hst, evt.job_id, COL_HOST);
+ wxGetApp().notification_manager()->upload_job_notification_show_canceled(evt.job_id + 1, boost::nowide::narrow(nm.GetString()), boost::nowide::narrow(hst.GetString()));
+}
+
+void PrintHostQueueDialog::get_active_jobs(std::vector>& ret)
+{
+ int ic = job_list->GetItemCount();
+ for (int i = 0; i < ic; i++)
+ {
+ auto item = job_list->RowToItem(i);
+ auto data = job_list->GetItemData(item);
+ JobState st = static_cast(data);
+ if(st == JobState::ST_NEW || st == JobState::ST_PROGRESS)
+ ret.emplace_back(upload_names[i]);
+ }
+ //job_list->data
+}
+void PrintHostQueueDialog::save_user_data(int udt)
+{
+ const auto em = GetTextExtent("m").x;
+ auto *app_config = wxGetApp().app_config;
+ if (udt & UserDataType::UDT_SIZE) {
+
+ app_config->set("print_host_queue_dialog_height", std::to_string(this->GetSize().x / em));
+ app_config->set("print_host_queue_dialog_width", std::to_string(this->GetSize().y / em));
+ }
+ if (udt & UserDataType::UDT_POSITION)
+ {
+ app_config->set("print_host_queue_dialog_x", std::to_string(this->GetPosition().x));
+ app_config->set("print_host_queue_dialog_y", std::to_string(this->GetPosition().y));
+ }
+ if (udt & UserDataType::UDT_COLS)
+ {
+ for (size_t i = 0; i < job_list->GetColumnCount() - 1; i++)
+ {
+ app_config->set("print_host_queue_dialog_column_" + std::to_string(i), std::to_string(job_list->GetColumn(i)->GetWidth()));
+ }
+ }
+}
+bool PrintHostQueueDialog::load_user_data(int udt, std::vector& vector)
+{
+ auto* app_config = wxGetApp().app_config;
+ auto hasget = [app_config](const std::string& name, std::vector& vector)->bool {
+ if (app_config->has(name)) {
+ vector.push_back(std::stoi(app_config->get(name)));
+ return true;
+ }
+ return false;
+ };
+ if (udt & UserDataType::UDT_SIZE) {
+ if (!hasget("print_host_queue_dialog_height",vector))
+ return false;
+ if (!hasget("print_host_queue_dialog_width", vector))
+ return false;
+ }
+ if (udt & UserDataType::UDT_POSITION)
+ {
+ if (!hasget("print_host_queue_dialog_x", vector))
+ return false;
+ if (!hasget("print_host_queue_dialog_y", vector))
+ return false;
+ }
+ if (udt & UserDataType::UDT_COLS)
+ {
+ for (size_t i = 0; i < 6; i++)
+ {
+ if (!hasget("print_host_queue_dialog_column_" + std::to_string(i), vector))
+ return false;
+ }
+ }
+ return true;
+}
+}}
diff --git a/src/slic3r/GUI/PrintHostDialogs.hpp b/src/slic3r/GUI/PrintHostDialogs.hpp
new file mode 100644
index 0000000000..ff3eb60125
--- /dev/null
+++ b/src/slic3r/GUI/PrintHostDialogs.hpp
@@ -0,0 +1,130 @@
+#ifndef slic3r_PrintHostSendDialog_hpp_
+#define slic3r_PrintHostSendDialog_hpp_
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include "GUI_Utils.hpp"
+#include "MsgDialog.hpp"
+#include "../Utils/PrintHost.hpp"
+
+class wxButton;
+class wxTextCtrl;
+class wxChoice;
+class wxComboBox;
+class wxDataViewListCtrl;
+
+namespace Slic3r {
+
+namespace GUI {
+
+class PrintHostSendDialog : public GUI::MsgDialog
+{
+public:
+ PrintHostSendDialog(const boost::filesystem::path &path, PrintHostPostUploadActions post_actions, const wxArrayString& groups);
+ boost::filesystem::path filename() const;
+ PrintHostPostUploadAction post_action() const;
+ std::string group() const;
+
+ virtual void EndModal(int ret) override;
+private:
+ wxTextCtrl *txt_filename;
+ wxComboBox *combo_groups;
+ PrintHostPostUploadAction post_upload_action;
+ wxString m_valid_suffix;
+};
+
+
+class PrintHostQueueDialog : public DPIDialog
+{
+public:
+ class Event : public wxEvent
+ {
+ public:
+ size_t job_id;
+ int progress = 0; // in percent
+ wxString error;
+
+ Event(wxEventType eventType, int winid, size_t job_id);
+ Event(wxEventType eventType, int winid, size_t job_id, int progress);
+ Event(wxEventType eventType, int winid, size_t job_id, wxString error);
+
+ virtual wxEvent *Clone() const;
+ };
+
+
+ PrintHostQueueDialog(wxWindow *parent);
+
+ void append_job(const PrintHostJob &job);
+ void get_active_jobs(std::vector>& ret);
+
+ virtual bool Show(bool show = true) override
+ {
+ if(!show)
+ save_user_data(UDT_SIZE | UDT_POSITION | UDT_COLS);
+ return DPIDialog::Show(show);
+ }
+protected:
+ void on_dpi_changed(const wxRect &suggested_rect) override;
+ void on_sys_color_changed() override;
+
+private:
+ enum Column {
+ COL_ID,
+ COL_PROGRESS,
+ COL_STATUS,
+ COL_HOST,
+ COL_SIZE,
+ COL_FILENAME,
+ COL_ERRORMSG
+ };
+
+ enum JobState {
+ ST_NEW,
+ ST_PROGRESS,
+ ST_ERROR,
+ ST_CANCELLING,
+ ST_CANCELLED,
+ ST_COMPLETED,
+ };
+
+ enum { HEIGHT = 60, WIDTH = 30, SPACING = 5 };
+
+ enum UserDataType{
+ UDT_SIZE = 1,
+ UDT_POSITION = 2,
+ UDT_COLS = 4
+ };
+
+ wxButton *btn_cancel;
+ wxButton *btn_error;
+ wxDataViewListCtrl *job_list;
+ // Note: EventGuard prevents delivery of progress evts to a freed PrintHostQueueDialog
+ EventGuard on_progress_evt;
+ EventGuard on_error_evt;
+ EventGuard on_cancel_evt;
+
+ JobState get_state(int idx);
+ void set_state(int idx, JobState);
+ void on_list_select();
+ void on_progress(Event&);
+ void on_error(Event&);
+ void on_cancel(Event&);
+ // This vector keep adress and filename of uploads. It is used when checking for running uploads during exit.
+ std::vector> upload_names;
+ void save_user_data(int);
+ bool load_user_data(int, std::vector&);
+};
+
+wxDECLARE_EVENT(EVT_PRINTHOST_PROGRESS, PrintHostQueueDialog::Event);
+wxDECLARE_EVENT(EVT_PRINTHOST_ERROR, PrintHostQueueDialog::Event);
+wxDECLARE_EVENT(EVT_PRINTHOST_CANCEL, PrintHostQueueDialog::Event);
+
+}}
+
+#endif
diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp
index 925a7ae6a7..c86d66d9d5 100644
--- a/src/slic3r/GUI/Tab.cpp
+++ b/src/slic3r/GUI/Tab.cpp
@@ -43,6 +43,7 @@
#include "MarkdownTip.hpp"
#include "Search.hpp"
+// #include "BonjourDialog.hpp"
#ifdef WIN32
#include
#endif // WIN32
@@ -2875,7 +2876,6 @@ void TabPrinter::build_fff()
// build_preset_description_line(optgroup.get());
#endif
-
build_unregular_pages(true);
}
diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp
index dfe8d1689c..e276840669 100644
--- a/src/slic3r/GUI/Tab.hpp
+++ b/src/slic3r/GUI/Tab.hpp
@@ -547,6 +547,7 @@ private:
std::vector m_pages_fff;
std::vector m_pages_sla;
+ wxBoxSizer* m_presets_sizer {nullptr};
public:
ScalableButton* m_reset_to_filament_color = nullptr;
@@ -585,6 +586,7 @@ public:
//wxSizer* create_bed_shape_widget(wxWindow* parent);
void cache_extruder_cnt();
bool apply_extruder_cnt_from_cache();
+
};
class TabSLAMaterial : public Tab
diff --git a/src/slic3r/Utils/AstroBox.cpp b/src/slic3r/Utils/AstroBox.cpp
new file mode 100644
index 0000000000..8781549a20
--- /dev/null
+++ b/src/slic3r/Utils/AstroBox.cpp
@@ -0,0 +1,173 @@
+#include "AstroBox.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include "libslic3r/PrintConfig.hpp"
+#include "slic3r/GUI/I18N.hpp"
+#include "slic3r/GUI/GUI.hpp"
+#include "Http.hpp"
+
+
+namespace fs = boost::filesystem;
+namespace pt = boost::property_tree;
+
+
+namespace Slic3r {
+
+AstroBox::AstroBox(DynamicPrintConfig *config) :
+ host(config->opt_string("print_host")),
+ apikey(config->opt_string("printhost_apikey")),
+ cafile(config->opt_string("printhost_cafile"))
+{}
+
+const char* AstroBox::get_name() const { return "AstroBox"; }
+
+bool AstroBox::test(wxString &msg) const
+{
+ // Since the request is performed synchronously here,
+ // it is ok to refer to `msg` from within the closure
+
+ const char *name = get_name();
+
+ bool res = true;
+ auto url = make_url("api/version");
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get version at: %2%") % name % url;
+
+ auto http = Http::get(std::move(url));
+ set_auth(http);
+ http.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;
+ msg = format_error(body, error, status);
+ })
+ .on_complete([&, this](std::string body, unsigned) {
+ BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got version: %2%") % name % body;
+
+ try {
+ std::stringstream ss(body);
+ pt::ptree ptree;
+ pt::read_json(ss, ptree);
+
+ if (! ptree.get_optional("api")) {
+ res = false;
+ return;
+ }
+
+ const auto text = ptree.get_optional("text");
+ res = validate_version_text(text);
+ if (! res) {
+ msg = GUI::from_u8((boost::format(_utf8(L("Mismatched type of print host: %s"))) % (text ? *text : "AstroBox")).str());
+ }
+ }
+ catch (const std::exception &) {
+ res = false;
+ msg = "Could not parse server response";
+ }
+ })
+ .perform_sync();
+
+ return res;
+}
+
+wxString AstroBox::get_test_ok_msg () const
+{
+ return _(L("Connection to AstroBox works correctly."));
+}
+
+wxString AstroBox::get_test_failed_msg (wxString &msg) const
+{
+ return GUI::from_u8((boost::format("%s: %s\n\n%s")
+ % _utf8(L("Could not connect to AstroBox"))
+ % std::string(msg.ToUTF8())
+ % _utf8(L("Note: AstroBox version at least 1.1.0 is required."))).str());
+}
+
+bool AstroBox::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const
+{
+ const char *name = get_name();
+
+ const auto upload_filename = upload_data.upload_path.filename();
+ const auto upload_parent_path = upload_data.upload_path.parent_path();
+
+ wxString test_msg;
+ if (! test(test_msg)) {
+ error_fn(std::move(test_msg));
+ return false;
+ }
+
+ bool res = true;
+
+ auto url = make_url("api/files/local");
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%")
+ % name
+ % upload_data.source_path
+ % url
+ % upload_filename.string()
+ % upload_parent_path.string()
+ % (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false");
+
+ auto http = Http::post(std::move(url));
+ 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 ???
+ .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;
+ })
+ .on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error uploading file: %2%, HTTP %3%, body: `%4%`") % name % error % status % body;
+ error_fn(format_error(body, error, status));
+ res = false;
+ })
+ .on_progress([&](Http::Progress progress, bool &cancel) {
+ prorgess_fn(std::move(progress), cancel);
+ if (cancel) {
+ // Upload was canceled
+ BOOST_LOG_TRIVIAL(info) << "AstroBox: Upload canceled";
+ res = false;
+ }
+ })
+ .perform_sync();
+
+ return res;
+}
+
+bool AstroBox::validate_version_text(const boost::optional &version_text) const
+{
+ return version_text ? boost::starts_with(*version_text, "AstroBox") : true;
+}
+
+void AstroBox::set_auth(Http &http) const
+{
+ http.header("X-Api-Key", apikey);
+
+ if (! cafile.empty()) {
+ http.ca_file(cafile);
+ }
+}
+
+std::string AstroBox::make_url(const std::string &path) const
+{
+ if (host.find("http://") == 0 || host.find("https://") == 0) {
+ if (host.back() == '/') {
+ return (boost::format("%1%%2%") % host % path).str();
+ } else {
+ return (boost::format("%1%/%2%") % host % path).str();
+ }
+ } else {
+ return (boost::format("http://%1%/%2%") % host % path).str();
+ }
+}
+
+}
diff --git a/src/slic3r/Utils/AstroBox.hpp b/src/slic3r/Utils/AstroBox.hpp
new file mode 100644
index 0000000000..15a8863a90
--- /dev/null
+++ b/src/slic3r/Utils/AstroBox.hpp
@@ -0,0 +1,46 @@
+#ifndef slic3r_AstroBox_hpp_
+#define slic3r_AstroBox_hpp_
+
+#include
+#include
+#include
+
+#include "PrintHost.hpp"
+
+namespace Slic3r {
+
+class DynamicPrintConfig;
+class Http;
+
+class AstroBox : public PrintHost
+{
+public:
+ AstroBox(DynamicPrintConfig *config);
+ ~AstroBox() override = default;
+
+ const char* get_name() const override;
+
+ bool test(wxString &curl_msg) const override;
+ wxString get_test_ok_msg () const override;
+ wxString get_test_failed_msg (wxString &msg) const override;
+ bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override;
+ bool has_auto_discovery() const override { return true; }
+ bool can_test() const override { return true; }
+ PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; }
+ std::string get_host() const override { return host; }
+
+protected:
+ bool validate_version_text(const boost::optional &version_text) const;
+
+private:
+ std::string host;
+ std::string apikey;
+ std::string cafile;
+
+ void set_auth(Http &http) const;
+ std::string make_url(const std::string &path) const;
+};
+
+}
+
+#endif
diff --git a/src/slic3r/Utils/Duet.cpp b/src/slic3r/Utils/Duet.cpp
new file mode 100644
index 0000000000..3293a3ff2b
--- /dev/null
+++ b/src/slic3r/Utils/Duet.cpp
@@ -0,0 +1,286 @@
+#include "Duet.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "libslic3r/PrintConfig.hpp"
+#include "slic3r/GUI/GUI.hpp"
+#include "slic3r/GUI/I18N.hpp"
+#include "slic3r/GUI/MsgDialog.hpp"
+#include "Http.hpp"
+
+namespace fs = boost::filesystem;
+namespace pt = boost::property_tree;
+
+namespace Slic3r {
+
+Duet::Duet(DynamicPrintConfig *config) :
+ host(config->opt_string("print_host")),
+ password(config->opt_string("printhost_apikey"))
+{}
+
+const char* Duet::get_name() const { return "Duet"; }
+
+bool Duet::test(wxString &msg) const
+{
+ auto connectionType = connect(msg);
+ disconnect(connectionType);
+
+ return connectionType != ConnectionType::error;
+}
+
+wxString Duet::get_test_ok_msg () const
+{
+ return _(L("Connection to Duet works correctly."));
+}
+
+wxString Duet::get_test_failed_msg (wxString &msg) const
+{
+ return GUI::from_u8((boost::format("%s: %s")
+ % _utf8(L("Could not connect to Duet"))
+ % std::string(msg.ToUTF8())).str());
+}
+
+bool Duet::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const
+{
+ wxString connect_msg;
+ auto connectionType = connect(connect_msg);
+ if (connectionType == ConnectionType::error) {
+ error_fn(std::move(connect_msg));
+ return false;
+ }
+
+ bool res = true;
+ bool dsf = (connectionType == ConnectionType::dsf);
+
+ auto upload_cmd = get_upload_url(upload_data.upload_path.string(), connectionType);
+ BOOST_LOG_TRIVIAL(info) << boost::format("Duet: Uploading file %1%, filepath: %2%, post_action: %3%, command: %4%")
+ % upload_data.source_path
+ % upload_data.upload_path
+ % int(upload_data.post_action)
+ % upload_cmd;
+
+ auto http = (dsf ? Http::put(std::move(upload_cmd)) : Http::post(std::move(upload_cmd)));
+ if (dsf) {
+ http.set_put_body(upload_data.source_path);
+ } else {
+ http.set_post_body(upload_data.source_path);
+ }
+ http.on_complete([&](std::string body, unsigned status) {
+ BOOST_LOG_TRIVIAL(debug) << boost::format("Duet: File uploaded: HTTP %1%: %2%") % status % body;
+
+ int err_code = dsf ? (status == 201 ? 0 : 1) : get_err_code_from_body(body);
+ if (err_code != 0) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("Duet: Request completed but error code was received: %1%") % err_code;
+ error_fn(format_error(body, L("Unknown error occured"), 0));
+ res = false;
+ } else if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) {
+ wxString errormsg;
+ res = start_print(errormsg, upload_data.upload_path.string(), connectionType, false);
+ if (! res) {
+ error_fn(std::move(errormsg));
+ }
+ } else if (upload_data.post_action == PrintHostPostUploadAction::StartSimulation) {
+ wxString errormsg;
+ res = start_print(errormsg, upload_data.upload_path.string(), connectionType, true);
+ if (! res) {
+ error_fn(std::move(errormsg));
+ }
+ }
+ })
+ .on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("Duet: Error uploading file: %1%, HTTP %2%, body: `%3%`") % error % status % body;
+ error_fn(format_error(body, error, status));
+ res = false;
+ })
+ .on_progress([&](Http::Progress progress, bool &cancel) {
+ prorgess_fn(std::move(progress), cancel);
+ if (cancel) {
+ // Upload was canceled
+ BOOST_LOG_TRIVIAL(info) << "Duet: Upload canceled";
+ res = false;
+ }
+ })
+ .perform_sync();
+
+ disconnect(connectionType);
+
+ return res;
+}
+
+Duet::ConnectionType Duet::connect(wxString &msg) const
+{
+ auto res = ConnectionType::error;
+ auto url = get_connect_url(false);
+
+ auto http = Http::get(std::move(url));
+ http.on_error([&](std::string body, std::string error, unsigned status) {
+ auto dsfUrl = get_connect_url(true);
+ auto dsfHttp = Http::get(std::move(dsfUrl));
+ dsfHttp.on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("Duet: Error connecting: %1%, HTTP %2%, body: `%3%`") % error % status % body;
+ msg = format_error(body, error, status);
+ })
+ .on_complete([&](std::string body, unsigned) {
+ res = ConnectionType::dsf;
+ })
+ .perform_sync();
+ })
+ .on_complete([&](std::string body, unsigned) {
+ BOOST_LOG_TRIVIAL(debug) << boost::format("Duet: Got: %1%") % body;
+
+ int err_code = get_err_code_from_body(body);
+ switch (err_code) {
+ case 0:
+ res = ConnectionType::rrf;
+ break;
+ case 1:
+ msg = format_error(body, L("Wrong password"), 0);
+ break;
+ case 2:
+ msg = format_error(body, L("Could not get resources to create a new connection"), 0);
+ break;
+ default:
+ msg = format_error(body, L("Unknown error occured"), 0);
+ break;
+ }
+
+ })
+ .perform_sync();
+
+ return res;
+}
+
+void Duet::disconnect(ConnectionType connectionType) const
+{
+ // we don't need to disconnect from DSF or if it failed anyway
+ if (connectionType != ConnectionType::rrf) {
+ return;
+ }
+ auto url = (boost::format("%1%rr_disconnect")
+ % get_base_url()).str();
+
+ auto http = Http::get(std::move(url));
+ http.on_error([&](std::string body, std::string error, unsigned status) {
+ // we don't care about it, if disconnect is not working Duet will disconnect automatically after some time
+ BOOST_LOG_TRIVIAL(error) << boost::format("Duet: Error disconnecting: %1%, HTTP %2%, body: `%3%`") % error % status % body;
+ })
+ .perform_sync();
+}
+
+std::string Duet::get_upload_url(const std::string &filename, ConnectionType connectionType) const
+{
+ assert(connectionType != ConnectionType::error);
+
+ if (connectionType == ConnectionType::dsf) {
+ return (boost::format("%1%machine/file/gcodes/%2%")
+ % get_base_url()
+ % Http::url_encode(filename)).str();
+ } else {
+ return (boost::format("%1%rr_upload?name=0:/gcodes/%2%&%3%")
+ % get_base_url()
+ % Http::url_encode(filename)
+ % timestamp_str()).str();
+ }
+}
+
+std::string Duet::get_connect_url(const bool dsfUrl) const
+{
+ if (dsfUrl) {
+ return (boost::format("%1%machine/status")
+ % get_base_url()).str();
+ } else {
+ return (boost::format("%1%rr_connect?password=%2%&%3%")
+ % get_base_url()
+ % (password.empty() ? "reprap" : password)
+ % timestamp_str()).str();
+ }
+}
+
+std::string Duet::get_base_url() const
+{
+ if (host.find("http://") == 0 || host.find("https://") == 0) {
+ if (host.back() == '/') {
+ return host;
+ } else {
+ return (boost::format("%1%/") % host).str();
+ }
+ } else {
+ return (boost::format("http://%1%/") % host).str();
+ }
+}
+
+std::string Duet::timestamp_str() const
+{
+ enum { BUFFER_SIZE = 32 };
+
+ auto t = std::time(nullptr);
+ auto tm = *std::localtime(&t);
+
+ char buffer[BUFFER_SIZE];
+ std::strftime(buffer, BUFFER_SIZE, "time=%Y-%m-%dT%H:%M:%S", &tm);
+
+ return std::string(buffer);
+}
+
+bool Duet::start_print(wxString &msg, const std::string &filename, ConnectionType connectionType, bool simulationMode) const
+{
+ assert(connectionType != ConnectionType::error);
+
+ bool res = false;
+ bool dsf = (connectionType == ConnectionType::dsf);
+
+ auto url = dsf
+ ? (boost::format("%1%machine/code")
+ % get_base_url()).str()
+ : (boost::format(simulationMode
+ ? "%1%rr_gcode?gcode=M37%%20P\"0:/gcodes/%2%\""
+ : "%1%rr_gcode?gcode=M32%%20\"0:/gcodes/%2%\"")
+ % get_base_url()
+ % Http::url_encode(filename)).str();
+
+ auto http = (dsf ? Http::post(std::move(url)) : Http::get(std::move(url)));
+ if (dsf) {
+ http.set_post_body(
+ (boost::format(simulationMode
+ ? "M37 P\"0:/gcodes/%1%\""
+ : "M32 \"0:/gcodes/%1%\"")
+ % filename).str()
+ );
+ }
+ http.on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("Duet: Error starting print: %1%, HTTP %2%, body: `%3%`") % error % status % body;
+ msg = format_error(body, error, status);
+ })
+ .on_complete([&](std::string body, unsigned) {
+ BOOST_LOG_TRIVIAL(debug) << boost::format("Duet: Got: %1%") % body;
+ res = true;
+ })
+ .perform_sync();
+
+ return res;
+}
+
+int Duet::get_err_code_from_body(const std::string &body) const
+{
+ pt::ptree root;
+ std::istringstream iss (body); // wrap returned json to istringstream
+ pt::read_json(iss, root);
+
+ return root.get("err", 0);
+}
+
+}
diff --git a/src/slic3r/Utils/Duet.hpp b/src/slic3r/Utils/Duet.hpp
new file mode 100644
index 0000000000..edca66ce0c
--- /dev/null
+++ b/src/slic3r/Utils/Duet.hpp
@@ -0,0 +1,48 @@
+#ifndef slic3r_Duet_hpp_
+#define slic3r_Duet_hpp_
+
+#include
+#include
+
+#include "PrintHost.hpp"
+
+namespace Slic3r {
+
+class DynamicPrintConfig;
+class Http;
+
+class Duet : public PrintHost
+{
+public:
+ explicit Duet(DynamicPrintConfig *config);
+ ~Duet() override = default;
+
+ const char* get_name() const override;
+
+ bool test(wxString &curl_msg) const override;
+ wxString get_test_ok_msg() const override;
+ wxString get_test_failed_msg(wxString &msg) const override;
+ bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override;
+ bool has_auto_discovery() const override { return false; }
+ bool can_test() const override { return true; }
+ PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint | PrintHostPostUploadAction::StartSimulation; }
+ std::string get_host() const override { return host; }
+
+private:
+ enum class ConnectionType { rrf, dsf, error };
+ std::string host;
+ std::string password;
+
+ std::string get_upload_url(const std::string &filename, ConnectionType connectionType) const;
+ std::string get_connect_url(const bool dsfUrl) const;
+ std::string get_base_url() const;
+ std::string timestamp_str() const;
+ ConnectionType connect(wxString &msg) const;
+ void disconnect(ConnectionType connectionType) const;
+ bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType, bool simulationMode) const;
+ int get_err_code_from_body(const std::string &body) const;
+};
+
+}
+
+#endif
diff --git a/src/slic3r/Utils/FlashAir.cpp b/src/slic3r/Utils/FlashAir.cpp
new file mode 100644
index 0000000000..2337ac2904
--- /dev/null
+++ b/src/slic3r/Utils/FlashAir.cpp
@@ -0,0 +1,229 @@
+#include "FlashAir.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "libslic3r/PrintConfig.hpp"
+#include "slic3r/GUI/GUI.hpp"
+#include "slic3r/GUI/I18N.hpp"
+#include "slic3r/GUI/MsgDialog.hpp"
+#include "Http.hpp"
+
+namespace fs = boost::filesystem;
+namespace pt = boost::property_tree;
+
+namespace Slic3r {
+
+FlashAir::FlashAir(DynamicPrintConfig *config) :
+ host(config->opt_string("print_host"))
+{}
+
+const char* FlashAir::get_name() const { return "FlashAir"; }
+
+bool FlashAir::test(wxString &msg) const
+{
+ // Since the request is performed synchronously here,
+ // it is ok to refer to `msg` from within the closure
+
+ const char *name = get_name();
+
+ bool res = false;
+ auto url = make_url("command.cgi", "op", "118");
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get upload enabled at: %2%") % name % url;
+
+ auto http = Http::get(std::move(url));
+ http.on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting upload enabled: %2%, HTTP %3%, body: `%4%`") % name % error % status % body;
+ res = false;
+ msg = format_error(body, error, status);
+ })
+ .on_complete([&](std::string body, unsigned) {
+ BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got upload enabled: %2%") % name % body;
+
+ res = boost::starts_with(body, "1");
+ if (! res) {
+ msg = _(L("Upload not enabled on FlashAir card."));
+ }
+ })
+ .perform_sync();
+
+ return res;
+}
+
+wxString FlashAir::get_test_ok_msg () const
+{
+ return _(L("Connection to FlashAir works correctly and upload is enabled."));
+}
+
+wxString FlashAir::get_test_failed_msg (wxString &msg) const
+{
+ return GUI::from_u8((boost::format("%s: %s\n%s")
+ % _utf8(L("Could not connect to FlashAir"))
+ % std::string(msg.ToUTF8())
+ % _utf8(L("Note: FlashAir with firmware 2.00.02 or newer and activated upload function is required."))).str());
+}
+
+bool FlashAir::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const
+{
+ const char *name = get_name();
+
+ const auto upload_filename = upload_data.upload_path.filename();
+ const auto upload_parent_path = upload_data.upload_path.parent_path();
+ wxString test_msg;
+ if (! test(test_msg)) {
+ error_fn(std::move(test_msg));
+ return false;
+ }
+
+ bool res = false;
+
+ std::string strDest = upload_parent_path.string();
+ if (strDest.front()!='/') // Needs a leading / else root uploads fail.
+ {
+ strDest.insert(0,"/");
+ }
+
+ auto urlPrepare = make_url("upload.cgi", "WRITEPROTECT=ON&FTIME", timestamp_str());
+ auto urlSetDir = make_url("upload.cgi","UPDIR",strDest);
+ auto urlUpload = make_url("upload.cgi");
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3% / %4%, filename: %5%")
+ % name
+ % upload_data.source_path
+ % urlPrepare
+ % urlUpload
+ % upload_filename.string();
+
+ // set filetime for upload and make card writeprotect to prevent filesystem damage
+ auto httpPrepare = Http::get(std::move(urlPrepare));
+ httpPrepare.on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error preparing upload: %2%, HTTP %3%, body: `%4%`") % name % error % status % body;
+ error_fn(format_error(body, error, status));
+ res = false;
+ })
+ .on_complete([&, this](std::string body, unsigned) {
+ BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got prepare result: %2%") % name % body;
+ res = boost::icontains(body, "SUCCESS");
+ if (! res) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Request completed but no SUCCESS message was received.") % name;
+ error_fn(format_error(body, L("Unknown error occured"), 0));
+ }
+ })
+ .perform_sync();
+
+ if(! res ) {
+ return res;
+ }
+
+ // start file upload
+ auto httpDir = Http::get(std::move(urlSetDir));
+ httpDir.on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error setting upload dir: %2%, HTTP %3%, body: `%4%`") % name % error % status % body;
+ error_fn(format_error(body, error, status));
+ res = false;
+ })
+ .on_complete([&, this](std::string body, unsigned) {
+ BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got dir select result: %2%") % name % body;
+ res = boost::icontains(body, "SUCCESS");
+ if (! res) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Request completed but no SUCCESS message was received.") % name;
+ error_fn(format_error(body, L("Unknown error occured"), 0));
+ }
+ })
+ .perform_sync();
+
+ if(! res ) {
+ return res;
+ }
+
+ auto http = Http::post(std::move(urlUpload));
+ 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;
+ res = boost::icontains(body, "SUCCESS");
+ if (! res) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Request completed but no SUCCESS message was received.") % name;
+ error_fn(format_error(body, L("Unknown error occured"), 0));
+ }
+ })
+ .on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error uploading file: %2%, HTTP %3%, body: `%4%`") % name % error % status % body;
+ error_fn(format_error(body, error, status));
+ res = false;
+ })
+ .on_progress([&](Http::Progress progress, bool &cancel) {
+ prorgess_fn(std::move(progress), cancel);
+ if (cancel) {
+ // Upload was canceled
+ BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Upload canceled") % name;
+ res = false;
+ }
+ })
+ .perform_sync();
+
+ return res;
+}
+
+std::string FlashAir::timestamp_str() const
+{
+ auto t = std::time(nullptr);
+ auto tm = *std::localtime(&t);
+
+ unsigned long fattime = ((tm.tm_year - 80) << 25) |
+ ((tm.tm_mon + 1) << 21) |
+ (tm.tm_mday << 16) |
+ (tm.tm_hour << 11) |
+ (tm.tm_min << 5) |
+ (tm.tm_sec >> 1);
+
+ return (boost::format("%1$#x") % fattime).str();
+}
+
+std::string FlashAir::make_url(const std::string &path) const
+{
+ if (host.find("http://") == 0 || host.find("https://") == 0) {
+ if (host.back() == '/') {
+ return (boost::format("%1%%2%") % host % path).str();
+ } else {
+ return (boost::format("%1%/%2%") % host % path).str();
+ }
+ } else {
+ if (host.back() == '/') {
+ return (boost::format("http://%1%%2%") % host % path).str();
+ } else {
+ return (boost::format("http://%1%/%2%") % host % path).str();
+ }
+ }
+}
+
+std::string FlashAir::make_url(const std::string &path, const std::string &arg, const std::string &val) const
+{
+ if (host.find("http://") == 0 || host.find("https://") == 0) {
+ if (host.back() == '/') {
+ return (boost::format("%1%%2%?%3%=%4%") % host % path % arg % val).str();
+ } else {
+ return (boost::format("%1%/%2%?%3%=%4%") % host % path % arg % val).str();
+ }
+ } else {
+ if (host.back() == '/') {
+ return (boost::format("http://%1%%2%?%3%=%4%") % host % path % arg % val).str();
+ } else {
+ return (boost::format("http://%1%/%2%?%3%=%4%") % host % path % arg % val).str();
+ }
+ }
+}
+
+}
diff --git a/src/slic3r/Utils/FlashAir.hpp b/src/slic3r/Utils/FlashAir.hpp
new file mode 100644
index 0000000000..14e3f00156
--- /dev/null
+++ b/src/slic3r/Utils/FlashAir.hpp
@@ -0,0 +1,42 @@
+#ifndef slic3r_FlashAir_hpp_
+#define slic3r_FlashAir_hpp_
+
+#include
+#include
+
+#include "PrintHost.hpp"
+
+
+namespace Slic3r {
+
+class DynamicPrintConfig;
+class Http;
+
+class FlashAir : public PrintHost
+{
+public:
+ FlashAir(DynamicPrintConfig *config);
+ ~FlashAir() override = default;
+
+ const char* get_name() const override;
+
+ bool test(wxString &curl_msg) const override;
+ wxString get_test_ok_msg() const override;
+ wxString get_test_failed_msg(wxString &msg) const override;
+ bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override;
+ bool has_auto_discovery() const override { return false; }
+ bool can_test() const override { return true; }
+ PrintHostPostUploadActions get_post_upload_actions() const override { return {}; }
+ std::string get_host() const override { return host; }
+
+private:
+ std::string host;
+
+ std::string timestamp_str() const;
+ std::string make_url(const std::string &path) const;
+ std::string make_url(const std::string &path, const std::string &arg, const std::string &val) const;
+};
+
+}
+
+#endif
diff --git a/src/slic3r/Utils/MKS.cpp b/src/slic3r/Utils/MKS.cpp
new file mode 100644
index 0000000000..80a79537d5
--- /dev/null
+++ b/src/slic3r/Utils/MKS.cpp
@@ -0,0 +1,150 @@
+#include "MKS.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "libslic3r/PrintConfig.hpp"
+#include "slic3r/GUI/GUI.hpp"
+#include "slic3r/GUI/I18N.hpp"
+#include "slic3r/GUI/MsgDialog.hpp"
+#include "Http.hpp"
+
+namespace fs = boost::filesystem;
+namespace pt = boost::property_tree;
+
+namespace Slic3r {
+
+MKS::MKS(DynamicPrintConfig* config) :
+ m_host(config->opt_string("print_host")), m_console_port("8080")
+{}
+
+const char* MKS::get_name() const { return "MKS"; }
+
+bool MKS::test(wxString& msg) const
+{
+ Utils::TCPConsole console(m_host, m_console_port);
+
+ console.enqueue_cmd("M105");
+ bool ret = console.run_queue();
+
+ if (!ret)
+ msg = wxString::FromUTF8(console.error_message().c_str());
+
+ return ret;
+}
+
+wxString MKS::get_test_ok_msg() const
+{
+ return _(L("Connection to MKS works correctly."));
+}
+
+wxString MKS::get_test_failed_msg(wxString& msg) const
+{
+ return GUI::from_u8((boost::format("%s: %s")
+ % _utf8(L("Could not connect to MKS"))
+ % std::string(msg.ToUTF8())).str());
+}
+
+bool MKS::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const
+{
+ bool res = true;
+
+ auto upload_cmd = get_upload_url(upload_data.upload_path.string());
+ BOOST_LOG_TRIVIAL(info) << boost::format("MKS: Uploading file %1%, filepath: %2%, print: %3%, command: %4%")
+ % upload_data.source_path
+ % upload_data.upload_path
+ % (upload_data.post_action == PrintHostPostUploadAction::StartPrint)
+ % upload_cmd;
+
+ auto http = Http::post(std::move(upload_cmd));
+ http.set_post_body(upload_data.source_path);
+
+ http.on_complete([&](std::string body, unsigned status) {
+ BOOST_LOG_TRIVIAL(debug) << boost::format("MKS: File uploaded: HTTP %1%: %2%") % status % body;
+
+ int err_code = get_err_code_from_body(body);
+ if (err_code != 0) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("MKS: Request completed but error code was received: %1%") % err_code;
+ error_fn(format_error(body, L("Unknown error occured"), 0));
+ res = false;
+ }
+ else if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) {
+ wxString errormsg;
+ res = start_print(errormsg, upload_data.upload_path.string());
+ if (!res) {
+ error_fn(std::move(errormsg));
+ }
+ }
+ })
+ .on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("MKS: Error uploading file: %1%, HTTP %2%, body: `%3%`") % error % status % body;
+ error_fn(format_error(body, error, status));
+ res = false;
+ })
+ .on_progress([&](Http::Progress progress, bool& cancel) {
+ prorgess_fn(std::move(progress), cancel);
+ if (cancel) {
+ // Upload was canceled
+ BOOST_LOG_TRIVIAL(info) << "MKS: Upload canceled";
+ res = false;
+ }
+ }).perform_sync();
+
+
+ return res;
+}
+
+std::string MKS::get_upload_url(const std::string& filename) const
+{
+ return (boost::format("http://%1%/upload?X-Filename=%2%")
+ % m_host
+ % Http::url_encode(filename)).str();
+}
+
+bool MKS::start_print(wxString& msg, const std::string& filename) const
+{
+ // For some reason printer firmware does not want to respond on gcode commands immediately after file upload.
+ // So we just introduce artificial delay to workaround it.
+ // TODO: Inspect reasons
+ std::this_thread::sleep_for(std::chrono::milliseconds(1500));
+
+ Utils::TCPConsole console(m_host, m_console_port);
+
+ console.enqueue_cmd(std::string("M23 ") + filename);
+ console.enqueue_cmd("M24");
+
+ bool ret = console.run_queue();
+
+ if (!ret)
+ msg = wxString::FromUTF8(console.error_message().c_str());
+
+ return ret;
+}
+
+int MKS::get_err_code_from_body(const std::string& body) const
+{
+ pt::ptree root;
+ std::istringstream iss(body); // wrap returned json to istringstream
+ pt::read_json(iss, root);
+
+ return root.get("err", 0);
+}
+
+} // Slic3r
diff --git a/src/slic3r/Utils/MKS.hpp b/src/slic3r/Utils/MKS.hpp
new file mode 100644
index 0000000000..22455436ae
--- /dev/null
+++ b/src/slic3r/Utils/MKS.hpp
@@ -0,0 +1,42 @@
+#ifndef slic3r_MKS_hpp_
+#define slic3r_MKS_hpp_
+
+#include
+#include
+
+#include "PrintHost.hpp"
+#include "TCPConsole.hpp"
+
+namespace Slic3r {
+class DynamicPrintConfig;
+class Http;
+
+class MKS : public PrintHost
+{
+public:
+ explicit MKS(DynamicPrintConfig* config);
+ ~MKS() override = default;
+
+ const char* get_name() const override;
+
+ bool test(wxString& curl_msg) const override;
+ wxString get_test_ok_msg() const override;
+ wxString get_test_failed_msg(wxString& msg) const override;
+ bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override;
+ bool has_auto_discovery() const override { return false; }
+ bool can_test() const override { return true; }
+ PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; }
+ std::string get_host() const override { return m_host; }
+
+private:
+ std::string m_host;
+ std::string m_console_port;
+
+ std::string get_upload_url(const std::string& filename) const;
+ bool start_print(wxString& msg, const std::string& filename) const;
+ int get_err_code_from_body(const std::string& body) const;
+};
+
+}
+
+#endif
diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp
new file mode 100644
index 0000000000..5116795c5e
--- /dev/null
+++ b/src/slic3r/Utils/OctoPrint.cpp
@@ -0,0 +1,367 @@
+#include "OctoPrint.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+
+#include "slic3r/GUI/GUI.hpp"
+#include "slic3r/GUI/I18N.hpp"
+#include "slic3r/GUI/GUI.hpp"
+#include "Http.hpp"
+#include "libslic3r/AppConfig.hpp"
+
+
+namespace fs = boost::filesystem;
+namespace pt = boost::property_tree;
+
+
+namespace Slic3r {
+
+#ifdef WIN32
+// Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail.
+namespace {
+std::string substitute_host(const std::string& orig_addr, std::string sub_addr)
+{
+ // put ipv6 into [] brackets
+ if (sub_addr.find(':') != std::string::npos && sub_addr.at(0) != '[')
+ sub_addr = "[" + sub_addr + "]";
+
+#if 0
+ //URI = scheme ":"["//"[userinfo "@"] host [":" port]] path["?" query]["#" fragment]
+ std::string final_addr = orig_addr;
+ // http
+ size_t double_dash = orig_addr.find("//");
+ size_t host_start = (double_dash == std::string::npos ? 0 : double_dash + 2);
+ // userinfo
+ size_t at = orig_addr.find("@");
+ host_start = (at != std::string::npos && at > host_start ? at + 1 : host_start);
+ // end of host, could be port(:), subpath(/) (could be query(?) or fragment(#)?)
+ // or it will be ']' if address is ipv6 )
+ size_t potencial_host_end = orig_addr.find_first_of(":/", host_start);
+ // if there are more ':' it must be ipv6
+ if (potencial_host_end != std::string::npos && orig_addr[potencial_host_end] == ':' && orig_addr.rfind(':') != potencial_host_end) {
+ size_t ipv6_end = orig_addr.find(']', host_start);
+ // DK: Uncomment and replace orig_addr.length() if we want to allow subpath after ipv6 without [] parentheses.
+ potencial_host_end = (ipv6_end != std::string::npos ? ipv6_end + 1 : orig_addr.length()); //orig_addr.find('/', host_start));
+ }
+ size_t host_end = (potencial_host_end != std::string::npos ? potencial_host_end : orig_addr.length());
+ // now host_start and host_end should mark where to put resolved addr
+ // check host_start. if its nonsense, lets just use original addr (or resolved addr?)
+ if (host_start >= orig_addr.length()) {
+ return final_addr;
+ }
+ final_addr.replace(host_start, host_end - host_start, sub_addr);
+ return final_addr;
+#else
+ // Using the new CURL API for handling URL. https://everything.curl.dev/libcurl/url
+ // If anything fails, return the input unchanged.
+ std::string out = orig_addr;
+ CURLU *hurl = curl_url();
+ if (hurl) {
+ // Parse the input URL.
+ CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, orig_addr.c_str(), 0);
+ if (rc == CURLUE_OK) {
+ // Replace the address.
+ rc = curl_url_set(hurl, CURLUPART_HOST, sub_addr.c_str(), 0);
+ if (rc == CURLUE_OK) {
+ // Extract a string fromt the CURL URL handle.
+ char *url;
+ rc = curl_url_get(hurl, CURLUPART_URL, &url, 0);
+ if (rc == CURLUE_OK) {
+ out = url;
+ curl_free(url);
+ } else
+ BOOST_LOG_TRIVIAL(error) << "OctoPrint substitute_host: failed to extract the URL after substitution";
+ } else
+ BOOST_LOG_TRIVIAL(error) << "OctoPrint substitute_host: failed to substitute host " << sub_addr << " in URL " << orig_addr;
+ } else
+ BOOST_LOG_TRIVIAL(error) << "OctoPrint substitute_host: failed to parse URL " << orig_addr;
+ curl_url_cleanup(hurl);
+ } else
+ BOOST_LOG_TRIVIAL(error) << "OctoPrint substitute_host: failed to allocate curl_url";
+ return out;
+#endif
+}
+} //namespace
+#endif // WIN32
+
+OctoPrint::OctoPrint(DynamicPrintConfig *config) :
+ m_host(config->opt_string("print_host")),
+ m_apikey(config->opt_string("printhost_apikey")),
+ m_cafile(config->opt_string("printhost_cafile")),
+ m_ssl_revoke_best_effort(config->opt_bool("printhost_ssl_ignore_revoke"))
+{}
+
+const char* OctoPrint::get_name() const { return "OctoPrint"; }
+
+bool OctoPrint::test(wxString &msg) const
+{
+ // Since the request is performed synchronously here,
+ // it is ok to refer to `msg` from within the closure
+
+ const char *name = get_name();
+
+ bool res = true;
+ auto url = make_url("api/version");
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get version at: %2%") % name % url;
+
+ auto http = Http::get(std::move(url));
+ set_auth(http);
+ http.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;
+ msg = format_error(body, error, status);
+ })
+ .on_complete([&, this](std::string body, unsigned) {
+ BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got version: %2%") % name % body;
+
+ try {
+ std::stringstream ss(body);
+ pt::ptree ptree;
+ pt::read_json(ss, ptree);
+
+ if (! ptree.get_optional("api")) {
+ res = false;
+ return;
+ }
+
+ const auto text = ptree.get_optional("text");
+ res = validate_version_text(text);
+ if (! res) {
+ msg = GUI::from_u8((boost::format(_utf8(L("Mismatched type of print host: %s"))) % (text ? *text : "OctoPrint")).str());
+ }
+ }
+ catch (const std::exception &) {
+ res = false;
+ msg = "Could not parse server response";
+ }
+ })
+#ifdef WIN32
+ .ssl_revoke_best_effort(m_ssl_revoke_best_effort)
+ .on_ip_resolve([&](std::string address) {
+ // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail.
+ // Remember resolved address to be reused at successive REST API call.
+ msg = GUI::from_u8(address);
+ })
+#endif // WIN32
+ .perform_sync();
+
+ return res;
+}
+
+wxString OctoPrint::get_test_ok_msg () const
+{
+ return _(L("Connection to OctoPrint works correctly."));
+}
+
+wxString OctoPrint::get_test_failed_msg (wxString &msg) const
+{
+ return GUI::from_u8((boost::format("%s: %s\n\n%s")
+ % _utf8(L("Could not connect to OctoPrint"))
+ % std::string(msg.ToUTF8())
+ % _utf8(L("Note: OctoPrint version at least 1.1.0 is required."))).str());
+}
+
+bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const
+{
+ const char *name = get_name();
+
+ const auto upload_filename = upload_data.upload_path.filename();
+ const auto upload_parent_path = upload_data.upload_path.parent_path();
+
+ // If test fails, test_msg_or_host_ip contains the error message.
+ // Otherwise on Windows it contains the resolved IP address of the host.
+ wxString test_msg_or_host_ip;
+ if (! test(test_msg_or_host_ip)) {
+ error_fn(std::move(test_msg_or_host_ip));
+ return false;
+ }
+
+ std::string url;
+ bool res = true;
+
+#ifdef WIN32
+ // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail.
+ if (m_host.find("https://") == 0 || test_msg_or_host_ip.empty())
+#endif // _WIN32
+ {
+ // If https is entered we assume signed ceritificate is being used
+ // IP resolving will not happen - it could resolve into address not being specified in cert
+ url = make_url("api/files/local");
+ }
+#ifdef WIN32
+ else {
+ // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail.
+ // Curl uses easy_getinfo to get ip address of last successful transaction.
+ // If it got the address use it instead of the stored in "host" variable.
+ // This new address returns in "test_msg_or_host_ip" variable.
+ // Solves troubles of uploades failing with name address.
+ // in original address (m_host) replace host for resolved ip
+ url = substitute_host(make_url("api/files/local"), GUI::into_u8(test_msg_or_host_ip));
+ BOOST_LOG_TRIVIAL(info) << "Upload address after ip resolve: " << url;
+ }
+#endif // _WIN32
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%")
+ % name
+ % upload_data.source_path
+ % url
+ % upload_filename.string()
+ % upload_parent_path.string()
+ % (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false");
+
+ auto http = Http::post(std::move(url));
+ 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 ???
+ .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;
+ })
+ .on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error uploading file: %2%, HTTP %3%, body: `%4%`") % name % error % status % body;
+ error_fn(format_error(body, error, status));
+ res = false;
+ })
+ .on_progress([&](Http::Progress progress, bool &cancel) {
+ prorgess_fn(std::move(progress), cancel);
+ if (cancel) {
+ // Upload was canceled
+ BOOST_LOG_TRIVIAL(info) << "Octoprint: Upload canceled";
+ res = false;
+ }
+ })
+#ifdef WIN32
+ .ssl_revoke_best_effort(m_ssl_revoke_best_effort)
+#endif
+ .perform_sync();
+
+ return res;
+}
+
+bool OctoPrint::validate_version_text(const boost::optional &version_text) const
+{
+ return version_text ? boost::starts_with(*version_text, "OctoPrint") : true;
+}
+
+void OctoPrint::set_auth(Http &http) const
+{
+ http.header("X-Api-Key", m_apikey);
+
+ if (!m_cafile.empty()) {
+ http.ca_file(m_cafile);
+ }
+}
+
+std::string OctoPrint::make_url(const std::string &path) const
+{
+ if (m_host.find("http://") == 0 || m_host.find("https://") == 0) {
+ if (m_host.back() == '/') {
+ return (boost::format("%1%%2%") % m_host % path).str();
+ } else {
+ return (boost::format("%1%/%2%") % m_host % path).str();
+ }
+ } else {
+ return (boost::format("http://%1%/%2%") % m_host % path).str();
+ }
+}
+
+SL1Host::SL1Host(DynamicPrintConfig *config) :
+ OctoPrint(config),
+ m_authorization_type(dynamic_cast*>(config->option("printhost_authorization_type"))->value),
+ m_username(config->opt_string("printhost_user")),
+ m_password(config->opt_string("printhost_password"))
+{
+}
+
+// SL1Host
+const char* SL1Host::get_name() const { return "SL1Host"; }
+
+wxString SL1Host::get_test_ok_msg () const
+{
+ return _(L("Connection to Prusa SL1 / SL1S works correctly."));
+}
+
+wxString SL1Host::get_test_failed_msg (wxString &msg) const
+{
+ return GUI::from_u8((boost::format("%s: %s")
+ % _utf8(L("Could not connect to Prusa SLA"))
+ % std::string(msg.ToUTF8())).str());
+}
+
+bool SL1Host::validate_version_text(const boost::optional &version_text) const
+{
+ return version_text ? boost::starts_with(*version_text, "Prusa SLA") : false;
+}
+
+void SL1Host::set_auth(Http &http) const
+{
+ switch (m_authorization_type) {
+ case atKeyPassword:
+ http.header("X-Api-Key", get_apikey());
+ break;
+ case atUserPassword:
+ http.auth_digest(m_username, m_password);
+ break;
+ }
+
+ if (! get_cafile().empty()) {
+ http.ca_file(get_cafile());
+ }
+}
+
+// PrusaLink
+PrusaLink::PrusaLink(DynamicPrintConfig* config) :
+ OctoPrint(config),
+ m_authorization_type(dynamic_cast*>(config->option("printhost_authorization_type"))->value),
+ m_username(config->opt_string("printhost_user")),
+ m_password(config->opt_string("printhost_password"))
+{
+}
+
+const char* PrusaLink::get_name() const { return "PrusaLink"; }
+
+wxString PrusaLink::get_test_ok_msg() const
+{
+ return _(L("Connection to PrusaLink works correctly."));
+}
+
+wxString PrusaLink::get_test_failed_msg(wxString& msg) const
+{
+ return GUI::from_u8((boost::format("%s: %s")
+ % _utf8(L("Could not connect to PrusaLink"))
+ % std::string(msg.ToUTF8())).str());
+}
+
+bool PrusaLink::validate_version_text(const boost::optional& version_text) const
+{
+ return version_text ? (boost::starts_with(*version_text, "PrusaLink") || boost::starts_with(*version_text, "OctoPrint")) : false;
+}
+
+void PrusaLink::set_auth(Http& http) const
+{
+ switch (m_authorization_type) {
+ case atKeyPassword:
+ http.header("X-Api-Key", get_apikey());
+ break;
+ case atUserPassword:
+ http.auth_digest(m_username, m_password);
+ break;
+ }
+
+ if (!get_cafile().empty()) {
+ http.ca_file(get_cafile());
+ }
+}
+
+}
diff --git a/src/slic3r/Utils/OctoPrint.hpp b/src/slic3r/Utils/OctoPrint.hpp
new file mode 100644
index 0000000000..262efe9ff5
--- /dev/null
+++ b/src/slic3r/Utils/OctoPrint.hpp
@@ -0,0 +1,101 @@
+#ifndef slic3r_OctoPrint_hpp_
+#define slic3r_OctoPrint_hpp_
+
+#include
+#include
+#include
+
+#include "PrintHost.hpp"
+#include "libslic3r/PrintConfig.hpp"
+
+
+namespace Slic3r {
+
+class DynamicPrintConfig;
+class Http;
+
+class OctoPrint : public PrintHost
+{
+public:
+ OctoPrint(DynamicPrintConfig *config);
+ ~OctoPrint() override = default;
+
+ const char* get_name() const override;
+
+ bool test(wxString &curl_msg) const override;
+ wxString get_test_ok_msg () const override;
+ wxString get_test_failed_msg (wxString &msg) const override;
+ bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override;
+ bool has_auto_discovery() const override { return true; }
+ bool can_test() const override { return true; }
+ PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; }
+ std::string get_host() const override { return m_host; }
+ const std::string& get_apikey() const { return m_apikey; }
+ const std::string& get_cafile() const { return m_cafile; }
+
+protected:
+ virtual bool validate_version_text(const boost::optional &version_text) const;
+
+private:
+ std::string m_host;
+ std::string m_apikey;
+ std::string m_cafile;
+ bool m_ssl_revoke_best_effort;
+
+ virtual void set_auth(Http &http) const;
+ std::string make_url(const std::string &path) const;
+};
+
+class SL1Host: public OctoPrint
+{
+public:
+ SL1Host(DynamicPrintConfig *config);
+ ~SL1Host() override = default;
+
+ const char* get_name() const override;
+
+ wxString get_test_ok_msg() const override;
+ wxString get_test_failed_msg(wxString &msg) const override;
+ PrintHostPostUploadActions get_post_upload_actions() const override { return {}; }
+
+protected:
+ bool validate_version_text(const boost::optional &version_text) const override;
+
+private:
+ void set_auth(Http &http) const override;
+
+ // Host authorization type.
+ AuthorizationType m_authorization_type;
+ // username and password for HTTP Digest Authentization (RFC RFC2617)
+ std::string m_username;
+ std::string m_password;
+};
+
+class PrusaLink : public OctoPrint
+{
+public:
+ PrusaLink(DynamicPrintConfig* config);
+ ~PrusaLink() override = default;
+
+ const char* get_name() const override;
+
+ wxString get_test_ok_msg() const override;
+ wxString get_test_failed_msg(wxString& msg) const override;
+ PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; }
+
+protected:
+ bool validate_version_text(const boost::optional& version_text) const override;
+
+private:
+ void set_auth(Http& http) const override;
+
+ // Host authorization type.
+ AuthorizationType m_authorization_type;
+ // username and password for HTTP Digest Authentization (RFC RFC2617)
+ std::string m_username;
+ std::string m_password;
+};
+
+}
+
+#endif
diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp
new file mode 100644
index 0000000000..86f6101b6d
--- /dev/null
+++ b/src/slic3r/Utils/PrintHost.cpp
@@ -0,0 +1,281 @@
+#include "PrintHost.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include "libslic3r/PrintConfig.hpp"
+#include "libslic3r/Channel.hpp"
+#include "OctoPrint.hpp"
+#include "Duet.hpp"
+#include "FlashAir.hpp"
+#include "AstroBox.hpp"
+#include "Repetier.hpp"
+#include "MKS.hpp"
+#include "../GUI/PrintHostDialogs.hpp"
+
+namespace fs = boost::filesystem;
+using boost::optional;
+using Slic3r::GUI::PrintHostQueueDialog;
+
+namespace Slic3r {
+
+
+PrintHost::~PrintHost() {}
+
+PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config)
+{
+ PrinterTechnology tech = ptFFF;
+
+ {
+ const auto opt = config->option>("printer_technology");
+ if (opt != nullptr) {
+ tech = opt->value;
+ }
+ }
+
+ if (tech == ptFFF) {
+ const auto opt = config->option>("host_type");
+ const auto host_type = opt != nullptr ? opt->value : htOctoPrint;
+
+ switch (host_type) {
+ case htOctoPrint: return new OctoPrint(config);
+ case htDuet: return new Duet(config);
+ case htFlashAir: return new FlashAir(config);
+ case htAstroBox: return new AstroBox(config);
+ case htRepetier: return new Repetier(config);
+ case htPrusaLink: return new PrusaLink(config);
+ case htMKS: return new MKS(config);
+ default: return nullptr;
+ }
+ } else {
+ return new SL1Host(config);
+ }
+}
+
+wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const
+{
+ if (status != 0) {
+ auto wxbody = wxString::FromUTF8(body.data());
+ return wxString::Format("HTTP %u: %s", status, wxbody);
+ } else {
+ return wxString::FromUTF8(error.data());
+ }
+}
+
+
+struct PrintHostJobQueue::priv
+{
+ // XXX: comment on how bg thread works
+
+ PrintHostJobQueue *q;
+
+ Channel channel_jobs;
+ Channel channel_cancels;
+ size_t job_id = 0;
+ int prev_progress = -1;
+ fs::path source_to_remove;
+
+ std::thread bg_thread;
+ bool bg_exit = false;
+
+ PrintHostQueueDialog *queue_dialog;
+
+ priv(PrintHostJobQueue *q) : q(q) {}
+
+ void emit_progress(int progress);
+ void emit_error(wxString error);
+ void emit_cancel(size_t id);
+ void start_bg_thread();
+ void stop_bg_thread();
+ void bg_thread_main();
+ void progress_fn(Http::Progress progress, bool &cancel);
+ void remove_source(const fs::path &path);
+ void remove_source();
+ void perform_job(PrintHostJob the_job);
+};
+
+PrintHostJobQueue::PrintHostJobQueue(PrintHostQueueDialog *queue_dialog)
+ : p(new priv(this))
+{
+ p->queue_dialog = queue_dialog;
+}
+
+PrintHostJobQueue::~PrintHostJobQueue()
+{
+ if (p) { p->stop_bg_thread(); }
+}
+
+void PrintHostJobQueue::priv::emit_progress(int progress)
+{
+ auto evt = new PrintHostQueueDialog::Event(GUI::EVT_PRINTHOST_PROGRESS, queue_dialog->GetId(), job_id, progress);
+ wxQueueEvent(queue_dialog, evt);
+}
+
+void PrintHostJobQueue::priv::emit_error(wxString error)
+{
+ auto evt = new PrintHostQueueDialog::Event(GUI::EVT_PRINTHOST_ERROR, queue_dialog->GetId(), job_id, std::move(error));
+ wxQueueEvent(queue_dialog, evt);
+}
+
+void PrintHostJobQueue::priv::emit_cancel(size_t id)
+{
+ auto evt = new PrintHostQueueDialog::Event(GUI::EVT_PRINTHOST_CANCEL, queue_dialog->GetId(), id);
+ wxQueueEvent(queue_dialog, evt);
+}
+
+void PrintHostJobQueue::priv::start_bg_thread()
+{
+ if (bg_thread.joinable()) { return; }
+
+ std::shared_ptr p2 = q->p;
+ bg_thread = std::thread([p2]() {
+ p2->bg_thread_main();
+ });
+}
+
+void PrintHostJobQueue::priv::stop_bg_thread()
+{
+ if (bg_thread.joinable()) {
+ bg_exit = true;
+ channel_jobs.push(PrintHostJob()); // Push an empty job to wake up bg_thread in case it's sleeping
+ bg_thread.detach(); // Let the background thread go, it should exit on its own
+ }
+}
+
+void PrintHostJobQueue::priv::bg_thread_main()
+{
+ // bg thread entry point
+
+ try {
+ // Pick up jobs from the job channel:
+ while (! bg_exit) {
+ auto job = channel_jobs.pop(); // Sleeps in a cond var if there are no jobs
+ if (job.empty()) {
+ // This happens when the thread is being stopped
+ break;
+ }
+
+ source_to_remove = job.upload_data.source_path;
+
+ BOOST_LOG_TRIVIAL(debug) << boost::format("PrintHostJobQueue/bg_thread: Received job: [%1%]: `%2%` -> `%3%`, cancelled: %4%")
+ % job_id
+ % job.upload_data.upload_path
+ % job.printhost->get_host()
+ % job.cancelled;
+
+ if (! job.cancelled) {
+ perform_job(std::move(job));
+ }
+
+ remove_source();
+ job_id++;
+ }
+ } catch (const std::exception &e) {
+ emit_error(e.what());
+ }
+
+ // Cleanup leftover files, if any
+ remove_source();
+ auto jobs = channel_jobs.lock_rw();
+ for (const PrintHostJob &job : *jobs) {
+ remove_source(job.upload_data.source_path);
+ }
+}
+
+void PrintHostJobQueue::priv::progress_fn(Http::Progress progress, bool &cancel)
+{
+ if (cancel) {
+ // When cancel is true from the start, Http indicates request has been cancelled
+ emit_cancel(job_id);
+ return;
+ }
+
+ if (bg_exit) {
+ cancel = true;
+ return;
+ }
+
+ if (channel_cancels.size_hint() > 0) {
+ // Lock both queues
+ auto cancels = channel_cancels.lock_rw();
+ auto jobs = channel_jobs.lock_rw();
+
+ for (size_t cancel_id : *cancels) {
+ if (cancel_id == job_id) {
+ cancel = true;
+ } else if (cancel_id > job_id) {
+ const size_t idx = cancel_id - job_id - 1;
+ if (idx < jobs->size()) {
+ jobs->at(idx).cancelled = true;
+ BOOST_LOG_TRIVIAL(debug) << boost::format("PrintHostJobQueue: Job id %1% cancelled") % cancel_id;
+ emit_cancel(cancel_id);
+ }
+ }
+ }
+
+ cancels->clear();
+ }
+
+ if (! cancel) {
+ int gui_progress = progress.ultotal > 0 ? 100*progress.ulnow / progress.ultotal : 0;
+ if (gui_progress != prev_progress) {
+ emit_progress(gui_progress);
+ prev_progress = gui_progress;
+ }
+ }
+}
+
+void PrintHostJobQueue::priv::remove_source(const fs::path &path)
+{
+ if (! path.empty()) {
+ boost::system::error_code ec;
+ fs::remove(path, ec);
+ if (ec) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("PrintHostJobQueue: Error removing file `%1%`: %2%") % path % ec;
+ }
+ }
+}
+
+void PrintHostJobQueue::priv::remove_source()
+{
+ remove_source(source_to_remove);
+ source_to_remove.clear();
+}
+
+void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job)
+{
+ emit_progress(0); // Indicate the upload is starting
+
+ bool success = the_job.printhost->upload(std::move(the_job.upload_data),
+ [this](Http::Progress progress, bool &cancel) { this->progress_fn(std::move(progress), cancel); },
+ [this](wxString error) {
+ emit_error(std::move(error));
+ }
+ );
+
+ if (success) {
+ emit_progress(100);
+ }
+}
+
+void PrintHostJobQueue::enqueue(PrintHostJob job)
+{
+ p->start_bg_thread();
+ p->queue_dialog->append_job(job);
+ p->channel_jobs.push(std::move(job));
+}
+
+void PrintHostJobQueue::cancel(size_t id)
+{
+ p->channel_cancels.push(id);
+}
+
+}
diff --git a/src/slic3r/Utils/PrintHost.hpp b/src/slic3r/Utils/PrintHost.hpp
new file mode 100644
index 0000000000..dd22e60b7d
--- /dev/null
+++ b/src/slic3r/Utils/PrintHost.hpp
@@ -0,0 +1,129 @@
+#ifndef slic3r_PrintHost_hpp_
+#define slic3r_PrintHost_hpp_
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include "Http.hpp"
+
+class wxArrayString;
+
+namespace Slic3r {
+
+class DynamicPrintConfig;
+
+enum class PrintHostPostUploadAction {
+ None,
+ StartPrint,
+ StartSimulation
+};
+using PrintHostPostUploadActions = enum_bitmask;
+ENABLE_ENUM_BITMASK_OPERATORS(PrintHostPostUploadAction);
+
+struct PrintHostUpload
+{
+ boost::filesystem::path source_path;
+ boost::filesystem::path upload_path;
+
+ std::string group;
+
+ PrintHostPostUploadAction post_action { PrintHostPostUploadAction::None };
+};
+
+class PrintHost
+{
+public:
+ virtual ~PrintHost();
+
+ typedef Http::ProgressFn ProgressFn;
+ typedef std::function ErrorFn;
+
+ virtual const char* get_name() const = 0;
+
+ virtual bool test(wxString &curl_msg) const = 0;
+ virtual wxString get_test_ok_msg () const = 0;
+ virtual wxString get_test_failed_msg (wxString &msg) const = 0;
+ virtual bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const = 0;
+ virtual bool has_auto_discovery() const = 0;
+ virtual bool can_test() const = 0;
+ virtual PrintHostPostUploadActions get_post_upload_actions() const = 0;
+ // A print host usually does not support multiple printers, with the exception of Repetier server.
+ virtual bool supports_multiple_printers() const { return false; }
+ virtual std::string get_host() const = 0;
+
+ // Support for Repetier server multiple groups & printers. Not supported by other print hosts.
+ // Returns false if not supported. May throw HostNetworkError.
+ virtual bool get_groups(wxArrayString & /* groups */) const { return false; }
+ virtual bool get_printers(wxArrayString & /* printers */) const { return false; }
+
+ static PrintHost* get_print_host(DynamicPrintConfig *config);
+
+protected:
+ virtual wxString format_error(const std::string &body, const std::string &error, unsigned status) const;
+};
+
+
+struct PrintHostJob
+{
+ PrintHostUpload upload_data;
+ std::unique_ptr printhost;
+ bool cancelled = false;
+
+ PrintHostJob() {}
+ PrintHostJob(const PrintHostJob&) = delete;
+ PrintHostJob(PrintHostJob &&other)
+ : upload_data(std::move(other.upload_data))
+ , printhost(std::move(other.printhost))
+ , cancelled(other.cancelled)
+ {}
+
+ PrintHostJob(DynamicPrintConfig *config)
+ : printhost(PrintHost::get_print_host(config))
+ {}
+
+ PrintHostJob& operator=(const PrintHostJob&) = delete;
+ PrintHostJob& operator=(PrintHostJob &&other)
+ {
+ upload_data = std::move(other.upload_data);
+ printhost = std::move(other.printhost);
+ cancelled = other.cancelled;
+ return *this;
+ }
+
+ bool empty() const { return !printhost; }
+ operator bool() const { return !!printhost; }
+};
+
+
+namespace GUI { class PrintHostQueueDialog; }
+
+class PrintHostJobQueue
+{
+public:
+ PrintHostJobQueue(GUI::PrintHostQueueDialog *queue_dialog);
+ PrintHostJobQueue(const PrintHostJobQueue &) = delete;
+ PrintHostJobQueue(PrintHostJobQueue &&other) = delete;
+ ~PrintHostJobQueue();
+
+ PrintHostJobQueue& operator=(const PrintHostJobQueue &) = delete;
+ PrintHostJobQueue& operator=(PrintHostJobQueue &&other) = delete;
+
+ void enqueue(PrintHostJob job);
+ void cancel(size_t id);
+
+private:
+ struct priv;
+ std::shared_ptr p;
+};
+
+
+
+}
+
+#endif
diff --git a/src/slic3r/Utils/Repetier.cpp b/src/slic3r/Utils/Repetier.cpp
new file mode 100644
index 0000000000..0569d97fae
--- /dev/null
+++ b/src/slic3r/Utils/Repetier.cpp
@@ -0,0 +1,274 @@
+#include "Repetier.hpp"
+
+#include
+#include
+#include