diff --git a/lib/Slic3r.pm b/lib/Slic3r.pm index 0c6c81bb5..5eaa0e522 100644 --- a/lib/Slic3r.pm +++ b/lib/Slic3r.pm @@ -167,6 +167,8 @@ sub thread_cleanup { *Slic3r::GUI::PresetHints::DESTROY = sub {}; *Slic3r::GUI::TabIface::DESTROY = sub {}; *Slic3r::OctoPrint::DESTROY = sub {}; + *Slic3r::Duet::DESTROY = sub {}; + *Slic3r::PrintHostFactory::DESTROY = sub {}; *Slic3r::PresetUpdater::DESTROY = sub {}; return undef; # this prevents a "Scalars leaked" warning } diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm index a0eef72fe..89f803228 100644 --- a/lib/Slic3r/GUI/Plater.pm +++ b/lib/Slic3r/GUI/Plater.pm @@ -53,7 +53,7 @@ sub new { my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); $self->{config} = Slic3r::Config::new_from_defaults_keys([qw( bed_shape complete_objects extruder_clearance_radius skirts skirt_distance brim_width variable_layer_height - serial_port serial_speed octoprint_host octoprint_apikey octoprint_cafile + serial_port serial_speed host_type print_host printhost_apikey printhost_cafile nozzle_diameter single_extruder_multi_material wipe_tower wipe_tower_x wipe_tower_y wipe_tower_width wipe_tower_rotation_angle extruder_colour filament_colour max_print_height printer_model )]); @@ -1569,7 +1569,7 @@ sub on_export_completed { $message = L("File added to print queue"); $do_print = 1; } elsif ($self->{send_gcode_file}) { - $message = L("Sending G-code file to the OctoPrint server..."); + $message = L("Sending G-code file to the Printer Host ..."); $send_gcode = 1; } else { $message = L("G-code file exported to ") . $self->{export_gcode_output_file}; @@ -1585,9 +1585,10 @@ sub on_export_completed { # Send $self->{send_gcode_file} to OctoPrint. if ($send_gcode) { - my $op = Slic3r::OctoPrint->new($self->{config}); - if ($op->send_gcode($self->{send_gcode_file})) { - $self->statusbar->SetStatusText(L("OctoPrint upload finished.")); + my $host = Slic3r::PrintHostFactory::get_print_host($self->{config}); + + if ($host->send_gcode($self->{send_gcode_file})) { + $self->statusbar->SetStatusText(L("Upload to host finished.")); } else { $self->statusbar->SetStatusText(""); } @@ -1914,8 +1915,8 @@ sub on_config_change { } elsif ($opt_key eq 'serial_port') { $self->{btn_print}->Show($config->get('serial_port')); $self->Layout; - } elsif ($opt_key eq 'octoprint_host') { - $self->{btn_send_gcode}->Show($config->get('octoprint_host')); + } elsif ($opt_key eq 'print_host') { + $self->{btn_send_gcode}->Show($config->get('print_host')); $self->Layout; } elsif ($opt_key eq 'variable_layer_height') { if ($config->get('variable_layer_height') != 1) { diff --git a/resources/profiles/PrusaResearch.ini b/resources/profiles/PrusaResearch.ini index 32ec800e7..7e358c77e 100644 --- a/resources/profiles/PrusaResearch.ini +++ b/resources/profiles/PrusaResearch.ini @@ -1007,8 +1007,8 @@ max_layer_height = 0.25 min_layer_height = 0.07 max_print_height = 200 nozzle_diameter = 0.4 -octoprint_apikey = -octoprint_host = +printhost_apikey = +print_host = printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK2\n printer_settings_id = retract_before_travel = 1 diff --git a/xs/CMakeLists.txt b/xs/CMakeLists.txt index d4306c525..998d44cb0 100644 --- a/xs/CMakeLists.txt +++ b/xs/CMakeLists.txt @@ -251,8 +251,14 @@ add_library(libslic3r_gui STATIC ${LIBDIR}/slic3r/Utils/Http.hpp ${LIBDIR}/slic3r/Utils/FixModelByWin10.cpp ${LIBDIR}/slic3r/Utils/FixModelByWin10.hpp + ${LIBDIR}/slic3r/Utils/PrintHostSendDialog.cpp + ${LIBDIR}/slic3r/Utils/PrintHostSendDialog.hpp ${LIBDIR}/slic3r/Utils/OctoPrint.cpp ${LIBDIR}/slic3r/Utils/OctoPrint.hpp + ${LIBDIR}/slic3r/Utils/Duet.cpp + ${LIBDIR}/slic3r/Utils/Duet.hpp + ${LIBDIR}/slic3r/Utils/PrintHostFactory.cpp + ${LIBDIR}/slic3r/Utils/PrintHostFactory.hpp ${LIBDIR}/slic3r/Utils/Bonjour.cpp ${LIBDIR}/slic3r/Utils/Bonjour.hpp ${LIBDIR}/slic3r/Utils/PresetUpdater.cpp @@ -411,7 +417,8 @@ set(XS_XSP_FILES ${XSP_DIR}/Surface.xsp ${XSP_DIR}/SurfaceCollection.xsp ${XSP_DIR}/TriangleMesh.xsp - ${XSP_DIR}/Utils_OctoPrint.xsp + ${XSP_DIR}/Utils_PrintHostFactory.xsp + ${XSP_DIR}/Utils_PrintHost.xsp ${XSP_DIR}/Utils_PresetUpdater.xsp ${XSP_DIR}/AppController.xsp ${XSP_DIR}/XS.xsp diff --git a/xs/src/libslic3r/PrintConfig.cpp b/xs/src/libslic3r/PrintConfig.cpp index a78e73fb5..794c27608 100644 --- a/xs/src/libslic3r/PrintConfig.cpp +++ b/xs/src/libslic3r/PrintConfig.cpp @@ -1137,24 +1137,36 @@ PrintConfigDef::PrintConfigDef() def->cli = "nozzle-diameter=f@"; def->default_value = new ConfigOptionFloats { 0.5 }; - def = this->add("octoprint_apikey", coString); - def->label = L("API Key"); - def->tooltip = L("Slic3r can upload G-code files to OctoPrint. This field should contain " - "the API Key required for authentication."); + def = this->add("host_type", coEnum); + def->label = L("Host Type"); + def->tooltip = L("Slic3r can upload G-code files to a printer host. This field must contain " + "the kind of the host."); + def->cli = "host-type=s"; + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("octoprint"); + def->enum_values.push_back("duet"); + def->enum_labels.push_back("OctoPrint"); + def->enum_labels.push_back("Duet"); + def->default_value = new ConfigOptionEnum(htOctoPrint); + + def = this->add("printhost_apikey", coString); + def->label = L("API Key / Password"); + def->tooltip = L("Slic3r can upload G-code files to a printer host. This field should contain " + "the API Key or the password required for authentication."); def->cli = "octoprint-apikey=s"; def->default_value = new ConfigOptionString(""); - def = this->add("octoprint_cafile", coString); + def = this->add("printhost_cafile", coString); def->label = "HTTPS CA file"; def->tooltip = "Custom CA certificate file can be specified for HTTPS OctoPrint connections, in crt/pem format. " "If left blank, the default OS CA certificate repository is used."; def->cli = "octoprint-cafile=s"; def->default_value = new ConfigOptionString(""); - def = this->add("octoprint_host", coString); + def = this->add("print_host", coString); def->label = L("Hostname, IP or URL"); - def->tooltip = L("Slic3r can upload G-code files to OctoPrint. This field should contain " - "the hostname, IP address or URL of the OctoPrint instance."); + def->tooltip = L("Slic3r can upload G-code files to a printer host. This field should contain " + "the hostname, IP address or URL of the printer host instance."); def->cli = "octoprint-host=s"; def->default_value = new ConfigOptionString(""); @@ -2107,10 +2119,6 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va std::ostringstream oss; oss << "0x0," << p.value.x << "x0," << p.value.x << "x" << p.value.y << ",0x" << p.value.y; value = oss.str(); -// Maybe one day we will rename octoprint_host to print_host as it has been done in the upstream Slic3r. -// Commenting this out fixes github issue #869 for now. -// } else if (opt_key == "octoprint_host" && !value.empty()) { -// opt_key = "print_host"; } else if ((opt_key == "perimeter_acceleration" && value == "25") || (opt_key == "infill_acceleration" && value == "50")) { /* For historical reasons, the world's full of configs having these very low values; diff --git a/xs/src/libslic3r/PrintConfig.hpp b/xs/src/libslic3r/PrintConfig.hpp index b18603d87..438e90681 100644 --- a/xs/src/libslic3r/PrintConfig.hpp +++ b/xs/src/libslic3r/PrintConfig.hpp @@ -27,6 +27,10 @@ enum GCodeFlavor { gcfSmoothie, gcfNoExtrusion, }; +enum PrintHostType { + htOctoPrint, htDuet, +}; + enum InfillPattern { ipRectilinear, ipGrid, ipTriangles, ipStars, ipCubic, ipLine, ipConcentric, ipHoneycomb, ip3DHoneycomb, ipGyroid, ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral, @@ -61,6 +65,15 @@ template<> inline t_config_enum_values& ConfigOptionEnum::get_enum_ return keys_map; } +template<> inline t_config_enum_values& ConfigOptionEnum::get_enum_values() { + static t_config_enum_values keys_map; + if (keys_map.empty()) { + keys_map["octoprint"] = htOctoPrint; + keys_map["duet"] = htDuet; + } + return keys_map; +} + template<> inline t_config_enum_values& ConfigOptionEnum::get_enum_values() { static t_config_enum_values keys_map; if (keys_map.empty()) { @@ -789,18 +802,20 @@ class HostConfig : public StaticPrintConfig { STATIC_PRINT_CONFIG_CACHE(HostConfig) public: - ConfigOptionString octoprint_host; - ConfigOptionString octoprint_apikey; - ConfigOptionString octoprint_cafile; + ConfigOptionEnum host_type; + ConfigOptionString print_host; + ConfigOptionString printhost_apikey; + ConfigOptionString printhost_cafile; ConfigOptionString serial_port; ConfigOptionInt serial_speed; protected: void initialize(StaticCacheBase &cache, const char *base_ptr) { - OPT_PTR(octoprint_host); - OPT_PTR(octoprint_apikey); - OPT_PTR(octoprint_cafile); + OPT_PTR(host_type); + OPT_PTR(print_host); + OPT_PTR(printhost_apikey); + OPT_PTR(printhost_cafile); OPT_PTR(serial_port); OPT_PTR(serial_speed); } diff --git a/xs/src/perlglue.cpp b/xs/src/perlglue.cpp index c8aadc8c3..997938a91 100644 --- a/xs/src/perlglue.cpp +++ b/xs/src/perlglue.cpp @@ -64,9 +64,10 @@ REGISTER_CLASS(PresetCollection, "GUI::PresetCollection"); REGISTER_CLASS(PresetBundle, "GUI::PresetBundle"); REGISTER_CLASS(TabIface, "GUI::Tab"); REGISTER_CLASS(PresetUpdater, "PresetUpdater"); -REGISTER_CLASS(OctoPrint, "OctoPrint"); REGISTER_CLASS(AppController, "AppController"); REGISTER_CLASS(PrintController, "PrintController"); +REGISTER_CLASS(PrintHost, "PrintHost"); +REGISTER_CLASS(PrintHostFactory, "PrintHostFactory"); SV* ConfigBase__as_hash(ConfigBase* THIS) { diff --git a/xs/src/slic3r/GUI/Field.cpp b/xs/src/slic3r/GUI/Field.cpp index 85fa790a5..757a18f11 100644 --- a/xs/src/slic3r/GUI/Field.cpp +++ b/xs/src/slic3r/GUI/Field.cpp @@ -586,6 +586,8 @@ boost::any& Choice::get_value() m_value = static_cast(ret_enum); else if (m_opt_id.compare("seam_position") == 0) m_value = static_cast(ret_enum); + else if (m_opt_id.compare("host_type") == 0) + m_value = static_cast(ret_enum); } return m_value; diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp index 8cd7ed776..8555f0b92 100644 --- a/xs/src/slic3r/GUI/GUI.cpp +++ b/xs/src/slic3r/GUI/GUI.cpp @@ -604,6 +604,8 @@ void change_opt_value(DynamicPrintConfig& config, const t_config_option_key& opt config.set_key_value(opt_key, new ConfigOptionEnum(boost::any_cast(value))); else if (opt_key.compare("seam_position") == 0) config.set_key_value(opt_key, new ConfigOptionEnum(boost::any_cast(value))); + else if (opt_key.compare("host_type") == 0) + config.set_key_value(opt_key, new ConfigOptionEnum(boost::any_cast(value))); } break; case coPoints:{ diff --git a/xs/src/slic3r/GUI/OptionsGroup.cpp b/xs/src/slic3r/GUI/OptionsGroup.cpp index d5cc29e19..a2d6559a9 100644 --- a/xs/src/slic3r/GUI/OptionsGroup.cpp +++ b/xs/src/slic3r/GUI/OptionsGroup.cpp @@ -459,8 +459,12 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config else if (opt_key.compare("support_material_pattern") == 0){ ret = static_cast(config.option>(opt_key)->value); } - else if (opt_key.compare("seam_position") == 0) + else if (opt_key.compare("seam_position") == 0){ ret = static_cast(config.option>(opt_key)->value); + } + else if (opt_key.compare("host_type") == 0){ + ret = static_cast(config.option>(opt_key)->value); + } } break; case coPoints: diff --git a/xs/src/slic3r/GUI/Preset.cpp b/xs/src/slic3r/GUI/Preset.cpp index 9f51f7b97..8335e48b5 100644 --- a/xs/src/slic3r/GUI/Preset.cpp +++ b/xs/src/slic3r/GUI/Preset.cpp @@ -329,8 +329,8 @@ const std::vector& Preset::printer_options() static std::vector s_opts; if (s_opts.empty()) { s_opts = { - "bed_shape", "z_offset", "gcode_flavor", "use_relative_e_distances", "serial_port", "serial_speed", - "octoprint_host", "octoprint_apikey", "octoprint_cafile", "use_firmware_retraction", "use_volumetric_e", "variable_layer_height", + "bed_shape", "z_offset", "gcode_flavor", "use_relative_e_distances", "serial_port", "serial_speed", "host_type", + "print_host", "printhost_apikey", "printhost_cafile", "use_firmware_retraction", "use_volumetric_e", "variable_layer_height", "single_extruder_multi_material", "start_gcode", "end_gcode", "before_layer_gcode", "layer_gcode", "toolchange_gcode", "between_objects_gcode", "printer_vendor", "printer_model", "printer_variant", "printer_notes", "cooling_tube_retraction", "cooling_tube_length", "parking_pos_retraction", "extra_loading_move", "max_print_height", "default_print_profile", "inherits", diff --git a/xs/src/slic3r/GUI/Tab.cpp b/xs/src/slic3r/GUI/Tab.cpp index 7c4322c5a..13b0ece5f 100644 --- a/xs/src/slic3r/GUI/Tab.cpp +++ b/xs/src/slic3r/GUI/Tab.cpp @@ -5,7 +5,8 @@ #include "../../libslic3r/Utils.hpp" #include "slic3r/Utils/Http.hpp" -#include "slic3r/Utils/OctoPrint.hpp" +#include "slic3r/Utils/PrintHostFactory.hpp" +#include "slic3r/Utils/PrintHost.hpp" #include "slic3r/Utils/Serial.hpp" #include "BonjourDialog.hpp" #include "WipeTowerDialog.hpp" @@ -1521,10 +1522,12 @@ void TabPrinter::build() optgroup->append_line(line); } - optgroup = page->new_optgroup(_(L("OctoPrint upload"))); + optgroup = page->new_optgroup(_(L("Printer Host upload"))); - auto octoprint_host_browse = [this, optgroup] (wxWindow* parent) { - auto btn = new wxButton(parent, wxID_ANY, _(L(" Browse "))+dots, wxDefaultPosition, wxDefaultSize, wxBU_LEFT); + optgroup->append_single_option_line("host_type"); + + auto printhost_browse = [this, optgroup] (wxWindow* parent) { + auto btn = m_printhost_browse_btn = new wxButton(parent, wxID_ANY, _(L(" Browse "))+dots, wxDefaultPosition, wxDefaultSize, wxBU_LEFT); btn->SetBitmap(wxBitmap(from_u8(Slic3r::var("zoom.png")), wxBITMAP_TYPE_PNG)); auto sizer = new wxBoxSizer(wxHORIZONTAL); sizer->Add(btn); @@ -1532,47 +1535,52 @@ void TabPrinter::build() btn->Bind(wxEVT_BUTTON, [this, parent, optgroup](wxCommandEvent e) { BonjourDialog dialog(parent); if (dialog.show_and_lookup()) { - optgroup->set_value("octoprint_host", std::move(dialog.get_selected()), true); + optgroup->set_value("print_host", std::move(dialog.get_selected()), true); } }); return sizer; }; - auto octoprint_host_test = [this](wxWindow* parent) { - auto btn = m_octoprint_host_test_btn = new wxButton(parent, wxID_ANY, _(L("Test")), + auto print_host_test = [this](wxWindow* parent) { + auto btn = m_print_host_test_btn = new wxButton(parent, wxID_ANY, _(L("Test")), wxDefaultPosition, wxDefaultSize, wxBU_LEFT | wxBU_EXACTFIT); btn->SetBitmap(wxBitmap(from_u8(Slic3r::var("wrench.png")), wxBITMAP_TYPE_PNG)); auto sizer = new wxBoxSizer(wxHORIZONTAL); sizer->Add(btn); btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent e) { - OctoPrint octoprint(m_config); - wxString msg; - if (octoprint.test(msg)) { - show_info(this, _(L("Connection to OctoPrint works correctly.")), _(L("Success!"))); - } else { - const auto text = wxString::Format("%s: %s\n\n%s", - _(L("Could not connect to OctoPrint")), msg, _(L("Note: OctoPrint version at least 1.1.0 is required.")) - ); + PrintHost *host = PrintHostFactory::get_print_host(m_config); + if (host == NULL) { + const auto text = wxString::Format("%s", + _(L("Could not get a valid Printer Host reference"))); show_error(this, text); + return; } + wxString msg; + if (host->test(msg)) { + show_info(this, host->get_test_ok_msg(), _(L("Success!"))); + } else { + show_error(this, host->get_test_failed_msg(msg)); + } + + delete (host); }); return sizer; }; - Line host_line = optgroup->create_single_option_line("octoprint_host"); - host_line.append_widget(octoprint_host_browse); - host_line.append_widget(octoprint_host_test); + Line host_line = optgroup->create_single_option_line("print_host"); + host_line.append_widget(printhost_browse); + host_line.append_widget(print_host_test); optgroup->append_line(host_line); - optgroup->append_single_option_line("octoprint_apikey"); + optgroup->append_single_option_line("printhost_apikey"); if (Http::ca_file_supported()) { - Line cafile_line = optgroup->create_single_option_line("octoprint_cafile"); + Line cafile_line = optgroup->create_single_option_line("printhost_cafile"); - auto octoprint_cafile_browse = [this, optgroup] (wxWindow* parent) { + auto printhost_cafile_browse = [this, optgroup] (wxWindow* parent) { auto btn = new wxButton(parent, wxID_ANY, _(L(" Browse "))+dots, wxDefaultPosition, wxDefaultSize, wxBU_LEFT); btn->SetBitmap(wxBitmap(from_u8(Slic3r::var("zoom.png")), wxBITMAP_TYPE_PNG)); auto sizer = new wxBoxSizer(wxHORIZONTAL); @@ -1582,17 +1590,17 @@ void TabPrinter::build() static const auto filemasks = _(L("Certificate files (*.crt, *.pem)|*.crt;*.pem|All files|*.*")); wxFileDialog openFileDialog(this, _(L("Open CA certificate file")), "", "", filemasks, wxFD_OPEN | wxFD_FILE_MUST_EXIST); if (openFileDialog.ShowModal() != wxID_CANCEL) { - optgroup->set_value("octoprint_cafile", std::move(openFileDialog.GetPath()), true); + optgroup->set_value("printhost_cafile", std::move(openFileDialog.GetPath()), true); } }); return sizer; }; - cafile_line.append_widget(octoprint_cafile_browse); + cafile_line.append_widget(printhost_cafile_browse); optgroup->append_line(cafile_line); - auto octoprint_cafile_hint = [this, optgroup] (wxWindow* parent) { + auto printhost_cafile_hint = [this, optgroup] (wxWindow* parent) { auto txt = new wxStaticText(parent, wxID_ANY, _(L("HTTPS CA file is optional. It is only needed if you use HTTPS with a self-signed certificate."))); auto sizer = new wxBoxSizer(wxHORIZONTAL); @@ -1602,7 +1610,7 @@ void TabPrinter::build() Line cafile_hint { "", "" }; cafile_hint.full_width = 1; - cafile_hint.widget = std::move(octoprint_cafile_hint); + cafile_hint.widget = std::move(printhost_cafile_hint); optgroup->append_line(cafile_hint); } @@ -1897,7 +1905,10 @@ void TabPrinter::update(){ m_serial_test_btn->Disable(); } - m_octoprint_host_test_btn->Enable(!m_config->opt_string("octoprint_host").empty()); + PrintHost *host = PrintHostFactory::get_print_host(m_config); + m_print_host_test_btn->Enable(!m_config->opt_string("print_host").empty() && host->can_test()); + m_printhost_browse_btn->Enable(host->have_auto_discovery()); + delete (host); bool have_multiple_extruders = m_extruders_count > 1; get_field("toolchange_gcode")->toggle(have_multiple_extruders); diff --git a/xs/src/slic3r/GUI/Tab.hpp b/xs/src/slic3r/GUI/Tab.hpp index 8b4eae7de..230fe659e 100644 --- a/xs/src/slic3r/GUI/Tab.hpp +++ b/xs/src/slic3r/GUI/Tab.hpp @@ -321,7 +321,8 @@ class TabPrinter : public Tab bool m_rebuild_kinematics_page = false; public: wxButton* m_serial_test_btn; - wxButton* m_octoprint_host_test_btn; + wxButton* m_print_host_test_btn; + wxButton* m_printhost_browse_btn; size_t m_extruders_count; size_t m_extruders_count_old = 0; diff --git a/xs/src/slic3r/Utils/Duet.cpp b/xs/src/slic3r/Utils/Duet.cpp new file mode 100644 index 000000000..aabc8eb40 --- /dev/null +++ b/xs/src/slic3r/Utils/Duet.cpp @@ -0,0 +1,281 @@ +#include "Duet.hpp" +#include "PrintHostSendDialog.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/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")) +{} + +bool Duet::test(wxString &msg) const +{ + bool connected = connect(msg); + if (connected) { + disconnect(); + } + + return connected; +} + +wxString Duet::get_test_ok_msg () const +{ + return wxString::Format("%s", _(L("Connection to Duet works correctly."))); +} + +wxString Duet::get_test_failed_msg (wxString &msg) const +{ + return wxString::Format("%s: %s", + _(L("Could not connect to Duet")), msg); +} + +bool Duet::send_gcode(const std::string &filename) const +{ + enum { PROGRESS_RANGE = 1000 }; + + const auto errortitle = _(L("Error while uploading to the Duet")); + fs::path filepath(filename); + + PrintHostSendDialog send_dialog(filepath.filename(), true); + if (send_dialog.ShowModal() != wxID_OK) { return false; } + + const bool print = send_dialog.print(); + const auto upload_filepath = send_dialog.filename(); + const auto upload_filename = upload_filepath.filename(); + const auto upload_parent_path = upload_filepath.parent_path(); + + wxProgressDialog progress_dialog( + _(L("Duet upload")), + _(L("Sending G-code file to Duet...")), + PROGRESS_RANGE, nullptr, wxPD_AUTO_HIDE | wxPD_APP_MODAL | wxPD_CAN_ABORT); + progress_dialog.Pulse(); + + wxString connect_msg; + if (!connect(connect_msg)) { + auto errormsg = wxString::Format("%s: %s", errortitle, connect_msg); + GUI::show_error(&progress_dialog, std::move(errormsg)); + return false; + } + + bool res = true; + + + auto upload_cmd = get_upload_url(upload_filepath.string()); + BOOST_LOG_TRIVIAL(info) << boost::format("Duet: Uploading file %1%, filename: %2%, path: %3%, print: %4%, command: %5%") + % filepath.string() + % upload_filename.string() + % upload_parent_path.string() + % print + % upload_cmd; + + auto http = Http::post(std::move(upload_cmd)); + http.postfield_add_file(filename) + .on_complete([&](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("Duet: File uploaded: HTTP %1%: %2%") % status % body; + progress_dialog.Update(PROGRESS_RANGE); + + int err_code = get_err_code_from_body(body); + switch (err_code) { + case 0: + break; + default: + auto msg = format_error(body, L("Unknown error occured"), 0); + GUI::show_error(&progress_dialog, std::move(msg)); + res = false; + break; + } + + if (err_code == 0 && print) { + wxString errormsg; + res = start_print(errormsg, upload_filepath.string()); + if (!res) { + GUI::show_error(&progress_dialog, 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; + auto errormsg = wxString::Format("%s: %s", errortitle, format_error(body, error, status)); + GUI::show_error(&progress_dialog, std::move(errormsg)); + res = false; + }) + .on_progress([&](Http::Progress progress, bool &cancel) { + if (cancel) { + // Upload was canceled + res = false; + } else if (progress.ultotal > 0) { + int value = PROGRESS_RANGE * progress.ulnow / progress.ultotal; + cancel = !progress_dialog.Update(std::min(value, PROGRESS_RANGE - 1)); // Cap the value to prevent premature dialog closing + } else { + cancel = !progress_dialog.Pulse(); + } + }) + .perform_sync(); + + disconnect(); + + return res; +} + +bool Duet::have_auto_discovery() const +{ + return false; +} + +bool Duet::can_test() const +{ + return true; +} + +bool Duet::connect(wxString &msg) const +{ + bool res = false; + auto url = get_connect_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("Duet: Error connecting: %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; + + int err_code = get_err_code_from_body(body); + switch (err_code) { + case 0: + res = true; + 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() const +{ + 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) const +{ + return (boost::format("%1%rr_upload?name=0:/gcodes/%2%&%3%") + % get_base_url() + % filename + % timestamp_str()).str(); +} + +std::string Duet::get_connect_url() const +{ + 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 +{ + auto t = std::time(nullptr); + auto tm = *std::localtime(&t); + std::stringstream ss; + ss << "time=" << std::put_time(&tm, "%Y-%d-%mT%H:%M:%S"); + + return ss.str(); +} + +wxString Duet::format_error(const std::string &body, const std::string &error, unsigned status) +{ + if (status != 0) { + auto wxbody = wxString::FromUTF8(body.data()); + return wxString::Format("HTTP %u: %s", status, wxbody); + } else { + return wxString::FromUTF8(error.data()); + } +} + +bool Duet::start_print(wxString &msg, const std::string &filename) const { + bool res = false; + auto url = (boost::format("%1%rr_gcode?gcode=M32%%20\"%2%\"") + % get_base_url() + % filename).str(); + + auto http = Http::get(std::move(url)); + 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/xs/src/slic3r/Utils/Duet.hpp b/xs/src/slic3r/Utils/Duet.hpp new file mode 100644 index 000000000..83ba0cbbb --- /dev/null +++ b/xs/src/slic3r/Utils/Duet.hpp @@ -0,0 +1,46 @@ +#ifndef slic3r_Duet_hpp_ +#define slic3r_Duet_hpp_ + +#include +#include + +#include "PrintHost.hpp" + + +namespace Slic3r { + + +class DynamicPrintConfig; +class Http; + +class Duet : public PrintHost +{ +public: + Duet(DynamicPrintConfig *config); + + bool test(wxString &curl_msg) const; + wxString get_test_ok_msg () const; + wxString get_test_failed_msg (wxString &msg) const; + // Send gcode file to duet, filename is expected to be in UTF-8 + bool send_gcode(const std::string &filename) const; + bool have_auto_discovery() const; + bool can_test() const; +private: + std::string host; + std::string password; + + std::string get_upload_url(const std::string &filename) const; + std::string get_connect_url() const; + std::string get_base_url() const; + std::string timestamp_str() const; + bool connect(wxString &msg) const; + void disconnect() const; + bool start_print(wxString &msg, const std::string &filename) const; + int get_err_code_from_body(const std::string &body) const; + static wxString format_error(const std::string &body, const std::string &error, unsigned status); +}; + + +} + +#endif diff --git a/xs/src/slic3r/Utils/Http.cpp b/xs/src/slic3r/Utils/Http.cpp index 37eb59a00..f5407b9fb 100644 --- a/xs/src/slic3r/Utils/Http.cpp +++ b/xs/src/slic3r/Utils/Http.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -42,6 +43,7 @@ struct Http::priv // Used for storing file streams added as multipart form parts // Using a deque here because unlike vector it doesn't ivalidate pointers on insertion std::deque form_files; + std::string postfields; size_t limit; bool cancel; @@ -60,6 +62,7 @@ struct Http::priv static size_t form_file_read_cb(char *buffer, size_t size, size_t nitems, void *userp); void form_add_file(const char *name, const fs::path &path, const char* filename); + void postfield_add_file(const fs::path &path); std::string curl_error(CURLcode curlcode); std::string body_size_error(); @@ -187,6 +190,16 @@ void Http::priv::form_add_file(const char *name, const fs::path &path, const cha } } +void Http::priv::postfield_add_file(const fs::path &path) +{ + std::ifstream f (path.string()); + std::string file_content { std::istreambuf_iterator(f), std::istreambuf_iterator() }; + if (!postfields.empty()) { + postfields += "&"; + } + postfields += file_content; +} + std::string Http::priv::curl_error(CURLcode curlcode) { return (boost::format("%1% (%2%)") @@ -229,6 +242,11 @@ void Http::priv::http_perform() ::curl_easy_setopt(curl, CURLOPT_HTTPPOST, form); } + if (!postfields.empty()) { + ::curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields.c_str()); + ::curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, postfields.size()); + } + CURLcode res = ::curl_easy_perform(curl); if (res != CURLE_OK) { @@ -338,6 +356,12 @@ Http& Http::form_add_file(const std::string &name, const fs::path &path, const s return *this; } +Http& Http::postfield_add_file(const fs::path &path) +{ + if (p) { p->postfield_add_file(path);} + return *this; +} + Http& Http::on_complete(CompleteFn fn) { if (p) { p->completefn = std::move(fn); } diff --git a/xs/src/slic3r/Utils/Http.hpp b/xs/src/slic3r/Utils/Http.hpp index ce4e438ca..cf5712d96 100644 --- a/xs/src/slic3r/Utils/Http.hpp +++ b/xs/src/slic3r/Utils/Http.hpp @@ -73,6 +73,9 @@ public: // Same as above except also override the file's filename with a custom one Http& form_add_file(const std::string &name, const boost::filesystem::path &path, const std::string &filename); + // Add the file as POSTFIELD to the request, this can be used for hosts which do not support multipart requests + Http& postfield_add_file(const boost::filesystem::path &path); + // Callback called on HTTP request complete Http& on_complete(CompleteFn fn); // Callback called on an error occuring at any stage of the requests: Url parsing, DNS lookup, diff --git a/xs/src/slic3r/Utils/OctoPrint.cpp b/xs/src/slic3r/Utils/OctoPrint.cpp index 97b4123d4..c62f9b55c 100644 --- a/xs/src/slic3r/Utils/OctoPrint.cpp +++ b/xs/src/slic3r/Utils/OctoPrint.cpp @@ -1,21 +1,11 @@ #include "OctoPrint.hpp" +#include "PrintHostSendDialog.hpp" #include -#include #include #include -#include -#include -#include -#include -#include -#include -#include - #include "libslic3r/PrintConfig.hpp" -#include "slic3r/GUI/GUI.hpp" -#include "slic3r/GUI/MsgDialog.hpp" #include "Http.hpp" namespace fs = boost::filesystem; @@ -23,47 +13,10 @@ namespace fs = boost::filesystem; namespace Slic3r { - -struct SendDialog : public GUI::MsgDialog -{ - wxTextCtrl *txt_filename; - wxCheckBox *box_print; - - SendDialog(const fs::path &path) : - MsgDialog(nullptr, _(L("Send G-Code to printer")), _(L("Upload to OctoPrint with the following filename:")), wxID_NONE), - txt_filename(new wxTextCtrl(this, wxID_ANY, path.filename().wstring())), - box_print(new wxCheckBox(this, wxID_ANY, _(L("Start printing after upload")))) - { - 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); - - content_sizer->Add(txt_filename, 0, wxEXPAND); - content_sizer->Add(label_dir_hint); - content_sizer->AddSpacer(VERT_SPACING); - content_sizer->Add(box_print, 0, wxBOTTOM, 2*VERT_SPACING); - - btn_sizer->Add(CreateStdDialogButtonSizer(wxOK | wxCANCEL)); - - txt_filename->SetFocus(); - wxString stem(path.stem().wstring()); - txt_filename->SetSelection(0, stem.Length()); - - Fit(); - } - - fs::path filename() const { - return fs::path(txt_filename->GetValue().wx_str()); - } - - bool print() const { return box_print->GetValue(); } -}; - - - OctoPrint::OctoPrint(DynamicPrintConfig *config) : - host(config->opt_string("octoprint_host")), - apikey(config->opt_string("octoprint_apikey")), - cafile(config->opt_string("octoprint_cafile")) + host(config->opt_string("print_host")), + apikey(config->opt_string("printhost_apikey")), + cafile(config->opt_string("printhost_cafile")) {} bool OctoPrint::test(wxString &msg) const @@ -91,6 +44,17 @@ bool OctoPrint::test(wxString &msg) const return res; } +wxString OctoPrint::get_test_ok_msg () const +{ + return wxString::Format("%s", _(L("Connection to OctoPrint works correctly."))); +} + +wxString OctoPrint::get_test_failed_msg (wxString &msg) const +{ + return wxString::Format("%s: %s\n\n%s", + _(L("Could not connect to OctoPrint")), msg, _(L("Note: OctoPrint version at least 1.1.0 is required."))); +} + bool OctoPrint::send_gcode(const std::string &filename) const { enum { PROGRESS_RANGE = 1000 }; @@ -98,7 +62,7 @@ bool OctoPrint::send_gcode(const std::string &filename) const const auto errortitle = _(L("Error while uploading to the OctoPrint server")); fs::path filepath(filename); - SendDialog send_dialog(filepath.filename()); + PrintHostSendDialog send_dialog(filepath.filename(), true); if (send_dialog.ShowModal() != wxID_OK) { return false; } const bool print = send_dialog.print(); @@ -161,6 +125,16 @@ bool OctoPrint::send_gcode(const std::string &filename) const return res; } +bool OctoPrint::have_auto_discovery() const +{ + return true; +} + +bool OctoPrint::can_test() const +{ + return true; +} + void OctoPrint::set_auth(Http &http) const { http.header("X-Api-Key", apikey); diff --git a/xs/src/slic3r/Utils/OctoPrint.hpp b/xs/src/slic3r/Utils/OctoPrint.hpp index 1e2098ae3..aea2ba58f 100644 --- a/xs/src/slic3r/Utils/OctoPrint.hpp +++ b/xs/src/slic3r/Utils/OctoPrint.hpp @@ -4,6 +4,8 @@ #include #include +#include "PrintHost.hpp" + namespace Slic3r { @@ -11,14 +13,18 @@ namespace Slic3r { class DynamicPrintConfig; class Http; -class OctoPrint +class OctoPrint : public PrintHost { public: OctoPrint(DynamicPrintConfig *config); bool test(wxString &curl_msg) const; + wxString get_test_ok_msg () const; + wxString get_test_failed_msg (wxString &msg) const; // Send gcode file to octoprint, filename is expected to be in UTF-8 bool send_gcode(const std::string &filename) const; + bool have_auto_discovery() const; + bool can_test() const; private: std::string host; std::string apikey; diff --git a/xs/src/slic3r/Utils/PrintHost.hpp b/xs/src/slic3r/Utils/PrintHost.hpp new file mode 100644 index 000000000..204740635 --- /dev/null +++ b/xs/src/slic3r/Utils/PrintHost.hpp @@ -0,0 +1,31 @@ +#ifndef slic3r_PrintHost_hpp_ +#define slic3r_PrintHost_hpp_ + +#include +#include + + +namespace Slic3r { + + +class DynamicPrintConfig; + +class PrintHost +{ +public: + + 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; + // Send gcode file to print host, filename is expected to be in UTF-8 + virtual bool send_gcode(const std::string &filename) const = 0; + virtual bool have_auto_discovery() const = 0; + virtual bool can_test() const = 0; +}; + + + + +} + +#endif diff --git a/xs/src/slic3r/Utils/PrintHostFactory.cpp b/xs/src/slic3r/Utils/PrintHostFactory.cpp new file mode 100644 index 000000000..173c5d743 --- /dev/null +++ b/xs/src/slic3r/Utils/PrintHostFactory.cpp @@ -0,0 +1,21 @@ +#include "PrintHostFactory.hpp" +#include "OctoPrint.hpp" +#include "Duet.hpp" + +#include "libslic3r/PrintConfig.hpp" + +namespace Slic3r { + + +PrintHost * PrintHostFactory::get_print_host(DynamicPrintConfig *config) +{ + PrintHostType kind = config->option>("host_type")->value; + if (kind == htOctoPrint) { + return new OctoPrint(config); + } else if (kind == htDuet) { + return new Duet(config); + } + return NULL; +} + +} diff --git a/xs/src/slic3r/Utils/PrintHostFactory.hpp b/xs/src/slic3r/Utils/PrintHostFactory.hpp new file mode 100644 index 000000000..4c9ff2bf2 --- /dev/null +++ b/xs/src/slic3r/Utils/PrintHostFactory.hpp @@ -0,0 +1,24 @@ +#ifndef slic3r_PrintHostFactory_hpp_ +#define slic3r_PrintHostFactory_hpp_ + +#include +#include + + +namespace Slic3r { + +class DynamicPrintConfig; +class PrintHost; + +class PrintHostFactory +{ +public: + PrintHostFactory() {}; + ~PrintHostFactory() {}; + static PrintHost * get_print_host(DynamicPrintConfig *config); +}; + + +} + +#endif diff --git a/xs/src/slic3r/Utils/PrintHostSendDialog.cpp b/xs/src/slic3r/Utils/PrintHostSendDialog.cpp new file mode 100644 index 000000000..b1dd86961 --- /dev/null +++ b/xs/src/slic3r/Utils/PrintHostSendDialog.cpp @@ -0,0 +1,54 @@ +#include "PrintHostSendDialog.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/MsgDialog.hpp" + + +namespace fs = boost::filesystem; + +namespace Slic3r { + +PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, bool can_start_print) : + MsgDialog(nullptr, _(L("Send G-Code to printer host")), _(L("Upload to Printer Host with the following filename:")), wxID_NONE), + txt_filename(new wxTextCtrl(this, wxID_ANY, path.filename().wstring())), + box_print(new wxCheckBox(this, wxID_ANY, _(L("Start printing after upload")))), + can_start_print(can_start_print) +{ + 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); + + content_sizer->Add(txt_filename, 0, wxEXPAND); + content_sizer->Add(label_dir_hint); + content_sizer->AddSpacer(VERT_SPACING); + content_sizer->Add(box_print, 0, wxBOTTOM, 2*VERT_SPACING); + + btn_sizer->Add(CreateStdDialogButtonSizer(wxOK | wxCANCEL)); + + txt_filename->SetFocus(); + wxString stem(path.stem().wstring()); + txt_filename->SetSelection(0, stem.Length()); + + if (!can_start_print) { + box_print->Disable(); + } + + Fit(); +} + +fs::path PrintHostSendDialog::filename() const +{ + return fs::path(txt_filename->GetValue().wx_str()); +} + +bool PrintHostSendDialog::print() const +{ + return box_print->GetValue(); } +} diff --git a/xs/src/slic3r/Utils/PrintHostSendDialog.hpp b/xs/src/slic3r/Utils/PrintHostSendDialog.hpp new file mode 100644 index 000000000..7d2040d97 --- /dev/null +++ b/xs/src/slic3r/Utils/PrintHostSendDialog.hpp @@ -0,0 +1,40 @@ +#ifndef slic3r_PrintHostSendDialog_hpp_ +#define slic3r_PrintHostSendDialog_hpp_ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/MsgDialog.hpp" + + +namespace Slic3r { + +class PrintHostSendDialog : public GUI::MsgDialog +{ + +private: + wxTextCtrl *txt_filename; + wxCheckBox *box_print; + bool can_start_print; + +public: + + PrintHostSendDialog(const boost::filesystem::path &path, bool can_start_print); + boost::filesystem::path filename() const; + bool print() const; +}; + +} + +#endif diff --git a/xs/xsp/Utils_OctoPrint.xsp b/xs/xsp/Utils_OctoPrint.xsp deleted file mode 100644 index 28610cb01..000000000 --- a/xs/xsp/Utils_OctoPrint.xsp +++ /dev/null @@ -1,13 +0,0 @@ -%module{Slic3r::XS}; - -%{ -#include -#include "slic3r/Utils/OctoPrint.hpp" -%} - -%name{Slic3r::OctoPrint} class OctoPrint { - OctoPrint(DynamicPrintConfig *config); - ~OctoPrint(); - - bool send_gcode(std::string filename) const; -}; diff --git a/xs/xsp/Utils_PrintHost.xsp b/xs/xsp/Utils_PrintHost.xsp new file mode 100644 index 000000000..0c3fea137 --- /dev/null +++ b/xs/xsp/Utils_PrintHost.xsp @@ -0,0 +1,10 @@ +%module{Slic3r::XS}; + +%{ +#include +#include "slic3r/Utils/PrintHost.hpp" +%} + +%name{Slic3r::PrintHost} class PrintHost { + bool send_gcode(std::string filename) const; +}; diff --git a/xs/xsp/Utils_PrintHostFactory.xsp b/xs/xsp/Utils_PrintHostFactory.xsp new file mode 100644 index 000000000..2b083c957 --- /dev/null +++ b/xs/xsp/Utils_PrintHostFactory.xsp @@ -0,0 +1,13 @@ +%module{Slic3r::XS}; + +%{ +#include +#include "slic3r/Utils/PrintHostFactory.hpp" +%} + +%name{Slic3r::PrintHostFactory} class PrintHostFactory { + PrintHostFactory(); + ~PrintHostFactory(); + + static PrintHost * get_print_host(DynamicPrintConfig *config); +}; diff --git a/xs/xsp/my.map b/xs/xsp/my.map index 4a14f483f..aefe7b345 100644 --- a/xs/xsp/my.map +++ b/xs/xsp/my.map @@ -239,9 +239,11 @@ Ref O_OBJECT_SLIC3R_T PresetUpdater* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T -OctoPrint* O_OBJECT_SLIC3R -Ref O_OBJECT_SLIC3R_T -Clone O_OBJECT_SLIC3R_T +PrintHostFactory* O_OBJECT_SLIC3R +Ref O_OBJECT_SLIC3R_T +Clone O_OBJECT_SLIC3R_T + +PrintHost* O_OBJECT_SLIC3R Axis T_UV ExtrusionLoopRole T_UV diff --git a/xs/xsp/typemap.xspt b/xs/xsp/typemap.xspt index b576b1373..cee75fe26 100644 --- a/xs/xsp/typemap.xspt +++ b/xs/xsp/typemap.xspt @@ -270,3 +270,4 @@ }; %typemap{AppController*}; %typemap{PrintController*}; +%typemap{PrintHost*};