diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 54bb06c705..e2074bd217 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -74,6 +74,7 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrinterTechnology) static t_config_enum_values s_keys_map_PrintHostType { { "prusalink", htPrusaLink }, + { "prusaconnect", htPrusaConnect }, { "octoprint", htOctoPrint }, { "duet", htDuet }, { "flashair", htFlashAir }, @@ -2403,6 +2404,7 @@ void PrintConfigDef::init_fff_params() "the kind of the host."); def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values.push_back("prusalink"); + def->enum_values.push_back("prusaconnect"); def->enum_values.push_back("octoprint"); def->enum_values.push_back("duet"); def->enum_values.push_back("flashair"); @@ -2410,6 +2412,7 @@ void PrintConfigDef::init_fff_params() def->enum_values.push_back("repetier"); def->enum_values.push_back("mks"); def->enum_labels.push_back("PrusaLink"); + def->enum_labels.push_back("PrusaConnect"); def->enum_labels.push_back("Octo/Klipper"); def->enum_labels.push_back("Duet"); def->enum_labels.push_back("FlashAir"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 9bd397b278..9c1d30d81f 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -44,7 +44,7 @@ enum class FuzzySkinType { }; enum PrintHostType { - htPrusaLink, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS + htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS }; enum AuthorizationType { diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 25cb64512d..ec8e583046 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -10966,6 +10966,7 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) upload_job.printhost->get_groups(groups); } + // orca merge todo PrintHostSendDialog dlg(default_output_file, upload_job.printhost->get_post_upload_actions(), groups); if (dlg.ShowModal() == wxID_OK) { upload_job.upload_data.upload_path = dlg.filename(); diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp index 1f89f467ce..ec93b70162 100644 --- a/src/slic3r/GUI/PrintHostDialogs.cpp +++ b/src/slic3r/GUI/PrintHostDialogs.cpp @@ -27,6 +27,7 @@ #include "libslic3r/AppConfig.hpp" #include "NotificationManager.hpp" #include "ExtraRenderers.hpp" +#include "format.hpp" namespace fs = boost::filesystem; @@ -35,11 +36,14 @@ namespace GUI { static const char *CONFIG_KEY_PATH = "printhost_path"; static const char *CONFIG_KEY_GROUP = "printhost_group"; +static const char* CONFIG_KEY_STORAGE = "printhost_storage"; -PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUploadActions post_actions, const wxArrayString &groups) - : MsgDialog(static_cast(wxGetApp().mainframe), _L("Send to print"), _L("Upload to Printer Host with the following filename:"),0) +PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUploadActions post_actions, const wxArrayString &groups, const wxArrayString& storage) + : MsgDialog(static_cast(wxGetApp().mainframe), _L("Send G-Code to printer host"), _L("Upload to Printer Host with the following filename:"), 0) // Set style = 0 to avoid default creation of the "OK" button. + // All buttons will be added later in this constructor , txt_filename(new wxTextCtrl(this, wxID_ANY)) , combo_groups(!groups.IsEmpty() ? new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, groups, wxCB_READONLY) : nullptr) + , combo_storage(storage.GetCount() > 1 ? new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, storage, wxCB_READONLY) : nullptr) , post_upload_action(PrintHostPostUploadAction::None) { #ifdef __APPLE__ @@ -50,9 +54,8 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo 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 | wxALL, FromDIP(10)); - content_sizer->Add(FromDIP(10), FromDIP(10), 0, 0); - content_sizer->Add(label_dir_hint, 0, 0, FromDIP(10)); + content_sizer->Add(txt_filename, 0, wxEXPAND); + content_sizer->Add(label_dir_hint); content_sizer->AddSpacer(VERT_SPACING); if (combo_groups != nullptr) { @@ -65,6 +68,23 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo combo_groups->SetValue(recent_group); } + if (combo_storage != nullptr) { + // PrusaLink specific: User needs to choose a storage + auto* label_group = new wxStaticText(this, wxID_ANY, _L("Upload to storage:")); + content_sizer->Add(label_group); + content_sizer->Add(combo_storage, 0, wxBOTTOM, 2 * VERT_SPACING); + combo_storage->SetValue(storage.front()); + wxString recent_storage = from_u8(app_config->get("recent", CONFIG_KEY_STORAGE)); + if (!recent_storage.empty()) + combo_storage->SetValue(recent_storage); + } else if (storage.GetCount() == 1){ + // PrusaLink specific: Show which storage has been detected. + auto* label_group = new wxStaticText(this, wxID_ANY, _L("Upload to storage: ") + storage.front()); + content_sizer->Add(label_group); + m_preselected_storage = storage.front(); + } + + 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 += '/'; @@ -75,9 +95,9 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo 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('.')); + + if (size_t extension_start = recent_path.find_last_of('.'); extension_start != std::string::npos) + m_valid_suffix = recent_path.substr(extension_start); // .gcode suffix control auto validate_path = [this](const wxString &path) -> bool { if (! path.Lower().EndsWith(m_valid_suffix.Lower())) { @@ -88,13 +108,24 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo return true; }; - auto* btn_upload = add_button(wxID_YES, false, _L("Upload")); - btn_upload->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) { + auto* btn_ok = add_button(wxID_OK, true, _L("Upload")); + btn_ok->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) { if (validate_path(txt_filename->GetValue())) { post_upload_action = PrintHostPostUploadAction::None; EndDialog(wxID_OK); } }); + txt_filename->SetFocus(); + + if (post_actions.has(PrintHostPostUploadAction::QueuePrint)) { + auto* btn_print = add_button(wxID_ADD, false, _L("Upload to Queue")); + btn_print->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) { + if (validate_path(txt_filename->GetValue())) { + post_upload_action = PrintHostPostUploadAction::QueuePrint; + EndDialog(wxID_OK); + } + }); + } if (post_actions.has(PrintHostPostUploadAction::StartPrint)) { auto* btn_print = add_button(wxID_YES, false, _L("Upload and Print")); @@ -108,7 +139,7 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo 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("Simulate")); + 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; @@ -117,7 +148,7 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo }); } - add_button(wxID_CANCEL,false, L("Cancel")); + add_button(wxID_CANCEL); finalize(); #ifdef __linux__ @@ -135,6 +166,7 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo // Another similar case where the function only works with EVT_SHOW + CallAfter, // this time on Mac. CallAfter([=]() { + txt_filename->SetInsertionPoint(0); txt_filename->SetSelection(recent_path_len, recent_path_len + stem_len); }); }); @@ -160,6 +192,13 @@ std::string PrintHostSendDialog::group() const } } +std::string PrintHostSendDialog::storage() const +{ + if (!combo_storage) + return GUI::format("%1%", m_preselected_storage); + return boost::nowide::narrow(combo_storage->GetValue()); +} + void PrintHostSendDialog::EndModal(int ret) { if (ret == wxID_OK) { @@ -178,6 +217,10 @@ void PrintHostSendDialog::EndModal(int ret) wxString group = combo_groups->GetValue(); app_config->set("recent", CONFIG_KEY_GROUP, into_u8(group)); } + if (combo_storage != nullptr) { + wxString storage = combo_storage->GetValue(); + app_config->set("recent", CONFIG_KEY_STORAGE, into_u8(storage)); + } } MsgDialog::EndModal(ret); @@ -188,6 +231,7 @@ void PrintHostSendDialog::EndModal(int ret) wxDEFINE_EVENT(EVT_PRINTHOST_PROGRESS, PrintHostQueueDialog::Event); wxDEFINE_EVENT(EVT_PRINTHOST_ERROR, PrintHostQueueDialog::Event); wxDEFINE_EVENT(EVT_PRINTHOST_CANCEL, PrintHostQueueDialog::Event); +wxDEFINE_EVENT(EVT_PRINTHOST_INFO, PrintHostQueueDialog::Event); PrintHostQueueDialog::Event::Event(wxEventType eventType, int winid, size_t job_id) : wxEvent(winid, eventType) @@ -203,7 +247,14 @@ PrintHostQueueDialog::Event::Event(wxEventType eventType, int winid, size_t job_ PrintHostQueueDialog::Event::Event(wxEventType eventType, int winid, size_t job_id, wxString error) : wxEvent(winid, eventType) , job_id(job_id) - , error(std::move(error)) + , status(std::move(error)) +{} + +PrintHostQueueDialog::Event::Event(wxEventType eventType, int winid, size_t job_id, wxString tag, wxString status) + : wxEvent(winid, eventType) + , job_id(job_id) + , tag(std::move(tag)) + , status(std::move(status)) {} wxEvent *PrintHostQueueDialog::Event::Clone() const @@ -216,16 +267,17 @@ PrintHostQueueDialog::PrintHostQueueDialog(wxWindow *parent) , 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) + , on_info_evt(this, EVT_PRINTHOST_INFO, &PrintHostQueueDialog::on_info, this) { const auto em = GetTextExtent("m").x; auto *topsizer = new wxBoxSizer(wxVERTICAL); std::vector widths; - widths.reserve(6); + widths.reserve(7); if (!load_user_data(UDT_COLS, widths)) { widths.clear(); - for (size_t i = 0; i < 6; i++) + for (size_t i = 0; i < 7; i++) widths.push_back(-1); } @@ -246,9 +298,10 @@ PrintHostQueueDialog::PrintHostQueueDialog(wxWindow *parent) 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(_CTX(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); + append_text_column(_L("Message"), widths[6]); + //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")); @@ -325,7 +378,7 @@ void PrintHostQueueDialog::append_job(const PrintHostJob &job) } 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(from_path(job.upload_data.upload_path))); fields.push_back(wxVariant("")); job_list->AppendItem(fields, static_cast(ST_NEW)); // Both strings are UTF-8 encoded. @@ -421,7 +474,7 @@ void PrintHostQueueDialog::on_error(Event &evt) 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()); + auto errormsg = from_u8((boost::format("%1%\n%2%") % _utf8(L("Error uploading to print host:")) % std::string(evt.status.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 @@ -450,6 +503,29 @@ void PrintHostQueueDialog::on_cancel(Event &evt) wxGetApp().notification_manager()->upload_job_notification_show_canceled(evt.job_id + 1, boost::nowide::narrow(nm.GetString()), boost::nowide::narrow(hst.GetString())); } +void PrintHostQueueDialog::on_info(Event& evt) +{ + wxCHECK_RET(evt.job_id < (size_t)job_list->GetItemCount(), "Out of bounds access to job list"); + + if (evt.tag == L"resolve") { + wxVariant hst(evt.status); + job_list->SetValue(hst, evt.job_id, COL_HOST); + wxGetApp().notification_manager()->set_upload_job_notification_host(evt.job_id + 1, boost::nowide::narrow(evt.status)); + } else if (evt.tag == L"complete") { + wxVariant hst(evt.status); + job_list->SetValue(hst, evt.job_id, COL_ERRORMSG); + wxGetApp().notification_manager()->set_upload_job_notification_completed(evt.job_id + 1); + wxGetApp().notification_manager()->set_upload_job_notification_status(evt.job_id + 1, boost::nowide::narrow(evt.status)); + } else if(evt.tag == L"complete_with_warning"){ + wxVariant hst(evt.status); + job_list->SetValue(hst, evt.job_id, COL_ERRORMSG); + wxGetApp().notification_manager()->set_upload_job_notification_completed_with_warning(evt.job_id + 1); + wxGetApp().notification_manager()->set_upload_job_notification_status(evt.job_id + 1, boost::nowide::narrow(evt.status)); + } else if (evt.tag == L"set_complete_off") { + wxGetApp().notification_manager()->set_upload_job_notification_comp_on_100(evt.job_id + 1, false); + } +} + void PrintHostQueueDialog::get_active_jobs(std::vector>& ret) { int ic = job_list->GetItemCount(); @@ -461,7 +537,6 @@ void PrintHostQueueDialog::get_active_jobs(std::vectordata } void PrintHostQueueDialog::save_user_data(int udt) { @@ -510,7 +585,7 @@ bool PrintHostQueueDialog::load_user_data(int udt, std::vector& vector) } if (udt & UserDataType::UDT_COLS) { - for (size_t i = 0; i < 6; i++) + for (size_t i = 0; i < 7; i++) { if (!hasget("print_host_queue_dialog_column_" + std::to_string(i), vector)) return false; diff --git a/src/slic3r/GUI/PrintHostDialogs.hpp b/src/slic3r/GUI/PrintHostDialogs.hpp index ff3eb60125..80e2a0f485 100644 --- a/src/slic3r/GUI/PrintHostDialogs.hpp +++ b/src/slic3r/GUI/PrintHostDialogs.hpp @@ -26,17 +26,20 @@ namespace GUI { class PrintHostSendDialog : public GUI::MsgDialog { public: - PrintHostSendDialog(const boost::filesystem::path &path, PrintHostPostUploadActions post_actions, const wxArrayString& groups); + PrintHostSendDialog(const boost::filesystem::path &path, PrintHostPostUploadActions post_actions, const wxArrayString& groups, const wxArrayString& storage); boost::filesystem::path filename() const; PrintHostPostUploadAction post_action() const; std::string group() const; + std::string storage() const; virtual void EndModal(int ret) override; private: wxTextCtrl *txt_filename; wxComboBox *combo_groups; + wxComboBox* combo_storage; PrintHostPostUploadAction post_upload_action; wxString m_valid_suffix; + wxString m_preselected_storage; }; @@ -48,11 +51,13 @@ public: public: size_t job_id; int progress = 0; // in percent - wxString error; + wxString tag; + wxString status; 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); + Event(wxEventType eventType, int winid, size_t job_id, wxString tag, wxString status); virtual wxEvent *Clone() const; }; @@ -108,6 +113,7 @@ private: EventGuard on_progress_evt; EventGuard on_error_evt; EventGuard on_cancel_evt; + EventGuard on_info_evt; JobState get_state(int idx); void set_state(int idx, JobState); @@ -115,6 +121,7 @@ private: void on_progress(Event&); void on_error(Event&); void on_cancel(Event&); + void on_info(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); @@ -124,7 +131,7 @@ private: wxDECLARE_EVENT(EVT_PRINTHOST_PROGRESS, PrintHostQueueDialog::Event); wxDECLARE_EVENT(EVT_PRINTHOST_ERROR, PrintHostQueueDialog::Event); wxDECLARE_EVENT(EVT_PRINTHOST_CANCEL, PrintHostQueueDialog::Event); - +wxDECLARE_EVENT(EVT_PRINTHOST_INFO, PrintHostQueueDialog::Event); }} #endif diff --git a/src/slic3r/Utils/AstroBox.cpp b/src/slic3r/Utils/AstroBox.cpp index 8781549a20..a2b5bca04d 100644 --- a/src/slic3r/Utils/AstroBox.cpp +++ b/src/slic3r/Utils/AstroBox.cpp @@ -92,7 +92,7 @@ wxString AstroBox::get_test_failed_msg (wxString &msg) const % _utf8(L("Note: AstroBox version at least 1.1.0 is required."))).str()); } -bool AstroBox::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const +bool AstroBox::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const { const char *name = get_name(); diff --git a/src/slic3r/Utils/AstroBox.hpp b/src/slic3r/Utils/AstroBox.hpp index 15a8863a90..72ab273228 100644 --- a/src/slic3r/Utils/AstroBox.hpp +++ b/src/slic3r/Utils/AstroBox.hpp @@ -23,7 +23,7 @@ public: 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 upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_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; } diff --git a/src/slic3r/Utils/Bonjour.cpp b/src/slic3r/Utils/Bonjour.cpp index f121e6e87a..e357083816 100644 --- a/src/slic3r/Utils/Bonjour.cpp +++ b/src/slic3r/Utils/Bonjour.cpp @@ -7,12 +7,11 @@ #include #include #include -#include -#include #include -#include #include #include +#include +#include using boost::optional; using boost::system::error_code; @@ -238,6 +237,7 @@ struct DnsRR_A enum { TAG = 0x1 }; asio::ip::address_v4 ip; + std::string name; static void decode(optional &result, const DnsResource &rr) { @@ -255,6 +255,7 @@ struct DnsRR_AAAA enum { TAG = 0x1c }; asio::ip::address_v6 ip; + std::string name; static void decode(optional &result, const DnsResource &rr) { @@ -402,7 +403,7 @@ struct DnsMessage DnsSDMap sdmap; - static optional decode(const std::vector &buffer, const Bonjour::TxtKeys &txt_keys) + static optional decode(const std::vector& buffer, const Bonjour::TxtKeys& txt_keys) { const auto size = buffer.size(); if (size < DnsHeader::SIZE + DnsQuestion::MIN_SIZE || size > MAX_SIZE) { @@ -426,30 +427,36 @@ struct DnsMessage auto rr = DnsResource::decode(buffer, offset, dataoffset); if (!rr) { return boost::none; - } else { + } + else { res.parse_rr(buffer, std::move(*rr), dataoffset, txt_keys); } } return std::move(res); } - private: - void parse_rr(const std::vector &buffer, DnsResource &&rr, size_t dataoffset, const Bonjour::TxtKeys &txt_keys) + void parse_rr(const std::vector& buffer, DnsResource&& rr, size_t dataoffset, const Bonjour::TxtKeys& txt_keys) { switch (rr.type) { - case DnsRR_A::TAG: DnsRR_A::decode(this->rr_a, rr); break; - case DnsRR_AAAA::TAG: DnsRR_AAAA::decode(this->rr_aaaa, rr); break; - case DnsRR_SRV::TAG: { - auto srv = DnsRR_SRV::decode(buffer, rr, dataoffset); - if (srv) { this->sdmap.insert_srv(std::move(rr.name), std::move(*srv)); } - break; - } - case DnsRR_TXT::TAG: { - auto txt = DnsRR_TXT::decode(rr, txt_keys); - if (txt) { this->sdmap.insert_txt(std::move(rr.name), std::move(*txt)); } - break; - } + case DnsRR_A::TAG: + DnsRR_A::decode(this->rr_a, rr); + this->rr_a->name = rr.name; + break; + case DnsRR_AAAA::TAG: + DnsRR_AAAA::decode(this->rr_aaaa, rr); + this->rr_aaaa->name = rr.name; + break; + case DnsRR_SRV::TAG: { + auto srv = DnsRR_SRV::decode(buffer, rr, dataoffset); + if (srv) { this->sdmap.insert_srv(std::move(rr.name), std::move(*srv)); } + break; + } + case DnsRR_TXT::TAG: { + auto txt = DnsRR_TXT::decode(rr, txt_keys); + if (txt) { this->sdmap.insert_txt(std::move(rr.name), std::move(*txt)); } + break; + } } } }; @@ -480,24 +487,11 @@ std::ostream& operator<<(std::ostream &os, const DnsMessage &msg) return os << "])"; } - -struct BonjourRequest -{ - static const asio::ip::address_v4 MCAST_IP4; - static const uint16_t MCAST_PORT; - - std::vector data; - - static optional make(const std::string &service, const std::string &protocol); - -private: - BonjourRequest(std::vector &&data) : data(std::move(data)) {} -}; - -const asio::ip::address_v4 BonjourRequest::MCAST_IP4{0xe00000fb}; +const asio::ip::address_v4 BonjourRequest::MCAST_IP4{ 0xe00000fb }; +const asio::ip::address_v6 BonjourRequest::MCAST_IP6 = asio::ip::make_address_v6("ff02::fb"); const uint16_t BonjourRequest::MCAST_PORT = 5353; -optional BonjourRequest::make(const std::string &service, const std::string &protocol) +optional BonjourRequest::make_PTR(const std::string &service, const std::string &protocol) { if (service.size() > 15 || protocol.size() > 15) { return boost::none; @@ -535,73 +529,227 @@ optional BonjourRequest::make(const std::string &service, const return BonjourRequest(std::move(data)); } - -// API - private part - -struct Bonjour::priv +optional BonjourRequest::make_A(const std::string& hostname) { - const std::string service; - std::string protocol; - std::string service_dn; - TxtKeys txt_keys; - unsigned timeout; - unsigned retries; + // todo: why is this and what is real max + if (hostname.size() > 30) { + return boost::none; + } - std::vector buffer; - std::thread io_thread; - Bonjour::ReplyFn replyfn; - Bonjour::CompleteFn completefn; + std::vector data; + data.reserve(hostname.size() + 18); - priv(std::string &&service); + // Add metadata + static const unsigned char rq_meta[] = { + 0x00, 0x00, // Query ID (zero for mDNS) + 0x00, 0x00, // Flags + 0x00, 0x01, // One query + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // Zero Answer, Authority, and Additional RRs + }; + std::copy(rq_meta, rq_meta + sizeof(rq_meta), std::back_inserter(data)); - std::string strip_service_dn(const std::string &service_name) const; - void udp_receive(udp::endpoint from, size_t bytes); - void lookup_perform(); -}; + // Add hostname without .local + data.push_back(hostname.size()); + data.insert(data.end(), hostname.begin(), hostname.end()); + + // Add the rest of A record + static const unsigned char ptr_tail[] = { + 0x05, // length of "local" + 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x00,// "local" string and terminator + 0x00, 0x01, // Type A + 0x00, 0xff, // Class - 01 is internet 0xff is any + }; + std::copy(ptr_tail, ptr_tail + sizeof(ptr_tail), std::back_inserter(data)); -Bonjour::priv::priv(std::string &&service) - : service(std::move(service)) - , protocol("tcp") - , timeout(10) - , retries(1) -{ - buffer.resize(DnsMessage::MAX_SIZE); + return BonjourRequest(std::move(data)); } -std::string Bonjour::priv::strip_service_dn(const std::string &service_name) const +optional BonjourRequest::make_AAAA(const std::string& hostname) +{ + // todo: why is this and what is real max + if (hostname.size() > 30) { + return boost::none; + } + + std::vector data; + data.reserve(hostname.size() + 18); + + // Add metadata + static const unsigned char rq_meta[] = { + 0x00, 0x00, // Query ID (zero for mDNS) + 0x00, 0x00, // Flags + 0x00, 0x01, // One query + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // Zero Answer, Authority, and Additional RRs + }; + std::copy(rq_meta, rq_meta + sizeof(rq_meta), std::back_inserter(data)); + + // Add hostname without .local + data.push_back(hostname.size()); + data.insert(data.end(), hostname.begin(), hostname.end()); + + // Add the rest of A record + static const unsigned char ptr_tail[] = { + 0x05, // length of "local" + 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x00, // "local" string and terminator + 0x00, 0x1c, // Type AAAA + 0x00, 0xff, // Class - 01 is internet 0xff is any + }; + std::copy(ptr_tail, ptr_tail + sizeof(ptr_tail), std::back_inserter(data)); + + return BonjourRequest(std::move(data)); +} + +namespace { +std::string strip_service_dn(const std::string& service_name, const std::string& service_dn) { if (service_name.size() <= service_dn.size()) { - return service_name; + return std::string(); } auto needle = service_name.rfind(service_dn); if (needle == service_name.size() - service_dn.size()) { return service_name.substr(0, needle - 1); } else { - return service_name; + return std::string(); + } +} +} // namespace + +UdpSession::UdpSession(Bonjour::ReplyFn rfn) : replyfn(rfn) +{ + buffer.resize(DnsMessage::MAX_SIZE); +} + +UdpSocket::UdpSocket( Bonjour::ReplyFn replyfn, const asio::ip::address& multicast_address, const asio::ip::address& interface_address, std::shared_ptr< boost::asio::io_service > io_service) + : replyfn(replyfn) + , multicast_address(multicast_address) + , socket(*io_service) + , io_service(io_service) +{ + try { + // open socket + boost::asio::ip::udp::endpoint listen_endpoint(multicast_address.is_v4() ? udp::v4() : udp::v6(), BonjourRequest::MCAST_PORT); + socket.open(listen_endpoint.protocol()); + // set socket to listen + socket.set_option(udp::socket::reuse_address(true)); + socket.bind(listen_endpoint); + if (interface_address.is_v4()) { + // listen for multicast on given interface + socket.set_option(boost::asio::ip::multicast::join_group(multicast_address.to_v4(), interface_address.to_v4())); + // send to interface + socket.set_option(asio::ip::multicast::outbound_interface(interface_address.to_v4())); + } else { + // listen for multicast on given interface + socket.set_option(boost::asio::ip::multicast::join_group(multicast_address.to_v6(), interface_address.to_v6().scope_id())); + // send to interface + socket.set_option(asio::ip::multicast::outbound_interface(interface_address.to_v6().scope_id())); + } + mcast_endpoint = udp::endpoint(multicast_address, BonjourRequest::MCAST_PORT); + + BOOST_LOG_TRIVIAL(info) << "Socket created. Multicast: " << multicast_address << ". Interface: " << interface_address; + } + catch (std::exception& e) { + BOOST_LOG_TRIVIAL(error) << e.what(); } } -void Bonjour::priv::udp_receive(udp::endpoint from, size_t bytes) + +UdpSocket::UdpSocket( Bonjour::ReplyFn replyfn, const asio::ip::address& multicast_address, std::shared_ptr< boost::asio::io_service > io_service) + : replyfn(replyfn) + , multicast_address(multicast_address) + , socket(*io_service) + , io_service(io_service) { + try { + // open socket + boost::asio::ip::udp::endpoint listen_endpoint(multicast_address.is_v4() ? udp::v4() : udp::v6(), BonjourRequest::MCAST_PORT); + socket.open(listen_endpoint.protocol()); + // set socket to listen + socket.set_option(udp::socket::reuse_address(true)); + socket.bind(listen_endpoint); + socket.set_option(boost::asio::ip::multicast::join_group(multicast_address)); + mcast_endpoint = udp::endpoint(multicast_address, BonjourRequest::MCAST_PORT); + + BOOST_LOG_TRIVIAL(info) << "Socket created. Multicast: " << multicast_address; + } + catch (std::exception& e) { + BOOST_LOG_TRIVIAL(error) << e.what(); + } +} + +void UdpSocket::send() +{ + try { + for (const auto& request : requests) + socket.send_to(asio::buffer(request.m_data), mcast_endpoint); + + // Should we care if this is called while already receiving? (async_receive call from receive_handler) + async_receive(); + } + catch (std::exception& e) { + BOOST_LOG_TRIVIAL(error) << e.what(); + } +} + +void UdpSocket::async_receive() +{ + try { + // our session to hold the buffer + endpoint + auto session = create_session(); + socket.async_receive_from(asio::buffer(session->buffer, session->buffer.size()) + , session->remote_endpoint + , boost::bind(&UdpSocket::receive_handler, this, session, asio::placeholders::error, asio::placeholders::bytes_transferred)); + } + catch (std::exception& e) { + BOOST_LOG_TRIVIAL(error) << e.what(); + } +} + +void UdpSocket::receive_handler(SharedSession session, const boost::system::error_code& error, size_t bytes) +{ + // let io_service to handle the datagram on session + // from boost documentation io_service::post: + // The io_service guarantees that the handler will only be called in a thread in which the run(), run_one(), poll() or poll_one() member functions is currently being invoked. + io_service->post(boost::bind(&UdpSession::handle_receive, session, error, bytes)); + // immediately accept new datagrams + async_receive(); +} + +SharedSession LookupSocket::create_session() const +{ + return std::shared_ptr< LookupSession >(new LookupSession(this, replyfn)); +} + + +void LookupSession::handle_receive(const error_code& error, size_t bytes) +{ + assert(socket); + + if (error) { + BOOST_LOG_TRIVIAL(error) << error.message(); + return; + } if (bytes == 0 || !replyfn) { return; } buffer.resize(bytes); - auto dns_msg = DnsMessage::decode(buffer, txt_keys); + auto dns_msg = DnsMessage::decode(buffer, socket->get_txt_keys()); if (dns_msg) { - asio::ip::address ip = from.address(); + asio::ip::address ip = remote_endpoint.address(); if (dns_msg->rr_a) { ip = dns_msg->rr_a->ip; } else if (dns_msg->rr_aaaa) { ip = dns_msg->rr_aaaa->ip; } - for (auto &sdpair : dns_msg->sdmap) { - if (! sdpair.second.srv) { + for (auto& sdpair : dns_msg->sdmap) { + if (!sdpair.second.srv) { continue; } - const auto &srv = *sdpair.second.srv; - auto service_name = strip_service_dn(sdpair.first); + const auto& srv = *sdpair.second.srv; + + auto service_name = strip_service_dn(sdpair.first, socket->get_service_dn()); + if (service_name.empty()) + continue; std::string path; std::string version; @@ -617,64 +765,282 @@ void Bonjour::priv::udp_receive(udp::endpoint from, size_t bytes) } } +SharedSession ResolveSocket::create_session() const +{ + return std::shared_ptr< ResolveSession > (new ResolveSession(this, replyfn)); +} + + +void ResolveSession::handle_receive(const error_code& error, size_t bytes) +{ + assert(socket); + if (error) { + // todo: what level? do we even log? There might be callbacks when timer runs out + BOOST_LOG_TRIVIAL(info) << error.message(); + return; + } + if (bytes == 0 || !replyfn) { + // todo: log something? + return; + } + + buffer.resize(bytes); +#if 0 + std::string str; + char const hex_chars[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + for (size_t i = 0; i < buffer.size(); i++) { + const char ch = buffer[i]; + str += hex_chars[(ch & 0xF0) >> 4]; + str += hex_chars[(ch & 0x0F) >> 0]; + } + BOOST_LOG_TRIVIAL(debug) << remote_endpoint.address()<< " " << str; +#endif + // decode buffer, txt keys are not needed for A / AAAA answer + auto dns_msg = DnsMessage::decode(buffer, Bonjour::TxtKeys()); + if (dns_msg) { + asio::ip::address ip; + std::string answer_name; + if (dns_msg->rr_a) { + ip = dns_msg->rr_a->ip; + answer_name = dns_msg->rr_a->name; + } + else if (dns_msg->rr_aaaa) { + ip = dns_msg->rr_aaaa->ip; + answer_name = dns_msg->rr_aaaa->name; + } + else + return; // not matching query type with answer type + + if (!answer_name.empty()) { + // transform both strings to lower. Should we really do it? + std::string name_tolower = answer_name; + std::transform(name_tolower.begin(), name_tolower.end(), name_tolower.begin(), + [](unsigned char c) { return std::tolower(c); }); + std::string hostname_tolower = socket->get_hostname(); + std::transform(hostname_tolower.begin(), hostname_tolower.end(), hostname_tolower.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (name_tolower == hostname_tolower) { + BonjourReply reply(ip, 0, std::string(), answer_name, BonjourReply::TxtData()); + replyfn(std::move(reply)); + } + } + } +} + +// API - private part + +struct Bonjour::priv +{ + const std::string service; + std::string protocol; + std::string service_dn; + TxtKeys txt_keys; + unsigned timeout; + unsigned retries; + std::string hostname; + +// std::vector replies; + + std::vector buffer; + std::thread io_thread; + Bonjour::ReplyFn replyfn; + Bonjour::CompleteFn completefn; + Bonjour::ResolveFn resolvefn; + + priv(std::string&& service); + + // void udp_receive_lookup(udp::endpoint from, size_t bytes); + void lookup_perform(); + void resolve_perform(); +}; + +Bonjour::priv::priv(std::string&& service) + : service(std::move(service)) + , protocol("tcp") + , timeout(10) + , retries(1) +{ + buffer.resize(DnsMessage::MAX_SIZE); +} + void Bonjour::priv::lookup_perform() { service_dn = (boost::format("_%1%._%2%.local") % service % protocol).str(); - const auto brq = BonjourRequest::make(service, protocol); - if (!brq) { - return; + std::shared_ptr< boost::asio::io_service > io_service(new boost::asio::io_service); + + std::vector sockets; + + // resolve intefaces - from PR#6646 + std::vector interfaces; + asio::ip::udp::resolver resolver(*io_service); + boost::system::error_code ec; + // ipv4 interfaces + auto results = resolver.resolve(udp::v4(), asio::ip::host_name(), "", ec); + if (!ec) { + for (const auto & r : results) { + const auto addr = r.endpoint().address(); + if (addr.is_loopback()) continue; + interfaces.emplace_back(std::move(addr)); + } + // create ipv4 socket for each interface + // each will send to querry to for both ipv4 and ipv6 + for (const auto& intrfc : interfaces) + sockets.emplace_back(new LookupSocket(txt_keys, service, service_dn, protocol, replyfn, BonjourRequest::MCAST_IP4, intrfc, io_service)); + } else { + BOOST_LOG_TRIVIAL(info) << "Failed to resolve ipv4 interfaces: " << ec.message(); } - - auto self = this; - + if (sockets.empty()) + sockets.emplace_back(new LookupSocket(txt_keys, service, service_dn, protocol, replyfn, BonjourRequest::MCAST_IP4, io_service)); + // ipv6 interfaces + interfaces.clear(); + //udp::resolver::query query(host, PORT, boost::asio::ip::resolver_query_base::numeric_service); + results = resolver.resolve(udp::v6(), asio::ip::host_name(), "", ec); + if (!ec) + { + for (const auto& r : results) { + const auto addr = r.endpoint().address(); + if (addr.is_loopback()) continue; + interfaces.emplace_back(std::move(addr)); + } + // create ipv6 socket for each interface + // each will send to querry to for both ipv4 and ipv6 + for (const auto& intrfc : interfaces) + sockets.emplace_back(new LookupSocket(txt_keys, service, service_dn, protocol, replyfn, BonjourRequest::MCAST_IP6, intrfc, io_service)); + if (interfaces.empty()) + sockets.emplace_back(new LookupSocket(txt_keys, service, service_dn, protocol, replyfn, BonjourRequest::MCAST_IP6, io_service)); + } else { + BOOST_LOG_TRIVIAL(info)<< "Failed to resolve ipv6 interfaces: " << ec.message(); + } + try { - boost::asio::io_service io_service; - udp::socket socket(io_service); - socket.open(udp::v4()); - socket.set_option(udp::socket::reuse_address(true)); - udp::endpoint mcast(BonjourRequest::MCAST_IP4, BonjourRequest::MCAST_PORT); - socket.send_to(asio::buffer(brq->data), mcast); + // send first queries + for (auto * socket : sockets) + socket->send(); - bool expired = false; - bool retry = false; - asio::deadline_timer timer(io_service); + // timer settings + asio::deadline_timer timer(*io_service); retries--; - std::function timer_handler = [&](const error_code &error) { + std::function timer_handler = [&](const error_code& error) { + // end if (retries == 0 || error) { - expired = true; - if (self->completefn) { - self->completefn(); + // is this correct ending? + io_service->stop(); + if (completefn) { + completefn(); } + // restart timer } else { - retry = true; retries--; timer.expires_from_now(boost::posix_time::seconds(timeout)); timer.async_wait(timer_handler); + // trigger another round of queries + for (auto * socket : sockets) + socket->send(); } }; - + // start timer timer.expires_from_now(boost::posix_time::seconds(timeout)); timer.async_wait(timer_handler); + // start io_service, it will run until it has something to do - so in this case until stop is called in timer + io_service->run(); + } + catch (std::exception& e) { + BOOST_LOG_TRIVIAL(error) << e.what(); + } +} - udp::endpoint recv_from; - const auto recv_handler = [&](const error_code &error, size_t bytes) { - if (!error) { self->udp_receive(recv_from, bytes); } - }; - socket.async_receive_from(asio::buffer(buffer, buffer.size()), recv_from, recv_handler); +void Bonjour::priv::resolve_perform() +{ + // reply callback is shared to every UDPSession which is called on same thread as io_service->run(); + // thus no need to mutex replies in reply_callback, same should go with the timer + std::vector replies; + // examples would store [self] to the lambda (and the timer one), is it ok not to do it? (Should be c++03) + const auto reply_callback = [&rpls = replies](BonjourReply&& reply) + { + if (std::find(rpls.begin(), rpls.end(), reply) == rpls.end()) + rpls.push_back(reply); + }; - while (io_service.run_one()) { - if (expired) { - socket.cancel(); - } else if (retry) { - retry = false; - socket.send_to(asio::buffer(brq->data), mcast); - } else { - buffer.resize(DnsMessage::MAX_SIZE); - socket.async_receive_from(asio::buffer(buffer, buffer.size()), recv_from, recv_handler); - } + std::shared_ptr< boost::asio::io_service > io_service(new boost::asio::io_service); + std::vector sockets; + + // resolve interfaces - from PR#6646 + std::vector interfaces; + asio::ip::udp::resolver resolver(*io_service); + boost::system::error_code ec; + // ipv4 interfaces + auto results = resolver.resolve(udp::v4(), asio::ip::host_name(), "", ec); + if (!ec) { + for (auto const& r : results) { + auto const addr = r.endpoint().address(); + if (addr.is_loopback()) continue; + interfaces.emplace_back(addr); } - } catch (std::exception& /* e */) { + // create ipv4 socket for each interface + // each will send to querry to for both ipv4 and ipv6 + for (const auto& intrfc : interfaces) + sockets.emplace_back(new ResolveSocket(hostname, reply_callback, BonjourRequest::MCAST_IP4, intrfc, io_service)); + } else { + BOOST_LOG_TRIVIAL(info) << "Failed to resolve ipv4 interfaces: " << ec.message(); + } + if (sockets.empty()) + sockets.emplace_back(new ResolveSocket(hostname, reply_callback, BonjourRequest::MCAST_IP4, io_service)); + + // ipv6 interfaces + interfaces.clear(); + results = resolver.resolve(udp::v6(), asio::ip::host_name(), "", ec); + if (!ec) { + for (auto const& r : results) { + auto const addr = r.endpoint().address(); + if (addr.is_loopback()) continue; + interfaces.emplace_back(addr); + } + // create ipv6 socket for each interface + // each will send to querry to for both ipv4 and ipv6 + for (const auto& intrfc : interfaces) + sockets.emplace_back(new ResolveSocket(hostname, reply_callback, BonjourRequest::MCAST_IP6, intrfc, io_service)); + if (interfaces.empty()) + sockets.emplace_back(new ResolveSocket(hostname, reply_callback, BonjourRequest::MCAST_IP6, io_service)); + } else { + BOOST_LOG_TRIVIAL(info) << "Failed to resolve ipv6 interfaces: " << ec.message(); + } + + try { + // send first queries + for (auto * socket : sockets) + socket->send(); + + // timer settings + asio::deadline_timer timer(*io_service); + retries--; + std::function timer_handler = [&](const error_code& error) { + int replies_count = replies.size(); + // end + if (retries == 0 || error || replies_count > 0) { + // is this correct ending? + io_service->stop(); + if (replies_count > 0 && resolvefn) { + resolvefn(replies); + } + // restart timer + } else { + retries--; + timer.expires_from_now(boost::posix_time::seconds(timeout)); + timer.async_wait(timer_handler); + // trigger another round of queries + for (auto * socket : sockets) + socket->send(); + } + }; + // start timer + timer.expires_from_now(boost::posix_time::seconds(timeout)); + timer.async_wait(timer_handler); + // start io_service, it will run until it has something to do - so in this case until stop is called in timer + io_service->run(); + } + catch (std::exception& e) { + BOOST_LOG_TRIVIAL(error) << e.what(); } } @@ -770,6 +1136,12 @@ Bonjour& Bonjour::set_timeout(unsigned timeout) return *this; } +Bonjour& Bonjour::set_hostname(const std::string& hostname) +{ + if (p) { p->hostname = hostname; } + return *this; +} + Bonjour& Bonjour::set_retries(unsigned retries) { if (p && retries > 0) { p->retries = retries; } @@ -788,6 +1160,12 @@ Bonjour& Bonjour::on_complete(CompleteFn fn) return *this; } +Bonjour& Bonjour::on_resolve(ResolveFn fn) +{ + if (p) { p->resolvefn = std::move(fn); } + return *this; +} + Bonjour::Ptr Bonjour::lookup() { auto self = std::make_shared(std::move(*this)); @@ -803,4 +1181,26 @@ Bonjour::Ptr Bonjour::lookup() } +Bonjour::Ptr Bonjour::resolve() +{ + auto self = std::make_shared(std::move(*this)); + + if (self->p) { + auto io_thread = std::thread([self]() { + self->p->resolve_perform(); + }); + self->p->io_thread = std::move(io_thread); + } + + return self; } + +void Bonjour::resolve_sync() +{ + if (p) + p->resolve_perform(); +} + + +} + diff --git a/src/slic3r/Utils/Duet.cpp b/src/slic3r/Utils/Duet.cpp index 3293a3ff2b..229d0c950e 100644 --- a/src/slic3r/Utils/Duet.cpp +++ b/src/slic3r/Utils/Duet.cpp @@ -54,7 +54,7 @@ wxString Duet::get_test_failed_msg (wxString &msg) const % std::string(msg.ToUTF8())).str()); } -bool Duet::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const +bool Duet::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const { wxString connect_msg; auto connectionType = connect(connect_msg); diff --git a/src/slic3r/Utils/Duet.hpp b/src/slic3r/Utils/Duet.hpp index edca66ce0c..2a91aa8536 100644 --- a/src/slic3r/Utils/Duet.hpp +++ b/src/slic3r/Utils/Duet.hpp @@ -22,7 +22,7 @@ public: 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 upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; bool has_auto_discovery() const override { return false; } bool can_test() const override { return true; } PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint | PrintHostPostUploadAction::StartSimulation; } diff --git a/src/slic3r/Utils/FlashAir.cpp b/src/slic3r/Utils/FlashAir.cpp index 2337ac2904..e54dca58fe 100644 --- a/src/slic3r/Utils/FlashAir.cpp +++ b/src/slic3r/Utils/FlashAir.cpp @@ -76,7 +76,7 @@ wxString FlashAir::get_test_failed_msg (wxString &msg) const % _utf8(L("Note: FlashAir with firmware 2.00.02 or newer and activated upload function is required."))).str()); } -bool FlashAir::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const +bool FlashAir::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const { const char *name = get_name(); diff --git a/src/slic3r/Utils/FlashAir.hpp b/src/slic3r/Utils/FlashAir.hpp index 14e3f00156..ba60644c04 100644 --- a/src/slic3r/Utils/FlashAir.hpp +++ b/src/slic3r/Utils/FlashAir.hpp @@ -23,7 +23,7 @@ public: 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 upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; bool has_auto_discovery() const override { return false; } bool can_test() const override { return true; } PrintHostPostUploadActions get_post_upload_actions() const override { return {}; } diff --git a/src/slic3r/Utils/MKS.cpp b/src/slic3r/Utils/MKS.cpp index 80a79537d5..109283fc66 100644 --- a/src/slic3r/Utils/MKS.cpp +++ b/src/slic3r/Utils/MKS.cpp @@ -62,7 +62,7 @@ wxString MKS::get_test_failed_msg(wxString& msg) const % std::string(msg.ToUTF8())).str()); } -bool MKS::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const +bool MKS::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const { bool res = true; diff --git a/src/slic3r/Utils/MKS.hpp b/src/slic3r/Utils/MKS.hpp index 22455436ae..79143fdd9a 100644 --- a/src/slic3r/Utils/MKS.hpp +++ b/src/slic3r/Utils/MKS.hpp @@ -22,7 +22,7 @@ public: 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 upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; bool has_auto_discovery() const override { return false; } bool can_test() const override { return true; } PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp index 9d9d7ef5a3..48ad440337 100644 --- a/src/slic3r/Utils/OctoPrint.cpp +++ b/src/slic3r/Utils/OctoPrint.cpp @@ -8,6 +8,9 @@ #include #include #include +#include +#include +#include #include @@ -15,10 +18,12 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/I18N.hpp" -#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/format.hpp" #include "Http.hpp" #include "libslic3r/AppConfig.hpp" - +#include "Bonjour.hpp" +#include "slic3r/GUI/BonjourDialog.hpp" namespace fs = boost::filesystem; namespace pt = boost::property_tree; @@ -26,9 +31,9 @@ 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 { +#ifdef WIN32 + // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. std::string substitute_host(const std::string& orig_addr, std::string sub_addr) { // put ipv6 into [] brackets @@ -91,8 +96,66 @@ std::string substitute_host(const std::string& orig_addr, std::string sub_addr) return out; #endif } -} //namespace + +std::string get_host_from_url(const std::string& url_in) +{ + std::string url = url_in; + // add http:// if there is no scheme + size_t double_slash = url.find("//"); + if (double_slash == std::string::npos) + url = "http://" + url; + std::string out = url; + CURLU* hurl = curl_url(); + if (hurl) { + // Parse the input URL. + CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, url.c_str(), 0); + if (rc == CURLUE_OK) { + // Replace the address. + char* host; + rc = curl_url_get(hurl, CURLUPART_HOST, &host, 0); + if (rc == CURLUE_OK) { + out = host; + curl_free(host); + } + else + BOOST_LOG_TRIVIAL(error) << "OctoPrint get_host_from_url: failed to get host form URL " << url; + } + else + BOOST_LOG_TRIVIAL(error) << "OctoPrint get_host_from_url: failed to parse URL " << url; + curl_url_cleanup(hurl); + } + else + BOOST_LOG_TRIVIAL(error) << "OctoPrint get_host_from_url: failed to allocate curl_url"; + return out; +} #endif // WIN32 +std::string escape_string(const std::string& unescaped) +{ + std::string ret_val; + CURL* curl = curl_easy_init(); + if (curl) { + char* decoded = curl_easy_escape(curl, unescaped.c_str(), unescaped.size()); + if (decoded) { + ret_val = std::string(decoded); + curl_free(decoded); + } + curl_easy_cleanup(curl); + } + return ret_val; +} +std::string escape_path_by_element(const boost::filesystem::path& path) +{ + std::string ret_val = escape_string(path.filename().string()); + boost::filesystem::path parent(path.parent_path()); + while (!parent.empty() && parent.string() != "/") // "/" check is for case "/file.gcode" was inserted. Then boost takes "/" as parent_path. + { + ret_val = escape_string(parent.filename().string()) + "/" + ret_val; + parent = parent.parent_path(); + } + return ret_val; +} +} //namespace + OctoPrint::OctoPrint(DynamicPrintConfig *config) : m_host(config->opt_string("print_host")), @@ -103,11 +166,62 @@ OctoPrint::OctoPrint(DynamicPrintConfig *config) : const char* OctoPrint::get_name() const { return "OctoPrint"; } -bool OctoPrint::test(wxString &msg) const +#ifdef WIN32 +bool OctoPrint::test_with_resolved_ip(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; + // Msg contains ip string. + auto url = substitute_host(make_url("api/version"), GUI::into_u8(msg)); + msg.Clear(); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get version at: %2%") % name % url; + + auto http = Http::get(url);//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 at %2% : %3%, HTTP %4%, body: `%5%`") % name % url % error % status % body; + res = false; + msg = format_error(body, error, status); + }) + .on_complete([&, this](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(info) << 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."; + } + }) + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) + .perform_sync(); + + return res; +} +#endif //WIN32 + +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; @@ -147,8 +261,8 @@ bool OctoPrint::test(wxString &msg) const } }) #ifdef WIN32 - .ssl_revoke_best_effort(m_ssl_revoke_best_effort) - .on_ip_resolve([&](std::string address) { + .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); @@ -159,9 +273,10 @@ bool OctoPrint::test(wxString &msg) const return res; } + wxString OctoPrint::get_test_ok_msg () const { - return _(L("Connection to OctoPrint/Klipper works correctly.")); + return _(L("Connection to OctoPrint works correctly.")); } wxString OctoPrint::get_test_failed_msg (wxString &msg) const @@ -172,9 +287,130 @@ wxString OctoPrint::get_test_failed_msg (wxString &msg) const % _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 +bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const { - const char *name = get_name(); +#ifndef WIN32 + return upload_inner_with_host(std::move(upload_data), prorgess_fn, error_fn, info_fn); +#else + std::string host = get_host_from_url(m_host); + + // decide what to do based on m_host - resolve hostname or upload to ip + std::vector resolved_addr; + boost::system::error_code ec; + boost::asio::ip::address host_ip = boost::asio::ip::make_address(host, ec); + if (!ec) { + resolved_addr.push_back(host_ip); + } else if ( GUI::get_app_config()->get("allow_ip_resolve") == "1" && boost::algorithm::ends_with(host, ".local")){ + Bonjour("octoprint") + .set_hostname(host) + .set_retries(5) // number of rounds of queries send + .set_timeout(1) // after each timeout, if there is any answer, the resolving will stop + .on_resolve([&ra = resolved_addr](const std::vector& replies) { + for (const auto & rpl : replies) { + boost::asio::ip::address ip(rpl.ip); + ra.emplace_back(ip); + BOOST_LOG_TRIVIAL(info) << "Resolved IP address: " << rpl.ip; + } + }) + .resolve_sync(); + } + if (resolved_addr.empty()) { + // no resolved addresses - try system resolving + BOOST_LOG_TRIVIAL(error) << "PrusaSlicer failed to resolve hostname " << m_host << " into the IP address. Starting upload with system resolving."; + return upload_inner_with_host(std::move(upload_data), prorgess_fn, error_fn, info_fn); + } else if (resolved_addr.size() == 1) { + // one address resolved - upload there + return upload_inner_with_resolved_ip(std::move(upload_data), prorgess_fn, error_fn, info_fn, resolved_addr.front()); + } else if (resolved_addr.size() == 2 && resolved_addr[0].is_v4() != resolved_addr[1].is_v4()) { + // there are just 2 addresses and 1 is ip_v4 and other is ip_v6 + // try sending to both. (Then if both fail, show both error msg after second try) + wxString error_message; + if (!upload_inner_with_resolved_ip(std::move(upload_data), prorgess_fn + , [&msg = error_message, resolved_addr](wxString error) { msg = GUI::format_wxstr("%1%: %2%", resolved_addr.front(), error); } + , info_fn, resolved_addr.front()) + && + !upload_inner_with_resolved_ip(std::move(upload_data), prorgess_fn + , [&msg = error_message, resolved_addr](wxString error) { msg += GUI::format_wxstr("\n%1%: %2%", resolved_addr.back(), error); } + , info_fn, resolved_addr.back()) + ) { + + error_fn(error_message); + return false; + } + return true; + } else { + // There are multiple addresses - user needs to choose which to use. + size_t selected_index = resolved_addr.size(); + IPListDialog dialog(nullptr, boost::nowide::widen(m_host), resolved_addr, selected_index); + if (dialog.ShowModal() == wxID_OK && selected_index < resolved_addr.size()) { + return upload_inner_with_resolved_ip(std::move(upload_data), prorgess_fn, error_fn, info_fn, resolved_addr[selected_index]); + } + } + return false; +#endif // WIN32 +} +#ifdef WIN32 +bool OctoPrint::upload_inner_with_resolved_ip(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn, const boost::asio::ip::address& resolved_addr) const +{ + info_fn(L"resolve", boost::nowide::widen(resolved_addr.to_string())); + + // If test fails, test_msg_or_host_ip contains the error message. + // Otherwise on Windows it contains the resolved IP address of the host. + // Test_msg already contains resolved ip and will be cleared on start of test(). + wxString test_msg_or_host_ip = GUI::from_u8(resolved_addr.to_string()); + if (!test_with_resolved_ip(test_msg_or_host_ip)) { + error_fn(std::move(test_msg_or_host_ip)); + return false; + } + + 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(); + std::string url = substitute_host(make_url("api/files/local"), resolved_addr.to_string()); + bool result = true; + + info_fn(L"resolve", boost::nowide::widen(url)); + + 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(url);//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 to %2%: %3%, HTTP %4%, body: `%5%`") % name % url % error % status % body; + error_fn(format_error(body, error, status)); + result = 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"; + result = false; + } + }) + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) + .perform_sync(); + + return result; +} +#endif //WIN32 + +bool OctoPrint::upload_inner_with_host(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_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(); @@ -182,7 +418,7 @@ bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Erro // 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)) { + if (!test(test_msg_or_host_ip)) { error_fn(std::move(test_msg_or_host_ip)); return false; } @@ -192,7 +428,7 @@ bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Erro #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()) + if (m_host.find("https://") == 0 || test_msg_or_host_ip.empty() || GUI::get_app_config()->get("allow_ip_resolve") != "1") #endif // _WIN32 { // If https is entered we assume signed ceritificate is being used @@ -207,6 +443,7 @@ bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Erro // 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 + info_fn(L"resolve", test_msg_or_host_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; } @@ -233,7 +470,7 @@ bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Erro error_fn(format_error(body, error, status)); res = false; }) - .on_progress([&](Http::Progress progress, bool &cancel) { + .on_progress([&](Http::Progress progress, bool& cancel) { prorgess_fn(std::move(progress), cancel); if (cancel) { // Upload was canceled @@ -321,11 +558,12 @@ void SL1Host::set_auth(Http &http) const } // PrusaLink -PrusaLink::PrusaLink(DynamicPrintConfig* config) : +PrusaLink::PrusaLink(DynamicPrintConfig* config, bool show_after_message) : 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")) + m_password(config->opt_string("printhost_password")), + m_show_after_message(show_after_message) { } @@ -364,4 +602,528 @@ void PrusaLink::set_auth(Http& http) const } } +#if 0 +bool PrusaLink::version_check(const boost::optional& version_text) const +{ + // version_text is in format OctoPrint 1.2.3 + // true (= use PUT) should return: + // PrusaLink 0.7+ + + try { + if (!version_text) + throw Slic3r::RuntimeError("no version_text was given"); + + std::vector name_and_version; + boost::algorithm::split(name_and_version, *version_text, boost::is_any_of(" ")); + + if (name_and_version.size() != 2) + throw Slic3r::RuntimeError("invalid version_text"); + + Semver semver(name_and_version[1]); // throws Slic3r::RuntimeError when unable to parse + if (name_and_version.front() == "PrusaLink" && semver >= Semver(0, 7, 0)) + return true; + } catch (const Slic3r::RuntimeError& ex) { + BOOST_LOG_TRIVIAL(error) << std::string("Print host version check failed: ") + ex.what(); + } + + return false; +} +#endif + +bool PrusaLink::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; +} + +bool PrusaLink::get_storage(wxArrayString& output) const +{ + const char* name = get_name(); + + bool res = true; + auto url = make_url("api/v1/storage"); + wxString error_msg; + + struct StorageInfo{ + wxString name; + bool read_only; + long long free_space; + }; + std::vector storage; + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get storage 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 storage: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + error_msg = L"\n\n" + boost::nowide::widen(error); + res = false; + // If status is 0, the communication with the printer has failed completely (most likely a timeout), if the status is <= 400, it is an error returned by the pritner. + // If 0, we can show error to the user now, as we know the communication has failed. (res = true will do the trick.) + // if not 0, we must not show error, as not all printers support api/v1/storage endpoint. + // So we must be extra careful here, or we might be showing errors on perfectly fine communication. + if (status == 0) + res = true; + + }) + .on_complete([&, this](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got storage: %2%") % name % body; + try + { + std::stringstream ss(body); + pt::ptree ptree; + pt::read_json(ss, ptree); + + // what if there is more structure added in the future? Enumerate all elements? + if (ptree.front().first != "storage_list") { + res = false; + return; + } + // each storage has own subtree of storage_list + for (const auto& section : ptree.front().second) { + const auto path = section.second.get_optional("path"); + const auto space = section.second.get_optional("free_space"); + const auto read_only = section.second.get_optional("read_only"); + const auto available = section.second.get_optional("available"); + if (path && (!available || *available)) { + StorageInfo si; + si.name = boost::nowide::widen(*path); + si.read_only = read_only ? *read_only : false; // If read_only is missing, assume it is NOT read only. + si.free_space = space ? std::stoll(*space) : 1; // If free_space is missing, assume there is free space. + storage.emplace_back(std::move(si)); + } + } + } + catch (const std::exception&) + { + res = false; + } + }) +#ifdef WIN32 + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) + +#endif // WIN32 + .perform_sync(); + + for (const auto& si : storage) { + if (!si.read_only && si.free_space > 0) + output.push_back(si.name); + } + + if (res && output.empty()) + { + if (!storage.empty()) { // otherwise error_msg is already filled + error_msg = L"\n\n" + _L("Storages found:") + L" \n"; + for (const auto& si : storage) { + error_msg += si.name + L" : " + (si.read_only ? _L("read only") : _L("no free space")) + L"\n"; + } + } + std::string message = GUI::format(_L("Upload has failed. There is no suitable storage found at %1%.%2%"), m_host, error_msg); + BOOST_LOG_TRIVIAL(error) << message; + throw Slic3r::IOError(message); + } + + return res; +} + +bool PrusaLink::test_with_method_check(wxString& msg, bool& use_put) 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()); + use_put = false; + return; + } + + // find capabilities subtree and read upload-by-put + for (const auto& section : ptree) { + if (section.first == "capabilities") { + const auto put_upload = section.second.get_optional("upload-by-put"); + if (put_upload) + use_put = *put_upload; + break; + } + } + + } + 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; +} + + + +#ifdef WIN32 +bool PrusaLink::test_with_resolved_ip_and_method_check(wxString& msg, bool& use_put) 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; + // Msg contains ip string. + auto url = substitute_host(make_url("api/version"), GUI::into_u8(msg)); + msg.Clear(); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get version at: %2%") % name % url; + + auto http = Http::get(url);//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 at %2% : %3%, HTTP %4%, body: `%5%`") % name % url % error % status % body; + res = false; + msg = format_error(body, error, status); + }) + .on_complete([&, this](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(info) << 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()); + use_put = false; + return; + } + + // find capabilities subtree and read upload-by-put + for (const auto& section : ptree) { + if (section.first == "capabilities") { + const auto put_upload = section.second.get_optional("upload-by-put"); + if (put_upload) + use_put = *put_upload; + break; + } + } + + } + catch (const std::exception&) { + res = false; + msg = "Could not parse server response"; + } + + }) + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) + .perform_sync(); + + return res; +} + +bool PrusaLink::upload_inner_with_resolved_ip(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn, const boost::asio::ip::address& resolved_addr) const +{ + info_fn(L"resolve", boost::nowide::widen(resolved_addr.to_string())); + + // If test fails, test_msg_or_host_ip contains the error message. + // Otherwise on Windows it contains the resolved IP address of the host. + // Test_msg already contains resolved ip and will be cleared on start of test(). + wxString test_msg_or_host_ip = GUI::from_u8(resolved_addr.to_string()); + bool use_put = false; + if (!test_with_resolved_ip_and_method_check(test_msg_or_host_ip, use_put)) { + error_fn(std::move(test_msg_or_host_ip)); + return false; + } + + 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(); + std::string storage_path = (use_put ? "api/v1/files" : "api/files"); + storage_path += (upload_data.storage.empty() ? "/local" : upload_data.storage); + std::string url = substitute_host(make_url(storage_path), resolved_addr.to_string()); + bool result = true; + info_fn(L"resolve", boost::nowide::widen(url)); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%, method: %7%") + % name + % upload_data.source_path + % url + % upload_filename.string() + % upload_parent_path.string() + % (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false") + % (use_put ? "PUT" : "POST"); + + if (use_put) + return put_inner(std::move(upload_data), std::move(url), name, prorgess_fn, error_fn, info_fn); + return post_inner(std::move(upload_data), std::move(url), name, prorgess_fn, error_fn, info_fn); +} + +#endif //WIN32 + +bool PrusaLink::upload_inner_with_host(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_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; + bool use_put = false; + if (!test_with_method_check(test_msg_or_host_ip, use_put)) { + error_fn(std::move(test_msg_or_host_ip)); + return false; + } + + std::string url; + bool res = true; + std::string storage_path = (use_put ? "api/v1/files" : "api/files"); + storage_path += (upload_data.storage.empty() ? "/local" : upload_data.storage); +#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() || GUI::get_app_config()->get("allow_ip_resolve") != "1") +#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(storage_path); + } +#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 + info_fn(L"resolve", test_msg_or_host_ip); + url = substitute_host(make_url(storage_path), 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%, method: %7%") + % name + % upload_data.source_path + % url + % upload_filename.string() + % upload_parent_path.string() + % (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false") + % (use_put ? "PUT" : "POST"); + + if (use_put) + return put_inner(std::move(upload_data), std::move(url), name, prorgess_fn, error_fn, info_fn); + return post_inner(std::move(upload_data), std::move(url), name, prorgess_fn, error_fn, info_fn); +} + +bool PrusaLink::put_inner(PrintHostUpload upload_data, std::string url, const std::string& name, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const +{ + info_fn(L"set_complete_off", wxString()); + + bool res = true; + // Percent escape all filenames in on path and add it to the url. This is different from POST. + url += "/" + escape_path_by_element(upload_data.upload_path); + + Http http = Http::put(std::move(url)); + set_auth(http); + // This is ugly, but works. There was an error at PrusaLink side that accepts any string at Print-After-Upload as true, thus False was also triggering print after upload. + if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) + http.header("Print-After-Upload", "True"); + + http.set_put_body(upload_data.source_path) + .header("Content-Type", "text/x.gcode") + .header("Overwrite", "?1") + .on_complete([&](std::string body, unsigned status) { + wxString widebody = wxString::FromUTF8(body); + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: File uploaded: HTTP %2%: %3%") % name % status % widebody; + std::string message = m_show_after_message ? (boost::format("%1%") % widebody).str() : std::string(); + info_fn(L"complete", message); + }) + .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 PrusaLink::post_inner(PrintHostUpload upload_data, std::string url, const std::string& name, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const +{ + info_fn(L"set_complete_off", wxString()); + bool res = true; + const auto upload_filename = upload_data.upload_path.filename(); + const auto upload_parent_path = upload_data.upload_path.parent_path(); + Http http = Http::post(std::move(url)); + set_auth(http); + set_http_post_header_args(http, upload_data.post_action); + http.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) { + if (m_show_after_message) { + // PrusaConnect message + wxString widebody = wxString::FromUTF8(body); + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: File uploaded: HTTP %2%: %3%") % name % status % widebody; + std::string message = m_show_after_message ? (boost::format("%1%") % widebody).str() : std::string(); + if (status == 202) + info_fn(L"complete_with_warning", message); + else + info_fn(L"complete", message); + } else { + // PrusaLink + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: File uploaded: HTTP %2%") % name % status; + info_fn(L"complete", wxString()); + } + + + }) + .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; +} + +void PrusaLink::set_http_post_header_args(Http& http, PrintHostPostUploadAction post_action) const +{ + http.form_add("print", post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false"); +} + +PrusaConnect::PrusaConnect(DynamicPrintConfig* config) + : PrusaLink(config, true) +{ +} + +void PrusaConnect::set_http_post_header_args(Http& http, PrintHostPostUploadAction post_action) const +{ + // Language for accept message + wxString wlang = GUI::wxGetApp().current_language_code(); + std::string lang = GUI::format(wlang.SubString(0, 1)); + http.header("Accept-Language", lang); + // Post action + if (post_action == PrintHostPostUploadAction::StartPrint) { + http.form_add("to_print", "True"); + } else if (post_action == PrintHostPostUploadAction::QueuePrint) { + http.form_add("to_queue", "True"); + } + +} + } diff --git a/src/slic3r/Utils/OctoPrint.hpp b/src/slic3r/Utils/OctoPrint.hpp index c30b49b26d..fadb5d924b 100644 --- a/src/slic3r/Utils/OctoPrint.hpp +++ b/src/slic3r/Utils/OctoPrint.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "PrintHost.hpp" #include "libslic3r/PrintConfig.hpp" @@ -22,10 +23,10 @@ public: const char* get_name() const override; - bool test(wxString &curl_msg) const override; + virtual 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 upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_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; } @@ -34,9 +35,12 @@ public: const std::string& get_cafile() const { return m_cafile; } protected: +#ifdef WIN32 + virtual bool upload_inner_with_resolved_ip(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn, const boost::asio::ip::address& resolved_addr) const; +#endif virtual bool validate_version_text(const boost::optional &version_text) const; + virtual bool upload_inner_with_host(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const; -private: std::string m_host; std::string m_apikey; std::string m_cafile; @@ -44,6 +48,11 @@ private: virtual void set_auth(Http &http) const; std::string make_url(const std::string &path) const; + +private: +#ifdef WIN32 + bool test_with_resolved_ip(wxString& curl_msg) const; +#endif }; class SL1Host: public OctoPrint @@ -74,51 +83,57 @@ private: class PrusaLink : public OctoPrint { public: - PrusaLink(DynamicPrintConfig* config); + PrusaLink(DynamicPrintConfig* config) : PrusaLink(config, false) {} + PrusaLink(DynamicPrintConfig* config, bool show_after_message); ~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; } + virtual PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } + // gets possible storage to be uploaded to. This allows different printer to have different storage. F.e. local vs sdcard vs usb. + bool get_storage(wxArrayString& /* storage */) const override; protected: + bool test(wxString& curl_msg) const override; bool validate_version_text(const boost::optional& version_text) const override; + bool upload_inner_with_host(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; + + void set_auth(Http& http) const override; + virtual void set_http_post_header_args(Http& http, PrintHostPostUploadAction post_action) const; +#ifdef WIN32 + bool upload_inner_with_resolved_ip(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn, const boost::asio::ip::address& resolved_addr) const override; +#endif private: - void set_auth(Http& http) const override; - + bool test_with_method_check(wxString& curl_msg, bool& use_put) const; + bool put_inner(PrintHostUpload upload_data, std::string url, const std::string& name, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const; + bool post_inner(PrintHostUpload upload_data, std::string url, const std::string& name, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const; +#ifdef WIN32 + bool test_with_resolved_ip_and_method_check(wxString& curl_msg, bool& use_put) const; +#endif // Host authorization type. AuthorizationType m_authorization_type; // username and password for HTTP Digest Authentization (RFC RFC2617) std::string m_username; std::string m_password; + bool m_show_after_message; + +#if 0 + bool version_check(const boost::optional& version_text) const; +#endif }; -class PrusaLink : public OctoPrint +class PrusaConnect : public PrusaLink { 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; - bool can_start_print() const override { return true; } - + PrusaConnect(DynamicPrintConfig* config); + ~PrusaConnect() override = default; + PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint | PrintHostPostUploadAction::QueuePrint; } + const char* get_name() const override { return "PrusaConnect"; } protected: - bool validate_version_text(const boost::optional& version_text) const override; - -private: - void set_auth(Http& http) const override; - - // Host authorization type. - AuthorizationType authorization_type; - // username and password for HTTP Digest Authentization (RFC RFC2617) - std::string username; - std::string password; + void set_http_post_header_args(Http& http, PrintHostPostUploadAction post_action) const override; }; } diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 86f6101b6d..c8f0e34bca 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -52,6 +52,7 @@ PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config) case htAstroBox: return new AstroBox(config); case htRepetier: return new Repetier(config); case htPrusaLink: return new PrusaLink(config); + case htPrusaConnect: return new PrusaConnect(config); case htMKS: return new MKS(config); default: return nullptr; } @@ -93,10 +94,13 @@ struct PrintHostJobQueue::priv void emit_progress(int progress); void emit_error(wxString error); void emit_cancel(size_t id); + void emit_info(wxString tag, wxString status); void start_bg_thread(); void stop_bg_thread(); void bg_thread_main(); void progress_fn(Http::Progress progress, bool &cancel); + void error_fn(wxString error); + void info_fn(wxString tag, wxString status); void remove_source(const fs::path &path); void remove_source(); void perform_job(PrintHostJob the_job); @@ -125,6 +129,12 @@ void PrintHostJobQueue::priv::emit_error(wxString error) wxQueueEvent(queue_dialog, evt); } +void PrintHostJobQueue::priv::emit_info(wxString tag, wxString status) +{ + auto evt = new PrintHostQueueDialog::Event(GUI::EVT_PRINTHOST_INFO, queue_dialog->GetId(), job_id, std::move(tag), std::move(status)); + 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); @@ -233,6 +243,40 @@ void PrintHostJobQueue::priv::progress_fn(Http::Progress progress, bool &cancel) } } +void PrintHostJobQueue::priv::error_fn(wxString error) +{ + // check if transfer was not canceled before error occured - than do not show the error + bool do_emit_err = true; + 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) { + do_emit_err = false; + emit_cancel(job_id); + } + 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 (do_emit_err) + emit_error(std::move(error)); +} + +void PrintHostJobQueue::priv::info_fn(wxString tag, wxString status) +{ + emit_info(tag, status); +} + void PrintHostJobQueue::priv::remove_source(const fs::path &path) { if (! path.empty()) { @@ -255,10 +299,9 @@ 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)); - } + [this](Http::Progress progress, bool &cancel) { this->progress_fn(std::move(progress), cancel); }, + [this](wxString error) { this->error_fn(std::move(error)); }, + [this](wxString tag, wxString host) { this->info_fn(std::move(tag), std::move(host)); } ); if (success) { diff --git a/src/slic3r/Utils/PrintHost.hpp b/src/slic3r/Utils/PrintHost.hpp index dd22e60b7d..c39f86288e 100644 --- a/src/slic3r/Utils/PrintHost.hpp +++ b/src/slic3r/Utils/PrintHost.hpp @@ -21,7 +21,8 @@ class DynamicPrintConfig; enum class PrintHostPostUploadAction { None, StartPrint, - StartSimulation + StartSimulation, + QueuePrint }; using PrintHostPostUploadActions = enum_bitmask; ENABLE_ENUM_BITMASK_OPERATORS(PrintHostPostUploadAction); @@ -32,7 +33,8 @@ struct PrintHostUpload boost::filesystem::path upload_path; std::string group; - + std::string storage; + PrintHostPostUploadAction post_action { PrintHostPostUploadAction::None }; }; @@ -43,13 +45,14 @@ public: typedef Http::ProgressFn ProgressFn; typedef std::function ErrorFn; + typedef std::function InfoFn; 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 upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const = 0; virtual bool has_auto_discovery() const = 0; virtual bool can_test() const = 0; virtual PrintHostPostUploadActions get_post_upload_actions() const = 0; @@ -61,6 +64,9 @@ public: // 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; } + // Support for PrusaLink uploading to different storage. Not supported by other print hosts. + // Returns false if not supported or fail. + virtual bool get_storage(wxArrayString& /* storage */) const { return false; } static PrintHost* get_print_host(DynamicPrintConfig *config); diff --git a/src/slic3r/Utils/Repetier.cpp b/src/slic3r/Utils/Repetier.cpp index 63f4384289..ad3c1a029c 100644 --- a/src/slic3r/Utils/Repetier.cpp +++ b/src/slic3r/Utils/Repetier.cpp @@ -35,6 +35,24 @@ Repetier::Repetier(DynamicPrintConfig *config) : const char* Repetier::get_name() const { return "Repetier"; } + + +static bool validate_repetier(const boost::optional& name, + const boost::optional& soft) +{ + if (soft) { + // See https://github.com/prusa3d/PrusaSlicer/issues/7807: + // Repetier allows "rebranding", so the "name" value is not reliable when detecting + // server type. Newer Repetier versions send "software", which should be invariant. + return ((*soft) == "Repetier-Server"); + } else { + // If there is no "software" value, validate as we did before: + return name ? boost::starts_with(*name, "Repetier") : true; + } +} + + + bool Repetier::test(wxString &msg) const { // Since the request is performed synchronously here, @@ -62,11 +80,12 @@ bool Repetier::test(wxString &msg) const std::stringstream ss(body); pt::ptree ptree; pt::read_json(ss, ptree); - + const auto text = ptree.get_optional("name"); - res = validate_version_text(text); + const auto soft = ptree.get_optional("software"); + res = validate_repetier(text, soft); if (! res) { - msg = GUI::from_u8((boost::format(_utf8(L("Mismatched type of print host: %s"))) % (text ? *text : "Repetier")).str()); + msg = GUI::from_u8((boost::format(_utf8(L("Mismatched type of print host: %s"))) % (soft ? *soft : (text ? *text : "Repetier"))).str()); } } catch (const std::exception &) { @@ -92,7 +111,7 @@ wxString Repetier::get_test_failed_msg (wxString &msg) const % _utf8(L("Note: Repetier version at least 0.90.0 is required."))).str()); } -bool Repetier::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const +bool Repetier::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const { const char *name = get_name(); @@ -129,6 +148,7 @@ bool Repetier::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Error if(upload_data.post_action == PrintHostPostUploadAction::StartPrint) { http.form_add("name", upload_filename.string()); + http.form_add("autostart", "true"); // See https://github.com/prusa3d/PrusaSlicer/issues/7807#issuecomment-1235519371 } http.form_add("a", "upload") @@ -154,10 +174,7 @@ bool Repetier::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Error return res; } -bool Repetier::validate_version_text(const boost::optional &version_text) const -{ - return version_text ? (!version_text->empty()) : true; -} + void Repetier::set_auth(Http &http) const { diff --git a/src/slic3r/Utils/Repetier.hpp b/src/slic3r/Utils/Repetier.hpp index 6f33102604..00bc92975a 100644 --- a/src/slic3r/Utils/Repetier.hpp +++ b/src/slic3r/Utils/Repetier.hpp @@ -23,7 +23,7 @@ public: 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 upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; bool has_auto_discovery() const override { return false; } bool can_test() const override { return true; } PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } @@ -33,9 +33,6 @@ public: bool get_groups(wxArrayString &groups) const override; bool get_printers(wxArrayString &printers) const override; -protected: - virtual bool validate_version_text(const boost::optional &version_text) const; - private: std::string host; std::string apikey;