#include "Exception.hpp" #include "GCodeTimeEstimator.hpp" #include "Utils.hpp" #include #include #include #include #include #include #if !ENABLE_GCODE_VIEWER 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.4f, 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 DEFAULT_EXTRUDE_FACTOR_OVERRIDE_PERCENTAGE = 1.0f; // 100 percent static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; #if ENABLE_MOVE_STATS static const std::string MOVE_TYPE_STR[Slic3r::GCodeTimeEstimator::Block::Num_Types] = { "Noop", "Retract", "Unretract", "Tool_change", "Move", "Extrude" }; #endif // ENABLE_MOVE_STATS namespace Slic3r { 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)); } float GCodeTimeEstimator::Block::Trapezoid::acceleration_time(float entry_feedrate, float acceleration) const { return acceleration_time_from_distance(entry_feedrate, accelerate_until, acceleration); } float GCodeTimeEstimator::Block::Trapezoid::cruise_time() const { return (cruise_feedrate != 0.0f) ? cruise_distance() / cruise_feedrate : 0.0f; } float GCodeTimeEstimator::Block::Trapezoid::deceleration_time(float distance, float acceleration) const { return acceleration_time_from_distance(cruise_feedrate, (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) { // to avoid invalid negative numbers due to numerical imprecision float value = std::max(0.0f, sqr(initial_feedrate) + 2.0f * acceleration * distance); return ::sqrt(value); } float GCodeTimeEstimator::Block::acceleration_time() const { return trapezoid.acceleration_time(feedrate.entry, acceleration); } float GCodeTimeEstimator::Block::cruise_time() const { return trapezoid.cruise_time(); } float GCodeTimeEstimator::Block::deceleration_time() const { return trapezoid.deceleration_time(distance, acceleration); } float GCodeTimeEstimator::Block::cruise_distance() const { return trapezoid.cruise_distance(); } void GCodeTimeEstimator::Block::calculate_trapezoid() { trapezoid.cruise_feedrate = feedrate.cruise; float accelerate_distance = std::max(0.0f, estimate_acceleration_distance(feedrate.entry, feedrate.cruise, acceleration)); float decelerate_distance = std::max(0.0f, 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 = std::clamp(intersection_distance(feedrate.entry, feedrate.exit, acceleration, distance), 0.0f, distance); cruise_distance = 0.0f; trapezoid.cruise_feedrate = Trapezoid::speed_from_distance(feedrate.entry, accelerate_distance, 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) { // to avoid invalid negative numbers due to numerical imprecision float value = std::max(0.0f, sqr(target_velocity) - 2.0f * acceleration * distance); return ::sqrt(value); } 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); } #if ENABLE_MOVE_STATS GCodeTimeEstimator::MoveStats::MoveStats() : count(0) , time(0.0f) { } #endif // ENABLE_MOVE_STATS const std::string GCodeTimeEstimator::Normal_First_M73_Output_Placeholder_Tag = "; _TE_NORMAL_FIRST_M73_OUTPUT_PLACEHOLDER"; const std::string GCodeTimeEstimator::Silent_First_M73_Output_Placeholder_Tag = "; _TE_SILENT_FIRST_M73_OUTPUT_PLACEHOLDER"; const std::string GCodeTimeEstimator::Normal_Last_M73_Output_Placeholder_Tag = "; _TE_NORMAL_LAST_M73_OUTPUT_PLACEHOLDER"; const std::string GCodeTimeEstimator::Silent_Last_M73_Output_Placeholder_Tag = "; _TE_SILENT_LAST_M73_OUTPUT_PLACEHOLDER"; const std::string GCodeTimeEstimator::Color_Change_Tag = "PRINT_COLOR_CHANGE"; const std::string GCodeTimeEstimator::Pause_Print_Tag = "PRINT_PAUSE"; GCodeTimeEstimator::GCodeTimeEstimator(EMode mode) : m_mode(mode) { reset(); set_default(); } void GCodeTimeEstimator::add_gcode_line(const std::string& gcode_line) { PROFILE_FUNC(); m_parser.parse_line(gcode_line, [this](GCodeReader &reader, const GCodeReader::GCodeLine &line) { this->_process_gcode_line(reader, line); }); } void GCodeTimeEstimator::add_gcode_block(const char *ptr) { PROFILE_FUNC(); GCodeReader::GCodeLine gline; auto action = [this](GCodeReader &reader, const GCodeReader::GCodeLine &line) { this->_process_gcode_line(reader, line); }; for (; *ptr != 0;) { gline.reset(); ptr = m_parser.parse_line(ptr, gline, action); } } void GCodeTimeEstimator::calculate_time(bool start_from_beginning) { PROFILE_FUNC(); if (start_from_beginning) _reset_time(); _calculate_time(0); if (m_needs_custom_gcode_times && (m_custom_gcode_time_cache != 0.0f)) m_custom_gcode_times.push_back({CustomGCode::ColorChange, m_custom_gcode_time_cache }); #if ENABLE_MOVE_STATS _log_moves_stats(); #endif // ENABLE_MOVE_STATS } #if 0 void GCodeTimeEstimator::calculate_time_from_text(const std::string& gcode) { reset(); m_parser.parse_buffer(gcode, [this](GCodeReader &reader, const GCodeReader::GCodeLine &line) { this->_process_gcode_line(reader, line); }); _calculate_time(0); if (m_needs_custom_gcode_times && (m_custom_gcode_time_cache != 0.0f)) m_custom_gcode_times.push_back({ cgtColorChange, m_custom_gcode_time_cache }); #if ENABLE_MOVE_STATS _log_moves_stats(); #endif // ENABLE_MOVE_STATS } void GCodeTimeEstimator::calculate_time_from_file(const std::string& file) { reset(); m_parser.parse_file(file, boost::bind(&GCodeTimeEstimator::_process_gcode_line, this, _1, _2)); _calculate_time(0); if (m_needs_custom_gcode_times && (m_custom_gcode_time_cache != 0.0f)) m_custom_gcode_times.push_back({ cgtColorChange, m_custom_gcode_time_cache }); #if ENABLE_MOVE_STATS _log_moves_stats(); #endif // ENABLE_MOVE_STATS } void GCodeTimeEstimator::calculate_time_from_lines(const std::vector& gcode_lines) { reset(); auto action = [this](GCodeReader &reader, const GCodeReader::GCodeLine &line) { this->_process_gcode_line(reader, line); }; for (const std::string& line : gcode_lines) m_parser.parse_line(line, action); _calculate_time(0); if (m_needs_custom_gcode_times && (m_custom_gcode_time_cache != 0.0f)) m_custom_gcode_times.push_back({ cgtColorChange, m_custom_gcode_time_cache}); #if ENABLE_MOVE_STATS _log_moves_stats(); #endif // ENABLE_MOVE_STATS } #endif bool GCodeTimeEstimator::post_process(const std::string& filename, float interval_sec, const PostProcessData* const normal_mode, const PostProcessData* const silent_mode) { boost::nowide::ifstream in(filename); if (!in.good()) throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nCannot open file for reading.\n")); std::string path_tmp = filename + ".postprocess"; FILE* out = boost::nowide::fopen(path_tmp.c_str(), "wb"); if (out == nullptr) throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nCannot open file for writing.\n")); std::string normal_time_mask = "M73 P%s R%s\n"; std::string silent_time_mask = "M73 Q%s S%s\n"; char line_M73[64]; std::string gcode_line; // buffer line to export only when greater than 64K to reduce writing calls std::string export_line; // helper function to write to disk auto write_string = [&](const std::string& str) { fwrite((const void*)export_line.c_str(), 1, export_line.length(), out); if (ferror(out)) { in.close(); fclose(out); boost::nowide::remove(path_tmp.c_str()); throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nIs the disk full?\n")); } export_line.clear(); }; GCodeReader parser; int g1_lines_count = 0; int normal_g1_line_id = 0; float normal_last_recorded_time = 0.0f; int silent_g1_line_id = 0; float silent_last_recorded_time = 0.0f; // helper function to process g1 lines auto process_g1_line = [&](const PostProcessData* const data, const GCodeReader::GCodeLine& line, int& g1_line_id, float& last_recorded_time, const std::string& time_mask) { if (data == nullptr) return; assert((g1_line_id >= (int)data->g1_times.size()) || (data->g1_times[g1_line_id].first >= (int)g1_lines_count)); float elapsed_time = -1.0f; if (g1_line_id < (int)data->g1_times.size()) { const G1LineIdTime& map_item = data->g1_times[g1_line_id]; if (map_item.first == g1_lines_count) { if (line.has_e()) elapsed_time = map_item.second; ++g1_line_id; } } if (elapsed_time != -1.0f) { float block_remaining_time = data->time - elapsed_time; if (std::abs(last_recorded_time - block_remaining_time) > interval_sec) { sprintf(line_M73, time_mask.c_str(), std::to_string((int)(100.0f * elapsed_time / data->time)).c_str(), _get_time_minutes(block_remaining_time).c_str()); gcode_line += line_M73; last_recorded_time = block_remaining_time; } } }; while (std::getline(in, gcode_line)) { if (!in.good()) { fclose(out); throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nError while reading from file.\n")); } // check tags // remove Color_Change_Tag and Pause_Print_Tag if (gcode_line == "; " + Color_Change_Tag || gcode_line == "; " + Pause_Print_Tag) continue; // replaces placeholders for initial line M73 with the real lines if ((normal_mode != nullptr) && (gcode_line == Normal_First_M73_Output_Placeholder_Tag)) { sprintf(line_M73, normal_time_mask.c_str(), "0", _get_time_minutes(normal_mode->time).c_str()); gcode_line = line_M73; } else if ((silent_mode != nullptr) && (gcode_line == Silent_First_M73_Output_Placeholder_Tag)) { sprintf(line_M73, silent_time_mask.c_str(), "0", _get_time_minutes(silent_mode->time).c_str()); gcode_line = line_M73; } // replaces placeholders for final line M73 with the real lines else if ((normal_mode != nullptr) && (gcode_line == Normal_Last_M73_Output_Placeholder_Tag)) { sprintf(line_M73, normal_time_mask.c_str(), "100", "0"); gcode_line = line_M73; } else if ((silent_mode != nullptr) && (gcode_line == Silent_Last_M73_Output_Placeholder_Tag)) { sprintf(line_M73, silent_time_mask.c_str(), "100", "0"); gcode_line = line_M73; } else gcode_line += "\n"; // add remaining time lines where needed parser.parse_line(gcode_line, [&](GCodeReader& reader, const GCodeReader::GCodeLine& line) { if (line.cmd_is("G1")) { ++g1_lines_count; process_g1_line(silent_mode, line, silent_g1_line_id, silent_last_recorded_time, silent_time_mask); process_g1_line(normal_mode, line, normal_g1_line_id, normal_last_recorded_time, normal_time_mask); } }); export_line += gcode_line; if (export_line.length() > 65535) write_string(export_line); } if (!export_line.empty()) write_string(export_line); fclose(out); in.close(); if (rename_file(path_tmp, filename)) throw Slic3r::RuntimeError(std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + filename + '\n' + "Is " + path_tmp + " locked?" + '\n'); return true; } void GCodeTimeEstimator::set_axis_position(EAxis axis, float position) { m_state.axis[axis].position = position; } void GCodeTimeEstimator::set_axis_origin(EAxis axis, float position) { m_state.axis[axis].origin = position; } void GCodeTimeEstimator::set_axis_max_feedrate(EAxis axis, float feedrate_mm_sec) { m_state.axis[axis].max_feedrate = feedrate_mm_sec; } void GCodeTimeEstimator::set_axis_max_acceleration(EAxis axis, float acceleration) { m_state.axis[axis].max_acceleration = acceleration; } void GCodeTimeEstimator::set_axis_max_jerk(EAxis axis, float jerk) { m_state.axis[axis].max_jerk = jerk; } float GCodeTimeEstimator::get_axis_position(EAxis axis) const { return m_state.axis[axis].position; } float GCodeTimeEstimator::get_axis_origin(EAxis axis) const { return m_state.axis[axis].origin; } float GCodeTimeEstimator::get_axis_max_feedrate(EAxis axis) const { return m_state.axis[axis].max_feedrate; } float GCodeTimeEstimator::get_axis_max_acceleration(EAxis axis) const { return m_state.axis[axis].max_acceleration; } float GCodeTimeEstimator::get_axis_max_jerk(EAxis axis) const { return m_state.axis[axis].max_jerk; } void GCodeTimeEstimator::set_feedrate(float feedrate_mm_sec) { m_state.feedrate = feedrate_mm_sec; } float GCodeTimeEstimator::get_feedrate() const { return m_state.feedrate; } void GCodeTimeEstimator::set_acceleration(float acceleration_mm_sec2) { m_state.acceleration = (m_state.max_acceleration == 0) ? acceleration_mm_sec2 : // Clamp the acceleration with the maximum. std::min(m_state.max_acceleration, acceleration_mm_sec2); } float GCodeTimeEstimator::get_acceleration() const { return m_state.acceleration; } void GCodeTimeEstimator::set_max_acceleration(float acceleration_mm_sec2) { m_state.max_acceleration = acceleration_mm_sec2; if (acceleration_mm_sec2 > 0) m_state.acceleration = acceleration_mm_sec2; } float GCodeTimeEstimator::get_max_acceleration() const { return m_state.max_acceleration; } void GCodeTimeEstimator::set_retract_acceleration(float acceleration_mm_sec2) { m_state.retract_acceleration = acceleration_mm_sec2; } float GCodeTimeEstimator::get_retract_acceleration() const { return m_state.retract_acceleration; } void GCodeTimeEstimator::set_minimum_feedrate(float feedrate_mm_sec) { m_state.minimum_feedrate = feedrate_mm_sec; } float GCodeTimeEstimator::get_minimum_feedrate() const { return m_state.minimum_feedrate; } void GCodeTimeEstimator::set_minimum_travel_feedrate(float feedrate_mm_sec) { m_state.minimum_travel_feedrate = feedrate_mm_sec; } float GCodeTimeEstimator::get_minimum_travel_feedrate() const { return m_state.minimum_travel_feedrate; } void GCodeTimeEstimator::set_filament_load_times(const std::vector &filament_load_times) { m_state.filament_load_times.clear(); for (double t : filament_load_times) m_state.filament_load_times.push_back((float)t); } void GCodeTimeEstimator::set_filament_unload_times(const std::vector &filament_unload_times) { m_state.filament_unload_times.clear(); for (double t : filament_unload_times) m_state.filament_unload_times.push_back((float)t); } float GCodeTimeEstimator::get_filament_load_time(unsigned int id_extruder) { return (m_state.filament_load_times.empty() || id_extruder == m_state.extruder_id_unloaded) ? 0 : (m_state.filament_load_times.size() <= id_extruder) ? m_state.filament_load_times.front() : m_state.filament_load_times[id_extruder]; } float GCodeTimeEstimator::get_filament_unload_time(unsigned int id_extruder) { return (m_state.filament_unload_times.empty() || id_extruder == m_state.extruder_id_unloaded) ? 0 : (m_state.filament_unload_times.size() <= id_extruder) ? m_state.filament_unload_times.front() : m_state.filament_unload_times[id_extruder]; } void GCodeTimeEstimator::set_extrude_factor_override_percentage(float percentage) { m_state.extrude_factor_override_percentage = percentage; } float GCodeTimeEstimator::get_extrude_factor_override_percentage() const { return m_state.extrude_factor_override_percentage; } void GCodeTimeEstimator::set_dialect(GCodeFlavor dialect) { m_state.dialect = dialect; } GCodeFlavor GCodeTimeEstimator::get_dialect() const { PROFILE_FUNC(); return m_state.dialect; } void GCodeTimeEstimator::set_units(GCodeTimeEstimator::EUnits units) { m_state.units = units; } GCodeTimeEstimator::EUnits GCodeTimeEstimator::get_units() const { return m_state.units; } void GCodeTimeEstimator::set_global_positioning_type(GCodeTimeEstimator::EPositioningType type) { m_state.global_positioning_type = type; } GCodeTimeEstimator::EPositioningType GCodeTimeEstimator::get_global_positioning_type() const { return m_state.global_positioning_type; } void GCodeTimeEstimator::set_e_local_positioning_type(GCodeTimeEstimator::EPositioningType type) { m_state.e_local_positioning_type = type; } GCodeTimeEstimator::EPositioningType GCodeTimeEstimator::get_e_local_positioning_type() const { return m_state.e_local_positioning_type; } int GCodeTimeEstimator::get_g1_line_id() const { return m_state.g1_line_id; } void GCodeTimeEstimator::increment_g1_line_id() { ++m_state.g1_line_id; } void GCodeTimeEstimator::reset_g1_line_id() { m_state.g1_line_id = 0; } void GCodeTimeEstimator::set_extruder_id(unsigned int id) { m_state.extruder_id = id; } unsigned int GCodeTimeEstimator::get_extruder_id() const { return m_state.extruder_id; } void GCodeTimeEstimator::reset_extruder_id() { // Set the initial extruder ID to unknown. For the multi-material setup it means // that all the filaments are parked in the MMU and no filament is loaded yet. m_state.extruder_id = m_state.extruder_id_unloaded; } void GCodeTimeEstimator::set_default() { set_units(Millimeters); set_dialect(gcfRepRapSprinter); set_global_positioning_type(Absolute); set_e_local_positioning_type(Absolute); set_feedrate(DEFAULT_FEEDRATE); // Setting the maximum acceleration to zero means that the there is no limit and the G-code // is allowed to set excessive values. set_max_acceleration(0); set_acceleration(DEFAULT_ACCELERATION); set_retract_acceleration(DEFAULT_RETRACT_ACCELERATION); set_minimum_feedrate(DEFAULT_MINIMUM_FEEDRATE); set_minimum_travel_feedrate(DEFAULT_MINIMUM_TRAVEL_FEEDRATE); set_extrude_factor_override_percentage(DEFAULT_EXTRUDE_FACTOR_OVERRIDE_PERCENTAGE); 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]); } m_state.filament_load_times.clear(); m_state.filament_unload_times.clear(); } void GCodeTimeEstimator::reset() { _reset_time(); #if ENABLE_MOVE_STATS _moves_stats.clear(); #endif // ENABLE_MOVE_STATS _reset_blocks(); _reset(); } float GCodeTimeEstimator::get_time() const { return m_time; } std::string GCodeTimeEstimator::get_time_dhms() const { return _get_time_dhms(get_time()); } std::string GCodeTimeEstimator::get_time_dhm() const { return _get_time_dhm(get_time()); } std::string GCodeTimeEstimator::get_time_minutes() const { return _get_time_minutes(get_time()); } std::vector> GCodeTimeEstimator::get_custom_gcode_times() const { return m_custom_gcode_times; } std::vector GCodeTimeEstimator::get_color_times_dhms(bool include_remaining) const { std::vector ret; float total_time = 0.0f; // for (float t : m_color_times) for (auto t : m_custom_gcode_times) { std::string time = _get_time_dhms(t.second); if (include_remaining) { time += " ("; time += _get_time_dhms(m_time - total_time); time += ")"; } total_time += t.second; ret.push_back(time); } return ret; } std::vector GCodeTimeEstimator::get_color_times_minutes(bool include_remaining) const { std::vector ret; float total_time = 0.0f; // for (float t : m_color_times) for (auto t : m_custom_gcode_times) { std::string time = _get_time_minutes(t.second); if (include_remaining) { time += " ("; time += _get_time_minutes(m_time - total_time); time += ")"; } total_time += t.second; } return ret; } std::vector> GCodeTimeEstimator::get_custom_gcode_times_dhm(bool include_remaining) const { std::vector> ret; float total_time = 0.0f; for (auto t : m_custom_gcode_times) { std::string time = _get_time_dhm(t.second); if (include_remaining) { time += " ("; time += _get_time_dhm(m_time - total_time); time += ")"; } total_time += t.second; ret.push_back({t.first, time}); } return ret; } // Return an estimate of the memory consumed by the time estimator. size_t GCodeTimeEstimator::memory_used() const { size_t out = sizeof(*this); out += SLIC3R_STDVEC_MEMSIZE(this->m_blocks, Block); out += SLIC3R_STDVEC_MEMSIZE(this->m_g1_times, G1LineIdTime); return out; } void GCodeTimeEstimator::_reset() { m_curr.reset(); m_prev.reset(); set_axis_position(X, 0.0f); set_axis_position(Y, 0.0f); set_axis_position(Z, 0.0f); set_axis_origin(X, 0.0f); set_axis_origin(Y, 0.0f); set_axis_origin(Z, 0.0f); if (get_e_local_positioning_type() == Absolute) set_axis_position(E, 0.0f); reset_extruder_id(); reset_g1_line_id(); m_g1_times.clear(); m_needs_custom_gcode_times = false; m_custom_gcode_times.clear(); m_custom_gcode_time_cache = 0.0f; } void GCodeTimeEstimator::_reset_time() { m_time = 0.0f; } void GCodeTimeEstimator::_reset_blocks() { m_blocks.clear(); } void GCodeTimeEstimator::_calculate_time(size_t keep_last_n_blocks) { PROFILE_FUNC(); assert(keep_last_n_blocks <= m_blocks.size()); _forward_pass(); _reverse_pass(); _recalculate_trapezoids(); size_t n_blocks_process = m_blocks.size() - keep_last_n_blocks; m_g1_times.reserve(m_g1_times.size() + n_blocks_process); for (size_t i = 0; i < n_blocks_process; ++ i) { Block& block = m_blocks[i]; float block_time = 0.0f; block_time += block.acceleration_time(); block_time += block.cruise_time(); block_time += block.deceleration_time(); m_time += block_time; if (block.g1_line_id >= 0) m_g1_times.emplace_back(block.g1_line_id, m_time); #if ENABLE_MOVE_STATS MovesStatsMap::iterator it = _moves_stats.find(block.move_type); if (it == _moves_stats.end()) it = _moves_stats.insert(MovesStatsMap::value_type(block.move_type, MoveStats())).first; it->second.count += 1; it->second.time += block_time; #endif // ENABLE_MOVE_STATS m_custom_gcode_time_cache += block_time; } if (keep_last_n_blocks) m_blocks.erase(m_blocks.begin(), m_blocks.begin() + n_blocks_process); else m_blocks.clear(); } void GCodeTimeEstimator::_process_gcode_line(GCodeReader&, const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); // processes 'special' comments contained in line if (_process_tags(line)) return; std::string cmd = line.cmd(); if (cmd.length() > 1) { switch (::toupper(cmd[0])) { case 'G': { switch (::atoi(&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(&cmd[1])) { case 1: // Sleep or Conditional stop { _processM1(line); break; } 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 221: // Set extrude factor override percentage { _processM221(line); break; } case 566: // Set allowable instantaneous speed change { _processM566(line); break; } case 702: // MK3 MMU2: Process the final filament unload. { _processM702(line); break; } } break; } case 'T': // Select Tools { _processT(line); break; } } } } void GCodeTimeEstimator::_processG1(const GCodeReader::GCodeLine& line) { auto axis_absolute_position = [this](GCodeTimeEstimator::EAxis axis, const GCodeReader::GCodeLine& lineG1) -> float { float current_absolute_position = get_axis_position(axis); float current_origin = get_axis_origin(axis); float lengthsScaleFactor = (get_units() == GCodeTimeEstimator::Inches) ? INCHES_TO_MM : 1.0f; bool is_relative = (get_global_positioning_type() == Relative); if (axis == E) is_relative |= (get_e_local_positioning_type() == Relative); if (lineG1.has(Slic3r::Axis(axis))) { float ret = lineG1.value(Slic3r::Axis(axis)) * lengthsScaleFactor; return is_relative ? current_absolute_position + ret : ret + current_origin; } else return current_absolute_position; }; // delta_pos must have size >= Num_Axis auto move_length = [](const float* delta_pos) { float xyz_length = std::sqrt(sqr(delta_pos[X]) + sqr(delta_pos[Y]) + sqr(delta_pos[Z])); return (xyz_length > 0.0f) ? xyz_length : std::abs(delta_pos[E]); }; // delta_pos must have size >= Num_Axis auto is_extruder_only_move = [](const float* delta_pos) { return (delta_pos[X] == 0.0f) && (delta_pos[Y] == 0.0f) && (delta_pos[Z] == 0.0f) && (delta_pos[E] != 0.0f); }; PROFILE_FUNC(); increment_g1_line_id(); // updates axes positions from line float new_pos[Num_Axis]; for (unsigned char a = X; a < Num_Axis; ++a) { new_pos[a] = axis_absolute_position((EAxis)a, line); } // updates feedrate from line, if present if (line.has_f()) set_feedrate(std::max(line.f() * MMMIN_TO_MMSEC, get_minimum_feedrate())); // fills block data Block block; // calculates block movement deltas float max_abs_delta = 0.0f; float delta_pos[Num_Axis]; for (unsigned char a = X; a < Num_Axis; ++a) { delta_pos[a] = new_pos[a] - get_axis_position((EAxis)a); max_abs_delta = std::max(max_abs_delta, std::abs(delta_pos[a])); } // is it a move ? if (max_abs_delta == 0.0f) return; // calculates block feedrate m_curr.feedrate = std::max(get_feedrate(), (delta_pos[E] == 0.0f) ? get_minimum_travel_feedrate() : get_minimum_feedrate()); block.distance = move_length(delta_pos); float invDistance = 1.0f / block.distance; float min_feedrate_factor = 1.0f; for (unsigned char a = X; a < Num_Axis; ++a) { m_curr.axis_feedrate[a] = m_curr.feedrate * delta_pos[a] * invDistance; if (a == E) m_curr.axis_feedrate[a] *= get_extrude_factor_override_percentage(); m_curr.abs_axis_feedrate[a] = std::abs(m_curr.axis_feedrate[a]); if (m_curr.abs_axis_feedrate[a] > 0.0f) min_feedrate_factor = std::min(min_feedrate_factor, get_axis_max_feedrate((EAxis)a) / m_curr.abs_axis_feedrate[a]); } block.feedrate.cruise = min_feedrate_factor * m_curr.feedrate; if (min_feedrate_factor < 1.0f) { for (unsigned char a = X; a < Num_Axis; ++a) { m_curr.axis_feedrate[a] *= min_feedrate_factor; m_curr.abs_axis_feedrate[a] *= min_feedrate_factor; } } // calculates block acceleration float acceleration = is_extruder_only_move(delta_pos) ? 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 * std::abs(delta_pos[a]) * invDistance > axis_max_acceleration) acceleration = axis_max_acceleration; } block.acceleration = acceleration; // calculates block exit feedrate m_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 (m_curr.abs_axis_feedrate[a] > axis_max_jerk) m_curr.safe_feedrate = std::min(m_curr.safe_feedrate, axis_max_jerk); } block.feedrate.exit = m_curr.safe_feedrate; // calculates block entry feedrate float vmax_junction = m_curr.safe_feedrate; if (!m_blocks.empty() && (m_prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD)) { bool prev_speed_larger = m_prev.feedrate > block.feedrate.cruise; float smaller_speed_factor = prev_speed_larger ? (block.feedrate.cruise / m_prev.feedrate) : (m_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 : m_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 = m_prev.axis_feedrate[a]; float v_entry = m_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 ((m_prev.safe_feedrate > vmax_junction_threshold) && (m_curr.safe_feedrate > vmax_junction_threshold)) vmax_junction = m_curr.safe_feedrate; } float v_allowable = Block::max_allowable_speed(-acceleration, m_curr.safe_feedrate, block.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 = m_curr.safe_feedrate; // calculates block trapezoid block.calculate_trapezoid(); // updates previous m_prev = m_curr; // updates axis positions for (unsigned char a = X; a < Num_Axis; ++a) { set_axis_position((EAxis)a, new_pos[a]); } #if ENABLE_MOVE_STATS // detects block move type block.move_type = Block::Noop; if (delta_pos[E] < 0.0f) { if ((delta_pos[X] != 0.0f) || (delta_pos[Y] != 0.0f) || (delta_pos[Z] != 0.0f)) block.move_type = Block::Move; else block.move_type = Block::Retract; } else if (delta_pos[E] > 0.0f) { if ((delta_pos[X] == 0.0f) && (delta_pos[Y] == 0.0f) && (delta_pos[Z] == 0.0f)) block.move_type = Block::Unretract; else if ((delta_pos[X] != 0.0f) || (delta_pos[Y] != 0.0f)) block.move_type = Block::Extrude; } else if ((delta_pos[X] != 0.0f) || (delta_pos[Y] != 0.0f) || (delta_pos[Z] != 0.0f)) block.move_type = Block::Move; #endif // ENABLE_MOVE_STATS // adds block to blocks list block.g1_line_id = this->get_g1_line_id(); m_blocks.emplace_back(block); if (m_blocks.size() > planner_refresh_if_larger) _calculate_time(planner_queue_size); } void GCodeTimeEstimator::_processG4(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); GCodeFlavor dialect = get_dialect(); float value; float extra_time = 0.f; if (line.has_value('P', value)) extra_time += value * MILLISEC_TO_SEC; // see: http://reprap.org/wiki/G-code#G4:_Dwell if ((dialect == gcfRepetier) || (dialect == gcfMarlin) || (dialect == gcfSmoothie) || (dialect == gcfRepRapSprinter) || (dialect == gcfRepRapFirmware)) { if (line.has_value('S', value)) extra_time += value; } _simulate_st_synchronize(extra_time); } void GCodeTimeEstimator::_processG20(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); set_units(Inches); } void GCodeTimeEstimator::_processG21(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); set_units(Millimeters); } void GCodeTimeEstimator::_processG28(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); // TODO } void GCodeTimeEstimator::_processG90(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); set_global_positioning_type(Absolute); } void GCodeTimeEstimator::_processG91(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); set_global_positioning_type(Relative); } void GCodeTimeEstimator::_processG92(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); float lengthsScaleFactor = (get_units() == Inches) ? INCHES_TO_MM : 1.0f; bool anyFound = false; if (line.has_x()) { set_axis_origin(X, get_axis_position(X) - line.x() * lengthsScaleFactor); anyFound = true; } if (line.has_y()) { set_axis_origin(Y, get_axis_position(Y) - line.y() * lengthsScaleFactor); anyFound = true; } if (line.has_z()) { set_axis_origin(Z, get_axis_position(Z) - line.z() * lengthsScaleFactor); anyFound = true; } if (line.has_e()) { // extruder coordinate can grow to the point where its float representation does not allow for proper addition with small increments, // we set the value taken from the G92 line as the new current position for it set_axis_position(E, line.e() * lengthsScaleFactor); anyFound = true; } else _simulate_st_synchronize(0.f); if (!anyFound) { for (unsigned char a = X; a < Num_Axis; ++a) { set_axis_origin((EAxis)a, get_axis_position((EAxis)a)); } } } void GCodeTimeEstimator::_processM1(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); _simulate_st_synchronize(0.f); } void GCodeTimeEstimator::_processM82(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); set_e_local_positioning_type(Absolute); } void GCodeTimeEstimator::_processM83(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); set_e_local_positioning_type(Relative); } void GCodeTimeEstimator::_processM109(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); // TODO } void GCodeTimeEstimator::_processM201(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); GCodeFlavor dialect = get_dialect(); // see http://reprap.org/wiki/G-code#M201:_Set_max_printing_acceleration float factor = ((dialect != gcfRepRapSprinter && dialect != gcfRepRapFirmware) && (get_units() == GCodeTimeEstimator::Inches)) ? INCHES_TO_MM : 1.0f; if (line.has_x()) set_axis_max_acceleration(X, line.x() * factor); if (line.has_y()) set_axis_max_acceleration(Y, line.y() * factor); if (line.has_z()) set_axis_max_acceleration(Z, line.z() * factor); if (line.has_e()) set_axis_max_acceleration(E, line.e() * factor); } void GCodeTimeEstimator::_processM203(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); GCodeFlavor dialect = get_dialect(); // see http://reprap.org/wiki/G-code#M203:_Set_maximum_feedrate if (dialect == gcfRepetier) return; // see http://reprap.org/wiki/G-code#M203:_Set_maximum_feedrate // http://smoothieware.org/supported-g-codes float factor = (dialect == gcfMarlin || dialect == gcfSmoothie) ? 1.0f : MMMIN_TO_MMSEC; if (line.has_x()) set_axis_max_feedrate(X, line.x() * factor); if (line.has_y()) set_axis_max_feedrate(Y, line.y() * factor); if (line.has_z()) set_axis_max_feedrate(Z, line.z() * factor); if (line.has_e()) set_axis_max_feedrate(E, line.e() * factor); } void GCodeTimeEstimator::_processM204(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); float value; if (line.has_value('S', value)) { // Legacy acceleration format. This format is used by the legacy Marlin, MK2 or MK3 firmware, // and it is also generated by Slic3r to control acceleration per extrusion type // (there is a separate acceleration settings in Slicer for perimeter, first layer etc). set_acceleration(value); if (line.has_value('T', value)) set_retract_acceleration(value); } else { // New acceleration format, compatible with the upstream Marlin. if (line.has_value('P', value)) set_acceleration(value); if (line.has_value('R', value)) set_retract_acceleration(value); if (line.has_value('T', value)) { // Interpret the T value as the travel acceleration in the new Marlin format. //FIXME Prusa3D firmware currently does not support travel acceleration value independent from the extruding acceleration value. // set_travel_acceleration(value); } } } void GCodeTimeEstimator::_processM205(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); if (line.has_x()) { float max_jerk = line.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.y()); if (line.has_z()) set_axis_max_jerk(Z, line.z()); if (line.has_e()) set_axis_max_jerk(E, line.e()); float value; if (line.has_value('S', value)) set_minimum_feedrate(value); if (line.has_value('T', value)) set_minimum_travel_feedrate(value); } void GCodeTimeEstimator::_processM221(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); float value_s; float value_t; if (line.has_value('S', value_s) && !line.has_value('T', value_t)) set_extrude_factor_override_percentage(value_s * 0.01f); } void GCodeTimeEstimator::_processM566(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); if (line.has_x()) set_axis_max_jerk(X, line.x() * MMMIN_TO_MMSEC); if (line.has_y()) set_axis_max_jerk(Y, line.y() * MMMIN_TO_MMSEC); if (line.has_z()) set_axis_max_jerk(Z, line.z() * MMMIN_TO_MMSEC); if (line.has_e()) set_axis_max_jerk(E, line.e() * MMMIN_TO_MMSEC); } void GCodeTimeEstimator::_processM702(const GCodeReader::GCodeLine& line) { PROFILE_FUNC(); if (line.has('C')) { // MK3 MMU2 specific M code: // M702 C is expected to be sent by the custom end G-code when finalizing a print. // The MK3 unit shall unload and park the active filament into the MMU2 unit. float extra_time = get_filament_unload_time(get_extruder_id()); reset_extruder_id(); _simulate_st_synchronize(extra_time); } } void GCodeTimeEstimator::_processT(const GCodeReader::GCodeLine& line) { std::string cmd = line.cmd(); if (cmd.length() > 1) { unsigned int id = (unsigned int)::strtol(cmd.substr(1).c_str(), nullptr, 10); if (get_extruder_id() != id) { // Specific to the MK3 MMU2: The initial extruder ID is set to -1 indicating // that the filament is parked in the MMU2 unit and there is nothing to be unloaded yet. float extra_time = get_filament_unload_time(get_extruder_id()); set_extruder_id(id); extra_time += get_filament_load_time(get_extruder_id()); _simulate_st_synchronize(extra_time); } } } bool GCodeTimeEstimator::_process_tags(const GCodeReader::GCodeLine& line) { std::string comment = line.comment(); // Color_Change_Tag size_t pos = comment.find(Color_Change_Tag); if (pos != comment.npos) { _process_custom_gcode_tag(CustomGCode::ColorChange); return true; } // Pause_Print_Tag pos = comment.find(Pause_Print_Tag); if (pos != comment.npos) { _process_custom_gcode_tag(CustomGCode::PausePrint); return true; } return false; } void GCodeTimeEstimator::_process_custom_gcode_tag(CustomGCode::Type code) { PROFILE_FUNC(); m_needs_custom_gcode_times = true; //FIXME this simulates st_synchronize! is it correct? // The estimated time may be longer than the real print time. _simulate_st_synchronize(0.f); if (m_custom_gcode_time_cache != 0.0f) { m_custom_gcode_times.push_back({code, m_custom_gcode_time_cache}); m_custom_gcode_time_cache = 0.0f; } } void GCodeTimeEstimator::_simulate_st_synchronize(float extra_time) { PROFILE_FUNC(); m_time += extra_time; m_custom_gcode_time_cache += extra_time; _calculate_time(0); } void GCodeTimeEstimator::_forward_pass() { PROFILE_FUNC(); for (int i = 0; i + 1 < (int)m_blocks.size(); ++i) _planner_forward_pass_kernel(m_blocks[i], m_blocks[i + 1]); } void GCodeTimeEstimator::_reverse_pass() { PROFILE_FUNC(); for (int i = (int)m_blocks.size() - 1; i > 0; -- i) _planner_reverse_pass_kernel(m_blocks[i - 1], m_blocks[i]); } void GCodeTimeEstimator::_planner_forward_pass_kernel(Block& prev, Block& curr) { PROFILE_FUNC(); // 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.distance)); // 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 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.distance)); else curr.feedrate.entry = curr.max_entry_speed; curr.flags.recalculate = true; } } void GCodeTimeEstimator::_recalculate_trapezoids() { PROFILE_FUNC(); Block* curr = nullptr; Block* next = nullptr; for (size_t i = 0; i < m_blocks.size(); ++ i) { Block& b = m_blocks[i]; 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; } } std::string GCodeTimeEstimator::_get_time_dhms(float time_in_secs) { int days = (int)(time_in_secs / 86400.0f); time_in_secs -= (float)days * 86400.0f; int hours = (int)(time_in_secs / 3600.0f); time_in_secs -= (float)hours * 3600.0f; int minutes = (int)(time_in_secs / 60.0f); time_in_secs -= (float)minutes * 60.0f; char buffer[64]; if (days > 0) ::sprintf(buffer, "%dd %dh %dm %ds", days, hours, minutes, (int)time_in_secs); else if (hours > 0) ::sprintf(buffer, "%dh %dm %ds", hours, minutes, (int)time_in_secs); else if (minutes > 0) ::sprintf(buffer, "%dm %ds", minutes, (int)time_in_secs); else ::sprintf(buffer, "%ds", (int)time_in_secs); return buffer; } std::string GCodeTimeEstimator::_get_time_dhm(float time_in_secs) { char buffer[64]; int minutes = int(std::round(time_in_secs / 60.)); if (minutes <= 0) { ::sprintf(buffer, "%ds", (int)time_in_secs); } else { int days = minutes / 1440; minutes -= days * 1440; int hours = minutes / 60; minutes -= hours * 60; if (days > 0) ::sprintf(buffer, "%dd %dh %dm", days, hours, minutes); else if (hours > 0) ::sprintf(buffer, "%dh %dm", hours, minutes); else ::sprintf(buffer, "%dm", minutes); } return buffer; } std::string GCodeTimeEstimator::_get_time_minutes(float time_in_secs) { return std::to_string((int)(::roundf(time_in_secs / 60.0f))); } #if ENABLE_MOVE_STATS void GCodeTimeEstimator::_log_moves_stats() const { float moves_count = 0.0f; for (const MovesStatsMap::value_type& move : _moves_stats) { moves_count += (float)move.second.count; } for (const MovesStatsMap::value_type& move : _moves_stats) { std::cout << MOVE_TYPE_STR[move.first]; std::cout << ": count " << move.second.count << " (" << 100.0f * (float)move.second.count / moves_count << "%)"; std::cout << " - time: " << move.second.time << "s (" << 100.0f * move.second.time / m_time << "%)"; std::cout << std::endl; } std::cout << std::endl; } #endif // ENABLE_MOVE_STATS } #endif // !ENABLE_GCODE_VIEWER