diff --git a/lib/Slic3r.pm b/lib/Slic3r.pm index 516eb2996..67c464f95 100644 --- a/lib/Slic3r.pm +++ b/lib/Slic3r.pm @@ -194,6 +194,7 @@ sub thread_cleanup { *Slic3r::ExtrusionPath::Collection::DESTROY = sub {}; *Slic3r::Flow::DESTROY = sub {}; *Slic3r::GCode::PlaceholderParser::DESTROY = sub {}; + *Slic3r::GCode::Sender::DESTROY = sub {}; *Slic3r::GCode::Writer::DESTROY = sub {}; *Slic3r::Geometry::BoundingBox::DESTROY = sub {}; *Slic3r::Geometry::BoundingBoxf::DESTROY = sub {}; diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index c0ba9fd8c..dd31ec8c7 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -68,6 +68,7 @@ our $small_font = Wx::SystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); $small_font->SetPointSize(11) if !&Wx::wxMSW; our $medium_font = Wx::SystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); $medium_font->SetPointSize(12); +our $grey = Wx::Colour->new(100,100,100); sub OnInit { my ($self) = @_; @@ -297,9 +298,22 @@ sub CallAfter { sub show_printer_controller { my ($self) = @_; - $self->{controller_frame} = Slic3r::GUI::Controller::Frame->new; + $self->{controller_frame} //= Slic3r::GUI::Controller::Frame->new; $self->{controller_frame}->Show; return $self->{controller_frame}; } +sub scan_serial_ports { + my ($self) = @_; + + my @ports = (); + + # TODO: Windows ports + + # UNIX and OS X + push @ports, glob '/dev/{ttyUSB,ttyACM,tty.,cu.,rfcomm}*'; + + return @ports; +} + 1; diff --git a/lib/Slic3r/GUI/Controller/Frame.pm b/lib/Slic3r/GUI/Controller/Frame.pm index a709b615c..131d5ea79 100644 --- a/lib/Slic3r/GUI/Controller/Frame.pm +++ b/lib/Slic3r/GUI/Controller/Frame.pm @@ -3,31 +3,94 @@ use strict; use warnings; use utf8; -use Wx qw(:frame :id :misc :sizer); -use Wx::Event qw(EVT_CLOSE); +use Wx qw(wxTheApp :frame :id :misc :sizer :bitmap :button); +use Wx::Event qw(EVT_CLOSE EVT_LEFT_DOWN EVT_MENU); use base 'Wx::Frame'; sub new { my ($class) = @_; - my $self = $class->SUPER::new(undef, -1, "Controller", wxDefaultPosition, [500,350], wxDEFAULT_FRAME_STYLE); + my $self = $class->SUPER::new(undef, -1, "Controller", wxDefaultPosition, [600,350], + wxDEFAULT_FRAME_STYLE | wxFRAME_EX_METAL); $self->{sizer} = my $sizer = Wx::BoxSizer->new(wxVERTICAL); - $sizer->Add(Slic3r::GUI::Controller::PrinterPanel->new($self), 1, wxEXPAND); + + { + my $btn = Wx::BitmapButton->new($self, -1, Wx::Bitmap->new("$Slic3r::var/add.png", wxBITMAP_TYPE_PNG), + wxDefaultPosition, wxDefaultSize, Wx::wxBORDER_NONE); + $btn->SetToolTipString("Add printer…") + if $btn->can('SetToolTipString'); + + EVT_LEFT_DOWN($btn, sub { + my $menu = Wx::Menu->new; + my %presets = wxTheApp->presets('printer'); + foreach my $preset_name (sort keys %presets) { + my $config = Slic3r::Config->load($presets{$preset_name}); + next if !$config->serial_port; + + my $id = &Wx::NewId(); + $menu->Append($id, $preset_name); + EVT_MENU($menu, $id, sub { + $self->add_printer($preset_name, $config); + }); + } + $self->PopupMenu($menu, $btn->GetPosition); + $menu->Destroy; + }); + $self->{sizer}->Add($btn, 0, wxTOP | wxLEFT, 10); + } $self->SetSizer($sizer); $self->SetMinSize($self->GetSize); - $sizer->SetSizeHints($self); - $self->Layout; + #$sizer->SetSizeHints($self); EVT_CLOSE($self, sub { my (undef, $event) = @_; - # ... + foreach my $panel ($self->print_panels) { + $panel->disconnect; + } + undef wxTheApp->{controller_frame}; $event->Skip; }); + # if only one preset exists, load it + { + my %presets = wxTheApp->presets('printer'); + my %configs = map { my $name = $_; $name => Slic3r::Config->load($presets{$name}) } keys %presets; + my @presets_with_printer = grep $configs{$_}->serial_port, keys %presets; + if (@presets_with_printer == 1) { + my $name = $presets_with_printer[0]; + $self->add_printer($name, $configs{$name}); + } + } + + $self->Layout; + return $self; } +sub add_printer { + my ($self, $printer_name, $config) = @_; + + # check that printer doesn't exist already + foreach my $panel ($self->print_panels) { + if ($panel->printer_name eq $printer_name) { + return $panel; + } + } + + my $printer_panel = Slic3r::GUI::Controller::PrinterPanel->new($self, $printer_name, $config); + $self->{sizer}->Prepend($printer_panel, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 10); + $self->Layout; + + return $printer_panel; +} + +sub print_panels { + my ($self) = @_; + return grep $_->isa('Slic3r::GUI::Controller::PrinterPanel'), + map $_->GetWindow, $self->{sizer}->GetChildren; +} + 1; diff --git a/lib/Slic3r/GUI/Controller/PrinterPanel.pm b/lib/Slic3r/GUI/Controller/PrinterPanel.pm index fcbbf16de..10de415f0 100644 --- a/lib/Slic3r/GUI/Controller/PrinterPanel.pm +++ b/lib/Slic3r/GUI/Controller/PrinterPanel.pm @@ -3,105 +3,193 @@ use strict; use warnings; use utf8; -use Wx qw(:panel :id :misc :sizer :button :bitmap); -use Wx::Event qw(EVT_BUTTON); +use Wx qw(wxTheApp :panel :id :misc :sizer :button :bitmap :window :gauge :timer); +use Wx::Event qw(EVT_BUTTON EVT_MOUSEWHEEL EVT_TIMER); use base qw(Wx::Panel Class::Accessor); -__PACKAGE__->mk_accessors(qw(sender)); +__PACKAGE__->mk_accessors(qw(printer_name config sender jobs + printing print_status_timer)); + +use constant PRINT_STATUS_TIMER_INTERVAL => 1000; # milliseconds sub new { - my ($class, $parent) = @_; - my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize); + my ($class, $parent, $printer_name, $config) = @_; + my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, [500, 250]); - $self->{sizer} = my $sizer = Wx::StaticBoxSizer->new(Wx::StaticBox->new($self, -1, "Printer"), wxVERTICAL); + $self->printer_name($printer_name || 'Printer'); + $self->config($config); + $self->jobs([]); + + { + my $timer_id = &Wx::NewId(); + $self->print_status_timer(Wx::Timer->new($self, $timer_id)); + EVT_TIMER($self, $timer_id, sub { + my ($self, $event) = @_; + + return if !$self->printing; + my $queue_size = $self->sender->queue_size; + printf "queue = %d\n", $queue_size; + $self->{gauge}->SetValue($self->{gauge}->GetRange - $queue_size); + if ($queue_size == 0) { + $self->print_completed; + return; + } + # TODO: get temperature messages + }); + } + + my $box = Wx::StaticBox->new($self, -1, ""); + my $sizer = Wx::StaticBoxSizer->new($box, wxHORIZONTAL); + my $left_sizer = Wx::BoxSizer->new(wxVERTICAL); + + # printer name + { + my $text = Wx::StaticText->new($box, -1, $self->printer_name, wxDefaultPosition, [250,-1]); + my $font = $text->GetFont; + $font->SetPointSize(20); + $text->SetFont($font); + $left_sizer->Add($text, 0, wxEXPAND, 0); + } # connection info { - my $conn_sizer = Wx::BoxSizer->new(wxHORIZONTAL); - $sizer->Add($conn_sizer, 0, wxEXPAND); + my $conn_sizer = Wx::FlexGridSizer->new(2, 2, 1, 0); + $conn_sizer->SetFlexibleDirection(wxHORIZONTAL); + $conn_sizer->AddGrowableCol(1, 1); + $left_sizer->Add($conn_sizer, 0, wxEXPAND | wxTOP, 5); { - my $text = Wx::StaticText->new($self, -1, "Port:", wxDefaultPosition, wxDefaultSize); + my $text = Wx::StaticText->new($box, -1, "Port:", wxDefaultPosition, wxDefaultSize); + $text->SetFont($Slic3r::GUI::small_font); $conn_sizer->Add($text, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, 5); } + my $serial_port_sizer = Wx::BoxSizer->new(wxHORIZONTAL); { - $self->{serial_port_combobox} = Wx::ComboBox->new($self, -1, "", wxDefaultPosition, wxDefaultSize, []); - $self->scan_serial_ports; - $conn_sizer->Add($self->{serial_port_combobox}, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, 1); + $self->{serial_port_combobox} = Wx::ComboBox->new($box, -1, $config->serial_port, wxDefaultPosition, wxDefaultSize, []); + $self->{serial_port_combobox}->SetFont($Slic3r::GUI::small_font); + $self->update_serial_ports; + $serial_port_sizer->Add($self->{serial_port_combobox}, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, 1); } { - $self->{btn_rescan_serial} = my $btn = Wx::BitmapButton->new($self, -1, Wx::Bitmap->new("$Slic3r::var/arrow_rotate_clockwise.png", wxBITMAP_TYPE_PNG), + $self->{btn_rescan_serial} = my $btn = Wx::BitmapButton->new($box, -1, Wx::Bitmap->new("$Slic3r::var/arrow_rotate_clockwise.png", wxBITMAP_TYPE_PNG), wxDefaultPosition, wxDefaultSize, &Wx::wxBORDER_NONE); - $conn_sizer->Add($btn, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, 5); - EVT_BUTTON($self, $btn, sub { $self->scan_serial_ports }); + $serial_port_sizer->Add($btn, 0, wxALIGN_CENTER_VERTICAL, 0); + EVT_BUTTON($self, $btn, sub { $self->update_serial_ports }); } + $conn_sizer->Add($serial_port_sizer, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, 5); + { - my $text = Wx::StaticText->new($self, -1, "Speed:", wxDefaultPosition, wxDefaultSize); - $conn_sizer->Add($text, 0, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL, 5); + my $text = Wx::StaticText->new($box, -1, "Speed:", wxDefaultPosition, wxDefaultSize); + $text->SetFont($Slic3r::GUI::small_font); + $conn_sizer->Add($text, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, 5); } + my $serial_speed_sizer = Wx::BoxSizer->new(wxHORIZONTAL); { - $self->{serial_speed_combobox} = Wx::ComboBox->new($self, -1, "250000", wxDefaultPosition, wxDefaultSize, + $self->{serial_speed_combobox} = Wx::ComboBox->new($box, -1, $config->serial_speed, wxDefaultPosition, wxDefaultSize, ["115200", "250000"]); - $conn_sizer->Add($self->{serial_speed_combobox}, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, 5); + $self->{serial_speed_combobox}->SetFont($Slic3r::GUI::small_font); + $serial_speed_sizer->Add($self->{serial_speed_combobox}, 0, wxALIGN_CENTER_VERTICAL, 0); } + { + $self->{btn_disconnect} = my $btn = Wx::Button->new($box, -1, "Disconnect", wxDefaultPosition, wxDefaultSize); + $btn->SetFont($Slic3r::GUI::small_font); + if ($Slic3r::GUI::have_button_icons) { + $btn->SetBitmap(Wx::Bitmap->new("$Slic3r::var/delete.png", wxBITMAP_TYPE_PNG)); + } + $serial_speed_sizer->Add($btn, 0, wxLEFT, 5); + EVT_BUTTON($self, $btn, \&disconnect); + } + $conn_sizer->Add($serial_speed_sizer, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, 5); } # buttons { - my $buttons_sizer = Wx::BoxSizer->new(wxHORIZONTAL); - $sizer->Add($buttons_sizer, 0, wxEXPAND); - { - $self->{btn_connect} = my $btn = Wx::Button->new($self, -1, "Connect", wxDefaultPosition, wxDefaultSize); - $buttons_sizer->Add($btn, 0, wxRIGHT, 5); - EVT_BUTTON($self, $btn, \&connect); - } - { - $self->{btn_disconnect} = my $btn = Wx::Button->new($self, -1, "Disconnect", wxDefaultPosition, wxDefaultSize); - $buttons_sizer->Add($btn, 0, wxRIGHT, 5); - EVT_BUTTON($self, $btn, \&disconnect); + $self->{btn_connect} = my $btn = Wx::Button->new($box, -1, "Connect", wxDefaultPosition, [-1, 40]); + my $font = $btn->GetFont; + $font->SetPointSize($font->GetPointSize + 2); + $btn->SetFont($font); + if ($Slic3r::GUI::have_button_icons) { + $btn->SetBitmap(Wx::Bitmap->new("$Slic3r::var/arrow_up.png", wxBITMAP_TYPE_PNG)); } + $left_sizer->Add($btn, 0, wxTOP, 15); + EVT_BUTTON($self, $btn, \&connect); + } + + # print progress bar + { + my $gauge = $self->{gauge} = Wx::Gauge->new($self, wxGA_HORIZONTAL, 100, wxDefaultPosition, wxDefaultSize); + $left_sizer->Add($self->{gauge}, 0, wxEXPAND | wxTOP, 15); + $gauge->Hide; } # status - $self->{status_text} = Wx::StaticText->new($self, -1, "Not connected", wxDefaultPosition, wxDefaultSize); - $sizer->Add($self->{status_text}, 0, wxEXPAND); + $self->{status_text} = Wx::StaticText->new($box, -1, "", wxDefaultPosition, [250,-1]); + $left_sizer->Add($self->{status_text}, 0, wxEXPAND | wxTOP, 15); + + # print jobs panel + my $print_jobs_sizer = Wx::BoxSizer->new(wxVERTICAL); + { + my $text = Wx::StaticText->new($box, -1, "Queue:", wxDefaultPosition, wxDefaultSize); + $text->SetFont($Slic3r::GUI::small_font); + $print_jobs_sizer->Add($text, 0, wxEXPAND, 0); + + $self->{jobs_panel} = Wx::ScrolledWindow->new($box, -1, wxDefaultPosition, wxDefaultSize, wxBORDER_SUNKEN); + $self->{jobs_panel}->SetScrollbars(0, 1, 0, 1); + $self->{jobs_panel_sizer} = Wx::BoxSizer->new(wxVERTICAL); + $self->{jobs_panel}->SetSizer($self->{jobs_panel_sizer}); + $print_jobs_sizer->Add($self->{jobs_panel}, 1, wxEXPAND, 0); + } + + $sizer->Add($left_sizer, 0, wxEXPAND | wxALL, 0); + $sizer->Add($print_jobs_sizer, 1, wxEXPAND | wxALL, 0); $self->SetSizer($sizer); $self->SetMinSize($self->GetSize); - $sizer->SetSizeHints($self); $self->_update_connection_controls; + $self->set_status('Printer is offline. Click the Connect button.'); return $self; } +sub is_connected { + my ($self) = @_; + return $self->sender && $self->sender->is_connected; +} + sub _update_connection_controls { my ($self) = @_; - if ($self->sender && $self->sender->is_connected) { + $self->{btn_connect}->Show; + $self->{btn_disconnect}->Hide; + $self->{serial_port_combobox}->Enable; + $self->{serial_speed_combobox}->Enable; + $self->{btn_rescan_serial}->Enable; + + if ($self->is_connected) { $self->{btn_connect}->Hide; - $self->{btn_disconnect}->Show; + if (!$self->printing) { + $self->{btn_disconnect}->Show; + } $self->{serial_port_combobox}->Disable; $self->{serial_speed_combobox}->Disable; $self->{btn_rescan_serial}->Disable; - } else { - $self->{btn_connect}->Show; - $self->{btn_disconnect}->Hide; - $self->{serial_port_combobox}->Enable; - $self->{serial_speed_combobox}->Enable; - $self->{btn_rescan_serial}->Enable; } + + $self->Layout; } sub set_status { my ($self, $status) = @_; $self->{status_text}->SetLabel($status); + $self->{status_text}->Wrap($self->{status_text}->GetSize->GetWidth - 30); $self->{status_text}->Refresh; + $self->Layout; } sub connect { my ($self) = @_; - return if $self->sender && $self->sender->is_connected; + return if $self->is_connected; $self->set_status("Connecting..."); $self->sender(Slic3r::GCode::Sender->new); @@ -113,30 +201,301 @@ sub connect { $self->set_status("Connection failed"); } 1 until $self->sender->is_connected; - $self->set_status("Connected"); + $self->set_status("Printer is online. You can now start printing from the queue on the right."); $self->_update_connection_controls; + $self->reload_jobs; } sub disconnect { my ($self) = @_; - return if !$self->sender || !$self->sender->is_connected; + $self->print_status_timer->Stop; + return if !$self->is_connected; + $self->printing->printing(0) if $self->printing; + $self->printing(undef); + $self->{gauge}->Hide; $self->sender->disconnect; $self->set_status("Not connected"); $self->_update_connection_controls; + $self->reload_jobs; } -sub scan_serial_ports { +sub update_serial_ports { my ($self) = @_; $self->{serial_port_combobox}->Clear; - - # TODO: Windows ports - - # UNIX and OS X $self->{serial_port_combobox}->Append($_) - for glob '/dev/{ttyUSB,ttyACM,tty.,cu.,rfcomm}*'; + for wxTheApp->scan_serial_ports; +} + +sub load_print_job { + my ($self, $gcode_file, $filament_stats) = @_; + + push @{$self->jobs}, my $job = Slic3r::GUI::Controller::PrinterPanel::PrintJob->new( + id => time() . $gcode_file . rand(1000), + gcode_file => $gcode_file, + filament_stats => $filament_stats, + ); + $self->reload_jobs; + return $job; +} + +sub delete_job { + my ($self, $job) = @_; + + $self->jobs([ grep $_->id ne $job->id, @{$self->jobs} ]); + $self->reload_jobs; +} + +sub print_job { + my ($self, $job) = @_; + + $self->printing($job); + $job->printing(1); + $self->reload_jobs; + + open my $fh, '<', $job->gcode_file; + my $line_count = 0; + while (my $row = <$fh>) { + $self->sender->send($row); + $line_count++; + } + close $fh; + + $self->_update_connection_controls; + $self->{gauge}->SetRange($line_count); + $self->{gauge}->SetValue(0); + $self->{gauge}->Enable; + $self->{gauge}->Show; + $self->Layout; + + $self->print_status_timer->Start(PRINT_STATUS_TIMER_INTERVAL, wxTIMER_CONTINUOUS); + $self->set_status('Printing...'); +} + +sub print_completed { + my ($self) = @_; + + my $job = $self->printing; + $self->printing(undef); + $job->printing(0); + $job->printed(1); + $self->_update_connection_controls; + $self->{gauge}->Hide; + $self->Layout; + $self->print_status_timer->Stop; + + $self->set_status('Print completed.'); + + # reorder jobs + @{$self->jobs} = sort { $a->printed <=> $b->printed } @{$self->jobs}; + + $self->reload_jobs; +} + +sub reload_jobs { + my ($self) = @_; + + # remove all panels + foreach my $child ($self->{jobs_panel_sizer}->GetChildren) { + my $window = $child->GetWindow; + $self->{jobs_panel_sizer}->Detach($window); + # now $child does not exist anymore + $window->Destroy; + } + + # re-add all panels + foreach my $job (@{$self->jobs}) { + my $panel = Slic3r::GUI::Controller::PrinterPanel::PrintJobPanel->new($self->{jobs_panel}, $job); + $self->{jobs_panel_sizer}->Add($panel, 0, wxEXPAND | wxBOTTOM, 5); + + $panel->on_delete_job(sub { + my ($job) = @_; + $self->delete_job($job); + }); + $panel->on_print_job(sub { + my ($job) = @_; + $self->print_job($job); + }); + $panel->on_pause_print(sub { + my ($job) = @_; + $self->sender->pause_queue; + $job->paused(1); + $self->reload_jobs; + $self->_update_connection_controls; + $self->{gauge}->Disable; + $self->set_status('Print is paused. Click on Resume to continue.'); + }); + $panel->on_resume_print(sub { + my ($job) = @_; + $self->sender->resume_queue; + $job->paused(0); + $self->reload_jobs; + $self->_update_connection_controls; + $self->{gauge}->Enable; + $self->set_status('Printing...'); + }); + $panel->enable_print if $self->is_connected && !$self->printing; + + EVT_MOUSEWHEEL($panel, sub { + my (undef, $event) = @_; + Wx::PostEvent($self->{jobs_panel}, $event); + $event->Skip; + }); + } + + $self->{jobs_panel}->Layout; +} + +package Slic3r::GUI::Controller::PrinterPanel::PrintJob; +use Moo; + +use File::Basename qw(basename); + +has 'id' => (is => 'ro', required => 1); +has 'gcode_file' => (is => 'ro', required => 1); +has 'filament_stats' => (is => 'rw'); +has 'printing' => (is => 'rw', default => sub { 0 }); +has 'paused' => (is => 'rw', default => sub { 0 }); +has 'printed' => (is => 'rw', default => sub { 0 }); + +sub name { + my ($self) = @_; + return basename($self->gcode_file); +} + +package Slic3r::GUI::Controller::PrinterPanel::PrintJobPanel; +use strict; +use warnings; +use utf8; + +use Wx qw(wxTheApp :panel :id :misc :sizer :button :bitmap :font :dialog :icon); +use Wx::Event qw(EVT_BUTTON); +use base qw(Wx::Panel Class::Accessor); + +__PACKAGE__->mk_accessors(qw(job on_delete_job on_print_job on_pause_print on_resume_print)); + +sub new { + my ($class, $parent, $job) = @_; + my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize); + + $self->job($job); + $self->SetBackgroundColour(Wx::wxWHITE); + + my $title_and_buttons_sizer = Wx::BoxSizer->new(wxHORIZONTAL); + { + my $text = Wx::StaticText->new($self, -1, $job->name, wxDefaultPosition, wxDefaultSize); + my $font = $text->GetFont; + $font->SetWeight(wxFONTWEIGHT_BOLD); + $text->SetFont($font); + if ($job->printing) { + $text->SetForegroundColour(Wx::wxGREEN); + } elsif ($job->printed) { + $text->SetForegroundColour($Slic3r::GUI::grey); + } + $title_and_buttons_sizer->Add($text, 1, wxRIGHT | wxALIGN_CENTER_VERTICAL, 5); + } + { + my $btn = $self->{btn_delete} = Wx::BitmapButton->new($self, -1, Wx::Bitmap->new("$Slic3r::var/delete.png", wxBITMAP_TYPE_PNG), + wxDefaultPosition, wxDefaultSize, Wx::wxBORDER_NONE); + $btn->SetToolTipString("Delete this job from print queue") + if $btn->can('SetToolTipString'); + $btn->SetFont($Slic3r::GUI::small_font); + $title_and_buttons_sizer->Add($btn, 0, wxEXPAND | wxBOTTOM, 0); + + EVT_BUTTON($self, $btn, sub { + my $res = Wx::MessageDialog->new($self, "Are you sure you want to delete this print job?", 'Delete Job', wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION)->ShowModal; + return unless $res == wxID_YES; + + wxTheApp->CallAfter(sub { + $self->on_delete_job->($job); + }); + }); + } + + my $left_sizer = Wx::BoxSizer->new(wxVERTICAL); + { + my $filament_stats = join "\n", + map "$_ (" . sprintf("%.2f", $job->filament_stats->{$_}/100) . "m)", + sort keys %{$job->filament_stats}; + my $text = Wx::StaticText->new($self, -1, $filament_stats, wxDefaultPosition, wxDefaultSize); + $text->SetFont($Slic3r::GUI::small_font); + if ($job->printed) { + $text->SetForegroundColour($Slic3r::GUI::grey); + } + $left_sizer->Add($text, 1, wxEXPAND | wxTOP | wxBOTTOM, 7); + } + + + my $right_sizer = Wx::BoxSizer->new(wxVERTICAL); + { + my $label = $job->printed ? 'Print Again' : 'Print This'; + my $btn = $self->{btn_print} = Wx::Button->new($self, -1, $label, wxDefaultPosition, wxDefaultSize); + $btn->Hide; + if ($Slic3r::GUI::have_button_icons) { + $self->{btn_print}->SetBitmap(Wx::Bitmap->new("$Slic3r::var/arrow_up.png", wxBITMAP_TYPE_PNG)); + } + $right_sizer->Add($btn, 0, wxEXPAND | wxBOTTOM, 7); + + EVT_BUTTON($self, $btn, sub { + wxTheApp->CallAfter(sub { + $self->on_print_job->($job); + }); + }); + } + { + my $btn = $self->{btn_pause} = Wx::Button->new($self, -1, "Pause", wxDefaultPosition, wxDefaultSize); + if (!$job->printing || $job->paused) { + $btn->Hide; + } + if ($Slic3r::GUI::have_button_icons) { + $self->{btn_print}->SetBitmap(Wx::Bitmap->new("$Slic3r::var/arrow_up.png", wxBITMAP_TYPE_PNG)); + } + $right_sizer->Add($btn, 0, wxEXPAND | wxBOTTOM, 7); + + EVT_BUTTON($self, $btn, sub { + wxTheApp->CallAfter(sub { + $self->on_pause_print->($job); + }); + }); + } + { + my $btn = $self->{btn_resume} = Wx::Button->new($self, -1, "Resume", wxDefaultPosition, wxDefaultSize); + if (!$job->printing || !$job->paused) { + $btn->Hide; + } + if ($Slic3r::GUI::have_button_icons) { + $self->{btn_print}->SetBitmap(Wx::Bitmap->new("$Slic3r::var/arrow_up.png", wxBITMAP_TYPE_PNG)); + } + $right_sizer->Add($btn, 0, wxEXPAND | wxBOTTOM, 7); + + EVT_BUTTON($self, $btn, sub { + wxTheApp->CallAfter(sub { + $self->on_resume_print->($job); + }); + }); + } + + my $middle_sizer = Wx::BoxSizer->new(wxHORIZONTAL); + $middle_sizer->Add($left_sizer, 1, wxEXPAND, 0); + $middle_sizer->Add($right_sizer, 0, wxEXPAND, 0); + + my $sizer = Wx::BoxSizer->new(wxVERTICAL); + $sizer->Add($title_and_buttons_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 3); + $sizer->Add($middle_sizer, 1, wxEXPAND, 0); + $self->SetSizer($sizer); + + return $self; +} + +sub enable_print { + my ($self) = @_; + + if (!$self->job->printing) { + $self->{btn_print}->Show; + } + $self->Layout; } 1; diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm index d23542e55..15b249255 100644 --- a/lib/Slic3r/GUI/MainFrame.pm +++ b/lib/Slic3r/GUI/MainFrame.pm @@ -229,7 +229,7 @@ sub _init_menubar { $self->select_tab($tab_count-1); }); $windowMenu->AppendSeparator(); - $self->_append_menu_item($windowMenu, "Printer Controller", 'Show the printer controller', sub { + $self->_append_menu_item($windowMenu, "Printer &Controller\tCtrl+T", 'Show the printer controller', sub { wxTheApp->show_printer_controller; }); } @@ -674,6 +674,17 @@ sub config { return $config; } +sub filament_preset_names { + my ($self) = @_; + + if ($self->{mode} eq 'simple') { + return ''; + } + + return map $self->{options_tabs}{filament}->get_preset($_)->name, + $self->{plater}->filament_presets; +} + sub check_unsaved_changes { my $self = shift; diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm index 2517cd893..830b573e9 100644 --- a/lib/Slic3r/GUI/Plater.pm +++ b/lib/Slic3r/GUI/Plater.pm @@ -48,7 +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 + serial_port serial_speed octoprint_host octoprint_apikey )); $self->{model} = Slic3r::Model->new; $self->{print} = Slic3r::Print->new; @@ -173,10 +173,12 @@ sub new { # right pane buttons $self->{btn_export_gcode} = Wx::Button->new($self, -1, "Export G-code…", wxDefaultPosition, [-1, 30], wxBU_LEFT); + $self->{btn_print} = Wx::Button->new($self, -1, "Print…", 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_print}->Hide; $self->{btn_send_gcode}->Hide; if ($Slic3r::GUI::have_button_icons) { @@ -186,6 +188,7 @@ sub new { reset cross.png arrange bricks.png export_gcode cog_go.png + print arrow_up.png send_gcode arrow_up.png export_stl brick_go.png @@ -208,6 +211,10 @@ sub new { $self->export_gcode; Slic3r::thread_cleanup(); }); + EVT_BUTTON($self, $self->{btn_print}, sub { + $self->{print_file} = $self->export_gcode(Wx::StandardPaths::Get->GetTempDir()); + 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(); @@ -354,6 +361,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_print}, 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); @@ -1064,8 +1072,12 @@ sub on_export_completed { my $message; my $send_gcode = 0; + my $do_print = 0; if ($result) { - if ($self->{send_gcode_file}) { + if ($self->{print_file}) { + $message = "Adding file to print queue..."; + $do_print = 1; + } elsif ($self->{send_gcode_file}) { $message = "Sending G-code file to the OctoPrint server..."; $send_gcode = 1; } else { @@ -1078,10 +1090,32 @@ sub on_export_completed { $self->statusbar->SetStatusText($message); wxTheApp->notify($message); + $self->do_print if $do_print; $self->send_gcode if $send_gcode; + $self->{print_file} = undef; $self->{send_gcode_file} = undef; } +sub do_print { + my ($self) = @_; + + my $printer_tab = $self->GetFrame->{options_tabs}{printer}; + my $printer_name = $printer_tab->get_current_preset->name; + + my $controller = wxTheApp->show_printer_controller; + my $printer_panel = $controller->add_printer($printer_name, $printer_tab->config); + + my $filament_stats = $self->{print}->filament_stats; + my @filament_names = $self->GetFrame->filament_preset_names; + $filament_stats = { map { $filament_names[$_] => $filament_stats->{$_} } keys %$filament_stats }; + $printer_panel->load_print_job($self->{print_file}, $filament_stats); + + $controller->Iconize(0); # restore the window if minimized + $controller->SetFocus(); # focus on my window + $controller->Raise(); # bring window to front + $controller->Show(1); # show the window +} + sub send_gcode { my ($self) = @_; @@ -1250,6 +1284,13 @@ sub on_config_change { $self->{canvas}->update_bed_size; $self->{canvas3D}->update_bed_size if $self->{canvas3D}; $self->update; + } elsif ($opt_key eq 'serial_port') { + if ($config->get('serial_port')) { + $self->{btn_print}->Show; + } else { + $self->{btn_print}->Hide; + } + $self->Layout; } elsif ($opt_key eq 'octoprint_host') { if ($config->get('octoprint_host')) { $self->{btn_send_gcode}->Show; @@ -1362,7 +1403,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 send_gcode); + for grep $self->{"btn_$_"}, qw(reset arrange export_gcode export_stl print 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 6aac1c862..741b5f99e 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 + serial_port serial_speed octoprint_host octoprint_apikey use_firmware_retraction pressure_advance vibration_limit start_gcode end_gcode layer_gcode toolchange_gcode @@ -997,6 +998,11 @@ sub build { } }); } + { + my $optgroup = $page->new_optgroup('USB/Serial connection'); + $optgroup->append_single_option_line('serial_port'); + $optgroup->append_single_option_line('serial_speed'); + } { my $optgroup = $page->new_optgroup('OctoPrint upload'); diff --git a/lib/Slic3r/Print/GCode.pm b/lib/Slic3r/Print/GCode.pm index a40a64e28..12f6093b3 100644 --- a/lib/Slic3r/Print/GCode.pm +++ b/lib/Slic3r/Print/GCode.pm @@ -243,11 +243,14 @@ sub export { printf $fh "%s\n", $gcodegen->placeholder_parser->process($self->config->end_gcode); print $fh $gcodegen->writer->update_progress($gcodegen->layer_count, $gcodegen->layer_count, 1); # 100% + # get filament stats + $self->print->clear_filament_stats; $self->print->total_used_filament(0); $self->print->total_extruded_volume(0); foreach my $extruder (@{$gcodegen->writer->extruders}) { my $used_filament = $extruder->used_filament; my $extruded_volume = $extruder->extruded_volume; + $self->print->set_filament_stats($extruder->id, $used_filament); printf $fh "; filament used = %.1fmm (%.1fcm3)\n", $used_filament, $extruded_volume/1000; diff --git a/xs/src/libslic3r/GCodeSender.cpp b/xs/src/libslic3r/GCodeSender.cpp index 8ed233c5f..c0871dc90 100644 --- a/xs/src/libslic3r/GCodeSender.cpp +++ b/xs/src/libslic3r/GCodeSender.cpp @@ -23,7 +23,8 @@ namespace Slic3r { namespace asio = boost::asio; GCodeSender::GCodeSender() - : io(), serial(io), can_send(false), sent(0), error(false), connected(false) + : io(), serial(io), can_send(false), sent(0), error(false), connected(false), + queue_paused(false) {} GCodeSender::~GCodeSender() @@ -140,6 +141,23 @@ GCodeSender::queue_size() const return this->queue.size(); } +void +GCodeSender::pause_queue() +{ + boost::lock_guard l(this->queue_mutex); + this->queue_paused = true; +} + +void +GCodeSender::resume_queue() +{ + { + boost::lock_guard l(this->queue_mutex); + this->queue_paused = false; + } + this->send(); +} + void GCodeSender::do_close() { @@ -260,6 +278,7 @@ GCodeSender::send() { // printer is not connected or we're still waiting for the previous ack if (!this->can_send) return; + if (this->queue_paused) return; boost::lock_guard l(this->queue_mutex); if (this->queue.empty()) return; diff --git a/xs/src/libslic3r/GCodeSender.hpp b/xs/src/libslic3r/GCodeSender.hpp index 17c4f0485..7961a755b 100644 --- a/xs/src/libslic3r/GCodeSender.hpp +++ b/xs/src/libslic3r/GCodeSender.hpp @@ -25,6 +25,8 @@ class GCodeSender : private boost::noncopyable { bool error_status() const; bool is_connected() const; size_t queue_size() const; + void pause_queue(); + void resume_queue(); private: asio::io_service io; @@ -39,6 +41,7 @@ class GCodeSender : private boost::noncopyable { mutable boost::mutex queue_mutex; std::queue queue; bool can_send; + bool queue_paused; size_t sent; void set_baud_rate(unsigned int baud_rate); diff --git a/xs/src/libslic3r/Print.hpp b/xs/src/libslic3r/Print.hpp index aa67e1c6d..86964df48 100644 --- a/xs/src/libslic3r/Print.hpp +++ b/xs/src/libslic3r/Print.hpp @@ -161,6 +161,7 @@ class Print PlaceholderParser placeholder_parser; // TODO: status_cb double total_used_filament, total_extruded_volume; + std::map filament_stats; PrintState state; // ordered collections of extrusion paths to build skirt loops and brim diff --git a/xs/src/libslic3r/PrintConfig.cpp b/xs/src/libslic3r/PrintConfig.cpp index 2ca83c15e..e2c69bb3a 100644 --- a/xs/src/libslic3r/PrintConfig.cpp +++ b/xs/src/libslic3r/PrintConfig.cpp @@ -661,6 +661,18 @@ PrintConfigDef::build_def() { Options["seam_position"].enum_labels.push_back("Nearest"); Options["seam_position"].enum_labels.push_back("Aligned"); + Options["serial_port"].type = coString; + Options["serial_port"].label = "Serial port"; + Options["serial_port"].tooltip = "USB/serial port for printer connection."; + Options["serial_port"].cli = "serial-port=s"; + + Options["serial_speed"].type = coInt; + Options["serial_speed"].label = "Speed"; + Options["serial_speed"].tooltip = "Speed (baud) of USB/serial port for printer connection."; + Options["serial_speed"].cli = "serial-speed=i"; + Options["serial_speed"].min = 1; + Options["serial_speed"].max = 300000; + Options["skirt_distance"].type = coFloat; Options["skirt_distance"].label = "Distance from object"; Options["skirt_distance"].tooltip = "Distance between skirt and object(s). Set this to zero to attach the skirt to the object(s) and get a brim for better adhesion."; diff --git a/xs/src/libslic3r/PrintConfig.hpp b/xs/src/libslic3r/PrintConfig.hpp index b647acfb8..f219a9de1 100644 --- a/xs/src/libslic3r/PrintConfig.hpp +++ b/xs/src/libslic3r/PrintConfig.hpp @@ -587,15 +587,21 @@ class HostConfig : public virtual StaticPrintConfig public: ConfigOptionString octoprint_host; ConfigOptionString octoprint_apikey; + ConfigOptionString serial_port; + ConfigOptionInt serial_speed; HostConfig() : StaticPrintConfig() { this->octoprint_host.value = ""; this->octoprint_apikey.value = ""; + this->serial_port.value = ""; + this->serial_speed.value = 250000; }; 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; + if (opt_key == "serial_port") return &this->serial_port; + if (opt_key == "serial_speed") return &this->serial_speed; return NULL; }; diff --git a/xs/xsp/GCodeSender.xsp b/xs/xsp/GCodeSender.xsp index b0ff3da22..a9c1393f4 100644 --- a/xs/xsp/GCodeSender.xsp +++ b/xs/xsp/GCodeSender.xsp @@ -16,6 +16,8 @@ bool is_connected() const; int queue_size() const; void send(std::string s); + void pause_queue(); + void resume_queue(); }; #endif diff --git a/xs/xsp/Print.xsp b/xs/xsp/Print.xsp index fb0d17721..94237da05 100644 --- a/xs/xsp/Print.xsp +++ b/xs/xsp/Print.xsp @@ -174,6 +174,28 @@ _constant() RETVAL.push_back(*e); } %}; + void clear_filament_stats() + %code%{ + THIS->filament_stats.clear(); + %}; + void set_filament_stats(int extruder_id, float length) + %code%{ + THIS->filament_stats.insert(std::pair(extruder_id, 0)); + THIS->filament_stats[extruder_id] += length; + %}; + SV* filament_stats() + %code%{ + HV* hv = newHV(); + for (std::map::const_iterator it = THIS->filament_stats.begin(); it != THIS->filament_stats.end(); ++it) { + // stringify extruder_id + std::ostringstream ss; + ss << it->first; + std::string str = ss.str(); + + (void)hv_store( hv, str.c_str(), str.length(), newSViv(it->second), 0 ); + RETVAL = newRV_noinc((SV*)hv); + } + %}; void _simplify_slices(double distance); double max_allowed_layer_height() const; bool has_support_material() const;