diff --git a/resources/web/device/css/dark.css b/resources/web/device/css/dark.css new file mode 100644 index 000000000..a5f041e8a --- /dev/null +++ b/resources/web/device/css/dark.css @@ -0,0 +1,25 @@ +body { + background-color:#4c4c54; + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.container { + text-align: center; + padding: 30px; + border-radius: 10px; + background-color: #272727; + box-shadow: 0 4px 6px rgba(39, 39, 39, 0.1); +} + +h1 { + color: #ffffff; +} + +p { + color: #ffffff; +} \ No newline at end of file diff --git a/resources/web/device/css/home.css b/resources/web/device/css/home.css new file mode 100644 index 000000000..3f4cc4eb9 --- /dev/null +++ b/resources/web/device/css/home.css @@ -0,0 +1,18 @@ +body +{ + background-color:#eeeeee; + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.container { + text-align: center; + padding: 30px; + border-radius: 10px; + background-color: #ffffff; + box-shadow: 0 4px 6px rgba(39, 39, 39, 0.1); +} \ No newline at end of file diff --git a/resources/web/device/missing_connection.html b/resources/web/device/missing_connection.html new file mode 100644 index 000000000..a66ea4561 --- /dev/null +++ b/resources/web/device/missing_connection.html @@ -0,0 +1,22 @@ + + + + + +Printer Connection Required + + + + + + + + + +
+

Printer Connection

+

Please set up your printer connection to view the device.

+ Printer connection setup demonstration +
+ + diff --git a/resources/web/device/setup_connection.gif b/resources/web/device/setup_connection.gif new file mode 100644 index 000000000..ba6220915 Binary files /dev/null and b/resources/web/device/setup_connection.gif differ diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 1621c42a6..39add72de 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -803,7 +803,8 @@ static std::vector s_Preset_printer_options { "scan_first_layer", "machine_load_filament_time", "machine_unload_filament_time", "machine_pause_gcode", "template_custom_gcode", "nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", //SoftFever - "host_type", "print_host", "printhost_apikey", + "host_type", "print_host", "printhost_apikey", + "print_host_webui", "printhost_cafile","printhost_port","printhost_authorization_type", "printhost_user", "printhost_password", "printhost_ssl_ignore_revoke" }; @@ -1694,6 +1695,11 @@ std::pair PresetCollection::load_external_preset( { // Load the preset over a default preset, so that the missing fields are filled in from the default preset. DynamicPrintConfig cfg(this->default_preset_for(combined_config).config); + // SoftFever: ignore print connection info from project + cfg.erase("print_host"); + cfg.erase("print_host_webui"); + cfg.erase("printhost_apikey"); + cfg.erase("printhost_cafile"); const auto &keys = cfg.keys(); cfg.apply_only(combined_config, keys, true); std::string &inherits = Preset::inherits(cfg); diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index f9ad98aa6..ef8989faf 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -1552,6 +1552,9 @@ DynamicPrintConfig PresetBundle::full_config_secure() const { DynamicPrintConfig config = this->full_config(); //BBS example: config.erase("print_host"); + config.erase("print_host_webui"); + config.erase("printhost_apikey"); + config.erase("printhost_cafile"); return config; } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 9600f8f10..b794be20e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -75,11 +75,11 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrinterTechnology) static t_config_enum_values s_keys_map_PrintHostType{ { "prusalink", htPrusaLink }, { "octoprint", htOctoPrint }, - //{ "duet", htDuet }, - //{ "flashair", htFlashAir }, - //{ "astrobox", htAstroBox }, - //{ "repetier", htRepetier }, - //{ "mks", htMKS } + { "duet", htDuet }, + { "flashair", htFlashAir }, + { "astrobox", htAstroBox }, + { "repetier", htRepetier }, + { "mks", htMKS } }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintHostType) @@ -404,6 +404,14 @@ void PrintConfigDef::init_common_params() def->cli = ConfigOptionDef::nocli; def->set_default_value(new ConfigOptionString("")); + def = this->add("print_host_webui", coString); + def->label = L("Device UI"); + def->tooltip = L("Specify the URL of your device user interface if it's not same as print_host"); + def->mode = comAdvanced; + def->cli = ConfigOptionDef::nocli; + def->set_default_value(new ConfigOptionString("")); + + def = this->add("printhost_apikey", coString); def->label = L("API Key / Password"); def->tooltip = L("Slic3r can upload G-code files to a printer host. This field should contain " @@ -2011,18 +2019,18 @@ void PrintConfigDef::init_fff_params() def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values.push_back("prusalink"); def->enum_values.push_back("octoprint"); - //def->enum_values.push_back("duet"); - //def->enum_values.push_back("flashair"); - //def->enum_values.push_back("astrobox"); - //def->enum_values.push_back("repetier"); - //def->enum_values.push_back("mks"); + def->enum_values.push_back("duet"); + def->enum_values.push_back("flashair"); + def->enum_values.push_back("astrobox"); + def->enum_values.push_back("repetier"); + def->enum_values.push_back("mks"); def->enum_labels.push_back("PrusaLink"); def->enum_labels.push_back("OctoPrint"); - //def->enum_labels.push_back("Duet"); - //def->enum_labels.push_back("FlashAir"); - //def->enum_labels.push_back("AstroBox"); - //def->enum_labels.push_back("Repetier"); - //def->enum_labels.push_back("MKS"); + def->enum_labels.push_back("Duet"); + def->enum_labels.push_back("FlashAir"); + def->enum_labels.push_back("AstroBox"); + def->enum_labels.push_back("Repetier"); + def->enum_labels.push_back("MKS"); def->mode = comAdvanced; def->cli = ConfigOptionDef::nocli; def->set_default_value(new ConfigOptionEnum(htOctoPrint)); diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 62caa3e26..5e66eed39 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -245,6 +245,8 @@ set(SLIC3R_GUI_SOURCES GUI/Monitor.hpp GUI/WebViewDialog.cpp GUI/WebViewDialog.hpp + GUI/PrinterWebView.cpp + GUI/PrinterWebView.hpp GUI/WebDownPluginDlg.hpp GUI/WebDownPluginDlg.cpp GUI/WebGuideDialog.hpp @@ -429,6 +431,17 @@ set(SLIC3R_GUI_SOURCES Utils/PrintHost.cpp Utils/NetworkAgent.cpp Utils/NetworkAgent.hpp + Utils/MKS.hpp + Utils/MKS.cpp + Utils/Duet.cpp + Utils/Duet.hpp + Utils/FlashAir.cpp + Utils/FlashAir.hpp + Utils/AstroBox.cpp + Utils/AstroBox.hpp + Utils/Repetier.cpp + Utils/Repetier.hpp + Utils/CalibUtils.cpp Utils/CalibUtils.hpp ) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index dd113dcbf..7994e5dfe 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -83,6 +83,7 @@ wxDEFINE_EVENT(EVT_UPDATE_PRESET_CB, SimpleEvent); // BBS: backup wxDEFINE_EVENT(EVT_BACKUP_POST, wxCommandEvent); wxDEFINE_EVENT(EVT_LOAD_URL, wxCommandEvent); +wxDEFINE_EVENT(EVT_LOAD_PRINTER_URL, wxCommandEvent); enum class ERescaleTarget { @@ -925,16 +926,35 @@ void MainFrame::init_tabpanel() m_tabpanel->Bind(wxEVT_NOTEBOOK_PAGE_CHANGING, [this](wxBookCtrlEvent& e) { int old_sel = e.GetOldSelection(); int new_sel = e.GetSelection(); - if (new_sel == tpMonitor) { + if (wxGetApp().preset_bundle && + wxGetApp().preset_bundle->printers.get_edited_preset().is_bbl_vendor_preset(wxGetApp().preset_bundle) && + new_sel == tpMonitor) { if (!wxGetApp().getAgent()) { e.Veto(); - BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins")%old_sel %new_sel; + BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins") % + old_sel % new_sel; if (m_plater) { - wxCommandEvent *evt = new wxCommandEvent(EVT_INSTALL_PLUGIN_HINT); + wxCommandEvent* evt = new wxCommandEvent(EVT_INSTALL_PLUGIN_HINT); wxQueueEvent(m_plater, evt); } } } + else { + if (new_sel == tpMonitor && wxGetApp().preset_bundle != nullptr) { + auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + wxString url; + if (cfg.has("print_host_webui") && !cfg.opt_string("print_host_webui").empty()) { + url = cfg.opt_string("print_host_webui"); + } + else { + url = cfg.opt_string("print_host"); + } + if (url.empty()) { + wxString url = wxString::Format("file://%s/web/device/missing_connection.html", from_u8(resources_dir())); + m_printer_view->load_url(url); + } + } + } }); #ifdef __WXMSW__ @@ -1017,6 +1037,14 @@ void MainFrame::init_tabpanel() m_monitor->SetBackgroundColour(*wxWHITE); m_tabpanel->AddPage(m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active")); + m_printer_view = new PrinterWebView(m_tabpanel); + Bind(EVT_LOAD_PRINTER_URL, [this](wxCommandEvent &evt) { + wxString url = evt.GetString(); + //select_tab(MainFrame::tpMonitor); + m_printer_view->load_url(url); + }); + m_printer_view->Hide(); + m_project = new ProjectPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_project->SetBackgroundColour(*wxWHITE); m_tabpanel->AddPage(m_project, _L("Project"), std::string("tab_auxiliary_avtice"), std::string("tab_auxiliary_avtice")); @@ -1039,6 +1067,36 @@ void MainFrame::init_tabpanel() } } + +// SoftFever +void MainFrame::show_device(bool bBBLPrinter) { + if (m_tabpanel->GetPage(tpMonitor) != m_monitor && + m_tabpanel->GetPage(tpMonitor) != m_printer_view) { + BOOST_LOG_TRIVIAL(error) << "Failed to find device tab"; + return; + } + if (bBBLPrinter) { + if (m_tabpanel->GetPage(tpMonitor) != m_monitor) { + m_printer_view->Hide(); + m_tabpanel->RemovePage(tpMonitor); + m_tabpanel->InsertPage(tpMonitor, m_monitor, _L("Device"), + std::string("tab_monitor_active"), + std::string("tab_monitor_active")); + m_tabpanel->SetSelection(tp3DEditor); + } + } else { + if (m_tabpanel->GetPage(tpMonitor) != m_printer_view) { + m_printer_view->Show(); + m_tabpanel->RemovePage(tpMonitor); + m_tabpanel->InsertPage(tpMonitor, m_printer_view, _L("Device"), + std::string("tab_monitor_active"), + std::string("tab_monitor_active")); + m_tabpanel->SetSelection(tp3DEditor); + } + } +} + + bool MainFrame::preview_only_hint() { if (m_plater && (m_plater->only_gcode_mode() || (m_plater->using_exported_file()))) { @@ -3244,6 +3302,34 @@ void MainFrame::load_url(wxString url) wxQueueEvent(this, evt); } +void MainFrame::load_printer_url(wxString url) +{ + BOOST_LOG_TRIVIAL(trace) << "load_printer_url:" << url; + auto evt = new wxCommandEvent(EVT_LOAD_PRINTER_URL, this->GetId()); + evt->SetString(url); + wxQueueEvent(this, evt); +} + +void MainFrame::load_printer_url() +{ + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + if (preset_bundle.printers.get_edited_preset().is_bbl_vendor_preset(&preset_bundle)) + return; + + auto cfg = preset_bundle.printers.get_edited_preset().config; + wxString url = + cfg.opt_string("print_host_webui").empty() ? cfg.opt_string("print_host") : cfg.opt_string("print_host_webui"); + if (!url.empty()) { + if (!url.Lower().starts_with("http")) + url = wxString::Format("http://%s", url); + + load_printer_url(url); + } +} + +bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelection() == TabPosition::tpMonitor; } + + void MainFrame::refresh_plugin_tips() { if (m_webview != nullptr) diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index e96ec8523..065dda196 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -28,6 +28,7 @@ // BBS #include "BBLTopbar.hpp" +#include "PrinterWebView.hpp" #define ENABEL_PRINT_ALL 0 @@ -328,8 +329,12 @@ public: //BBS void load_url(wxString url); + void load_printer_url(wxString url); + void load_printer_url(); + bool is_printer_view() const; void refresh_plugin_tips(); void RunScript(wxString js); + void show_device(bool bBBLPrinter); // BBS. Replace title bar and menu bar with top bar. BBLTopbar* m_topbar{ nullptr }; @@ -343,6 +348,7 @@ public: CalibrationPanel* m_calibration{ nullptr }; WebViewPanel* m_webview { nullptr }; + PrinterWebView* m_printer_view{nullptr}; wxLogWindow* m_log_window { nullptr }; // BBS //wxBookCtrlBase* m_tabpanel { nullptr }; diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 5304955bc..4d6d48a26 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -219,6 +219,10 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr host_line.append_widget(print_host_test); m_optgroup->append_line(host_line); + option = m_optgroup->get_option("print_host_webui"); + option.opt.width = Field::def_width_wider(); + m_optgroup->append_single_option_line(option); + m_optgroup->append_single_option_line("printhost_authorization_type"); option = m_optgroup->get_option("printhost_apikey"); @@ -233,7 +237,7 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr const auto ca_file_hint = _u8L("HTTPS CA file is optional. It is only needed if you use HTTPS with a self-signed certificate."); - /*if (Http::ca_file_supported()) { + if (Http::ca_file_supported()) { option = m_optgroup->get_option("printhost_cafile"); option.opt.width = Field::def_width_wider(); Line cafile_line = m_optgroup->create_single_option_line(option); @@ -253,37 +257,37 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr }; cafile_line.append_widget(printhost_cafile_browse); - //m_optgroup->append_line(cafile_line); + m_optgroup->append_line(cafile_line); - //Line cafile_hint{ "", "" }; - //cafile_hint.full_width = 1; - //cafile_hint.widget = [ca_file_hint](wxWindow* parent) { - // auto txt = new wxStaticText(parent, wxID_ANY, ca_file_hint); - // auto sizer = new wxBoxSizer(wxHORIZONTAL); - // sizer->Add(txt); - // return sizer; - //}; - //m_optgroup->append_line(cafile_hint); + Line cafile_hint{ "", "" }; + cafile_hint.full_width = 1; + cafile_hint.widget = [ca_file_hint](wxWindow* parent) { + auto txt = new wxStaticText(parent, wxID_ANY, ca_file_hint); + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(txt); + return sizer; + }; + m_optgroup->append_line(cafile_hint); } else { - //Line line{ "", "" }; - //line.full_width = 1; + Line line{ "", "" }; + line.full_width = 1; - //line.widget = [ca_file_hint](wxWindow* parent) { - // std::string info = _u8L("HTTPS CA File") + ":\n\t" + - // (boost::format(_u8L("On this system, %s uses HTTPS certificates from the system Certificate Store or Keychain.")) % SLIC3R_APP_NAME).str() + - // "\n\t" + _u8L("To use a custom CA file, please import your CA file into Certificate Store / Keychain."); + line.widget = [ca_file_hint](wxWindow* parent) { + std::string info = _u8L("HTTPS CA File") + ":\n\t" + + (boost::format(_u8L("On this system, %s uses HTTPS certificates from the system Certificate Store or Keychain.")) % SLIC3R_APP_NAME).str() + + "\n\t" + _u8L("To use a custom CA file, please import your CA file into Certificate Store / Keychain."); - // //auto txt = new wxStaticText(parent, wxID_ANY, from_u8((boost::format("%1%\n\n\t%2%") % info % ca_file_hint).str())); - // auto txt = new wxStaticText(parent, wxID_ANY, from_u8((boost::format("%1%\n\t%2%") % info % ca_file_hint).str())); - // txt->SetFont(wxGetApp().normal_font()); - // auto sizer = new wxBoxSizer(wxHORIZONTAL); - // sizer->Add(txt, 1, wxEXPAND); - // return sizer; - //}; - //m_optgroup->append_line(line); - }*/ + //auto txt = new wxStaticText(parent, wxID_ANY, from_u8((boost::format("%1%\n\n\t%2%") % info % ca_file_hint).str())); + auto txt = new wxStaticText(parent, wxID_ANY, from_u8((boost::format("%1%\n\t%2%") % info % ca_file_hint).str())); + txt->SetFont(wxGetApp().normal_font()); + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(txt, 1, wxEXPAND|wxALIGN_LEFT); + return sizer; + }; + m_optgroup->append_line(line); + } for (const std::string& opt_key : std::vector{ "printhost_user", "printhost_password" }) { option = m_optgroup->get_option(opt_key); @@ -292,9 +296,9 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr } #ifdef WIN32 - //option = m_optgroup->get_option("printhost_ssl_ignore_revoke"); - //option.opt.width = Field::def_width_wider(); - //m_optgroup->append_single_option_line(option); + option = m_optgroup->get_option("printhost_ssl_ignore_revoke"); + option.opt.width = Field::def_width_wider(); + m_optgroup->append_single_option_line(option); #endif m_optgroup->activate(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 260102c21..0ca2bd09e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1026,17 +1026,39 @@ void Sidebar::update_all_preset_comboboxes() bool is_bbl_preset = preset_bundle.printers.get_edited_preset().is_bbl_vendor_preset(&preset_bundle); + auto p_mainframe = wxGetApp().mainframe; + + p_mainframe->show_device(is_bbl_preset); if (is_bbl_preset) { //only show connection button for not-BBL printer connection_btn->Hide(); //only show sync-ams button for BBL printer ams_btn->Show(); //update print button default value for bbl or third-party printer - wxGetApp().mainframe->set_print_button_to_default(MainFrame::PrintSelectType::ePrintPlate); + p_mainframe->set_print_button_to_default(MainFrame::PrintSelectType::ePrintPlate); + m_bed_type_list->SelectAndNotify(btPC - 1); + m_bed_type_list->Enable(); } else { connection_btn->Show(); ams_btn->Hide(); - wxGetApp().mainframe->set_print_button_to_default(MainFrame::PrintSelectType::eSendGcode); + p_mainframe->set_print_button_to_default(MainFrame::PrintSelectType::eSendGcode); + auto cfg = preset_bundle.printers.get_edited_preset().config; + wxString url; + if (cfg.has("print_host_webui") && !cfg.opt_string("print_host_webui").empty()) { + url = cfg.opt_string("print_host_webui"); + } else { + url = cfg.opt_string("print_host"); + } + if(!url.empty()) + { + if(!url.Lower().starts_with("http")) + url = wxString::Format("http://%s",url); + + p_mainframe->load_printer_url(url); + } + + m_bed_type_list->SelectAndNotify(btPEI - 1); + m_bed_type_list->Disable(); } // Update the print choosers to only contain the compatible presets, update the dirty flags. diff --git a/src/slic3r/GUI/PrinterWebView.cpp b/src/slic3r/GUI/PrinterWebView.cpp new file mode 100644 index 000000000..5830d1379 --- /dev/null +++ b/src/slic3r/GUI/PrinterWebView.cpp @@ -0,0 +1,123 @@ +#include "PrinterWebView.hpp" + +#include "I18N.hpp" +#include "slic3r/GUI/wxExtensions.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/MainFrame.hpp" +#include "libslic3r_version.h" + +#include +#include +#include + +#include + +namespace pt = boost::property_tree; + +namespace Slic3r { +namespace GUI { + +PrinterWebView::PrinterWebView(wxWindow *parent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) + { + + wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); + + // Create the webview + m_browser = WebView::CreateWebView(this, ""); + if (m_browser == nullptr) { + wxLogError("Could not init m_browser"); + return; + } + + Bind(wxEVT_WEBVIEW_ERROR, &PrinterWebView::OnError, this); + + SetSizer(topsizer); + + topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); + + // Log backend information + if (wxGetApp().get_mode() == comDevelop) { + wxLogMessage(wxWebView::GetBackendVersionInfo().ToString()); + wxLogMessage("Backend: %s Version: %s", m_browser->GetClassInfo()->GetClassName(), + wxWebView::GetBackendVersionInfo().ToString()); + wxLogMessage("User Agent: %s", m_browser->GetUserAgent()); + } + + //Zoom + m_zoomFactor = 100; + + //Connect the idle events + Bind(wxEVT_CLOSE_WINDOW, &PrinterWebView::OnClose, this); + + } + +PrinterWebView::~PrinterWebView() +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Start"; + SetEvtHandlerEnabled(false); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " End"; +} + + +void PrinterWebView::load_url(wxString& url) +{ +// this->Show(); +// this->Raise(); + if (m_browser == nullptr) + return; + m_browser->LoadURL(url); + //m_browser->SetFocus(); + UpdateState(); +} +/** + * Method that retrieves the current state from the web control and updates the + * GUI the reflect this current state. + */ +void PrinterWebView::UpdateState() { + // SetTitle(m_browser->GetCurrentTitle()); + +} + +void PrinterWebView::OnClose(wxCloseEvent& evt) +{ + this->Hide(); +} + +void PrinterWebView::OnError(wxWebViewEvent &evt) +{ + auto e = "unknown error"; + switch (evt.GetInt()) { + case wxWEBVIEW_NAV_ERR_CONNECTION: + e = "wxWEBVIEW_NAV_ERR_CONNECTION"; + break; + case wxWEBVIEW_NAV_ERR_CERTIFICATE: + e = "wxWEBVIEW_NAV_ERR_CERTIFICATE"; + break; + case wxWEBVIEW_NAV_ERR_AUTH: + e = "wxWEBVIEW_NAV_ERR_AUTH"; + break; + case wxWEBVIEW_NAV_ERR_SECURITY: + e = "wxWEBVIEW_NAV_ERR_SECURITY"; + break; + case wxWEBVIEW_NAV_ERR_NOT_FOUND: + e = "wxWEBVIEW_NAV_ERR_NOT_FOUND"; + break; + case wxWEBVIEW_NAV_ERR_REQUEST: + e = "wxWEBVIEW_NAV_ERR_REQUEST"; + break; + case wxWEBVIEW_NAV_ERR_USER_CANCELLED: + e = "wxWEBVIEW_NAV_ERR_USER_CANCELLED"; + break; + case wxWEBVIEW_NAV_ERR_OTHER: + e = "wxWEBVIEW_NAV_ERR_OTHER"; + break; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": error loading page %1% %2% %3% %4%") %evt.GetURL() %evt.GetTarget() %e %evt.GetString(); +} + + + +} // GUI +} // Slic3r diff --git a/src/slic3r/GUI/PrinterWebView.hpp b/src/slic3r/GUI/PrinterWebView.hpp new file mode 100644 index 000000000..bdf5f4c68 --- /dev/null +++ b/src/slic3r/GUI/PrinterWebView.hpp @@ -0,0 +1,54 @@ +#ifndef slic3r_PrinterWebView_hpp_ +#define slic3r_PrinterWebView_hpp_ + + +#include "wx/artprov.h" +#include "wx/cmdline.h" +#include "wx/notifmsg.h" +#include "wx/settings.h" +#include "wx/webview.h" + +#if wxUSE_WEBVIEW_EDGE +#include "wx/msw/webview_edge.h" +#endif + +#include "wx/webviewarchivehandler.h" +#include "wx/webviewfshandler.h" +#include "wx/numdlg.h" +#include "wx/infobar.h" +#include "wx/filesys.h" +#include "wx/fs_arc.h" +#include "wx/fs_mem.h" +#include "wx/stdpaths.h" +#include +#include +#include "wx/textctrl.h" +#include + + +namespace Slic3r { +namespace GUI { + + +class PrinterWebView : public wxPanel { +public: + PrinterWebView(wxWindow *parent); + virtual ~PrinterWebView(); + + void load_url(wxString& url); + void UpdateState(); + void OnClose(wxCloseEvent& evt); + void OnError(wxWebViewEvent& evt); + +private: + + wxWebView* m_browser; + long m_zoomFactor; + + // DECLARE_EVENT_TABLE() +}; + +} // GUI +} // Slic3r + +#endif /* slic3r_Tab_hpp_ */ diff --git a/src/slic3r/GUI/Widgets/ComboBox.cpp b/src/slic3r/GUI/Widgets/ComboBox.cpp index 1ba91ddf7..cbc7cedd4 100644 --- a/src/slic3r/GUI/Widgets/ComboBox.cpp +++ b/src/slic3r/GUI/Widgets/ComboBox.cpp @@ -75,6 +75,10 @@ void ComboBox::SetSelection(int n) if (drop.selection >= 0) SetIcon(icons[drop.selection]); } +void ComboBox::SelectAndNotify(int n) { + SetSelection(n); + sendComboBoxEvent(); +} void ComboBox::Rescale() { diff --git a/src/slic3r/GUI/Widgets/ComboBox.hpp b/src/slic3r/GUI/Widgets/ComboBox.hpp index 7158cb2fb..bac352307 100644 --- a/src/slic3r/GUI/Widgets/ComboBox.hpp +++ b/src/slic3r/GUI/Widgets/ComboBox.hpp @@ -43,6 +43,8 @@ public: void SetSelection(int n) override; + void SelectAndNotify(int n); + virtual void Rescale() override; wxString GetValue() const; diff --git a/src/slic3r/Utils/AstroBox.cpp b/src/slic3r/Utils/AstroBox.cpp new file mode 100644 index 000000000..8781549a2 --- /dev/null +++ b/src/slic3r/Utils/AstroBox.cpp @@ -0,0 +1,173 @@ +#include "AstroBox.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "libslic3r/PrintConfig.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "Http.hpp" + + +namespace fs = boost::filesystem; +namespace pt = boost::property_tree; + + +namespace Slic3r { + +AstroBox::AstroBox(DynamicPrintConfig *config) : + host(config->opt_string("print_host")), + apikey(config->opt_string("printhost_apikey")), + cafile(config->opt_string("printhost_cafile")) +{} + +const char* AstroBox::get_name() const { return "AstroBox"; } + +bool AstroBox::test(wxString &msg) const +{ + // Since the request is performed synchronously here, + // it is ok to refer to `msg` from within the closure + + const char *name = get_name(); + + bool res = true; + auto url = make_url("api/version"); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get version at: %2%") % name % url; + + auto http = Http::get(std::move(url)); + set_auth(http); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting version: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + res = false; + msg = format_error(body, error, status); + }) + .on_complete([&, this](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got version: %2%") % name % body; + + try { + std::stringstream ss(body); + pt::ptree ptree; + pt::read_json(ss, ptree); + + if (! ptree.get_optional("api")) { + res = false; + return; + } + + const auto text = ptree.get_optional("text"); + res = validate_version_text(text); + if (! res) { + msg = GUI::from_u8((boost::format(_utf8(L("Mismatched type of print host: %s"))) % (text ? *text : "AstroBox")).str()); + } + } + catch (const std::exception &) { + res = false; + msg = "Could not parse server response"; + } + }) + .perform_sync(); + + return res; +} + +wxString AstroBox::get_test_ok_msg () const +{ + return _(L("Connection to AstroBox works correctly.")); +} + +wxString AstroBox::get_test_failed_msg (wxString &msg) const +{ + return GUI::from_u8((boost::format("%s: %s\n\n%s") + % _utf8(L("Could not connect to AstroBox")) + % std::string(msg.ToUTF8()) + % _utf8(L("Note: AstroBox version at least 1.1.0 is required."))).str()); +} + +bool AstroBox::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const +{ + const char *name = get_name(); + + const auto upload_filename = upload_data.upload_path.filename(); + const auto upload_parent_path = upload_data.upload_path.parent_path(); + + wxString test_msg; + if (! test(test_msg)) { + error_fn(std::move(test_msg)); + return false; + } + + bool res = true; + + auto url = make_url("api/files/local"); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%") + % name + % upload_data.source_path + % url + % upload_filename.string() + % upload_parent_path.string() + % (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false"); + + auto http = Http::post(std::move(url)); + set_auth(http); + http.form_add("print", upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false") + .form_add("path", upload_parent_path.string()) // XXX: slashes on windows ??? + .form_add_file("file", upload_data.source_path.string(), upload_filename.string()) + .on_complete([&](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: File uploaded: HTTP %2%: %3%") % name % status % body; + }) + .on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error uploading file: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + error_fn(format_error(body, error, status)); + res = false; + }) + .on_progress([&](Http::Progress progress, bool &cancel) { + prorgess_fn(std::move(progress), cancel); + if (cancel) { + // Upload was canceled + BOOST_LOG_TRIVIAL(info) << "AstroBox: Upload canceled"; + res = false; + } + }) + .perform_sync(); + + return res; +} + +bool AstroBox::validate_version_text(const boost::optional &version_text) const +{ + return version_text ? boost::starts_with(*version_text, "AstroBox") : true; +} + +void AstroBox::set_auth(Http &http) const +{ + http.header("X-Api-Key", apikey); + + if (! cafile.empty()) { + http.ca_file(cafile); + } +} + +std::string AstroBox::make_url(const std::string &path) const +{ + if (host.find("http://") == 0 || host.find("https://") == 0) { + if (host.back() == '/') { + return (boost::format("%1%%2%") % host % path).str(); + } else { + return (boost::format("%1%/%2%") % host % path).str(); + } + } else { + return (boost::format("http://%1%/%2%") % host % path).str(); + } +} + +} diff --git a/src/slic3r/Utils/AstroBox.hpp b/src/slic3r/Utils/AstroBox.hpp new file mode 100644 index 000000000..15a8863a9 --- /dev/null +++ b/src/slic3r/Utils/AstroBox.hpp @@ -0,0 +1,46 @@ +#ifndef slic3r_AstroBox_hpp_ +#define slic3r_AstroBox_hpp_ + +#include +#include +#include + +#include "PrintHost.hpp" + +namespace Slic3r { + +class DynamicPrintConfig; +class Http; + +class AstroBox : public PrintHost +{ +public: + AstroBox(DynamicPrintConfig *config); + ~AstroBox() override = default; + + const char* get_name() const override; + + bool test(wxString &curl_msg) const override; + wxString get_test_ok_msg () const override; + wxString get_test_failed_msg (wxString &msg) const override; + bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override; + bool has_auto_discovery() const override { return true; } + bool can_test() const override { return true; } + PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } + std::string get_host() const override { return host; } + +protected: + bool validate_version_text(const boost::optional &version_text) const; + +private: + std::string host; + std::string apikey; + std::string cafile; + + void set_auth(Http &http) const; + std::string make_url(const std::string &path) const; +}; + +} + +#endif diff --git a/src/slic3r/Utils/Duet.cpp b/src/slic3r/Utils/Duet.cpp new file mode 100644 index 000000000..3293a3ff2 --- /dev/null +++ b/src/slic3r/Utils/Duet.cpp @@ -0,0 +1,286 @@ +#include "Duet.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/PrintConfig.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "Http.hpp" + +namespace fs = boost::filesystem; +namespace pt = boost::property_tree; + +namespace Slic3r { + +Duet::Duet(DynamicPrintConfig *config) : + host(config->opt_string("print_host")), + password(config->opt_string("printhost_apikey")) +{} + +const char* Duet::get_name() const { return "Duet"; } + +bool Duet::test(wxString &msg) const +{ + auto connectionType = connect(msg); + disconnect(connectionType); + + return connectionType != ConnectionType::error; +} + +wxString Duet::get_test_ok_msg () const +{ + return _(L("Connection to Duet works correctly.")); +} + +wxString Duet::get_test_failed_msg (wxString &msg) const +{ + return GUI::from_u8((boost::format("%s: %s") + % _utf8(L("Could not connect to Duet")) + % std::string(msg.ToUTF8())).str()); +} + +bool Duet::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const +{ + wxString connect_msg; + auto connectionType = connect(connect_msg); + if (connectionType == ConnectionType::error) { + error_fn(std::move(connect_msg)); + return false; + } + + bool res = true; + bool dsf = (connectionType == ConnectionType::dsf); + + auto upload_cmd = get_upload_url(upload_data.upload_path.string(), connectionType); + BOOST_LOG_TRIVIAL(info) << boost::format("Duet: Uploading file %1%, filepath: %2%, post_action: %3%, command: %4%") + % upload_data.source_path + % upload_data.upload_path + % int(upload_data.post_action) + % upload_cmd; + + auto http = (dsf ? Http::put(std::move(upload_cmd)) : Http::post(std::move(upload_cmd))); + if (dsf) { + http.set_put_body(upload_data.source_path); + } else { + http.set_post_body(upload_data.source_path); + } + http.on_complete([&](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("Duet: File uploaded: HTTP %1%: %2%") % status % body; + + int err_code = dsf ? (status == 201 ? 0 : 1) : get_err_code_from_body(body); + if (err_code != 0) { + BOOST_LOG_TRIVIAL(error) << boost::format("Duet: Request completed but error code was received: %1%") % err_code; + error_fn(format_error(body, L("Unknown error occured"), 0)); + res = false; + } else if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) { + wxString errormsg; + res = start_print(errormsg, upload_data.upload_path.string(), connectionType, false); + if (! res) { + error_fn(std::move(errormsg)); + } + } else if (upload_data.post_action == PrintHostPostUploadAction::StartSimulation) { + wxString errormsg; + res = start_print(errormsg, upload_data.upload_path.string(), connectionType, true); + if (! res) { + error_fn(std::move(errormsg)); + } + } + }) + .on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("Duet: Error uploading file: %1%, HTTP %2%, body: `%3%`") % error % status % body; + error_fn(format_error(body, error, status)); + res = false; + }) + .on_progress([&](Http::Progress progress, bool &cancel) { + prorgess_fn(std::move(progress), cancel); + if (cancel) { + // Upload was canceled + BOOST_LOG_TRIVIAL(info) << "Duet: Upload canceled"; + res = false; + } + }) + .perform_sync(); + + disconnect(connectionType); + + return res; +} + +Duet::ConnectionType Duet::connect(wxString &msg) const +{ + auto res = ConnectionType::error; + auto url = get_connect_url(false); + + auto http = Http::get(std::move(url)); + http.on_error([&](std::string body, std::string error, unsigned status) { + auto dsfUrl = get_connect_url(true); + auto dsfHttp = Http::get(std::move(dsfUrl)); + dsfHttp.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("Duet: Error connecting: %1%, HTTP %2%, body: `%3%`") % error % status % body; + msg = format_error(body, error, status); + }) + .on_complete([&](std::string body, unsigned) { + res = ConnectionType::dsf; + }) + .perform_sync(); + }) + .on_complete([&](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("Duet: Got: %1%") % body; + + int err_code = get_err_code_from_body(body); + switch (err_code) { + case 0: + res = ConnectionType::rrf; + break; + case 1: + msg = format_error(body, L("Wrong password"), 0); + break; + case 2: + msg = format_error(body, L("Could not get resources to create a new connection"), 0); + break; + default: + msg = format_error(body, L("Unknown error occured"), 0); + break; + } + + }) + .perform_sync(); + + return res; +} + +void Duet::disconnect(ConnectionType connectionType) const +{ + // we don't need to disconnect from DSF or if it failed anyway + if (connectionType != ConnectionType::rrf) { + return; + } + auto url = (boost::format("%1%rr_disconnect") + % get_base_url()).str(); + + auto http = Http::get(std::move(url)); + http.on_error([&](std::string body, std::string error, unsigned status) { + // we don't care about it, if disconnect is not working Duet will disconnect automatically after some time + BOOST_LOG_TRIVIAL(error) << boost::format("Duet: Error disconnecting: %1%, HTTP %2%, body: `%3%`") % error % status % body; + }) + .perform_sync(); +} + +std::string Duet::get_upload_url(const std::string &filename, ConnectionType connectionType) const +{ + assert(connectionType != ConnectionType::error); + + if (connectionType == ConnectionType::dsf) { + return (boost::format("%1%machine/file/gcodes/%2%") + % get_base_url() + % Http::url_encode(filename)).str(); + } else { + return (boost::format("%1%rr_upload?name=0:/gcodes/%2%&%3%") + % get_base_url() + % Http::url_encode(filename) + % timestamp_str()).str(); + } +} + +std::string Duet::get_connect_url(const bool dsfUrl) const +{ + if (dsfUrl) { + return (boost::format("%1%machine/status") + % get_base_url()).str(); + } else { + return (boost::format("%1%rr_connect?password=%2%&%3%") + % get_base_url() + % (password.empty() ? "reprap" : password) + % timestamp_str()).str(); + } +} + +std::string Duet::get_base_url() const +{ + if (host.find("http://") == 0 || host.find("https://") == 0) { + if (host.back() == '/') { + return host; + } else { + return (boost::format("%1%/") % host).str(); + } + } else { + return (boost::format("http://%1%/") % host).str(); + } +} + +std::string Duet::timestamp_str() const +{ + enum { BUFFER_SIZE = 32 }; + + auto t = std::time(nullptr); + auto tm = *std::localtime(&t); + + char buffer[BUFFER_SIZE]; + std::strftime(buffer, BUFFER_SIZE, "time=%Y-%m-%dT%H:%M:%S", &tm); + + return std::string(buffer); +} + +bool Duet::start_print(wxString &msg, const std::string &filename, ConnectionType connectionType, bool simulationMode) const +{ + assert(connectionType != ConnectionType::error); + + bool res = false; + bool dsf = (connectionType == ConnectionType::dsf); + + auto url = dsf + ? (boost::format("%1%machine/code") + % get_base_url()).str() + : (boost::format(simulationMode + ? "%1%rr_gcode?gcode=M37%%20P\"0:/gcodes/%2%\"" + : "%1%rr_gcode?gcode=M32%%20\"0:/gcodes/%2%\"") + % get_base_url() + % Http::url_encode(filename)).str(); + + auto http = (dsf ? Http::post(std::move(url)) : Http::get(std::move(url))); + if (dsf) { + http.set_post_body( + (boost::format(simulationMode + ? "M37 P\"0:/gcodes/%1%\"" + : "M32 \"0:/gcodes/%1%\"") + % filename).str() + ); + } + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("Duet: Error starting print: %1%, HTTP %2%, body: `%3%`") % error % status % body; + msg = format_error(body, error, status); + }) + .on_complete([&](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("Duet: Got: %1%") % body; + res = true; + }) + .perform_sync(); + + return res; +} + +int Duet::get_err_code_from_body(const std::string &body) const +{ + pt::ptree root; + std::istringstream iss (body); // wrap returned json to istringstream + pt::read_json(iss, root); + + return root.get("err", 0); +} + +} diff --git a/src/slic3r/Utils/Duet.hpp b/src/slic3r/Utils/Duet.hpp new file mode 100644 index 000000000..edca66ce0 --- /dev/null +++ b/src/slic3r/Utils/Duet.hpp @@ -0,0 +1,48 @@ +#ifndef slic3r_Duet_hpp_ +#define slic3r_Duet_hpp_ + +#include +#include + +#include "PrintHost.hpp" + +namespace Slic3r { + +class DynamicPrintConfig; +class Http; + +class Duet : public PrintHost +{ +public: + explicit Duet(DynamicPrintConfig *config); + ~Duet() override = default; + + const char* get_name() const override; + + bool test(wxString &curl_msg) const override; + wxString get_test_ok_msg() const override; + wxString get_test_failed_msg(wxString &msg) const override; + bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override; + bool has_auto_discovery() const override { return false; } + bool can_test() const override { return true; } + PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint | PrintHostPostUploadAction::StartSimulation; } + std::string get_host() const override { return host; } + +private: + enum class ConnectionType { rrf, dsf, error }; + std::string host; + std::string password; + + std::string get_upload_url(const std::string &filename, ConnectionType connectionType) const; + std::string get_connect_url(const bool dsfUrl) const; + std::string get_base_url() const; + std::string timestamp_str() const; + ConnectionType connect(wxString &msg) const; + void disconnect(ConnectionType connectionType) const; + bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType, bool simulationMode) const; + int get_err_code_from_body(const std::string &body) const; +}; + +} + +#endif diff --git a/src/slic3r/Utils/FlashAir.cpp b/src/slic3r/Utils/FlashAir.cpp new file mode 100644 index 000000000..2337ac290 --- /dev/null +++ b/src/slic3r/Utils/FlashAir.cpp @@ -0,0 +1,229 @@ +#include "FlashAir.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/PrintConfig.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "Http.hpp" + +namespace fs = boost::filesystem; +namespace pt = boost::property_tree; + +namespace Slic3r { + +FlashAir::FlashAir(DynamicPrintConfig *config) : + host(config->opt_string("print_host")) +{} + +const char* FlashAir::get_name() const { return "FlashAir"; } + +bool FlashAir::test(wxString &msg) const +{ + // Since the request is performed synchronously here, + // it is ok to refer to `msg` from within the closure + + const char *name = get_name(); + + bool res = false; + auto url = make_url("command.cgi", "op", "118"); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get upload enabled at: %2%") % name % url; + + auto http = Http::get(std::move(url)); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting upload enabled: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + res = false; + msg = format_error(body, error, status); + }) + .on_complete([&](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got upload enabled: %2%") % name % body; + + res = boost::starts_with(body, "1"); + if (! res) { + msg = _(L("Upload not enabled on FlashAir card.")); + } + }) + .perform_sync(); + + return res; +} + +wxString FlashAir::get_test_ok_msg () const +{ + return _(L("Connection to FlashAir works correctly and upload is enabled.")); +} + +wxString FlashAir::get_test_failed_msg (wxString &msg) const +{ + return GUI::from_u8((boost::format("%s: %s\n%s") + % _utf8(L("Could not connect to FlashAir")) + % std::string(msg.ToUTF8()) + % _utf8(L("Note: FlashAir with firmware 2.00.02 or newer and activated upload function is required."))).str()); +} + +bool FlashAir::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const +{ + const char *name = get_name(); + + const auto upload_filename = upload_data.upload_path.filename(); + const auto upload_parent_path = upload_data.upload_path.parent_path(); + wxString test_msg; + if (! test(test_msg)) { + error_fn(std::move(test_msg)); + return false; + } + + bool res = false; + + std::string strDest = upload_parent_path.string(); + if (strDest.front()!='/') // Needs a leading / else root uploads fail. + { + strDest.insert(0,"/"); + } + + auto urlPrepare = make_url("upload.cgi", "WRITEPROTECT=ON&FTIME", timestamp_str()); + auto urlSetDir = make_url("upload.cgi","UPDIR",strDest); + auto urlUpload = make_url("upload.cgi"); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3% / %4%, filename: %5%") + % name + % upload_data.source_path + % urlPrepare + % urlUpload + % upload_filename.string(); + + // set filetime for upload and make card writeprotect to prevent filesystem damage + auto httpPrepare = Http::get(std::move(urlPrepare)); + httpPrepare.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error preparing upload: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + error_fn(format_error(body, error, status)); + res = false; + }) + .on_complete([&, this](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got prepare result: %2%") % name % body; + res = boost::icontains(body, "SUCCESS"); + if (! res) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Request completed but no SUCCESS message was received.") % name; + error_fn(format_error(body, L("Unknown error occured"), 0)); + } + }) + .perform_sync(); + + if(! res ) { + return res; + } + + // start file upload + auto httpDir = Http::get(std::move(urlSetDir)); + httpDir.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error setting upload dir: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + error_fn(format_error(body, error, status)); + res = false; + }) + .on_complete([&, this](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got dir select result: %2%") % name % body; + res = boost::icontains(body, "SUCCESS"); + if (! res) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Request completed but no SUCCESS message was received.") % name; + error_fn(format_error(body, L("Unknown error occured"), 0)); + } + }) + .perform_sync(); + + if(! res ) { + return res; + } + + auto http = Http::post(std::move(urlUpload)); + http.form_add_file("file", upload_data.source_path.string(), upload_filename.string()) + .on_complete([&](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: File uploaded: HTTP %2%: %3%") % name % status % body; + res = boost::icontains(body, "SUCCESS"); + if (! res) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Request completed but no SUCCESS message was received.") % name; + error_fn(format_error(body, L("Unknown error occured"), 0)); + } + }) + .on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error uploading file: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + error_fn(format_error(body, error, status)); + res = false; + }) + .on_progress([&](Http::Progress progress, bool &cancel) { + prorgess_fn(std::move(progress), cancel); + if (cancel) { + // Upload was canceled + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Upload canceled") % name; + res = false; + } + }) + .perform_sync(); + + return res; +} + +std::string FlashAir::timestamp_str() const +{ + auto t = std::time(nullptr); + auto tm = *std::localtime(&t); + + unsigned long fattime = ((tm.tm_year - 80) << 25) | + ((tm.tm_mon + 1) << 21) | + (tm.tm_mday << 16) | + (tm.tm_hour << 11) | + (tm.tm_min << 5) | + (tm.tm_sec >> 1); + + return (boost::format("%1$#x") % fattime).str(); +} + +std::string FlashAir::make_url(const std::string &path) const +{ + if (host.find("http://") == 0 || host.find("https://") == 0) { + if (host.back() == '/') { + return (boost::format("%1%%2%") % host % path).str(); + } else { + return (boost::format("%1%/%2%") % host % path).str(); + } + } else { + if (host.back() == '/') { + return (boost::format("http://%1%%2%") % host % path).str(); + } else { + return (boost::format("http://%1%/%2%") % host % path).str(); + } + } +} + +std::string FlashAir::make_url(const std::string &path, const std::string &arg, const std::string &val) const +{ + if (host.find("http://") == 0 || host.find("https://") == 0) { + if (host.back() == '/') { + return (boost::format("%1%%2%?%3%=%4%") % host % path % arg % val).str(); + } else { + return (boost::format("%1%/%2%?%3%=%4%") % host % path % arg % val).str(); + } + } else { + if (host.back() == '/') { + return (boost::format("http://%1%%2%?%3%=%4%") % host % path % arg % val).str(); + } else { + return (boost::format("http://%1%/%2%?%3%=%4%") % host % path % arg % val).str(); + } + } +} + +} diff --git a/src/slic3r/Utils/FlashAir.hpp b/src/slic3r/Utils/FlashAir.hpp new file mode 100644 index 000000000..14e3f0015 --- /dev/null +++ b/src/slic3r/Utils/FlashAir.hpp @@ -0,0 +1,42 @@ +#ifndef slic3r_FlashAir_hpp_ +#define slic3r_FlashAir_hpp_ + +#include +#include + +#include "PrintHost.hpp" + + +namespace Slic3r { + +class DynamicPrintConfig; +class Http; + +class FlashAir : public PrintHost +{ +public: + FlashAir(DynamicPrintConfig *config); + ~FlashAir() override = default; + + const char* get_name() const override; + + bool test(wxString &curl_msg) const override; + wxString get_test_ok_msg() const override; + wxString get_test_failed_msg(wxString &msg) const override; + bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override; + bool has_auto_discovery() const override { return false; } + bool can_test() const override { return true; } + PrintHostPostUploadActions get_post_upload_actions() const override { return {}; } + std::string get_host() const override { return host; } + +private: + std::string host; + + std::string timestamp_str() const; + std::string make_url(const std::string &path) const; + std::string make_url(const std::string &path, const std::string &arg, const std::string &val) const; +}; + +} + +#endif diff --git a/src/slic3r/Utils/MKS.cpp b/src/slic3r/Utils/MKS.cpp new file mode 100644 index 000000000..80a79537d --- /dev/null +++ b/src/slic3r/Utils/MKS.cpp @@ -0,0 +1,150 @@ +#include "MKS.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/PrintConfig.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "Http.hpp" + +namespace fs = boost::filesystem; +namespace pt = boost::property_tree; + +namespace Slic3r { + +MKS::MKS(DynamicPrintConfig* config) : + m_host(config->opt_string("print_host")), m_console_port("8080") +{} + +const char* MKS::get_name() const { return "MKS"; } + +bool MKS::test(wxString& msg) const +{ + Utils::TCPConsole console(m_host, m_console_port); + + console.enqueue_cmd("M105"); + bool ret = console.run_queue(); + + if (!ret) + msg = wxString::FromUTF8(console.error_message().c_str()); + + return ret; +} + +wxString MKS::get_test_ok_msg() const +{ + return _(L("Connection to MKS works correctly.")); +} + +wxString MKS::get_test_failed_msg(wxString& msg) const +{ + return GUI::from_u8((boost::format("%s: %s") + % _utf8(L("Could not connect to MKS")) + % std::string(msg.ToUTF8())).str()); +} + +bool MKS::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const +{ + bool res = true; + + auto upload_cmd = get_upload_url(upload_data.upload_path.string()); + BOOST_LOG_TRIVIAL(info) << boost::format("MKS: Uploading file %1%, filepath: %2%, print: %3%, command: %4%") + % upload_data.source_path + % upload_data.upload_path + % (upload_data.post_action == PrintHostPostUploadAction::StartPrint) + % upload_cmd; + + auto http = Http::post(std::move(upload_cmd)); + http.set_post_body(upload_data.source_path); + + http.on_complete([&](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("MKS: File uploaded: HTTP %1%: %2%") % status % body; + + int err_code = get_err_code_from_body(body); + if (err_code != 0) { + BOOST_LOG_TRIVIAL(error) << boost::format("MKS: Request completed but error code was received: %1%") % err_code; + error_fn(format_error(body, L("Unknown error occured"), 0)); + res = false; + } + else if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) { + wxString errormsg; + res = start_print(errormsg, upload_data.upload_path.string()); + if (!res) { + error_fn(std::move(errormsg)); + } + } + }) + .on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("MKS: Error uploading file: %1%, HTTP %2%, body: `%3%`") % error % status % body; + error_fn(format_error(body, error, status)); + res = false; + }) + .on_progress([&](Http::Progress progress, bool& cancel) { + prorgess_fn(std::move(progress), cancel); + if (cancel) { + // Upload was canceled + BOOST_LOG_TRIVIAL(info) << "MKS: Upload canceled"; + res = false; + } + }).perform_sync(); + + + return res; +} + +std::string MKS::get_upload_url(const std::string& filename) const +{ + return (boost::format("http://%1%/upload?X-Filename=%2%") + % m_host + % Http::url_encode(filename)).str(); +} + +bool MKS::start_print(wxString& msg, const std::string& filename) const +{ + // For some reason printer firmware does not want to respond on gcode commands immediately after file upload. + // So we just introduce artificial delay to workaround it. + // TODO: Inspect reasons + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + + Utils::TCPConsole console(m_host, m_console_port); + + console.enqueue_cmd(std::string("M23 ") + filename); + console.enqueue_cmd("M24"); + + bool ret = console.run_queue(); + + if (!ret) + msg = wxString::FromUTF8(console.error_message().c_str()); + + return ret; +} + +int MKS::get_err_code_from_body(const std::string& body) const +{ + pt::ptree root; + std::istringstream iss(body); // wrap returned json to istringstream + pt::read_json(iss, root); + + return root.get("err", 0); +} + +} // Slic3r diff --git a/src/slic3r/Utils/MKS.hpp b/src/slic3r/Utils/MKS.hpp new file mode 100644 index 000000000..22455436a --- /dev/null +++ b/src/slic3r/Utils/MKS.hpp @@ -0,0 +1,42 @@ +#ifndef slic3r_MKS_hpp_ +#define slic3r_MKS_hpp_ + +#include +#include + +#include "PrintHost.hpp" +#include "TCPConsole.hpp" + +namespace Slic3r { +class DynamicPrintConfig; +class Http; + +class MKS : public PrintHost +{ +public: + explicit MKS(DynamicPrintConfig* config); + ~MKS() override = default; + + const char* get_name() const override; + + bool test(wxString& curl_msg) const override; + wxString get_test_ok_msg() const override; + wxString get_test_failed_msg(wxString& msg) const override; + bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override; + bool has_auto_discovery() const override { return false; } + bool can_test() const override { return true; } + PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } + std::string get_host() const override { return m_host; } + +private: + std::string m_host; + std::string m_console_port; + + std::string get_upload_url(const std::string& filename) const; + bool start_print(wxString& msg, const std::string& filename) const; + int get_err_code_from_body(const std::string& body) const; +}; + +} + +#endif diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index aa50b1b1c..86f6101b6 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -14,11 +14,11 @@ #include "libslic3r/PrintConfig.hpp" #include "libslic3r/Channel.hpp" #include "OctoPrint.hpp" -//#include "Duet.hpp" -//#include "FlashAir.hpp" -//#include "AstroBox.hpp" -//#include "Repetier.hpp" -//#include "MKS.hpp" +#include "Duet.hpp" +#include "FlashAir.hpp" +#include "AstroBox.hpp" +#include "Repetier.hpp" +#include "MKS.hpp" #include "../GUI/PrintHostDialogs.hpp" namespace fs = boost::filesystem; @@ -47,12 +47,12 @@ PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config) switch (host_type) { case htOctoPrint: return new OctoPrint(config); - //case htDuet: return new Duet(config); - //case htFlashAir: return new FlashAir(config); - //case htAstroBox: return new AstroBox(config); - //case htRepetier: return new Repetier(config); + case htDuet: return new Duet(config); + case htFlashAir: return new FlashAir(config); + case htAstroBox: return new AstroBox(config); + case htRepetier: return new Repetier(config); case htPrusaLink: return new PrusaLink(config); - //case htMKS: return new MKS(config); + case htMKS: return new MKS(config); default: return nullptr; } } else { diff --git a/src/slic3r/Utils/Repetier.cpp b/src/slic3r/Utils/Repetier.cpp new file mode 100644 index 000000000..63f438428 --- /dev/null +++ b/src/slic3r/Utils/Repetier.cpp @@ -0,0 +1,274 @@ +#include "Repetier.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + + +#include "libslic3r/PrintConfig.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/format.hpp" +#include "Http.hpp" + + +namespace fs = boost::filesystem; +namespace pt = boost::property_tree; + + +namespace Slic3r { + +Repetier::Repetier(DynamicPrintConfig *config) : + host(config->opt_string("print_host")), + apikey(config->opt_string("printhost_apikey")), + cafile(config->opt_string("printhost_cafile")), + port(config->opt_string("printhost_port")) +{} + +const char* Repetier::get_name() const { return "Repetier"; } + +bool Repetier::test(wxString &msg) const +{ + // Since the request is performed synchronously here, + // it is ok to refer to `msg` from within the closure + + const char *name = get_name(); + + bool res = true; + auto url = make_url("printer/info"); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: List version at: %2%") % name % url; + + auto http = Http::get(std::move(url)); + set_auth(http); + + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting version: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + res = false; + msg = format_error(body, error, status); + }) + .on_complete([&, this](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got version: %2%") % name % body; + + try { + std::stringstream ss(body); + pt::ptree ptree; + pt::read_json(ss, ptree); + + const auto text = ptree.get_optional("name"); + res = validate_version_text(text); + if (! res) { + msg = GUI::from_u8((boost::format(_utf8(L("Mismatched type of print host: %s"))) % (text ? *text : "Repetier")).str()); + } + } + catch (const std::exception &) { + res = false; + msg = "Could not parse server response"; + } + }) + .perform_sync(); + + return res; +} + +wxString Repetier::get_test_ok_msg () const +{ + return _(L("Connection to Repetier works correctly.")); +} + +wxString Repetier::get_test_failed_msg (wxString &msg) const +{ + return GUI::from_u8((boost::format("%s: %s\n\n%s") + % _utf8(L("Could not connect to Repetier")) + % std::string(msg.ToUTF8()) + % _utf8(L("Note: Repetier version at least 0.90.0 is required."))).str()); +} + +bool Repetier::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const +{ + const char *name = get_name(); + + const auto upload_filename = upload_data.upload_path.filename(); + const auto upload_parent_path = upload_data.upload_path.parent_path(); + + wxString test_msg; + if (! test(test_msg)) { + error_fn(std::move(test_msg)); + return false; + } + + bool res = true; + + auto url = upload_data.post_action == PrintHostPostUploadAction::StartPrint + ? make_url((boost::format("printer/job/%1%") % port).str()) + : make_url((boost::format("printer/model/%1%") % port).str()); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%, group: %7%") + % name + % upload_data.source_path + % url + % upload_filename.string() + % upload_parent_path.string() + % (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false") + % upload_data.group; + + auto http = Http::post(std::move(url)); + set_auth(http); + + if (! upload_data.group.empty() && upload_data.group != _utf8(L("Default"))) { + http.form_add("group", upload_data.group); + } + + if(upload_data.post_action == PrintHostPostUploadAction::StartPrint) { + http.form_add("name", upload_filename.string()); + } + + http.form_add("a", "upload") + .form_add_file("filename", upload_data.source_path.string(), upload_filename.string()) + .on_complete([&](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: File uploaded: HTTP %2%: %3%") % name % status % body; + }) + .on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error uploading file: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + error_fn(format_error(body, error, status)); + res = false; + }) + .on_progress([&](Http::Progress progress, bool &cancel) { + prorgess_fn(std::move(progress), cancel); + if (cancel) { + // Upload was canceled + BOOST_LOG_TRIVIAL(info) << "Repetier: Upload canceled"; + res = false; + } + }) + .perform_sync(); + + return res; +} + +bool Repetier::validate_version_text(const boost::optional &version_text) const +{ + return version_text ? (!version_text->empty()) : true; +} + +void Repetier::set_auth(Http &http) const +{ + http.header("X-Api-Key", apikey); + + if (! cafile.empty()) { + http.ca_file(cafile); + } +} + +std::string Repetier::make_url(const std::string &path) const +{ + if (host.find("http://") == 0 || host.find("https://") == 0) { + if (host.back() == '/') { + return (boost::format("%1%%2%") % host % path).str(); + } else { + return (boost::format("%1%/%2%") % host % path).str(); + } + } else { + return (boost::format("http://%1%/%2%") % host % path).str(); + } +} + +bool Repetier::get_groups(wxArrayString& groups) const +{ + bool res = true; + + const char *name = get_name(); + auto url = make_url((boost::format("printer/api/%1%") % port).str()); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get groups at: %2%") % name % url; + + auto http = Http::get(std::move(url)); + set_auth(http); + http.form_add("a", "listModelGroups"); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting version: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + }) + .on_complete([&](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got groups: %2%") % name % body; + + try { + std::stringstream ss(body); + pt::ptree ptree; + pt::read_json(ss, ptree); + + BOOST_FOREACH(boost::property_tree::ptree::value_type &v, ptree.get_child("groupNames.")) { + if (v.second.data() == "#") { + groups.push_back(_utf8(L("Default"))); + } else { + // Is it safe to assume that the data are utf-8 encoded? + groups.push_back(GUI::from_u8(v.second.data())); + } + } + } + catch (const std::exception &) { + //msg = "Could not parse server response"; + res = false; + } + }) + .perform_sync(); + + return res; +} + +bool Repetier::get_printers(wxArrayString& printers) const +{ + const char *name = get_name(); + + bool res = true; + auto url = make_url("printer/list"); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: List printers at: %2%") % name % url; + + auto http = Http::get(std::move(url)); + set_auth(http); + + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error listing printers: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + res = false; + }) + .on_complete([&](std::string body, unsigned http_status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got printers: %2%, HTTP status: %3%") % name % body % http_status; + + if (http_status != 200) + throw HostNetworkError(GUI::format(_L("HTTP status: %1%\nMessage body: \"%2%\""), http_status, body)); + + std::stringstream ss(body); + pt::ptree ptree; + try { + pt::read_json(ss, ptree); + } catch (const pt::ptree_error &err) { + throw HostNetworkError(GUI::format(_L("Parsing of host response failed.\nMessage body: \"%1%\"\nError: \"%2%\""), body, err.what())); + } + + const auto error = ptree.get_optional("error"); + if (error) + throw HostNetworkError(*error); + + try { + BOOST_FOREACH(boost::property_tree::ptree::value_type &v, ptree.get_child("data.")) { + const auto port = v.second.get("slug"); + printers.push_back(Slic3r::GUI::from_u8(port)); + } + } catch (const std::exception &err) { + throw HostNetworkError(GUI::format(_L("Enumeration of host printers failed.\nMessage body: \"%1%\"\nError: \"%2%\""), body, err.what())); + } + }) + .perform_sync(); + + return res; +} + +} diff --git a/src/slic3r/Utils/Repetier.hpp b/src/slic3r/Utils/Repetier.hpp new file mode 100644 index 000000000..6f3310260 --- /dev/null +++ b/src/slic3r/Utils/Repetier.hpp @@ -0,0 +1,51 @@ +#ifndef slic3r_Repetier_hpp_ +#define slic3r_Repetier_hpp_ + +#include +#include +#include + +#include "PrintHost.hpp" + +namespace Slic3r { + +class DynamicPrintConfig; +class Http; + +class Repetier : public PrintHost +{ +public: + Repetier(DynamicPrintConfig *config); + ~Repetier() override = default; + + const char* get_name() const override; + + bool test(wxString &curl_msg) const override; + wxString get_test_ok_msg () const override; + wxString get_test_failed_msg (wxString &msg) const override; + bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override; + bool has_auto_discovery() const override { return false; } + bool can_test() const override { return true; } + PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } + bool supports_multiple_printers() const override { return true; } + std::string get_host() const override { return host; } + + bool get_groups(wxArrayString &groups) const override; + bool get_printers(wxArrayString &printers) const override; + +protected: + virtual bool validate_version_text(const boost::optional &version_text) const; + +private: + std::string host; + std::string apikey; + std::string cafile; + std::string port; + + void set_auth(Http &http) const; + std::string make_url(const std::string &path) const; +}; + +} + +#endif