diff --git a/Build.PL b/Build.PL index fc5b16211..3921e37f9 100644 --- a/Build.PL +++ b/Build.PL @@ -27,6 +27,7 @@ my %prereqs = qw( ); my %recommends = qw( Class::XSAccessor 0 + LWP::UserAgent 0 XML::SAX::ExpatXS 0 ); diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index d089bbf29..e9e1ff387 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -27,6 +27,7 @@ use Slic3r::GUI::SimpleTab; use Slic3r::GUI::Tab; our $have_OpenGL = eval "use Slic3r::GUI::PreviewCanvas; 1"; +our $have_LWP = eval "use LWP::UserAgent; 1"; use Wx 0.9901 qw(:bitmap :dialog :icon :id :misc :systemsettings :toplevelwindow :filedialog); @@ -228,7 +229,7 @@ sub have_version_check { my ($self) = @_; # return an explicit 0 - return ($Slic3r::have_threads && $Slic3r::build && eval "use LWP::UserAgent; 1") || 0; + return ($Slic3r::have_threads && $Slic3r::build && $have_LWP) || 0; } sub check_version { diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm index 42b875036..b874cd958 100644 --- a/lib/Slic3r/GUI/Plater.pm +++ b/lib/Slic3r/GUI/Plater.pm @@ -48,6 +48,7 @@ sub new { my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); $self->{config} = Slic3r::Config->new_from_defaults(qw( bed_shape complete_objects extruder_clearance_radius skirts skirt_distance brim_width + octoprint_host octoprint_apikey )); $self->{model} = Slic3r::Model->new; $self->{print} = Slic3r::Print->new; @@ -181,9 +182,11 @@ sub new { # right pane buttons $self->{btn_export_gcode} = Wx::Button->new($self, -1, "Export G-code…", wxDefaultPosition, [-1, 30], wxBU_LEFT); + $self->{btn_send_gcode} = Wx::Button->new($self, -1, "Send to printer", wxDefaultPosition, [-1, 30], wxBU_LEFT); $self->{btn_export_stl} = Wx::Button->new($self, -1, "Export STL…", wxDefaultPosition, [-1, 30], wxBU_LEFT); #$self->{btn_export_gcode}->SetFont($Slic3r::GUI::small_font); #$self->{btn_export_stl}->SetFont($Slic3r::GUI::small_font); + $self->{btn_send_gcode}->Hide; if ($Slic3r::GUI::have_button_icons) { my %icons = qw( @@ -192,6 +195,7 @@ sub new { reset cross.png arrange bricks.png export_gcode cog_go.png + send_gcode cog_go.png export_stl brick_go.png increase add.png @@ -213,6 +217,10 @@ sub new { $self->export_gcode; Slic3r::thread_cleanup(); }); + EVT_BUTTON($self, $self->{btn_send_gcode}, sub { + $self->{send_gcode_file} = $self->export_gcode(Wx::StandardPaths::Get->GetTempDir()); + Slic3r::thread_cleanup(); + }); #EVT_BUTTON($self, $self->{btn_export_stl}, \&export_stl); if ($self->{htoolbar}) { @@ -355,6 +363,7 @@ sub new { my $buttons_sizer = Wx::BoxSizer->new(wxHORIZONTAL); $buttons_sizer->AddStretchSpacer(1); $buttons_sizer->Add($self->{btn_export_stl}, 0, wxALIGN_RIGHT, 0); + $buttons_sizer->Add($self->{btn_send_gcode}, 0, wxALIGN_RIGHT, 0); $buttons_sizer->Add($self->{btn_export_gcode}, 0, wxALIGN_RIGHT, 0); my $right_sizer = Wx::BoxSizer->new(wxVERTICAL); @@ -944,7 +953,7 @@ sub resume_background_process { } sub export_gcode { - my $self = shift; + my ($self, $output_file) = @_; return if !@{$self->{objects}}; @@ -976,14 +985,14 @@ sub export_gcode { } # select output file - $self->{export_gcode_output_file} = $main::opt{output}; - { - my $default_output_file = $self->{print}->expanded_output_filepath($self->{export_gcode_output_file}); + if ($output_file) { + $self->{export_gcode_output_file} = $self->{print}->expanded_output_filepath($output_file); + } else { + my $default_output_file = $self->{print}->expanded_output_filepath($main::opt{output}); my $dlg = Wx::FileDialog->new($self, 'Save G-code file as:', wxTheApp->output_path(dirname($default_output_file)), basename($default_output_file), &Slic3r::GUI::FILE_WILDCARDS->{gcode}, wxFD_SAVE); if ($dlg->ShowModal != wxID_OK) { $dlg->Destroy; - $self->{export_gcode_output_file} = undef; return; } $Slic3r::GUI::Settings->{_}{last_output_path} = dirname($dlg->GetPath); @@ -1011,6 +1020,8 @@ sub export_gcode { my $result = !Slic3r::GUI::catch_error($self); $self->on_export_completed($result); } + + return $self->{export_gcode_output_file}; } # This gets called only if we have threads. @@ -1072,14 +1083,49 @@ sub on_export_completed { $self->{export_thread} = undef; my $message; + my $send_gcode = 0; if ($result) { - $message = "G-code file exported to " . $self->{export_gcode_output_file}; + if ($self->{send_gcode_file}) { + $message = "Sending G-code file to the Octoprint server..."; + $send_gcode = 1; + } else { + $message = "G-code file exported to " . $self->{export_gcode_output_file}; + } } else { $message = "Export failed"; } $self->{export_gcode_output_file} = undef; $self->statusbar->SetStatusText($message); wxTheApp->notify($message); + + $self->send_gcode if $send_gcode; + $self->{send_gcode_file} = undef; +} + +sub send_gcode { + my ($self) = @_; + + $self->statusbar->StartBusy; + + my $ua = LWP::UserAgent->new; + $ua->timeout(10); + + my $res = $ua->post( + "http://" . $self->{config}->octoprint_host . "/api/files/local", + Content_Type => 'form-data', + 'X-Api-Key' => $self->{config}->octoprint_apikey, + Content => [ + fn => [$self->{send_gcode_file}], + ], + ); + + $self->statusbar->StopBusy; + + if ($res->is_success) { + $self->statusbar->SetStatusText("G-code file successfully uploaded to the Octoprint server"); + } else { + $self->statusbar->SetStatusText("Error while uploading to the Octoprint server: " . $res->status_line); + } } sub export_stl { @@ -1208,6 +1254,13 @@ sub on_config_change { $self->{canvas}->update_bed_size; $self->{canvas3D}->update_bed_size if $self->{canvas3D}; $self->update; + } elsif ($opt_key eq 'octoprint_host') { + if ($config->get('octoprint_host')) { + $self->{btn_send_gcode}->Show; + } else { + $self->{btn_send_gcode}->Hide; + } + $self->Layout; } } @@ -1313,7 +1366,7 @@ sub object_list_changed { my $have_objects = @{$self->{objects}} ? 1 : 0; my $method = $have_objects ? 'Enable' : 'Disable'; $self->{"btn_$_"}->$method - for grep $self->{"btn_$_"}, qw(reset arrange export_gcode export_stl); + for grep $self->{"btn_$_"}, qw(reset arrange export_gcode export_stl send_gcode); if ($self->{htoolbar}) { $self->{htoolbar}->EnableTool($_, $have_objects) diff --git a/lib/Slic3r/GUI/Tab.pm b/lib/Slic3r/GUI/Tab.pm index 398aecac2..a1b4768b5 100644 --- a/lib/Slic3r/GUI/Tab.pm +++ b/lib/Slic3r/GUI/Tab.pm @@ -923,6 +923,7 @@ sub build { $self->init_config_options(qw( bed_shape z_offset gcode_flavor use_relative_e_distances + octoprint_host octoprint_apikey use_firmware_retraction pressure_advance vibration_limit start_gcode end_gcode layer_gcode toolchange_gcode nozzle_diameter extruder_offset @@ -996,6 +997,11 @@ sub build { } }); } + { + my $optgroup = $page->new_optgroup('Octoprint upload'); + $optgroup->append_single_option_line('octoprint_host'); + $optgroup->append_single_option_line('octoprint_apikey'); + } { my $optgroup = $page->new_optgroup('Advanced'); $optgroup->append_single_option_line('use_firmware_retraction'); @@ -1129,6 +1135,8 @@ sub _update { my $config = $self->{config}; + $self->get_field('octoprint_apikey')->toggle($config->get('octoprint_host')); + my $have_multiple_extruders = $self->{extruders_count} > 1; $self->get_field('toolchange_gcode')->toggle($have_multiple_extruders); @@ -1319,8 +1327,8 @@ sub config { } # apply preset values on top of defaults + my $config = Slic3r::Config->new_from_defaults(@$keys); my $external_config = Slic3r::Config->load($self->file); - my $config = Slic3r::Config->new; $config->set($_, $external_config->get($_)) for grep $external_config->has($_), @$keys; diff --git a/xs/src/libslic3r/PrintConfig.cpp b/xs/src/libslic3r/PrintConfig.cpp index 4565ba5af..805667a8a 100644 --- a/xs/src/libslic3r/PrintConfig.cpp +++ b/xs/src/libslic3r/PrintConfig.cpp @@ -498,6 +498,16 @@ PrintConfigDef::build_def() { Options["nozzle_diameter"].sidetext = "mm"; Options["nozzle_diameter"].cli = "nozzle-diameter=f@"; + Options["octoprint_apikey"].type = coString; + Options["octoprint_apikey"].label = "API Key"; + Options["octoprint_apikey"].tooltip = "Slic3r can upload G-code files to Octoprint. This field should contain the API Key required for authentication."; + Options["octoprint_apikey"].cli = "octoprint-apikey=s"; + + Options["octoprint_host"].type = coString; + Options["octoprint_host"].label = "Host or IP"; + Options["octoprint_host"].tooltip = "Slic3r can upload G-code files to Octoprint. This field should contain the hostname or IP address of the Octoprint instance."; + Options["octoprint_host"].cli = "octoprint-host=s"; + Options["only_retract_when_crossing_perimeters"].type = coBool; Options["only_retract_when_crossing_perimeters"].label = "Only retract when crossing perimeters"; Options["only_retract_when_crossing_perimeters"].tooltip = "Disables retraction when the travel path does not exceed the upper layer's perimeters (and thus any ooze will be probably invisible)."; diff --git a/xs/src/libslic3r/PrintConfig.hpp b/xs/src/libslic3r/PrintConfig.hpp index 48235c7d1..b647acfb8 100644 --- a/xs/src/libslic3r/PrintConfig.hpp +++ b/xs/src/libslic3r/PrintConfig.hpp @@ -582,7 +582,27 @@ class PrintConfig : public GCodeConfig }; }; -class FullPrintConfig : public PrintObjectConfig, public PrintRegionConfig, public PrintConfig +class HostConfig : public virtual StaticPrintConfig +{ + public: + ConfigOptionString octoprint_host; + ConfigOptionString octoprint_apikey; + + HostConfig() : StaticPrintConfig() { + this->octoprint_host.value = ""; + this->octoprint_apikey.value = ""; + }; + + ConfigOption* option(const t_config_option_key opt_key, bool create = false) { + if (opt_key == "octoprint_host") return &this->octoprint_host; + if (opt_key == "octoprint_apikey") return &this->octoprint_apikey; + + return NULL; + }; +}; + +class FullPrintConfig + : public PrintObjectConfig, public PrintRegionConfig, public PrintConfig, public HostConfig { public: ConfigOption* option(const t_config_option_key opt_key, bool create = false) { @@ -590,6 +610,7 @@ class FullPrintConfig : public PrintObjectConfig, public PrintRegionConfig, publ if ((opt = PrintObjectConfig::option(opt_key, create)) != NULL) return opt; if ((opt = PrintRegionConfig::option(opt_key, create)) != NULL) return opt; if ((opt = PrintConfig::option(opt_key, create)) != NULL) return opt; + if ((opt = HostConfig::option(opt_key, create)) != NULL) return opt; return NULL; }; };