From 91d5ba2870ec3753065a83327988357dc7aff992 Mon Sep 17 00:00:00 2001 From: Stone Li Date: Mon, 19 Sep 2022 09:18:48 +0800 Subject: [PATCH] NEW: support send gcode to third-party printer Thanks SoftForever for your works to support sending a gcode file to third-party printer Change-Id: I3cba43c8bd878f1f1c2fd5fae202ed4d922e8727 Signed-off-by: Stone Li --- src/libslic3r/Preset.cpp | 37 +- src/libslic3r/Preset.hpp | 2 + src/libslic3r/PrintConfig.cpp | 117 +++++ src/libslic3r/PrintConfig.hpp | 11 + src/slic3r/CMakeLists.txt | 10 + src/slic3r/GUI/BackgroundSlicingProcess.cpp | 54 ++ src/slic3r/GUI/BackgroundSlicingProcess.hpp | 9 + src/slic3r/GUI/BonjourDialog.cpp | 239 +++++++++ src/slic3r/GUI/BonjourDialog.hpp | 53 ++ src/slic3r/GUI/Field.cpp | 12 +- src/slic3r/GUI/GLToolbar.cpp | 2 + src/slic3r/GUI/GLToolbar.hpp | 2 + src/slic3r/GUI/GUI_App.cpp | 33 ++ src/slic3r/GUI/GUI_App.hpp | 5 + src/slic3r/GUI/MainFrame.cpp | 124 ++++- src/slic3r/GUI/MainFrame.hpp | 5 + src/slic3r/GUI/NotificationManager.cpp | 195 +++++++ src/slic3r/GUI/NotificationManager.hpp | 47 ++ src/slic3r/GUI/PhysicalPrinterDialog.cpp | 407 +++++++++++++++ src/slic3r/GUI/PhysicalPrinterDialog.hpp | 59 +++ src/slic3r/GUI/Plater.cpp | 129 +++++ src/slic3r/GUI/Plater.hpp | 3 + src/slic3r/GUI/PrintHostDialogs.cpp | 532 ++++++++++++++++++++ src/slic3r/GUI/PrintHostDialogs.hpp | 130 +++++ src/slic3r/GUI/Tab.cpp | 12 +- src/slic3r/Utils/OctoPrint.cpp | 367 ++++++++++++++ src/slic3r/Utils/OctoPrint.hpp | 101 ++++ src/slic3r/Utils/PrintHost.cpp | 281 +++++++++++ src/slic3r/Utils/PrintHost.hpp | 129 +++++ 29 files changed, 3067 insertions(+), 40 deletions(-) create mode 100644 src/slic3r/GUI/BonjourDialog.cpp create mode 100644 src/slic3r/GUI/BonjourDialog.hpp create mode 100644 src/slic3r/GUI/PhysicalPrinterDialog.cpp create mode 100644 src/slic3r/GUI/PhysicalPrinterDialog.hpp create mode 100644 src/slic3r/GUI/PrintHostDialogs.cpp create mode 100644 src/slic3r/GUI/PrintHostDialogs.hpp create mode 100644 src/slic3r/Utils/OctoPrint.cpp create mode 100644 src/slic3r/Utils/OctoPrint.hpp create mode 100644 src/slic3r/Utils/PrintHost.cpp create mode 100644 src/slic3r/Utils/PrintHost.hpp diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index f8e6df560..365a85655 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -2,6 +2,7 @@ #include "Exception.hpp" #include "Preset.hpp" +#include "PresetBundle.hpp" #include "AppConfig.hpp" #ifdef _MSC_VER @@ -643,6 +644,26 @@ std::string Preset::get_filament_type(std::string &display_filament_type) return config.get_filament_type(display_filament_type); } +bool Preset::is_bbl_vendor_preset(PresetBundle *preset_bundle) +{ + bool is_bbl_vendor_preset = true; + if (preset_bundle) { + auto config = &preset_bundle->printers.get_edited_preset().config; + std::string vendor_name; + for (auto vendor_profile : preset_bundle->vendors) { + for (auto vendor_model : vendor_profile.second.models) + if (vendor_model.name == config->opt_string("printer_model")) + { + vendor_name = vendor_profile.first; + break; + } + } + if (!vendor_name.empty()) + is_bbl_vendor_preset = vendor_name.compare("BBL") == 0 ? true : false; + } + return is_bbl_vendor_preset; +} + static std::vector s_Preset_print_options { "layer_height", "initial_layer_print_height", "wall_loops", "slice_closing_radius", "spiral_mode", "top_shell_layers", "top_shell_thickness", "bottom_shell_layers", "bottom_shell_thickness", @@ -725,7 +746,11 @@ 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 + "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 { @@ -2525,6 +2550,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() diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index f0495f739..843a49417 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -297,6 +297,8 @@ public: // special for upport G and Support W std::string get_filament_type(std::string &display_filament_type); + bool is_bbl_vendor_preset(PresetBundle *m_preset_bundle); + static const std::vector& print_options(); static const std::vector& filament_options(); // Printer options contain the nozzle options. diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 7d196dc23..4ee4f466b 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -72,6 +72,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 }, @@ -343,6 +360,83 @@ void PrintConfigDef::init_common_params() def->mode = comDevelop; def->set_default_value(new ConfigOptionStrings()); + 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); @@ -1686,6 +1780,29 @@ 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 3fb8c7bb4..ad42c9f30 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 { @@ -253,6 +261,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. diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 34591722c..56cff773c 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -201,6 +201,8 @@ set(SLIC3R_GUI_SOURCES GUI/ParamsDialog.hpp GUI/ParamsPanel.cpp GUI/ParamsPanel.hpp + GUI/PrintHostDialogs.cpp + GUI/PrintHostDialogs.hpp GUI/AmsWidgets.cpp GUI/AmsWidgets.hpp GUI/MediaFilePanel.cpp @@ -307,6 +309,8 @@ set(SLIC3R_GUI_SOURCES GUI/Jobs/BindJob.cpp GUI/Jobs/NotificationProgressIndicator.hpp GUI/Jobs/NotificationProgressIndicator.cpp + GUI/PhysicalPrinterDialog.hpp + GUI/PhysicalPrinterDialog.cpp GUI/ProgressStatusBar.hpp GUI/ProgressStatusBar.cpp GUI/BBLStatusBar.hpp @@ -348,6 +352,8 @@ set(SLIC3R_GUI_SOURCES GUI/PublishDialog.hpp GUI/RecenterDialog.cpp GUI/RecenterDialog.hpp + GUI/BonjourDialog.cpp + GUI/BonjourDialog.hpp GUI/BindDialog.cpp GUI/BindDialog.hpp GUI/SelectMachine.hpp @@ -387,6 +393,10 @@ set(SLIC3R_GUI_SOURCES Utils/minilzo_extension.cpp Utils/ColorSpaceConvert.hpp Utils/ColorSpaceConvert.cpp + Utils/OctoPrint.hpp + Utils/OctoPrint.cpp + Utils/PrintHost.hpp + Utils/PrintHost.cpp Utils/NetworkAgent.cpp Utils/NetworkAgent.hpp ) diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index 4ef4a0e07..6da922ce4 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,44 @@ 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 9a28cdab9..d369b010b 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 000000000..516b1ab4a --- /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 000000000..def0838d7 --- /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 968d43e9e..1a07cb147 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -1236,6 +1236,10 @@ void Choice::set_value(const boost::any& value, bool change_event) case coEnums: { int val = boost::any_cast(value); + // Support ThirdPartyPrinter + 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; @@ -1315,11 +1319,15 @@ 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 == "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)); } - else + // Support ThirdPartyPrinter + else 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 m_value = field->GetSelection(); } else if (m_opt.gui_type == ConfigOptionDef::GUIType::f_enum_open || m_opt.gui_type == ConfigOptionDef::GUIType::i_enum_open) { diff --git a/src/slic3r/GUI/GLToolbar.cpp b/src/slic3r/GUI/GLToolbar.cpp index 01e8bab0d..24e84ef53 100644 --- a/src/slic3r/GUI/GLToolbar.cpp +++ b/src/slic3r/GUI/GLToolbar.cpp @@ -24,6 +24,8 @@ 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_UPLOAD_GCODE, SimpleEvent); wxDEFINE_EVENT(EVT_GLTOOLBAR_EXPORT_SLICED_FILE, SimpleEvent); wxDEFINE_EVENT(EVT_GLTOOLBAR_PRINT_SELECT, SimpleEvent); wxDEFINE_EVENT(EVT_GLTOOLBAR_SEND_TO_PRINTER, SimpleEvent); diff --git a/src/slic3r/GUI/GLToolbar.hpp b/src/slic3r/GUI/GLToolbar.hpp index 61aa09a31..3c96d9f86 100644 --- a/src/slic3r/GUI/GLToolbar.hpp +++ b/src/slic3r/GUI/GLToolbar.hpp @@ -24,6 +24,8 @@ 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_UPLOAD_GCODE, SimpleEvent); wxDECLARE_EVENT(EVT_GLTOOLBAR_EXPORT_SLICED_FILE, SimpleEvent); wxDECLARE_EVENT(EVT_GLTOOLBAR_PRINT_SELECT, SimpleEvent); wxDECLARE_EVENT(EVT_GLTOOLBAR_SEND_TO_PRINTER, SimpleEvent); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index c537dc502..01ed936b1 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" @@ -2242,6 +2244,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) @@ -2759,6 +2763,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(); @@ -4530,6 +4535,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 0f1960cfa..e5fcba012 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -13,6 +13,7 @@ #include "slic3r/GUI/WebViewDialog.hpp" #include "slic3r/GUI/HMS.hpp" #include "slic3r/GUI/Jobs/UpgradeNetworkJob.hpp" +#include "../Utils/PrintHost.hpp" #include #include @@ -248,6 +249,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; @@ -427,6 +429,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); @@ -496,6 +499,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 ba2d1912a..f908ed8d4 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) @@ -418,6 +420,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) { @@ -512,6 +518,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ } return;} #endif + if (evt.CmdDown() && evt.GetKeyCode() == 'J') { m_printhost_queue_dlg->Show(); return; } if (evt.CmdDown() && evt.GetKeyCode() == 'N') { m_plater->new_project(); return;} if (evt.CmdDown() && evt.GetKeyCode() == 'O') { m_plater->load_project(); return;} if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'S') { if (m_plater) m_plater->save_project(true); return;} @@ -1143,6 +1150,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_edited_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) @@ -1269,6 +1287,10 @@ 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 == eUploadGcode) + wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_UPLOAD_GCODE)); else if (m_print_select == eExportSlicedFile) wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_EXPORT_SLICED_FILE)); else if (m_print_select == eSendToPrinter) @@ -1324,21 +1346,84 @@ wxBoxSizer* MainFrame::create_side_tools() p->Dismiss(); }); #endif - SideButton* print_plate_btn = new SideButton(p, _L("Send and Print"), ""); - print_plate_btn->SetCornerRadius(0); - SideButton* export_sliced_file_btn = new SideButton(p, _L("Export sliced file"), ""); - export_sliced_file_btn->SetCornerRadius(0); +#if ENABEL_PRINT_ALL + p->append_button(print_all_btn); +#endif + if (wxGetApp().preset_bundle + && !wxGetApp().preset_bundle->printers.get_edited_preset().is_bbl_vendor_preset(wxGetApp().preset_bundle)) { + // ThirdParty Buttons + SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), ""); + export_gcode_btn->SetCornerRadius(0); + export_gcode_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { + m_print_btn->SetLabel(_L("Export G-code file")); + m_print_select = eExportGcode; + if (m_print_enable) + m_print_enable = get_enable_print_status(); + m_print_btn->Enable(m_print_enable); + this->Layout(); + p->Dismiss(); + }); - print_plate_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { + // upload and print + SideButton* send_gcode_btn = new SideButton(p, _L("Send and Print"), ""); + send_gcode_btn->SetCornerRadius(0); + send_gcode_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { + m_print_btn->SetLabel(_L("Send and Print")); + 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(); + }); + + // upload only + SideButton* upload_gcode_btn = new SideButton(p, _L("Send"), ""); + upload_gcode_btn->SetCornerRadius(0); + upload_gcode_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { + m_print_btn->SetLabel(_L("Send")); + m_print_select = eUploadGcode; + 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(); + }); + + p->append_button(send_gcode_btn); + p->append_button(upload_gcode_btn); + p->append_button(export_gcode_btn); + } else { + //Bambu Studio Buttons + SideButton* print_plate_btn = new SideButton(p, _L("Send and Print"), ""); + print_plate_btn->SetCornerRadius(0); + + SideButton* send_to_printer_btn = new SideButton(p, _L("Send"), ""); + send_to_printer_btn->SetCornerRadius(0); + + SideButton* export_sliced_file_btn = new SideButton(p, _L("Export sliced file"), ""); + export_sliced_file_btn->SetCornerRadius(0); + + print_plate_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { m_print_btn->SetLabel(_L("Send and Print")); m_print_select = ePrintPlate; m_print_enable = get_enable_print_status(); m_print_btn->Enable(m_print_enable); this->Layout(); p->Dismiss(); - }); + }); - export_sliced_file_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { + send_to_printer_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { + m_print_btn->SetLabel(_L("Send")); + m_print_select = eSendToPrinter; + if (m_print_enable) + m_print_enable = get_enable_print_status(); + m_print_btn->Enable(m_print_enable); + this->Layout(); + p->Dismiss(); + }); + + export_sliced_file_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { m_print_btn->SetLabel(_L("Export Sliced File")); m_print_select = eExportSlicedFile; if (m_print_enable) @@ -1346,25 +1431,12 @@ wxBoxSizer* MainFrame::create_side_tools() m_print_btn->Enable(m_print_enable); this->Layout(); p->Dismiss(); - }); - - SideButton* send_to_printer_btn = new SideButton(p, _L("Send"), ""); - send_to_printer_btn->SetCornerRadius(0); - send_to_printer_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { - m_print_btn->SetLabel(_L("Send")); - m_print_select = eSendToPrinter; - if (m_print_enable) - m_print_enable = get_enable_print_status(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - p->Dismiss(); - }); -#if ENABEL_PRINT_ALL - p->append_button(print_all_btn); -#endif - p->append_button(print_plate_btn); - p->append_button(export_sliced_file_btn); - p->append_button(send_to_printer_btn); + }); + p->append_button(print_plate_btn); + p->append_button(send_to_printer_btn); + p->append_button(export_sliced_file_btn); + } + p->Popup(m_print_btn); } ); diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 489999846..1a9eb1eb8 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; @@ -111,6 +112,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; @@ -175,6 +177,7 @@ class MainFrame : public DPIFrame eExportGcode = 3, eSendGcode = 4, eSendToPrinter = 5, + eUploadGcode = 6 }; enum SliceSelectType @@ -309,6 +312,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 }; @@ -325,6 +329,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/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 4d8aadd9a..0da971614 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1001,6 +1001,148 @@ 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() { @@ -1614,6 +1756,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 a731c551a..41dd810b2 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 000000000..c1394a72a --- /dev/null +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -0,0 +1,407 @@ +#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->CenterOnParent(); +} + +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, "monitor_signal_strong", _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, "monitor_signal_strong", _(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); + //do not support now + 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, "monitor_signal_strong", _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)); + if (host) { + 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) +{ + wxGetApp().get_tab(Preset::TYPE_PRINTER)->save_preset(); + event.Skip(); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.hpp b/src/slic3r/GUI/PhysicalPrinterDialog.hpp new file mode 100644 index 000000000..e6d2015e6 --- /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 038849545..b31c5cc13 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -125,6 +125,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; @@ -502,12 +505,23 @@ Sidebar::Sidebar(Plater *parent) combo_printer->edit_btn = edit_btn; p->combo_printer = combo_printer; + 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 @@ -893,6 +907,9 @@ void Sidebar::update_all_preset_comboboxes() PresetBundle &preset_bundle = *wxGetApp().preset_bundle; const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); + //only show connection button for not-BBL printer + update_printer_host_icon(); + // Update the print choosers to only contain the compatible presets, update the dirty flags. //BBS @@ -1296,6 +1313,19 @@ void Sidebar::enable_buttons(bool enable) #endif } +void Sidebar::update_printer_host_icon() { + bool is_bbl_vendor_preset = false; + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + if (preset_bundle) { + is_bbl_vendor_preset = preset_bundle->printers.get_edited_preset().is_bbl_vendor_preset(preset_bundle); + } + if (is_bbl_vendor_preset) { + connection_btn->Hide(); + }else{ + connection_btn->Show(); + } +} + bool Sidebar::show_reslice(bool show) const { return p->btn_reslice->Show(show); } bool Sidebar::show_export(bool show) const { return p->btn_export_gcode->Show(show); } bool Sidebar::show_send(bool show) const { return p->btn_send_gcode->Show(show); } @@ -1779,6 +1809,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(); @@ -1835,6 +1867,8 @@ 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_upload_gcode(SimpleEvent&); void on_action_export_sliced_file(SimpleEvent&); void on_action_select_sliced_plate(wxCommandEvent& evt); @@ -2223,6 +2257,8 @@ 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_UPLOAD_GCODE, &priv::on_action_upload_gcode, this); q->Bind(EVT_GLTOOLBAR_EXPORT_SLICED_FILE, &priv::on_action_export_sliced_file, this); q->Bind(EVT_GLTOOLBAR_SEND_TO_PRINTER, &priv::on_action_export_to_sdcard, this); q->Bind(EVT_GLCANVAS_PLATE_SELECT, &priv::on_plate_selected, this); @@ -4152,7 +4188,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; @@ -5639,6 +5706,22 @@ void Plater::priv::on_action_export_gcode(SimpleEvent&) } } +void Plater::priv::on_action_upload_gcode(SimpleEvent&) +{ + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export gcode event\n"; + q->send_gcode_legacy(-1, nullptr, true); + } +} + +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(-1, nullptr, false); + } +} + void Plater::priv::on_action_export_sliced_file(SimpleEvent&) { if (q != nullptr) { @@ -8919,7 +9002,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, bool upload_only) +{ + // 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, upload_only); + 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 04f436fa9..ab1e7e571 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -120,6 +120,7 @@ public: void on_bed_type_change(BedType bed_type); void load_ams_list(std::map const &list); void sync_ams_list(); + void update_printer_host_icon(); ObjectList* obj_list(); ObjectSettings* obj_settings(); @@ -161,6 +162,7 @@ private: wxBoxSizer* m_scrolled_sizer = nullptr; ComboBox* m_bed_type_list = nullptr; + ScalableButton* connection_btn = nullptr; }; class Plater: public wxPanel @@ -332,6 +334,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, bool upload_only = false); 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 000000000..2a1d6d2a2 --- /dev/null +++ b/src/slic3r/GUI/PrintHostDialogs.cpp @@ -0,0 +1,532 @@ +#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, bool upload_only) + : MsgDialog(static_cast(wxGetApp().mainframe), _L("Upload and Print"), _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, FromDIP(10)); + content_sizer->Add(FromDIP(10), FromDIP(10), 0, 0); + content_sizer->Add(label_dir_hint, 0, 0, FromDIP(10)); + 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_confirm = add_button(wxID_YES, false, _L("Confirm")); + btn_confirm->Bind(wxEVT_BUTTON, [this, upload_only, validate_path](wxCommandEvent&) { + if (validate_path(txt_filename->GetValue())) { + if (upload_only) { + post_upload_action = PrintHostPostUploadAction::None; + } else { + post_upload_action = PrintHostPostUploadAction::StartPrint; + } + EndDialog(wxID_OK); + } + }); + + /*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) && !upload_only) { + 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 000000000..83bf6a966 --- /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, bool upload_only); + 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 d430460ab..3d3daaa87 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3381,17 +3381,7 @@ void TabPrinter::toggle_options() //BBS: whether the preset is Bambu Lab printer bool is_BBL_printer = false; if (m_preset_bundle) { - std::string vendor_name; - for (auto vendor_profile : m_preset_bundle->vendors) { - for (auto vendor_model : vendor_profile.second.models) - if (vendor_model.name == m_config->opt_string("printer_model")) - { - vendor_name = vendor_profile.first; - break; - } - } - if (!vendor_name.empty()) - is_BBL_printer = (vendor_name.compare("BBL") == 0); + is_BBL_printer = m_preset_bundle->printers.get_edited_preset().is_bbl_vendor_preset(m_preset_bundle); } bool have_multiple_extruders = m_extruders_count > 1; diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp new file mode 100644 index 000000000..5116795c5 --- /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 000000000..262efe9ff --- /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 000000000..aa50b1b1c --- /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 000000000..dd22e60b7 --- /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