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 +#include +#include +#include +#include +#include +#include + +#include + + +#include "libslic3r/PrintConfig.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/format.hpp" +#include "Http.hpp" + + +namespace fs = boost::filesystem; +namespace pt = boost::property_tree; + + +namespace Slic3r { + +Repetier::Repetier(DynamicPrintConfig *config) : + host(config->opt_string("print_host")), + apikey(config->opt_string("printhost_apikey")), + cafile(config->opt_string("printhost_cafile")), + port(config->opt_string("printhost_port")) +{} + +const char* Repetier::get_name() const { return "Repetier"; } + +bool Repetier::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("printer/info"); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: List 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); + + const auto text = ptree.get_optional("name"); + res = validate_version_text(text); + if (! res) { + msg = GUI::from_u8((boost::format(_utf8(L("Mismatched type of print host: %s"))) % (text ? *text : "Repetier")).str()); + } + } + catch (const std::exception &) { + res = false; + msg = "Could not parse server response"; + } + }) + .perform_sync(); + + return res; +} + +wxString Repetier::get_test_ok_msg () const +{ + return _(L("Connection to Repetier works correctly.")); +} + +wxString Repetier::get_test_failed_msg (wxString &msg) const +{ + return GUI::from_u8((boost::format("%s: %s\n\n%s") + % _utf8(L("Could not connect to Repetier")) + % std::string(msg.ToUTF8()) + % _utf8(L("Note: Repetier version at least 0.90.0 is required."))).str()); +} + +bool Repetier::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 = upload_data.post_action == PrintHostPostUploadAction::StartPrint + ? make_url((boost::format("printer/job/%1%") % port).str()) + : make_url((boost::format("printer/model/%1%") % port).str()); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%, group: %7%") + % name + % upload_data.source_path + % url + % upload_filename.string() + % upload_parent_path.string() + % (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false") + % upload_data.group; + + auto http = Http::post(std::move(url)); + set_auth(http); + + if (! upload_data.group.empty() && upload_data.group != _utf8(L("Default"))) { + http.form_add("group", upload_data.group); + } + + if(upload_data.post_action == PrintHostPostUploadAction::StartPrint) { + http.form_add("name", upload_filename.string()); + } + + http.form_add("a", "upload") + .form_add_file("filename", 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) << "Repetier: Upload canceled"; + res = false; + } + }) + .perform_sync(); + + return res; +} + +bool Repetier::validate_version_text(const boost::optional &version_text) const +{ + return version_text ? boost::starts_with(*version_text, "Repetier") : true; +} + +void Repetier::set_auth(Http &http) const +{ + http.header("X-Api-Key", apikey); + + if (! cafile.empty()) { + http.ca_file(cafile); + } +} + +std::string Repetier::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(); + } +} + +bool Repetier::get_groups(wxArrayString& groups) const +{ + bool res = true; + + const char *name = get_name(); + auto url = make_url((boost::format("printer/api/%1%") % port).str()); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get groups at: %2%") % name % url; + + auto http = Http::get(std::move(url)); + set_auth(http); + http.form_add("a", "listModelGroups"); + 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; + }) + .on_complete([&](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got groups: %2%") % name % body; + + try { + std::stringstream ss(body); + pt::ptree ptree; + pt::read_json(ss, ptree); + + BOOST_FOREACH(boost::property_tree::ptree::value_type &v, ptree.get_child("groupNames.")) { + if (v.second.data() == "#") { + groups.push_back(_utf8(L("Default"))); + } else { + // Is it safe to assume that the data are utf-8 encoded? + groups.push_back(GUI::from_u8(v.second.data())); + } + } + } + catch (const std::exception &) { + //msg = "Could not parse server response"; + res = false; + } + }) + .perform_sync(); + + return res; +} + +bool Repetier::get_printers(wxArrayString& printers) const +{ + const char *name = get_name(); + + bool res = true; + auto url = make_url("printer/list"); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: List printers 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 listing printers: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + res = false; + }) + .on_complete([&](std::string body, unsigned http_status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got printers: %2%, HTTP status: %3%") % name % body % http_status; + + if (http_status != 200) + throw HostNetworkError(GUI::format(_L("HTTP status: %1%\nMessage body: \"%2%\""), http_status, body)); + + std::stringstream ss(body); + pt::ptree ptree; + try { + pt::read_json(ss, ptree); + } catch (const pt::ptree_error &err) { + throw HostNetworkError(GUI::format(_L("Parsing of host response failed.\nMessage body: \"%1%\"\nError: \"%2%\""), body, err.what())); + } + + const auto error = ptree.get_optional("error"); + if (error) + throw HostNetworkError(*error); + + try { + BOOST_FOREACH(boost::property_tree::ptree::value_type &v, ptree.get_child("data.")) { + const auto port = v.second.get("slug"); + printers.push_back(Slic3r::GUI::from_u8(port)); + } + } catch (const std::exception &err) { + throw HostNetworkError(GUI::format(_L("Enumeration of host printers failed.\nMessage body: \"%1%\"\nError: \"%2%\""), body, err.what())); + } + }) + .perform_sync(); + + return res; +} + +} diff --git a/src/slic3r/Utils/Repetier.hpp b/src/slic3r/Utils/Repetier.hpp new file mode 100644 index 0000000000..6f33102604 --- /dev/null +++ b/src/slic3r/Utils/Repetier.hpp @@ -0,0 +1,51 @@ +#ifndef slic3r_Repetier_hpp_ +#define slic3r_Repetier_hpp_ + +#include +#include +#include + +#include "PrintHost.hpp" + +namespace Slic3r { + +class DynamicPrintConfig; +class Http; + +class Repetier : public PrintHost +{ +public: + Repetier(DynamicPrintConfig *config); + ~Repetier() 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; } + bool supports_multiple_printers() const override { return true; } + std::string get_host() const override { return host; } + + bool get_groups(wxArrayString &groups) const override; + bool get_printers(wxArrayString &printers) const override; + +protected: + virtual bool validate_version_text(const boost::optional &version_text) const; + +private: + std::string host; + std::string apikey; + std::string cafile; + std::string port; + + void set_auth(Http &http) const; + std::string make_url(const std::string &path) const; +}; + +} + +#endif diff --git a/src/slic3r/Utils/Serial.cpp b/src/slic3r/Utils/Serial.cpp new file mode 100644 index 0000000000..4db1acc6b6 --- /dev/null +++ b/src/slic3r/Utils/Serial.cpp @@ -0,0 +1,504 @@ +#include "Serial.hpp" + +#include "libslic3r/Exception.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#if _WIN32 + #include + #include + #include + #include + #include + // Undefine min/max macros incompatible with the standard library + // For example, std::numeric_limits::max() + // produces some weird errors + #ifdef min + #undef min + #endif + #ifdef max + #undef max + #endif + #include "boost/nowide/convert.hpp" + #pragma comment(lib, "user32.lib") +#elif __APPLE__ + #include + #include + #include + #include + #include + #include +#endif + +#ifndef _WIN32 + #include + #include + #include + #include +#endif + +#if defined(__APPLE__) || defined(__OpenBSD__) + #include +#elif defined __linux__ + #include + #include +#endif + +using boost::optional; + + +namespace Slic3r { +namespace Utils { + +static bool looks_like_printer(const std::string &friendly_name) +{ + return friendly_name.find("Original Prusa") != std::string::npos; +} + +#if _WIN32 +void parse_hardware_id(const std::string &hardware_id, SerialPortInfo &spi) +{ + unsigned vid, pid; + std::regex pattern("USB\\\\.*VID_([[:xdigit:]]+)&PID_([[:xdigit:]]+).*"); + std::smatch matches; + if (std::regex_match(hardware_id, matches, pattern)) { + vid = std::stoul(matches[1].str(), 0, 16); + pid = std::stoul(matches[2].str(), 0, 16); + spi.id_vendor = vid; + spi.id_product = pid; + } +} +#endif + +#ifdef __linux__ +optional sysfs_tty_prop(const std::string &tty_dev, const std::string &name) +{ + const auto prop_path = (boost::format("/sys/class/tty/%1%/device/../%2%") % tty_dev % name).str(); + std::ifstream file(prop_path); + std::string res; + + std::getline(file, res); + if (file.good()) { return res; } + else { return boost::none; } +} + +optional sysfs_tty_prop_hex(const std::string &tty_dev, const std::string &name) +{ + auto prop = sysfs_tty_prop(tty_dev, name); + if (!prop) { return boost::none; } + + try { return std::stoul(*prop, 0, 16); } + catch (const std::exception&) { return boost::none; } +} +#endif + +std::vector scan_serial_ports_extended() +{ + std::vector output; + +#ifdef _WIN32 + SP_DEVINFO_DATA devInfoData = { 0 }; + devInfoData.cbSize = sizeof(devInfoData); + // Get the tree containing the info for the ports. + HDEVINFO hDeviceInfo = SetupDiGetClassDevs(&GUID_DEVCLASS_PORTS, 0, nullptr, DIGCF_PRESENT); + if (hDeviceInfo != INVALID_HANDLE_VALUE) { + // Iterate over all the devices in the tree. + for (int nDevice = 0; SetupDiEnumDeviceInfo(hDeviceInfo, nDevice, &devInfoData); ++ nDevice) { + SerialPortInfo port_info; + // Get the registry key which stores the ports settings. + HKEY hDeviceKey = SetupDiOpenDevRegKey(hDeviceInfo, &devInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_QUERY_VALUE); + if (hDeviceKey) { + // Read in the name of the port. + wchar_t pszPortName[4096]; + DWORD dwSize = sizeof(pszPortName); + DWORD dwType = 0; + if (RegQueryValueEx(hDeviceKey, L"PortName", NULL, &dwType, (LPBYTE)pszPortName, &dwSize) == ERROR_SUCCESS) + port_info.port = boost::nowide::narrow(pszPortName); + RegCloseKey(hDeviceKey); + if (port_info.port.empty()) + continue; + } + + // Find the size required to hold the device info. + DWORD regDataType; + DWORD reqSize = 0; + SetupDiGetDeviceRegistryProperty(hDeviceInfo, &devInfoData, SPDRP_HARDWAREID, nullptr, nullptr, 0, &reqSize); + std::vector hardware_id(reqSize > 1 ? reqSize : 1); + // Now store it in a buffer. + if (! SetupDiGetDeviceRegistryProperty(hDeviceInfo, &devInfoData, SPDRP_HARDWAREID, ®DataType, (BYTE*)hardware_id.data(), reqSize, nullptr)) + continue; + parse_hardware_id(boost::nowide::narrow(hardware_id.data()), port_info); + + // Find the size required to hold the friendly name. + reqSize = 0; + SetupDiGetDeviceRegistryProperty(hDeviceInfo, &devInfoData, SPDRP_FRIENDLYNAME, nullptr, nullptr, 0, &reqSize); + std::vector friendly_name; + friendly_name.reserve(reqSize > 1 ? reqSize : 1); + // Now store it in a buffer. + if (! SetupDiGetDeviceRegistryProperty(hDeviceInfo, &devInfoData, SPDRP_FRIENDLYNAME, nullptr, (BYTE*)friendly_name.data(), reqSize, nullptr)) { + port_info.friendly_name = port_info.port; + } else { + port_info.friendly_name = boost::nowide::narrow(friendly_name.data()); + port_info.is_printer = looks_like_printer(port_info.friendly_name); + } + output.emplace_back(std::move(port_info)); + } + } +#elif __APPLE__ + // inspired by https://sigrok.org/wiki/Libserialport + CFMutableDictionaryRef classes = IOServiceMatching(kIOSerialBSDServiceValue); + if (classes != 0) { + io_iterator_t iter; + if (IOServiceGetMatchingServices(kIOMasterPortDefault, classes, &iter) == KERN_SUCCESS) { + io_object_t port; + while ((port = IOIteratorNext(iter)) != 0) { + CFTypeRef cf_property = IORegistryEntryCreateCFProperty(port, CFSTR(kIOCalloutDeviceKey), kCFAllocatorDefault, 0); + if (cf_property) { + char path[PATH_MAX]; + Boolean result = CFStringGetCString((CFStringRef)cf_property, path, sizeof(path), kCFStringEncodingUTF8); + CFRelease(cf_property); + if (result) { + SerialPortInfo port_info; + port_info.port = path; + + // Attempt to read out the device friendly name + if ((cf_property = IORegistryEntrySearchCFProperty(port, kIOServicePlane, + CFSTR("USB Interface Name"), kCFAllocatorDefault, + kIORegistryIterateRecursively | kIORegistryIterateParents)) || + (cf_property = IORegistryEntrySearchCFProperty(port, kIOServicePlane, + CFSTR("USB Product Name"), kCFAllocatorDefault, + kIORegistryIterateRecursively | kIORegistryIterateParents)) || + (cf_property = IORegistryEntrySearchCFProperty(port, kIOServicePlane, + CFSTR("Product Name"), kCFAllocatorDefault, + kIORegistryIterateRecursively | kIORegistryIterateParents)) || + (cf_property = IORegistryEntryCreateCFProperty(port, + CFSTR(kIOTTYDeviceKey), kCFAllocatorDefault, 0))) { + // Description limited to 127 char, anything longer would not be user friendly anyway. + char description[128]; + if (CFStringGetCString((CFStringRef)cf_property, description, sizeof(description), kCFStringEncodingUTF8)) { + port_info.friendly_name = std::string(description) + " (" + port_info.port + ")"; + port_info.is_printer = looks_like_printer(port_info.friendly_name); + } + CFRelease(cf_property); + } + if (port_info.friendly_name.empty()) + port_info.friendly_name = port_info.port; + + // Attempt to read out the VID & PID + int vid, pid; + auto cf_vendor = IORegistryEntrySearchCFProperty(port, kIOServicePlane, CFSTR("idVendor"), + kCFAllocatorDefault, kIORegistryIterateRecursively | kIORegistryIterateParents); + auto cf_product = IORegistryEntrySearchCFProperty(port, kIOServicePlane, CFSTR("idProduct"), + kCFAllocatorDefault, kIORegistryIterateRecursively | kIORegistryIterateParents); + if (cf_vendor && cf_product) { + if (CFNumberGetValue((CFNumberRef)cf_vendor, kCFNumberIntType, &vid) && + CFNumberGetValue((CFNumberRef)cf_product, kCFNumberIntType, &pid)) { + port_info.id_vendor = vid; + port_info.id_product = pid; + } + } + if (cf_vendor) { CFRelease(cf_vendor); } + if (cf_product) { CFRelease(cf_product); } + + output.emplace_back(std::move(port_info)); + } + } + IOObjectRelease(port); + } + } + } +#else + // UNIX / Linux + std::initializer_list prefixes { "ttyUSB" , "ttyACM", "tty.", "cu.", "rfcomm" }; + for (auto &dir_entry : boost::filesystem::directory_iterator(boost::filesystem::path("/dev"))) { + std::string name = dir_entry.path().filename().string(); + for (const char *prefix : prefixes) { + if (boost::starts_with(name, prefix)) { + const auto path = dir_entry.path().string(); + SerialPortInfo spi; + spi.port = path; +#ifdef __linux__ + auto friendly_name = sysfs_tty_prop(name, "product"); + if (friendly_name) { + spi.is_printer = looks_like_printer(*friendly_name); + spi.friendly_name = (boost::format("%1% (%2%)") % *friendly_name % path).str(); + } else { + spi.friendly_name = path; + } + auto vid = sysfs_tty_prop_hex(name, "idVendor"); + auto pid = sysfs_tty_prop_hex(name, "idProduct"); + if (vid && pid) { + spi.id_vendor = *vid; + spi.id_product = *pid; + } +#else + spi.friendly_name = path; +#endif + output.emplace_back(std::move(spi)); + break; + } + } + } +#endif + + output.erase(std::remove_if(output.begin(), output.end(), + [](const SerialPortInfo &info) { + return boost::starts_with(info.port, "Bluetooth") || boost::starts_with(info.port, "FireFly"); + }), + output.end()); + return output; +} + +std::vector scan_serial_ports() +{ + std::vector ports = scan_serial_ports_extended(); + std::vector output; + output.reserve(ports.size()); + for (const SerialPortInfo &spi : ports) + output.emplace_back(std::move(spi.port)); + return output; +} + + + +// Class Serial + +namespace asio = boost::asio; +using boost::system::error_code; + +Serial::Serial(asio::io_service& io_service) : + asio::serial_port(io_service) +{} + +Serial::Serial(asio::io_service& io_service, const std::string &name, unsigned baud_rate) : + asio::serial_port(io_service, name) +{ + set_baud_rate(baud_rate); +} + +Serial::~Serial() {} + +void Serial::set_baud_rate(unsigned baud_rate) +{ + try { + // This does not support speeds > 115200 + set_option(boost::asio::serial_port_base::baud_rate(baud_rate)); + } catch (boost::system::system_error &) { + auto handle = native_handle(); + + auto handle_errno = [](int retval) { + if (retval != 0) { + throw Slic3r::RuntimeError( + (boost::format("Could not set baud rate: %1%") % strerror(errno)).str() + ); + } + }; + +#if __APPLE__ + termios ios; + handle_errno(::tcgetattr(handle, &ios)); + handle_errno(::cfsetspeed(&ios, baud_rate)); + speed_t newSpeed = baud_rate; + handle_errno(::ioctl(handle, IOSSIOSPEED, &newSpeed)); + handle_errno(::tcsetattr(handle, TCSANOW, &ios)); +#elif __linux__ + + /* The following definitions are kindly borrowed from: + /usr/include/asm-generic/termbits.h + Unfortunately we cannot just include that one because + it would redefine the "struct termios" already defined + the already included by Boost.ASIO. */ +#define K_NCCS 19 + struct termios2 { + tcflag_t c_iflag; + tcflag_t c_oflag; + tcflag_t c_cflag; + tcflag_t c_lflag; + cc_t c_line; + cc_t c_cc[K_NCCS]; + speed_t c_ispeed; + speed_t c_ospeed; + }; +#define BOTHER CBAUDEX + + termios2 ios; + handle_errno(::ioctl(handle, TCGETS2, &ios)); + ios.c_ispeed = ios.c_ospeed = baud_rate; + ios.c_cflag &= ~CBAUD; + ios.c_cflag |= BOTHER | CLOCAL | CREAD; + ios.c_cc[VMIN] = 1; // Minimum of characters to read, prevents eof errors when 0 bytes are read + ios.c_cc[VTIME] = 1; + handle_errno(::ioctl(handle, TCSETS2, &ios)); + +#elif __OpenBSD__ + struct termios ios; + handle_errno(::tcgetattr(handle, &ios)); + handle_errno(::cfsetspeed(&ios, baud_rate)); + handle_errno(::tcsetattr(handle, TCSAFLUSH, &ios)); +#else + throw Slic3r::RuntimeError("Custom baud rates are not currently supported on this OS"); +#endif + } +} + + +/* +void Serial::set_DTR(bool on) +{ + auto handle = native_handle(); +#if defined(_WIN32) && !defined(__SYMBIAN32__) + if (! EscapeCommFunction(handle, on ? SETDTR : CLRDTR)) { + throw Slic3r::RuntimeError("Could not set serial port DTR"); + } +#else + int status; + if (::ioctl(handle, TIOCMGET, &status) == 0) { + on ? status |= TIOCM_DTR : status &= ~TIOCM_DTR; + if (::ioctl(handle, TIOCMSET, &status) == 0) { + return; + } + } + + throw Slic3r::RuntimeError( + (boost::format("Could not set serial port DTR: %1%") % strerror(errno)).str() + ); +#endif +} + +void Serial::reset_line_num() +{ + // See https://github.com/MarlinFirmware/Marlin/wiki/M110 + write_string("M110 N0\n"); + m_line_num = 0; +} + +bool Serial::read_line(unsigned timeout, std::string &line, error_code &ec) +{ + auto& io_service = +#if BOOST_VERSION >= 107000 + //FIXME this is most certainly wrong! + (boost::asio::io_context&)this->get_executor().context(); + #else + this->get_io_service(); +#endif + asio::deadline_timer timer(io_service); + char c = 0; + bool fail = false; + + while (true) { + io_service.reset(); + + asio::async_read(*this, boost::asio::buffer(&c, 1), [&](const error_code &read_ec, size_t size) { + if (ec || size == 0) { + fail = true; + ec = read_ec; // FIXME: only if operation not aborted + } + timer.cancel(); // FIXME: ditto + }); + + if (timeout > 0) { + timer.expires_from_now(boost::posix_time::milliseconds(timeout)); + timer.async_wait([&](const error_code &ec) { + // Ignore timer aborts + if (!ec) { + fail = true; + this->cancel(); + } + }); + } + + io_service.run(); + + if (fail) { + return false; + } else if (c != '\n') { + line += c; + } else { + return true; + } + } +} + +void Serial::printer_setup() +{ + printer_reset(); + write_string("\r\r\r\r\r\r\r\r\r\r"); // Gets rid of line noise, if any +} + +size_t Serial::write_string(const std::string &str) +{ + // TODO: might be wise to timeout here as well + return asio::write(*this, asio::buffer(str)); +} + +bool Serial::printer_ready_wait(unsigned retries, unsigned timeout) +{ + std::string line; + error_code ec; + + for (; retries > 0; retries--) { + reset_line_num(); + + while (read_line(timeout, line, ec)) { + if (line == "ok") { + return true; + } + line.clear(); + } + + line.clear(); + } + + return false; +} + +size_t Serial::printer_write_line(const std::string &line, unsigned line_num) +{ + const auto formatted_line = Utils::Serial::printer_format_line(line, line_num); + return write_string(formatted_line); +} + +size_t Serial::printer_write_line(const std::string &line) +{ + m_line_num++; + return printer_write_line(line, m_line_num); +} + +void Serial::printer_reset() +{ + this->set_DTR(false); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + this->set_DTR(true); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + this->set_DTR(false); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); +} + +std::string Serial::printer_format_line(const std::string &line, unsigned line_num) +{ + const auto line_num_str = std::to_string(line_num); + + unsigned checksum = 'N'; + for (auto c : line_num_str) { checksum ^= c; } + checksum ^= ' '; + for (auto c : line) { checksum ^= c; } + + return (boost::format("N%1% %2%*%3%\n") % line_num_str % line % checksum).str(); +} +*/ + + +} // namespace Utils +} // namespace Slic3r diff --git a/src/slic3r/Utils/Serial.hpp b/src/slic3r/Utils/Serial.hpp new file mode 100644 index 0000000000..8bad75b315 --- /dev/null +++ b/src/slic3r/Utils/Serial.hpp @@ -0,0 +1,97 @@ +#ifndef slic3r_GUI_Utils_Serial_hpp_ +#define slic3r_GUI_Utils_Serial_hpp_ + +#include +#include +#include +#include + + +namespace Slic3r { +namespace Utils { + +struct SerialPortInfo { + std::string port; + unsigned id_vendor = -1; + unsigned id_product = -1; + std::string friendly_name; + bool is_printer = false; + + SerialPortInfo() {} + SerialPortInfo(std::string port) : port(port), friendly_name(std::move(port)) {} + + bool id_match(unsigned id_vendor, unsigned id_product) const { return id_vendor == this->id_vendor && id_product == this->id_product; } +}; + +inline bool operator==(const SerialPortInfo &sp1, const SerialPortInfo &sp2) +{ + return + sp1.port == sp2.port && + sp1.id_vendor == sp2.id_vendor && + sp1.id_product == sp2.id_product && + sp1.is_printer == sp2.is_printer; +} + +extern std::vector scan_serial_ports(); +extern std::vector scan_serial_ports_extended(); + + +class Serial : public boost::asio::serial_port +{ +public: + Serial(boost::asio::io_service &io_service); + Serial(boost::asio::io_service &io_service, const std::string &name, unsigned baud_rate); + Serial(const Serial &) = delete; + Serial &operator=(const Serial &) = delete; + ~Serial(); + + void set_baud_rate(unsigned baud_rate); + + // The Serial implementation is currently in disarray and therefore commented out. + // The boost implementation seems to have several problems, such as lack of support + // for custom baud rates, few weird implementation bugs and a history of API breakages. + // It's questionable whether it solves more problems than causes. Probably not. + // TODO: Custom implementation not based on asio. + // + // As of now, this class is only kept for the purpose of rebooting AVR109, + // see FirmwareDialog::priv::avr109_reboot() + +/* + void set_DTR(bool on); + + // Resets the line number both internally as well as with the firmware using M110 + void reset_line_num(); + + // Reads a line or times out, the timeout is in milliseconds + bool read_line(unsigned timeout, std::string &line, boost::system::error_code &ec); + + // Perform an initial setup for communicating with a printer + void printer_setup(); + + // Write data from a string + size_t write_string(const std::string &str); + + // Attempts to reset the line numer and waits until the printer says "ok" + bool printer_ready_wait(unsigned retries, unsigned timeout); + + // Write Marlin-formatted line, with a line number and a checksum + size_t printer_write_line(const std::string &line, unsigned line_num); + + // Same as above, but with internally-managed line number + size_t printer_write_line(const std::string &line); + + // Toggles DTR to reset the printer + void printer_reset(); + + // Formats a line Marlin-style, ie. with a sequential number and a checksum + static std::string printer_format_line(const std::string &line, unsigned line_num); +private: + unsigned m_line_num = 0; +*/ +}; + + +} // Utils +} // Slic3r + +#endif /* slic3r_GUI_Utils_Serial_hpp_ */