diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm index eb3aac0e2..10d65475c 100644 --- a/lib/Slic3r/GUI/Plater.pm +++ b/lib/Slic3r/GUI/Plater.pm @@ -434,6 +434,7 @@ sub new { fil_mm3 => "Used Filament (mm^3)", fil_g => "Used Filament (g)", cost => "Cost", + time => "Estimated printing time", ); while (my $field = shift @info) { my $label = shift @info; @@ -1428,6 +1429,7 @@ sub on_export_completed { $self->{"print_info_cost"}->SetLabel(sprintf("%.2f" , $self->{print}->total_cost)); $self->{"print_info_fil_g"}->SetLabel(sprintf("%.2f" , $self->{print}->total_weight)); $self->{"print_info_fil_mm3"}->SetLabel(sprintf("%.2f" , $self->{print}->total_extruded_volume)); + $self->{"print_info_time"}->SetLabel($self->{print}->estimated_print_time); $self->{"print_info_fil_m"}->SetLabel(sprintf("%.2f" , $self->{print}->total_used_filament / 1000)); $self->{"print_info_box_show"}->(1); diff --git a/xs/src/libslic3r/GCode.cpp b/xs/src/libslic3r/GCode.cpp index 47695a230..6da860d36 100644 --- a/xs/src/libslic3r/GCode.cpp +++ b/xs/src/libslic3r/GCode.cpp @@ -267,22 +267,6 @@ std::string WipeTowerIntegration::finalize(GCode &gcodegen) #define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer.extruder()->id()) -inline void write(FILE *file, const std::string &what) -{ - fwrite(what.data(), 1, what.size(), file); -} - -// Write a string into a file. Add a newline, if the string does not end with a newline already. -// Used to export a custom G-code section processed by the PlaceholderParser. -inline void writeln(FILE *file, const std::string &what) -{ - if (! what.empty()) { - write(file, what); - if (what.back() != '\n') - fprintf(file, "\n"); - } -} - // Collect pairs of object_layer + support_layer sorted by print_z. // object_layer & support_layer are considered to be on the same print_z, if they are not further than EPSILON. std::vector GCode::collect_layers_to_print(const PrintObject &object) @@ -395,6 +379,7 @@ void GCode::do_export(Print *print, const char *path) msg += " !!!!! End of an error report for the custom G-code template ...\n"; throw std::runtime_error(msg); } + if (boost::nowide::rename(path_tmp.c_str(), path) != 0) throw std::runtime_error( std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + path + '\n' + @@ -403,6 +388,9 @@ void GCode::do_export(Print *print, const char *path) void GCode::_do_export(Print &print, FILE *file) { + // resets time estimator + m_time_estimator.reset(); + // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. m_layer_count = 0; @@ -486,7 +474,7 @@ void GCode::_do_export(Print &print, FILE *file) m_enable_extrusion_role_markers = (bool)m_pressure_equalizer; // Write information on the generator. - fprintf(file, "; %s\n\n", Slic3r::header_slic3r_generated().c_str()); + _write_format(file, "; %s\n\n", Slic3r::header_slic3r_generated().c_str()); // Write notes (content of the Print Settings tab -> Notes) { std::list lines; @@ -495,10 +483,10 @@ void GCode::_do_export(Print &print, FILE *file) // Remove the trailing '\r' from the '\r\n' sequence. if (! line.empty() && line.back() == '\r') line.pop_back(); - fprintf(file, "; %s\n", line.c_str()); + _write_format(file, "; %s\n", line.c_str()); } if (! lines.empty()) - fprintf(file, "\n"); + _write(file, "\n"); } // Write some terse information on the slicing parameters. const PrintObject *first_object = print.objects.front(); @@ -506,16 +494,16 @@ void GCode::_do_export(Print &print, FILE *file) const double first_layer_height = first_object->config.first_layer_height.get_abs_value(layer_height); for (size_t region_id = 0; region_id < print.regions.size(); ++ region_id) { auto region = print.regions[region_id]; - fprintf(file, "; external perimeters extrusion width = %.2fmm\n", region->flow(frExternalPerimeter, layer_height, false, false, -1., *first_object).width); - fprintf(file, "; perimeters extrusion width = %.2fmm\n", region->flow(frPerimeter, layer_height, false, false, -1., *first_object).width); - fprintf(file, "; infill extrusion width = %.2fmm\n", region->flow(frInfill, layer_height, false, false, -1., *first_object).width); - fprintf(file, "; solid infill extrusion width = %.2fmm\n", region->flow(frSolidInfill, layer_height, false, false, -1., *first_object).width); - fprintf(file, "; top infill extrusion width = %.2fmm\n", region->flow(frTopSolidInfill, layer_height, false, false, -1., *first_object).width); + _write_format(file, "; external perimeters extrusion width = %.2fmm\n", region->flow(frExternalPerimeter, layer_height, false, false, -1., *first_object).width); + _write_format(file, "; perimeters extrusion width = %.2fmm\n", region->flow(frPerimeter, layer_height, false, false, -1., *first_object).width); + _write_format(file, "; infill extrusion width = %.2fmm\n", region->flow(frInfill, layer_height, false, false, -1., *first_object).width); + _write_format(file, "; solid infill extrusion width = %.2fmm\n", region->flow(frSolidInfill, layer_height, false, false, -1., *first_object).width); + _write_format(file, "; top infill extrusion width = %.2fmm\n", region->flow(frTopSolidInfill, layer_height, false, false, -1., *first_object).width); if (print.has_support_material()) - fprintf(file, "; support material extrusion width = %.2fmm\n", support_material_flow(first_object).width); + _write_format(file, "; support material extrusion width = %.2fmm\n", support_material_flow(first_object).width); if (print.config.first_layer_extrusion_width.value > 0) - fprintf(file, "; first layer extrusion width = %.2fmm\n", region->flow(frPerimeter, first_layer_height, false, true, -1., *first_object).width); - fprintf(file, "\n"); + _write_format(file, "; first layer extrusion width = %.2fmm\n", region->flow(frPerimeter, first_layer_height, false, true, -1., *first_object).width); + _write_format(file, "\n"); } // Prepare the helper object for replacing placeholders in custom G-code and output filename. @@ -558,7 +546,7 @@ void GCode::_do_export(Print &print, FILE *file) // Disable fan. if (! print.config.cooling.get_at(initial_extruder_id) || print.config.disable_fan_first_layers.get_at(initial_extruder_id)) - write(file, m_writer.set_fan(0, true)); + _write(file, m_writer.set_fan(0, true)); // Let the start-up script prime the 1st printing tool. m_placeholder_parser.set("initial_tool", initial_extruder_id); @@ -575,24 +563,24 @@ void GCode::_do_export(Print &print, FILE *file) // Set extruder(s) temperature before and after start G-code. this->_print_first_layer_extruder_temperatures(file, print, start_gcode, initial_extruder_id, false); // Write the custom start G-code - writeln(file, start_gcode); + _writeln(file, start_gcode); // Process filament-specific gcode in extruder order. if (print.config.single_extruder_multi_material) { if (has_wipe_tower) { // Wipe tower will control the extruder switching, it will call the start_filament_gcode. } else { // Only initialize the initial extruder. - writeln(file, this->placeholder_parser_process("start_filament_gcode", print.config.start_filament_gcode.values[initial_extruder_id], initial_extruder_id)); + _writeln(file, this->placeholder_parser_process("start_filament_gcode", print.config.start_filament_gcode.values[initial_extruder_id], initial_extruder_id)); } } else { for (const std::string &start_gcode : print.config.start_filament_gcode.values) - writeln(file, this->placeholder_parser_process("start_gcode", start_gcode, (unsigned int)(&start_gcode - &print.config.start_filament_gcode.values.front()))); + _writeln(file, this->placeholder_parser_process("start_gcode", start_gcode, (unsigned int)(&start_gcode - &print.config.start_filament_gcode.values.front()))); } this->_print_first_layer_extruder_temperatures(file, print, start_gcode, initial_extruder_id, true); // Set other general things. - write(file, this->preamble()); - + _write(file, this->preamble()); + // Initialize a motion planner for object-to-object travel moves. if (print.config.avoid_crossing_perimeters.value) { // Collect outer contours of all objects over all layers. @@ -640,7 +628,7 @@ void GCode::_do_export(Print &print, FILE *file) } // Set initial extruder only after custom start G-code. - write(file, this->set_extruder(initial_extruder_id)); + _write(file, this->set_extruder(initial_extruder_id)); // Do all objects for each layer. if (print.config.complete_objects.value) { @@ -670,8 +658,8 @@ void GCode::_do_export(Print &print, FILE *file) // This happens before Z goes down to layer 0 again, so that no collision happens hopefully. m_enable_cooling_markers = false; // we're not filtering these moves through CoolingBuffer m_avoid_crossing_perimeters.use_external_mp_once = true; - write(file, this->retract()); - write(file, this->travel_to(Point(0, 0), erNone, "move to origin position for next object")); + _write(file, this->retract()); + _write(file, this->travel_to(Point(0, 0), erNone, "move to origin position for next object")); m_enable_cooling_markers = true; // Disable motion planner when traveling to first object point. m_avoid_crossing_perimeters.disable_once = true; @@ -683,7 +671,7 @@ void GCode::_do_export(Print &print, FILE *file) // Set first layer bed and extruder temperatures, don't wait for it to reach the temperature. this->_print_first_layer_bed_temperature(file, print, between_objects_gcode, initial_extruder_id, false); this->_print_first_layer_extruder_temperatures(file, print, between_objects_gcode, initial_extruder_id, false); - writeln(file, between_objects_gcode); + _writeln(file, between_objects_gcode); } // Reset the cooling buffer internal state (the current position, feed rate, accelerations). m_cooling_buffer->reset(); @@ -696,7 +684,7 @@ void GCode::_do_export(Print &print, FILE *file) this->process_layer(file, print, lrs, tool_ordering.tools_for_layer(ltp.print_z()), © - object._shifted_copies.data()); } if (m_pressure_equalizer) - write(file, m_pressure_equalizer->process("", true)); + _write(file, m_pressure_equalizer->process("", true)); ++ finished_objects; // Flag indicating whether the nozzle temperature changes from 1st to 2nd layer were performed. // Reset it when starting another object from 1st layer. @@ -716,8 +704,8 @@ void GCode::_do_export(Print &print, FILE *file) // Prusa Multi-Material wipe tower. if (has_wipe_tower && ! layers_to_print.empty()) { m_wipe_tower.reset(new WipeTowerIntegration(print.config, *print.m_wipe_tower_priming.get(), print.m_wipe_tower_tool_changes, *print.m_wipe_tower_final_purge.get())); - write(file, m_writer.travel_to_z(first_layer_height + m_config.z_offset.value, "Move to the first layer height")); - write(file, m_wipe_tower->prime(*this)); + _write(file, m_writer.travel_to_z(first_layer_height + m_config.z_offset.value, "Move to the first layer height")); + _write(file, m_wipe_tower->prime(*this)); // Verify, whether the print overaps the priming extrusions. BoundingBoxf bbox_print(get_print_extrusions_extents(print)); coordf_t twolayers_printz = ((layers_to_print.size() == 1) ? layers_to_print.front() : layers_to_print[1]).first + EPSILON; @@ -727,16 +715,17 @@ void GCode::_do_export(Print &print, FILE *file) BoundingBoxf bbox_prime(get_wipe_tower_priming_extrusions_extents(print)); bbox_prime.offset(0.5f); // Beep for 500ms, tone 800Hz. Yet better, play some Morse. - write(file, this->retract()); - fprintf(file, "M300 S800 P500\n"); + _write(file, this->retract()); + _write(file, "M300 S800 P500\n"); if (bbox_prime.overlap(bbox_print)) { // Wait for the user to remove the priming extrusions, otherwise they would // get covered by the print. - fprintf(file, "M1 Remove priming towers and click button.\n"); - } else { + _write(file, "M1 Remove priming towers and click button.\n"); + } + else { // Just wait for a bit to let the user check, that the priming succeeded. //TODO Add a message explaining what the printer is waiting for. This needs a firmware fix. - fprintf(file, "M1 S10\n"); + _write(file, "M1 S10\n"); } } // Extrude the layers. @@ -747,26 +736,29 @@ void GCode::_do_export(Print &print, FILE *file) this->process_layer(file, print, layer.second, layer_tools, size_t(-1)); } if (m_pressure_equalizer) - write(file, m_pressure_equalizer->process("", true)); + _write(file, m_pressure_equalizer->process("", true)); if (m_wipe_tower) // Purge the extruder, pull out the active filament. - write(file, m_wipe_tower->finalize(*this)); + _write(file, m_wipe_tower->finalize(*this)); } // Write end commands to file. - write(file, this->retract()); - write(file, m_writer.set_fan(false)); + _write(file, this->retract()); + _write(file, m_writer.set_fan(false)); // Process filament-specific gcode in extruder order. if (print.config.single_extruder_multi_material) { // Process the end_filament_gcode for the active filament only. - writeln(file, this->placeholder_parser_process("end_filament_gcode", print.config.end_filament_gcode.get_at(m_writer.extruder()->id()), m_writer.extruder()->id())); + _writeln(file, this->placeholder_parser_process("end_filament_gcode", print.config.end_filament_gcode.get_at(m_writer.extruder()->id()), m_writer.extruder()->id())); } else { for (const std::string &end_gcode : print.config.end_filament_gcode.values) - writeln(file, this->placeholder_parser_process("end_gcode", end_gcode, (unsigned int)(&end_gcode - &print.config.end_filament_gcode.values.front()))); + _writeln(file, this->placeholder_parser_process("end_gcode", end_gcode, (unsigned int)(&end_gcode - &print.config.end_filament_gcode.values.front()))); } - writeln(file, this->placeholder_parser_process("end_gcode", print.config.end_gcode, m_writer.extruder()->id())); - write(file, m_writer.update_progress(m_layer_count, m_layer_count, true)); // 100% - write(file, m_writer.postamble()); + _writeln(file, this->placeholder_parser_process("end_gcode", print.config.end_gcode, m_writer.extruder()->id())); + _write(file, m_writer.update_progress(m_layer_count, m_layer_count, true)); // 100% + _write(file, m_writer.postamble()); + + // calculates estimated printing time + m_time_estimator.calculate_time(); // Get filament stats. print.filament_stats.clear(); @@ -774,35 +766,37 @@ void GCode::_do_export(Print &print, FILE *file) print.total_extruded_volume = 0.; print.total_weight = 0.; print.total_cost = 0.; + print.estimated_print_time = m_time_estimator.get_time_hms(); for (const Extruder &extruder : m_writer.extruders()) { double used_filament = extruder.used_filament(); double extruded_volume = extruder.extruded_volume(); double filament_weight = extruded_volume * extruder.filament_density() * 0.001; double filament_cost = filament_weight * extruder.filament_cost() * 0.001; print.filament_stats.insert(std::pair(extruder.id(), used_filament)); - fprintf(file, "; filament used = %.1lfmm (%.1lfcm3)\n", used_filament, extruded_volume * 0.001); + _write_format(file, "; filament used = %.1lfmm (%.1lfcm3)\n", used_filament, extruded_volume * 0.001); if (filament_weight > 0.) { print.total_weight = print.total_weight + filament_weight; - fprintf(file, "; filament used = %.1lf\n", filament_weight); + _write_format(file, "; filament used = %.1lf\n", filament_weight); if (filament_cost > 0.) { print.total_cost = print.total_cost + filament_cost; - fprintf(file, "; filament cost = %.1lf\n", filament_cost); + _write_format(file, "; filament cost = %.1lf\n", filament_cost); } } - print.total_used_filament = print.total_used_filament + used_filament; + print.total_used_filament = print.total_used_filament + used_filament; print.total_extruded_volume = print.total_extruded_volume + extruded_volume; } - fprintf(file, "; total filament cost = %.1lf\n", print.total_cost); + _write_format(file, "; total filament cost = %.1lf\n", print.total_cost); + _write_format(file, "; estimated printing time = %s\n", m_time_estimator.get_time_hms()); // Append full config. - fprintf(file, "\n"); + _write(file, "\n"); { StaticPrintConfig *configs[] = { &print.config, &print.default_object_config, &print.default_region_config }; for (size_t i = 0; i < sizeof(configs) / sizeof(configs[0]); ++ i) { StaticPrintConfig *cfg = configs[i]; for (const std::string &key : cfg->keys()) if (key != "compatible_printers") - fprintf(file, "; %s = %s\n", key.c_str(), cfg->serialize(key).c_str()); + _write_format(file, "; %s = %s\n", key.c_str(), cfg->serialize(key).c_str()); } } } @@ -893,7 +887,7 @@ void GCode::_print_first_layer_bed_temperature(FILE *file, Print &print, const s // the custom start G-code emited these. std::string set_temp_gcode = m_writer.set_bed_temperature(temp, wait); if (! temp_set_by_gcode) - write(file, set_temp_gcode); + _write(file, set_temp_gcode); } // Write 1st layer extruder temperatures into the G-code. @@ -916,7 +910,7 @@ void GCode::_print_first_layer_extruder_temperatures(FILE *file, Print &print, c // Set temperature of the first printing extruder only. int temp = print.config.first_layer_temperature.get_at(first_printing_extruder_id); if (temp > 0) - write(file, m_writer.set_temperature(temp, wait, first_printing_extruder_id)); + _write(file, m_writer.set_temperature(temp, wait, first_printing_extruder_id)); } else { // Set temperatures of all the printing extruders. for (unsigned int tool_id : print.extruders()) { @@ -924,7 +918,7 @@ void GCode::_print_first_layer_extruder_temperatures(FILE *file, Print &print, c if (print.config.ooze_prevention.value) temp += print.config.standby_temperature_delta.value; if (temp > 0) - write(file, m_writer.set_temperature(temp, wait, tool_id)); + _write(file, m_writer.set_temperature(temp, wait, tool_id)); } } } @@ -1358,7 +1352,7 @@ void GCode::process_layer( gcode = m_pressure_equalizer->process(gcode.c_str(), false); // printf("G-code after filter:\n%s\n", out.c_str()); - write(file, gcode); + _write(file, gcode); } void GCode::apply_print_config(const PrintConfig &print_config) @@ -1993,6 +1987,46 @@ std::string GCode::extrude_support(const ExtrusionEntityCollection &support_fill return gcode; } +void GCode::_write(FILE* file, const std::string& what) +{ + if (!what.empty()) { + // writes string to file + fwrite(what.data(), 1, what.size(), file); + // updates time estimator and gcode lines vector + const char endLine = '\n'; + std::string::size_type beginPos = 0; + std::string::size_type endPos = what.find_first_of(endLine, beginPos); + while (endPos != std::string::npos) { + std::string line = what.substr(beginPos, endPos - beginPos + 1); + m_time_estimator.add_gcode_line(line); + beginPos = endPos + 1; + endPos = what.find_first_of(endLine, beginPos); + } + } +} + +void GCode::_writeln(FILE* file, const std::string& what) +{ + if (!what.empty()) { + if (what.back() != '\n') + _write_format(file, "%s\n", what.c_str()); + else + _write(file, what); + } +} + +void GCode::_write_format(FILE* file, const char* format, ...) +{ + char buffer[1024]; + va_list args; + va_start(args, format); + int res = ::vsnprintf(buffer, 1024, format, args); + va_end(args); + + if (res >= 0) + _writeln(file, buffer); +} + std::string GCode::_extrude(const ExtrusionPath &path, std::string description, double speed) { std::string gcode; @@ -2182,8 +2216,7 @@ bool GCode::needs_retraction(const Polyline &travel, ExtrusionRole role) return true; } -std::string -GCode::retract(bool toolchange) +std::string GCode::retract(bool toolchange) { std::string gcode; diff --git a/xs/src/libslic3r/GCode.hpp b/xs/src/libslic3r/GCode.hpp index 2fd3b39d3..cb8b0027a 100644 --- a/xs/src/libslic3r/GCode.hpp +++ b/xs/src/libslic3r/GCode.hpp @@ -15,6 +15,7 @@ #include "GCode/SpiralVase.hpp" #include "GCode/ToolOrdering.hpp" #include "GCode/WipeTower.hpp" +#include "GCodeTimeEstimator.hpp" #include "EdgeGrid.hpp" #include @@ -273,6 +274,20 @@ protected: // Index of a last object copy extruded. std::pair m_last_obj_copy; + // Time estimator + GCodeTimeEstimator m_time_estimator; + + // Write a string into a file. + void _write(FILE* file, const std::string& what); + + // Write a string into a file. + // Add a newline, if the string does not end with a newline already. + // Used to export a custom G-code section processed by the PlaceholderParser. + void _writeln(FILE* file, const std::string& what); + + // Formats and write into a file the given data. + void _write_format(FILE* file, const char* format, ...); + std::string _extrude(const ExtrusionPath &path, std::string description = "", double speed = -1); void _print_first_layer_bed_temperature(FILE *file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait); void _print_first_layer_extruder_temperatures(FILE *file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait); diff --git a/xs/src/libslic3r/GCodeTimeEstimator.cpp b/xs/src/libslic3r/GCodeTimeEstimator.cpp index c6fa353b4..56cff252c 100644 --- a/xs/src/libslic3r/GCodeTimeEstimator.cpp +++ b/xs/src/libslic3r/GCodeTimeEstimator.cpp @@ -2,77 +2,980 @@ #include #include +static const std::string AXIS_STR = "XYZE"; +static const float MMMIN_TO_MMSEC = 1.0f / 60.0f; +static const float MILLISEC_TO_SEC = 0.001f; +static const float INCHES_TO_MM = 25.4f; +static const float DEFAULT_FEEDRATE = 1500.0f; // from Prusa Firmware (Marlin_main.cpp) +static const float DEFAULT_ACCELERATION = 1500.0f; // Prusa Firmware 1_75mm_MK2 +static const float DEFAULT_RETRACT_ACCELERATION = 1500.0f; // Prusa Firmware 1_75mm_MK2 +static const float DEFAULT_AXIS_MAX_FEEDRATE[] = { 500.0f, 500.0f, 12.0f, 120.0f }; // Prusa Firmware 1_75mm_MK2 +static const float DEFAULT_AXIS_MAX_ACCELERATION[] = { 9000.0f, 9000.0f, 500.0f, 10000.0f }; // Prusa Firmware 1_75mm_MK2 +static const float DEFAULT_AXIS_MAX_JERK[] = { 10.0f, 10.0f, 0.2f, 2.5f }; // from Prusa Firmware (Configuration.h) +static const float DEFAULT_MINIMUM_FEEDRATE = 0.0f; // from Prusa Firmware (Configuration_adv.h) +static const float DEFAULT_MINIMUM_TRAVEL_FEEDRATE = 0.0f; // from Prusa Firmware (Configuration_adv.h) + +static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; + namespace Slic3r { -void -GCodeTimeEstimator::parse(const std::string &gcode) -{ - GCodeReader::parse(gcode, boost::bind(&GCodeTimeEstimator::_parser, this, _1, _2)); -} + void GCodeTimeEstimator::Feedrates::reset() + { + feedrate = 0.0f; + safe_feedrate = 0.0f; + ::memset(axis_feedrate, 0, Num_Axis * sizeof(float)); + ::memset(abs_axis_feedrate, 0, Num_Axis * sizeof(float)); + } -void -GCodeTimeEstimator::parse_file(const std::string &file) -{ - GCodeReader::parse_file(file, boost::bind(&GCodeTimeEstimator::_parser, this, _1, _2)); -} + float GCodeTimeEstimator::Block::Trapezoid::acceleration_time(float acceleration) const + { + return acceleration_time_from_distance(feedrate.entry, accelerate_until, acceleration); + } -void -GCodeTimeEstimator::_parser(GCodeReader&, const GCodeReader::GCodeLine &line) -{ - // std::cout << "[" << this->time << "] " << line.raw << std::endl; - if (line.cmd == "G1") { - const float dist_XY = line.dist_XY(); - const float new_F = line.new_F(); - - if (dist_XY > 0) { - //this->time += dist_XY / new_F * 60; - this->time += _accelerated_move(dist_XY, new_F/60, this->acceleration); - } else { - //this->time += std::abs(line.dist_E()) / new_F * 60; - this->time += _accelerated_move(std::abs(line.dist_E()), new_F/60, this->acceleration); - } - //this->time += std::abs(line.dist_Z()) / new_F * 60; - this->time += _accelerated_move(std::abs(line.dist_Z()), new_F/60, this->acceleration); - } else if (line.cmd == "M204" && line.has('S')) { - this->acceleration = line.get_float('S'); - } else if (line.cmd == "G4") { // swell - if (line.has('S')) { - this->time += line.get_float('S'); - } else if (line.has('P')) { - this->time += line.get_float('P')/1000; - } + float GCodeTimeEstimator::Block::Trapezoid::cruise_time() const + { + return (feedrate.cruise != 0.0f) ? cruise_distance() / feedrate.cruise : 0.0f; + } + + float GCodeTimeEstimator::Block::Trapezoid::deceleration_time(float acceleration) const + { + return acceleration_time_from_distance(feedrate.cruise, (distance - decelerate_after), -acceleration); + } + + float GCodeTimeEstimator::Block::Trapezoid::cruise_distance() const + { + return decelerate_after - accelerate_until; + } + + float GCodeTimeEstimator::Block::Trapezoid::acceleration_time_from_distance(float initial_feedrate, float distance, float acceleration) + { + return (acceleration != 0.0f) ? (speed_from_distance(initial_feedrate, distance, acceleration) - initial_feedrate) / acceleration : 0.0f; + } + + float GCodeTimeEstimator::Block::Trapezoid::speed_from_distance(float initial_feedrate, float distance, float acceleration) + { + return ::sqrt(sqr(initial_feedrate) + 2.0f * acceleration * distance); + } + + float GCodeTimeEstimator::Block::move_length() const + { + float length = ::sqrt(sqr(delta_pos[X]) + sqr(delta_pos[Y]) + sqr(delta_pos[Z])); + return (length > 0.0f) ? length : ::abs(delta_pos[E]); + } + + float GCodeTimeEstimator::Block::is_extruder_only_move() const + { + return (delta_pos[X] == 0.0f) && (delta_pos[Y] == 0.0f) && (delta_pos[Z] == 0.0f) && (delta_pos[E] != 0.0f); + } + + float GCodeTimeEstimator::Block::is_travel_move() const + { + return delta_pos[E] == 0.0f; + } + + float GCodeTimeEstimator::Block::acceleration_time() const + { + return trapezoid.acceleration_time(acceleration); + } + + float GCodeTimeEstimator::Block::cruise_time() const + { + return trapezoid.cruise_time(); + } + + float GCodeTimeEstimator::Block::deceleration_time() const + { + return trapezoid.deceleration_time(acceleration); + } + + float GCodeTimeEstimator::Block::cruise_distance() const + { + return trapezoid.cruise_distance(); + } + + void GCodeTimeEstimator::Block::calculate_trapezoid() + { + float distance = move_length(); + + trapezoid.distance = distance; + trapezoid.feedrate = feedrate; + + float accelerate_distance = estimate_acceleration_distance(feedrate.entry, feedrate.cruise, acceleration); + float decelerate_distance = estimate_acceleration_distance(feedrate.cruise, feedrate.exit, -acceleration); + float cruise_distance = distance - accelerate_distance - decelerate_distance; + + // Not enough space to reach the nominal feedrate. + // This means no cruising, and we'll have to use intersection_distance() to calculate when to abort acceleration + // and start braking in order to reach the exit_feedrate exactly at the end of this block. + if (cruise_distance < 0.0f) + { + accelerate_distance = clamp(0.0f, distance, intersection_distance(feedrate.entry, feedrate.exit, acceleration, distance)); + cruise_distance = 0.0f; + trapezoid.feedrate.cruise = Trapezoid::speed_from_distance(feedrate.entry, accelerate_distance, acceleration); } -} -// Wildly optimistic acceleration "bell" curve modeling. -// Returns an estimate of how long the move with a given accel -// takes in seconds. -// It is assumed that the movement is smooth and uniform. -float -GCodeTimeEstimator::_accelerated_move(double length, double v, double acceleration) -{ - // for half of the move, there are 2 zones, where the speed is increasing/decreasing and - // where the speed is constant. - // Since the slowdown is assumed to be uniform, calculate the average velocity for half of the - // expected displacement. - // final velocity v = a*t => a * (dx / 0.5v) => v^2 = 2*a*dx - // v_avg = 0.5v => 2*v_avg = v - // d_x = v_avg*t => t = d_x / v_avg - acceleration = (acceleration == 0.0 ? 4000.0 : acceleration); // Set a default accel to use for print time in case it's 0 somehow. - auto half_length = length / 2.0; - auto t_init = v / acceleration; // time to final velocity - auto dx_init = (0.5*v*t_init); // Initial displacement for the time to get to final velocity - auto t = 0.0; - if (half_length >= dx_init) { - half_length -= (0.5*v*t_init); - t += t_init; - t += (half_length / v); // rest of time is at constant speed. - } else { - // If too much displacement for the expected final velocity, we don't hit the max, so reduce - // the average velocity to fit the displacement we actually are looking for. - t += std::sqrt(std::abs(length) * 2.0 * acceleration) / acceleration; + trapezoid.accelerate_until = accelerate_distance; + trapezoid.decelerate_after = accelerate_distance + cruise_distance; + } + + float GCodeTimeEstimator::Block::max_allowable_speed(float acceleration, float target_velocity, float distance) + { + return ::sqrt(sqr(target_velocity) - 2.0f * acceleration * distance); + } + + float GCodeTimeEstimator::Block::estimate_acceleration_distance(float initial_rate, float target_rate, float acceleration) + { + return (acceleration == 0.0f) ? 0.0f : (sqr(target_rate) - sqr(initial_rate)) / (2.0f * acceleration); + } + + float GCodeTimeEstimator::Block::intersection_distance(float initial_rate, float final_rate, float acceleration, float distance) + { + return (acceleration == 0.0f) ? 0.0f : (2.0f * acceleration * distance - sqr(initial_rate) + sqr(final_rate)) / (4.0f * acceleration); + } + + GCodeTimeEstimator::GCodeTimeEstimator() + { + reset(); + set_default(); + } + + void GCodeTimeEstimator::calculate_time_from_text(const std::string& gcode) + { + _parser.parse(gcode, boost::bind(&GCodeTimeEstimator::_process_gcode_line, this, _1, _2)); + _calculate_time(); + reset(); + } + + void GCodeTimeEstimator::calculate_time_from_file(const std::string& file) + { + _parser.parse_file(file, boost::bind(&GCodeTimeEstimator::_process_gcode_line, this, _1, _2)); + _calculate_time(); + reset(); + } + + void GCodeTimeEstimator::calculate_time_from_lines(const std::vector& gcode_lines) + { + for (const std::string& line : gcode_lines) + { + _parser.parse_line(line, boost::bind(&GCodeTimeEstimator::_process_gcode_line, this, _1, _2)); } - return 2.0*t; // cut in half before, so double to get full time spent. -} + _calculate_time(); + reset(); + } + void GCodeTimeEstimator::add_gcode_line(const std::string& gcode_line) + { + _parser.parse_line(gcode_line, boost::bind(&GCodeTimeEstimator::_process_gcode_line, this, _1, _2)); + } + + void GCodeTimeEstimator::calculate_time() + { + _calculate_time(); + _reset(); + } + + void GCodeTimeEstimator::set_axis_position(EAxis axis, float position) + { + _state.axis[axis].position = position; + } + + void GCodeTimeEstimator::set_axis_max_feedrate(EAxis axis, float feedrate_mm_sec) + { + _state.axis[axis].max_feedrate = feedrate_mm_sec; + } + + void GCodeTimeEstimator::set_axis_max_acceleration(EAxis axis, float acceleration) + { + _state.axis[axis].max_acceleration = acceleration; + } + + void GCodeTimeEstimator::set_axis_max_jerk(EAxis axis, float jerk) + { + _state.axis[axis].max_jerk = jerk; + } + + float GCodeTimeEstimator::get_axis_position(EAxis axis) const + { + return _state.axis[axis].position; + } + + float GCodeTimeEstimator::get_axis_max_feedrate(EAxis axis) const + { + return _state.axis[axis].max_feedrate; + } + + float GCodeTimeEstimator::get_axis_max_acceleration(EAxis axis) const + { + return _state.axis[axis].max_acceleration; + } + + float GCodeTimeEstimator::get_axis_max_jerk(EAxis axis) const + { + return _state.axis[axis].max_jerk; + } + + void GCodeTimeEstimator::set_feedrate(float feedrate_mm_sec) + { + _state.feedrate = feedrate_mm_sec; + } + + float GCodeTimeEstimator::get_feedrate() const + { + return _state.feedrate; + } + + void GCodeTimeEstimator::set_acceleration(float acceleration_mm_sec2) + { + _state.acceleration = acceleration_mm_sec2; + } + + float GCodeTimeEstimator::get_acceleration() const + { + return _state.acceleration; + } + + void GCodeTimeEstimator::set_retract_acceleration(float acceleration_mm_sec2) + { + _state.retract_acceleration = acceleration_mm_sec2; + } + + float GCodeTimeEstimator::get_retract_acceleration() const + { + return _state.retract_acceleration; + } + + void GCodeTimeEstimator::set_minimum_feedrate(float feedrate_mm_sec) + { + _state.minimum_feedrate = feedrate_mm_sec; + } + + float GCodeTimeEstimator::get_minimum_feedrate() const + { + return _state.minimum_feedrate; + } + + void GCodeTimeEstimator::set_minimum_travel_feedrate(float feedrate_mm_sec) + { + _state.minimum_travel_feedrate = feedrate_mm_sec; + } + + float GCodeTimeEstimator::get_minimum_travel_feedrate() const + { + return _state.minimum_travel_feedrate; + } + + void GCodeTimeEstimator::set_dialect(GCodeTimeEstimator::EDialect dialect) + { + _state.dialect = dialect; + } + + GCodeTimeEstimator::EDialect GCodeTimeEstimator::get_dialect() const + { + return _state.dialect; + } + + void GCodeTimeEstimator::set_units(GCodeTimeEstimator::EUnits units) + { + _state.units = units; + } + + GCodeTimeEstimator::EUnits GCodeTimeEstimator::get_units() const + { + return _state.units; + } + + void GCodeTimeEstimator::set_positioning_xyz_type(GCodeTimeEstimator::EPositioningType type) + { + _state.positioning_xyz_type = type; + } + + GCodeTimeEstimator::EPositioningType GCodeTimeEstimator::get_positioning_xyz_type() const + { + return _state.positioning_xyz_type; + } + + void GCodeTimeEstimator::set_positioning_e_type(GCodeTimeEstimator::EPositioningType type) + { + _state.positioning_e_type = type; + } + + GCodeTimeEstimator::EPositioningType GCodeTimeEstimator::get_positioning_e_type() const + { + return _state.positioning_e_type; + } + + void GCodeTimeEstimator::add_additional_time(float timeSec) + { + _state.additional_time += timeSec; + } + + void GCodeTimeEstimator::set_additional_time(float timeSec) + { + _state.additional_time = timeSec; + } + + float GCodeTimeEstimator::get_additional_time() const + { + return _state.additional_time; + } + + void GCodeTimeEstimator::set_default() + { + set_units(Millimeters); + set_dialect(Unknown); + set_positioning_xyz_type(Absolute); + set_positioning_e_type(Relative); + + set_feedrate(DEFAULT_FEEDRATE); + set_acceleration(DEFAULT_ACCELERATION); + set_retract_acceleration(DEFAULT_RETRACT_ACCELERATION); + set_minimum_feedrate(DEFAULT_MINIMUM_FEEDRATE); + set_minimum_travel_feedrate(DEFAULT_MINIMUM_TRAVEL_FEEDRATE); + + for (unsigned char a = X; a < Num_Axis; ++a) + { + EAxis axis = (EAxis)a; + set_axis_max_feedrate(axis, DEFAULT_AXIS_MAX_FEEDRATE[a]); + set_axis_max_acceleration(axis, DEFAULT_AXIS_MAX_ACCELERATION[a]); + set_axis_max_jerk(axis, DEFAULT_AXIS_MAX_JERK[a]); + } + } + + void GCodeTimeEstimator::reset() + { + _blocks.clear(); + _reset(); + } + + float GCodeTimeEstimator::get_time() const + { + return _time; + } + + std::string GCodeTimeEstimator::get_time_hms() const + { + float timeinsecs = get_time(); + int hours = (int)(timeinsecs / 3600.0f); + timeinsecs -= (float)hours * 3600.0f; + int minutes = (int)(timeinsecs / 60.0f); + timeinsecs -= (float)minutes * 60.0f; + + char buffer[64]; + if (hours > 0) + ::sprintf(buffer, "%dh %dm %ds", hours, minutes, (int)timeinsecs); + else if (minutes > 0) + ::sprintf(buffer, "%dm %ds", minutes, (int)timeinsecs); + else + ::sprintf(buffer, "%ds", (int)timeinsecs); + + return buffer; + } + + void GCodeTimeEstimator::_reset() + { + _curr.reset(); + _prev.reset(); + + set_axis_position(X, 0.0f); + set_axis_position(Y, 0.0f); + set_axis_position(Z, 0.0f); + + set_additional_time(0.0f); + } + + void GCodeTimeEstimator::_calculate_time() + { + _forward_pass(); + _reverse_pass(); + _recalculate_trapezoids(); + + _time = get_additional_time(); + + for (const Block& block : _blocks) + { + _time += block.acceleration_time(); + _time += block.cruise_time(); + _time += block.deceleration_time(); + } + } + + void GCodeTimeEstimator::_process_gcode_line(GCodeReader&, const GCodeReader::GCodeLine& line) + { + if (line.cmd.length() > 1) + { + switch (::toupper(line.cmd[0])) + { + case 'G': + { + switch (::atoi(&line.cmd[1])) + { + case 1: // Move + { + _processG1(line); + break; + } + case 4: // Dwell + { + _processG4(line); + break; + } + case 20: // Set Units to Inches + { + _processG20(line); + break; + } + case 21: // Set Units to Millimeters + { + _processG21(line); + break; + } + case 28: // Move to Origin (Home) + { + _processG28(line); + break; + } + case 90: // Set to Absolute Positioning + { + _processG90(line); + break; + } + case 91: // Set to Relative Positioning + { + _processG91(line); + break; + } + case 92: // Set Position + { + _processG92(line); + break; + } + } + + break; + } + case 'M': + { + switch (::atoi(&line.cmd[1])) + { + case 82: // Set extruder to absolute mode + { + _processM82(line); + break; + } + case 83: // Set extruder to relative mode + { + _processM83(line); + break; + } + case 109: // Set Extruder Temperature and Wait + { + _processM109(line); + break; + } + case 201: // Set max printing acceleration + { + _processM201(line); + break; + } + case 203: // Set maximum feedrate + { + _processM203(line); + break; + } + case 204: // Set default acceleration + { + _processM204(line); + break; + } + case 205: // Advanced settings + { + _processM205(line); + break; + } + case 566: // Set allowable instantaneous speed change + { + _processM566(line); + break; + } + } + + break; + } + } + } + } + + // Returns the new absolute position on the given axis in dependence of the given parameters + float axis_absolute_position_from_G1_line(GCodeTimeEstimator::EAxis axis, const GCodeReader::GCodeLine& lineG1, GCodeTimeEstimator::EUnits units, GCodeTimeEstimator::EPositioningType type, float current_absolute_position) + { + float lengthsScaleFactor = (units == GCodeTimeEstimator::Inches) ? INCHES_TO_MM : 1.0f; + if (lineG1.has(AXIS_STR[axis])) + { + float ret = lineG1.get_float(AXIS_STR[axis]) * lengthsScaleFactor; + return (type == GCodeTimeEstimator::Absolute) ? ret : current_absolute_position + ret; + } + else + return current_absolute_position; + } + + void GCodeTimeEstimator::_processG1(const GCodeReader::GCodeLine& line) + { + // updates axes positions from line + EUnits units = get_units(); + float new_pos[Num_Axis]; + for (unsigned char a = X; a < Num_Axis; ++a) + { + new_pos[a] = axis_absolute_position_from_G1_line((EAxis)a, line, units, (a == E) ? get_positioning_e_type() : get_positioning_xyz_type(), get_axis_position((EAxis)a)); + } + + // updates feedrate from line, if present + if (line.has('F')) + set_feedrate(std::max(line.get_float('F') * MMMIN_TO_MMSEC, get_minimum_feedrate())); + + // fills block data + Block block; + + // calculates block movement deltas + float max_abs_delta = 0.0f; + for (unsigned char a = X; a < Num_Axis; ++a) + { + block.delta_pos[a] = new_pos[a] - get_axis_position((EAxis)a); + max_abs_delta = std::max(max_abs_delta, ::abs(block.delta_pos[a])); + } + + // is it a move ? + if (max_abs_delta == 0.0f) + return; + + // calculates block feedrate + _curr.feedrate = std::max(get_feedrate(), block.is_travel_move() ? get_minimum_travel_feedrate() : get_minimum_feedrate()); + + float distance = block.move_length(); + float invDistance = 1.0f / distance; + + float min_feedrate_factor = 1.0f; + for (unsigned char a = X; a < Num_Axis; ++a) + { + _curr.axis_feedrate[a] = _curr.feedrate * block.delta_pos[a] * invDistance; + _curr.abs_axis_feedrate[a] = ::abs(_curr.axis_feedrate[a]); + if (_curr.abs_axis_feedrate[a] > 0.0f) + min_feedrate_factor = std::min(min_feedrate_factor, get_axis_max_feedrate((EAxis)a) / _curr.abs_axis_feedrate[a]); + } + + block.feedrate.cruise = min_feedrate_factor * _curr.feedrate; + + for (unsigned char a = X; a < Num_Axis; ++a) + { + _curr.axis_feedrate[a] *= min_feedrate_factor; + _curr.abs_axis_feedrate[a] *= min_feedrate_factor; + } + + // calculates block acceleration + float acceleration = block.is_extruder_only_move() ? get_retract_acceleration() : get_acceleration(); + + for (unsigned char a = X; a < Num_Axis; ++a) + { + float axis_max_acceleration = get_axis_max_acceleration((EAxis)a); + if (acceleration * ::abs(block.delta_pos[a]) * invDistance > axis_max_acceleration) + acceleration = axis_max_acceleration; + } + + block.acceleration = acceleration; + + // calculates block exit feedrate + _curr.safe_feedrate = block.feedrate.cruise; + + for (unsigned char a = X; a < Num_Axis; ++a) + { + float axis_max_jerk = get_axis_max_jerk((EAxis)a); + if (_curr.abs_axis_feedrate[a] > axis_max_jerk) + _curr.safe_feedrate = std::min(_curr.safe_feedrate, axis_max_jerk); + } + + block.feedrate.exit = _curr.safe_feedrate; + + // calculates block entry feedrate + float vmax_junction = _curr.safe_feedrate; + if (!_blocks.empty() && (_prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD)) + { + bool prev_speed_larger = _prev.feedrate > block.feedrate.cruise; + float smaller_speed_factor = prev_speed_larger ? (block.feedrate.cruise / _prev.feedrate) : (_prev.feedrate / block.feedrate.cruise); + // Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting. + vmax_junction = prev_speed_larger ? block.feedrate.cruise : _prev.feedrate; + + float v_factor = 1.0f; + bool limited = false; + + for (unsigned char a = X; a < Num_Axis; ++a) + { + // Limit an axis. We have to differentiate coasting from the reversal of an axis movement, or a full stop. + float v_exit = _prev.axis_feedrate[a]; + float v_entry = _curr.axis_feedrate[a]; + + if (prev_speed_larger) + v_exit *= smaller_speed_factor; + + if (limited) + { + v_exit *= v_factor; + v_entry *= v_factor; + } + + // Calculate the jerk depending on whether the axis is coasting in the same direction or reversing a direction. + float jerk = + (v_exit > v_entry) ? + (((v_entry > 0.0f) || (v_exit < 0.0f)) ? + // coasting + (v_exit - v_entry) : + // axis reversal + std::max(v_exit, -v_entry)) : + // v_exit <= v_entry + (((v_entry < 0.0f) || (v_exit > 0.0f)) ? + // coasting + (v_entry - v_exit) : + // axis reversal + std::max(-v_exit, v_entry)); + + float axis_max_jerk = get_axis_max_jerk((EAxis)a); + if (jerk > axis_max_jerk) + { + v_factor *= axis_max_jerk / jerk; + limited = true; + } + } + + if (limited) + vmax_junction *= v_factor; + + // Now the transition velocity is known, which maximizes the shared exit / entry velocity while + // respecting the jerk factors, it may be possible, that applying separate safe exit / entry velocities will achieve faster prints. + float vmax_junction_threshold = vmax_junction * 0.99f; + + // Not coasting. The machine will stop and start the movements anyway, better to start the segment from start. + if ((_prev.safe_feedrate > vmax_junction_threshold) && (_curr.safe_feedrate > vmax_junction_threshold)) + vmax_junction = _curr.safe_feedrate; + } + + float v_allowable = Block::max_allowable_speed(-acceleration, _curr.safe_feedrate, distance); + block.feedrate.entry = std::min(vmax_junction, v_allowable); + + block.max_entry_speed = vmax_junction; + block.flags.nominal_length = (block.feedrate.cruise <= v_allowable); + block.flags.recalculate = true; + block.safe_feedrate = _curr.safe_feedrate; + + // calculates block trapezoid + block.calculate_trapezoid(); + + // updates previous + _prev = _curr; + + // updates axis positions + for (unsigned char a = X; a < Num_Axis; ++a) + { + set_axis_position((EAxis)a, new_pos[a]); + } + + // adds block to blocks list + _blocks.push_back(block); + } + + void GCodeTimeEstimator::_processG4(const GCodeReader::GCodeLine& line) + { + EDialect dialect = get_dialect(); + + if (line.has('P')) + add_additional_time(line.get_float('P') * MILLISEC_TO_SEC); + + // see: http://reprap.org/wiki/G-code#G4:_Dwell + if ((dialect == Repetier) || + (dialect == Marlin) || + (dialect == Smoothieware) || + (dialect == RepRapFirmware)) + { + if (line.has('S')) + add_additional_time(line.get_float('S')); + } + } + + void GCodeTimeEstimator::_processG20(const GCodeReader::GCodeLine& line) + { + set_units(Inches); + } + + void GCodeTimeEstimator::_processG21(const GCodeReader::GCodeLine& line) + { + set_units(Millimeters); + } + + void GCodeTimeEstimator::_processG28(const GCodeReader::GCodeLine& line) + { + // TODO + } + + void GCodeTimeEstimator::_processG90(const GCodeReader::GCodeLine& line) + { + set_positioning_xyz_type(Absolute); + } + + void GCodeTimeEstimator::_processG91(const GCodeReader::GCodeLine& line) + { + // TODO: THERE ARE DIALECT VARIANTS + + set_positioning_xyz_type(Relative); + } + + void GCodeTimeEstimator::_processM82(const GCodeReader::GCodeLine& line) + { + set_positioning_e_type(Absolute); + } + + void GCodeTimeEstimator::_processM83(const GCodeReader::GCodeLine& line) + { + set_positioning_e_type(Relative); + } + + void GCodeTimeEstimator::_processG92(const GCodeReader::GCodeLine& line) + { + float lengthsScaleFactor = (get_units() == Inches) ? INCHES_TO_MM : 1.0f; + bool anyFound = false; + + if (line.has('X')) + { + set_axis_position(X, line.get_float('X') * lengthsScaleFactor); + anyFound = true; + } + + if (line.has('Y')) + { + set_axis_position(Y, line.get_float('Y') * lengthsScaleFactor); + anyFound = true; + } + + if (line.has('Z')) + { + set_axis_position(Z, line.get_float('Z') * lengthsScaleFactor); + anyFound = true; + } + + if (line.has('E')) + { + set_axis_position(E, line.get_float('E') * lengthsScaleFactor); + anyFound = true; + } + + if (!anyFound) + { + for (unsigned char a = X; a < Num_Axis; ++a) + { + set_axis_position((EAxis)a, 0.0f); + } + } + } + + void GCodeTimeEstimator::_processM109(const GCodeReader::GCodeLine& line) + { + // TODO + } + + void GCodeTimeEstimator::_processM201(const GCodeReader::GCodeLine& line) + { + EDialect dialect = get_dialect(); + + // see http://reprap.org/wiki/G-code#M201:_Set_max_printing_acceleration + float factor = ((dialect != RepRapFirmware) && (get_units() == GCodeTimeEstimator::Inches)) ? INCHES_TO_MM : 1.0f; + + if (line.has('X')) + set_axis_max_acceleration(X, line.get_float('X') * factor); + + if (line.has('Y')) + set_axis_max_acceleration(Y, line.get_float('Y') * factor); + + if (line.has('Z')) + set_axis_max_acceleration(Z, line.get_float('Z') * factor); + + if (line.has('E')) + set_axis_max_acceleration(E, line.get_float('E') * factor); + } + + void GCodeTimeEstimator::_processM203(const GCodeReader::GCodeLine& line) + { + EDialect dialect = get_dialect(); + + // see http://reprap.org/wiki/G-code#M203:_Set_maximum_feedrate + if (dialect == Repetier) + return; + + // see http://reprap.org/wiki/G-code#M203:_Set_maximum_feedrate + float factor = (dialect == Marlin) ? 1.0f : MMMIN_TO_MMSEC; + + if (line.has('X')) + set_axis_max_feedrate(X, line.get_float('X') * factor); + + if (line.has('Y')) + set_axis_max_feedrate(Y, line.get_float('Y') * factor); + + if (line.has('Z')) + set_axis_max_feedrate(Z, line.get_float('Z') * factor); + + if (line.has('E')) + set_axis_max_feedrate(E, line.get_float('E') * factor); + } + + void GCodeTimeEstimator::_processM204(const GCodeReader::GCodeLine& line) + { + if (line.has('S')) + set_acceleration(line.get_float('S')); + + if (line.has('T')) + set_retract_acceleration(line.get_float('T')); + } + + void GCodeTimeEstimator::_processM205(const GCodeReader::GCodeLine& line) + { + if (line.has('X')) + { + float max_jerk = line.get_float('X'); + set_axis_max_jerk(X, max_jerk); + set_axis_max_jerk(Y, max_jerk); + } + + if (line.has('Y')) + set_axis_max_jerk(Y, line.get_float('Y')); + + if (line.has('Z')) + set_axis_max_jerk(Z, line.get_float('Z')); + + if (line.has('E')) + set_axis_max_jerk(E, line.get_float('E')); + + if (line.has('S')) + set_minimum_feedrate(line.get_float('S')); + + if (line.has('T')) + set_minimum_travel_feedrate(line.get_float('T')); + } + + void GCodeTimeEstimator::_processM566(const GCodeReader::GCodeLine& line) + { + if (line.has('X')) + set_axis_max_jerk(X, line.get_float('X') * MMMIN_TO_MMSEC); + + if (line.has('Y')) + set_axis_max_jerk(Y, line.get_float('Y') * MMMIN_TO_MMSEC); + + if (line.has('Z')) + set_axis_max_jerk(Z, line.get_float('Z') * MMMIN_TO_MMSEC); + + if (line.has('E')) + set_axis_max_jerk(E, line.get_float('E') * MMMIN_TO_MMSEC); + } + + void GCodeTimeEstimator::_forward_pass() + { + Block* block[2] = { nullptr, nullptr }; + + for (Block& b : _blocks) + { + block[0] = block[1]; + block[1] = &b; + _planner_forward_pass_kernel(block[0], block[1]); + } + + _planner_forward_pass_kernel(block[1], nullptr); + } + + void GCodeTimeEstimator::_reverse_pass() + { + Block* block[2] = { nullptr, nullptr }; + + for (int i = (int)_blocks.size() - 1; i >= 0; --i) + { + block[1] = block[0]; + block[0] = &_blocks[i]; + _planner_reverse_pass_kernel(block[0], block[1]); + } + } + + void GCodeTimeEstimator::_planner_forward_pass_kernel(Block* prev, Block* curr) + { + if (prev == nullptr) + return; + + // If the previous block is an acceleration block, but it is not long enough to complete the + // full speed change within the block, we need to adjust the entry speed accordingly. Entry + // speeds have already been reset, maximized, and reverse planned by reverse planner. + // If nominal length is true, max junction speed is guaranteed to be reached. No need to recheck. + if (!prev->flags.nominal_length) + { + if (prev->feedrate.entry < curr->feedrate.entry) + { + float entry_speed = std::min(curr->feedrate.entry, Block::max_allowable_speed(-prev->acceleration, prev->feedrate.entry, prev->move_length())); + + // Check for junction speed change + if (curr->feedrate.entry != entry_speed) + { + curr->feedrate.entry = entry_speed; + curr->flags.recalculate = true; + } + } + } + } + + void GCodeTimeEstimator::_planner_reverse_pass_kernel(Block* curr, Block* next) + { + if ((curr == nullptr) || (next == nullptr)) + return; + + // If entry speed is already at the maximum entry speed, no need to recheck. Block is cruising. + // If not, block in state of acceleration or deceleration. Reset entry speed to maximum and + // check for maximum allowable speed reductions to ensure maximum possible planned speed. + if (curr->feedrate.entry != curr->max_entry_speed) + { + // If nominal length true, max junction speed is guaranteed to be reached. Only compute + // for max allowable speed if block is decelerating and nominal length is false. + if (!curr->flags.nominal_length && (curr->max_entry_speed > next->feedrate.entry)) + curr->feedrate.entry = std::min(curr->max_entry_speed, Block::max_allowable_speed(-curr->acceleration, next->feedrate.entry, curr->move_length())); + else + curr->feedrate.entry = curr->max_entry_speed; + + curr->flags.recalculate = true; + } + } + + void GCodeTimeEstimator::_recalculate_trapezoids() + { + Block* curr = nullptr; + Block* next = nullptr; + + for (Block& b : _blocks) + { + curr = next; + next = &b; + + if (curr != nullptr) + { + // Recalculate if current block entry or exit junction speed has changed. + if (curr->flags.recalculate || next->flags.recalculate) + { + // NOTE: Entry and exit factors always > 0 by all previous logic operations. + Block block = *curr; + block.feedrate.exit = next->feedrate.entry; + block.calculate_trapezoid(); + curr->trapezoid = block.trapezoid; + curr->flags.recalculate = false; // Reset current only to ensure next trapezoid is computed + } + } + } + + // Last/newest block in buffer. Always recalculated. + if (next != nullptr) + { + Block block = *next; + block.feedrate.exit = next->safe_feedrate; + block.calculate_trapezoid(); + next->trapezoid = block.trapezoid; + next->flags.recalculate = false; + } + } } diff --git a/xs/src/libslic3r/GCodeTimeEstimator.hpp b/xs/src/libslic3r/GCodeTimeEstimator.hpp index dd301c929..894d00ef3 100644 --- a/xs/src/libslic3r/GCodeTimeEstimator.hpp +++ b/xs/src/libslic3r/GCodeTimeEstimator.hpp @@ -6,18 +6,307 @@ namespace Slic3r { -class GCodeTimeEstimator : public GCodeReader { - public: - float time = 0; // in seconds - - void parse(const std::string &gcode); - void parse_file(const std::string &file); - - protected: - float acceleration = 9000; - void _parser(GCodeReader&, const GCodeReader::GCodeLine &line); - static float _accelerated_move(double length, double v, double acceleration); -}; + class GCodeTimeEstimator + { + public: + enum EUnits : unsigned char + { + Millimeters, + Inches + }; + + enum EAxis : unsigned char + { + X, + Y, + Z, + E, + Num_Axis + }; + + enum EDialect : unsigned char + { + Unknown, + Marlin, + Repetier, + Smoothieware, + RepRapFirmware, + Teacup, + Num_Dialects + }; + + enum EPositioningType : unsigned char + { + Absolute, + Relative + }; + + private: + struct Axis + { + float position; // mm + float max_feedrate; // mm/s + float max_acceleration; // mm/s^2 + float max_jerk; // mm/s + }; + + struct Feedrates + { + float feedrate; // mm/s + float axis_feedrate[Num_Axis]; // mm/s + float abs_axis_feedrate[Num_Axis]; // mm/s + float safe_feedrate; // mm/s + + void reset(); + }; + + struct State + { + EDialect dialect; + EUnits units; + EPositioningType positioning_xyz_type; + EPositioningType positioning_e_type; + Axis axis[Num_Axis]; + float feedrate; // mm/s + float acceleration; // mm/s^2 + float retract_acceleration; // mm/s^2 + float additional_time; // s + float minimum_feedrate; // mm/s + float minimum_travel_feedrate; // mm/s + }; + + public: + struct Block + { + struct FeedrateProfile + { + float entry; // mm/s + float cruise; // mm/s + float exit; // mm/s + }; + + struct Trapezoid + { + float distance; // mm + float accelerate_until; // mm + float decelerate_after; // mm + FeedrateProfile feedrate; + + float acceleration_time(float acceleration) const; + float cruise_time() const; + float deceleration_time(float acceleration) const; + float cruise_distance() const; + + // This function gives the time needed to accelerate from an initial speed to reach a final distance. + static float acceleration_time_from_distance(float initial_feedrate, float distance, float acceleration); + + // This function gives the final speed while accelerating at the given constant acceleration from the given initial speed along the given distance. + static float speed_from_distance(float initial_feedrate, float distance, float acceleration); + }; + + struct Flags + { + bool recalculate; + bool nominal_length; + }; + + Flags flags; + + float delta_pos[Num_Axis]; // mm + float acceleration; // mm/s^2 + float max_entry_speed; // mm/s + float safe_feedrate; // mm/s + + FeedrateProfile feedrate; + Trapezoid trapezoid; + + // Returns the length of the move covered by this block, in mm + float move_length() const; + + // Returns true if this block is a retract/unretract move only + float is_extruder_only_move() const; + + // Returns true if this block is a move with no extrusion + float is_travel_move() const; + + // Returns the time spent accelerating toward cruise speed, in seconds + float acceleration_time() const; + + // Returns the time spent at cruise speed, in seconds + float cruise_time() const; + + // Returns the time spent decelerating from cruise speed, in seconds + float deceleration_time() const; + + // Returns the distance covered at cruise speed, in mm + float cruise_distance() const; + + // Calculates this block's trapezoid + void calculate_trapezoid(); + + // Calculates the maximum allowable speed at this point when you must be able to reach target_velocity using the + // acceleration within the allotted distance. + static float max_allowable_speed(float acceleration, float target_velocity, float distance); + + // Calculates the distance (not time) it takes to accelerate from initial_rate to target_rate using the given acceleration: + static float estimate_acceleration_distance(float initial_rate, float target_rate, float acceleration); + + // This function gives you the point at which you must start braking (at the rate of -acceleration) if + // you started at speed initial_rate and accelerated until this point and want to end at the final_rate after + // a total travel of distance. This can be used to compute the intersection point between acceleration and + // deceleration in the cases where the trapezoid has no plateau (i.e. never reaches maximum speed) + static float intersection_distance(float initial_rate, float final_rate, float acceleration, float distance); + }; + + typedef std::vector BlocksList; + + private: + GCodeReader _parser; + State _state; + Feedrates _curr; + Feedrates _prev; + BlocksList _blocks; + float _time; // s + + public: + GCodeTimeEstimator(); + + // Calculates the time estimate from the given gcode in string format + void calculate_time_from_text(const std::string& gcode); + + // Calculates the time estimate from the gcode contained in the file with the given filename + void calculate_time_from_file(const std::string& file); + + // Calculates the time estimate from the gcode contained in given list of gcode lines + void calculate_time_from_lines(const std::vector& gcode_lines); + + // Adds the given gcode line + void add_gcode_line(const std::string& gcode_line); + + // Calculates the time estimate from the gcode lines added using add_gcode_line() + void calculate_time(); + + // Set current position on the given axis with the given value + void set_axis_position(EAxis axis, float position); + + void set_axis_max_feedrate(EAxis axis, float feedrate_mm_sec); + void set_axis_max_acceleration(EAxis axis, float acceleration); + void set_axis_max_jerk(EAxis axis, float jerk); + + // Returns current position on the given axis + float get_axis_position(EAxis axis) const; + + float get_axis_max_feedrate(EAxis axis) const; + float get_axis_max_acceleration(EAxis axis) const; + float get_axis_max_jerk(EAxis axis) const; + + void set_feedrate(float feedrate_mm_sec); + float get_feedrate() const; + + void set_acceleration(float acceleration_mm_sec2); + float get_acceleration() const; + + void set_retract_acceleration(float acceleration_mm_sec2); + float get_retract_acceleration() const; + + void set_minimum_feedrate(float feedrate_mm_sec); + float get_minimum_feedrate() const; + + void set_minimum_travel_feedrate(float feedrate_mm_sec); + float get_minimum_travel_feedrate() const; + + void set_dialect(EDialect dialect); + EDialect get_dialect() const; + + void set_units(EUnits units); + EUnits get_units() const; + + void set_positioning_xyz_type(EPositioningType type); + EPositioningType get_positioning_xyz_type() const; + + void set_positioning_e_type(EPositioningType type); + EPositioningType get_positioning_e_type() const; + + void add_additional_time(float timeSec); + void set_additional_time(float timeSec); + float get_additional_time() const; + + void set_default(); + + // Call this method before to start adding lines using add_gcode_line() when reusing an instance of GCodeTimeEstimator + void reset(); + + // Returns the estimated time, in seconds + float get_time() const; + + // Returns the estimated time, in format HHh MMm SSs + std::string get_time_hms() const; + + private: + void _reset(); + + // Calculates the time estimate + void _calculate_time(); + + // Processes the given gcode line + void _process_gcode_line(GCodeReader&, const GCodeReader::GCodeLine& line); + + // Move + void _processG1(const GCodeReader::GCodeLine& line); + + // Dwell + void _processG4(const GCodeReader::GCodeLine& line); + + // Set Units to Inches + void _processG20(const GCodeReader::GCodeLine& line); + + // Set Units to Millimeters + void _processG21(const GCodeReader::GCodeLine& line); + + // Move to Origin (Home) + void _processG28(const GCodeReader::GCodeLine& line); + + // Set to Absolute Positioning + void _processG90(const GCodeReader::GCodeLine& line); + + // Set to Relative Positioning + void _processG91(const GCodeReader::GCodeLine& line); + + // Set Position + void _processG92(const GCodeReader::GCodeLine& line); + + // Set extruder to absolute mode + void _processM82(const GCodeReader::GCodeLine& line); + + // Set extruder to relative mode + void _processM83(const GCodeReader::GCodeLine& line); + + // Set Extruder Temperature and Wait + void _processM109(const GCodeReader::GCodeLine& line); + + // Set max printing acceleration + void _processM201(const GCodeReader::GCodeLine& line); + + // Set maximum feedrate + void _processM203(const GCodeReader::GCodeLine& line); + + // Set default acceleration + void _processM204(const GCodeReader::GCodeLine& line); + + // Advanced settings + void _processM205(const GCodeReader::GCodeLine& line); + + // Set allowable instantaneous speed change + void _processM566(const GCodeReader::GCodeLine& line); + + void _forward_pass(); + void _reverse_pass(); + + void _planner_forward_pass_kernel(Block* prev, Block* curr); + void _planner_reverse_pass_kernel(Block* curr, Block* next); + + void _recalculate_trapezoids(); + }; } /* namespace Slic3r */ diff --git a/xs/src/libslic3r/Print.hpp b/xs/src/libslic3r/Print.hpp index c4093b795..c56e64c6c 100644 --- a/xs/src/libslic3r/Print.hpp +++ b/xs/src/libslic3r/Print.hpp @@ -233,8 +233,9 @@ public: PrintRegionPtrs regions; PlaceholderParser placeholder_parser; // TODO: status_cb + std::string estimated_print_time; double total_used_filament, total_extruded_volume, total_cost, total_weight; - std::map filament_stats; + std::map filament_stats; PrintState state; // ordered collections of extrusion paths to build skirt loops and brim diff --git a/xs/xsp/Print.xsp b/xs/xsp/Print.xsp index 2e418253b..cbc04a804 100644 --- a/xs/xsp/Print.xsp +++ b/xs/xsp/Print.xsp @@ -152,6 +152,8 @@ _constant() %code%{ RETVAL = &THIS->skirt; %}; Ref brim() %code%{ RETVAL = &THIS->brim; %}; + std::string estimated_print_time() + %code%{ RETVAL = THIS->estimated_print_time; %}; PrintObjectPtrs* objects() %code%{ RETVAL = &THIS->objects; %}; @@ -280,7 +282,6 @@ Print::total_cost(...) } RETVAL = THIS->total_cost; OUTPUT: - RETVAL - + RETVAL %} };