OrcaSlicer/src/libslic3r/GCodeTimeEstimator.cpp
bubnikv fd3fe75d1c Reworked the rename_file() function on Windows to work reliably and
atomically. The code was taken from the llvm project, it is complex
and hopefully it covers all the Windows file system quirks. Vojtech
has highest hopes, that this will fix the various PrusaSlicer.ini
file corruptions.

Enabled the locales switching and error handling on Linux as well,
where now the missing locales are reported and running the locale-gen
tool is recommended.
2019-08-20 16:19:30 +02:00

1598 lines
54 KiB
C++

#include "GCodeTimeEstimator.hpp"
#include "Utils.hpp"
#include <boost/bind.hpp>
#include <cmath>
#include <Shiny/Shiny.h>
#include <boost/nowide/fstream.hpp>
#include <boost/nowide/cstdio.hpp>
#include <boost/algorithm/string/predicate.hpp>
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 acceleration) const
{
return acceleration_time_from_distance(feedrate.entry, accelerate_until, acceleration);
}
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)
{
// 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);
}
GCodeTimeEstimator::Block::Block()
{
}
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 : std::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 = 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 = 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);
}
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 = "; NORMAL_FIRST_M73_OUTPUT_PLACEHOLDER";
const std::string GCodeTimeEstimator::Silent_First_M73_Output_Placeholder_Tag = "; SILENT_FIRST_M73_OUTPUT_PLACEHOLDER";
const std::string GCodeTimeEstimator::Normal_Last_M73_Output_Placeholder_Tag = "; NORMAL_LAST_M73_OUTPUT_PLACEHOLDER";
const std::string GCodeTimeEstimator::Silent_Last_M73_Output_Placeholder_Tag = "; SILENT_LAST_M73_OUTPUT_PLACEHOLDER";
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();
m_last_st_synchronized_block_id = -1;
}
_calculate_time();
if (m_needs_color_times && (m_color_time_cache != 0.0f))
m_color_times.push_back(m_color_time_cache);
#if ENABLE_MOVE_STATS
_log_moves_stats();
#endif // ENABLE_MOVE_STATS
}
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();
if (m_needs_color_times && (m_color_time_cache != 0.0f))
m_color_times.push_back(m_color_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();
if (m_needs_color_times && (m_color_time_cache != 0.0f))
m_color_times.push_back(m_color_time_cache);
#if ENABLE_MOVE_STATS
_log_moves_stats();
#endif // ENABLE_MOVE_STATS
}
void GCodeTimeEstimator::calculate_time_from_lines(const std::vector<std::string>& 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();
if (m_needs_color_times && (m_color_time_cache != 0.0f))
m_color_times.push_back(m_color_time_cache);
#if ENABLE_MOVE_STATS
_log_moves_stats();
#endif // ENABLE_MOVE_STATS
}
bool GCodeTimeEstimator::post_process_remaining_times(const std::string& filename, float interval)
{
boost::nowide::ifstream in(filename);
if (!in.good())
throw std::runtime_error(std::string("Remaining times export failed.\nCannot open file for reading.\n"));
std::string path_tmp = filename + ".times";
FILE* out = boost::nowide::fopen(path_tmp.c_str(), "wb");
if (out == nullptr)
throw std::runtime_error(std::string("Remaining times export failed.\nCannot open file for writing.\n"));
std::string time_mask;
switch (m_mode)
{
default:
case Normal:
{
time_mask = "M73 P%s R%s\n";
break;
}
case Silent:
{
time_mask = "M73 Q%s S%s\n";
break;
}
}
unsigned int g1_lines_count = 0;
float last_recorded_time = 0.0f;
std::string gcode_line;
// buffer line to export only when greater than 64K to reduce writing calls
std::string export_line;
char time_line[64];
G1LineIdToBlockIdMap::const_iterator it_line_id = m_g1_line_ids.begin();
while (std::getline(in, gcode_line))
{
if (!in.good())
{
fclose(out);
throw std::runtime_error(std::string("Remaining times export failed.\nError while reading from file.\n"));
}
// replaces placeholders for initial line M73 with the real lines
if (((m_mode == Normal) && (gcode_line == Normal_First_M73_Output_Placeholder_Tag)) ||
((m_mode == Silent) && (gcode_line == Silent_First_M73_Output_Placeholder_Tag)))
{
sprintf(time_line, time_mask.c_str(), "0", _get_time_minutes(m_time).c_str());
gcode_line = time_line;
}
// replaces placeholders for final line M73 with the real lines
else if (((m_mode == Normal) && (gcode_line == Normal_Last_M73_Output_Placeholder_Tag)) ||
((m_mode == Silent) && (gcode_line == Silent_Last_M73_Output_Placeholder_Tag)))
{
sprintf(time_line, time_mask.c_str(), "100", "0");
gcode_line = time_line;
}
else
gcode_line += "\n";
// add remaining time lines where needed
m_parser.parse_line(gcode_line,
[this, &it_line_id, &g1_lines_count, &last_recorded_time, &time_line, &gcode_line, time_mask, interval](GCodeReader& reader, const GCodeReader::GCodeLine& line)
{
if (line.cmd_is("G1"))
{
++g1_lines_count;
assert(it_line_id == m_g1_line_ids.end() || it_line_id->first >= g1_lines_count);
const Block *block = nullptr;
if (it_line_id != m_g1_line_ids.end() && it_line_id->first == g1_lines_count) {
if (line.has_e() && it_line_id->second < (unsigned int)m_blocks.size())
block = &m_blocks[it_line_id->second];
++it_line_id;
}
if (block != nullptr && block->elapsed_time != -1.0f) {
float block_remaining_time = m_time - block->elapsed_time;
if (std::abs(last_recorded_time - block_remaining_time) > interval)
{
sprintf(time_line, time_mask.c_str(), std::to_string((int)(100.0f * block->elapsed_time / m_time)).c_str(), _get_time_minutes(block_remaining_time).c_str());
gcode_line += time_line;
last_recorded_time = block_remaining_time;
}
}
}
});
export_line += gcode_line;
if (export_line.length() > 65535)
{
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 std::runtime_error(std::string("Remaining times export failed.\nIs the disk full?\n"));
}
export_line.clear();
}
}
if (export_line.length() > 0)
{
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 std::runtime_error(std::string("Remaining times export failed.\nIs the disk full?\n"));
}
}
fclose(out);
in.close();
if (rename_file(path_tmp, filename))
throw std::runtime_error(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_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_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<double> &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<double> &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::add_additional_time(float timeSec)
{
PROFILE_FUNC();
m_state.additional_time += timeSec;
}
void GCodeTimeEstimator::set_additional_time(float timeSec)
{
m_state.additional_time = timeSec;
}
float GCodeTimeEstimator::get_additional_time() const
{
return m_state.additional_time;
}
void GCodeTimeEstimator::set_default()
{
set_units(Millimeters);
set_dialect(gcfRepRap);
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_minutes() const
{
return _get_time_minutes(get_time());
}
std::vector<float> GCodeTimeEstimator::get_color_times() const
{
return m_color_times;
}
std::vector<std::string> GCodeTimeEstimator::get_color_times_dhms(bool include_remaining) const
{
std::vector<std::string> ret;
float total_time = 0.0f;
for (float t : m_color_times)
{
std::string time = _get_time_dhms(t);
if (include_remaining)
{
time += " (";
time += _get_time_dhms(m_time - total_time);
time += ")";
}
total_time += t;
ret.push_back(time);
}
return ret;
}
std::vector<std::string> GCodeTimeEstimator::get_color_times_minutes(bool include_remaining) const
{
std::vector<std::string> ret;
float total_time = 0.0f;
for (float t : m_color_times)
{
std::string time = _get_time_minutes(t);
if (include_remaining)
{
time += " (";
time += _get_time_minutes(m_time - total_time);
time += ")";
}
total_time += t;
}
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_line_ids, G1LineIdToBlockId);
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);
if (get_e_local_positioning_type() == Absolute)
set_axis_position(E, 0.0f);
set_additional_time(0.0f);
reset_extruder_id();
reset_g1_line_id();
m_g1_line_ids.clear();
m_last_st_synchronized_block_id = -1;
m_needs_color_times = false;
m_color_times.clear();
m_color_time_cache = 0.0f;
}
void GCodeTimeEstimator::_reset_time()
{
m_time = 0.0f;
}
void GCodeTimeEstimator::_reset_blocks()
{
m_blocks.clear();
}
void GCodeTimeEstimator::_calculate_time()
{
PROFILE_FUNC();
_forward_pass();
_reverse_pass();
_recalculate_trapezoids();
m_time += get_additional_time();
m_color_time_cache += get_additional_time();
for (int i = m_last_st_synchronized_block_id + 1; i < (int)m_blocks.size(); ++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;
block.elapsed_time = 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_color_time_cache += block_time;
}
m_last_st_synchronized_block_id = (int)m_blocks.size() - 1;
// The additional time has been consumed (added to the total time), reset it to zero.
set_additional_time(0.);
}
void GCodeTimeEstimator::_process_gcode_line(GCodeReader&, const GCodeReader::GCodeLine& line)
{
PROFILE_FUNC();
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 600: // Set color change
{
_processM600(line);
break;
}
case 702: // MK3 MMU2: Process the final filament unload.
{
_processM702(line);
break;
}
}
break;
}
case 'T': // Select Tools
{
_processT(line);
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, bool is_relative, float current_absolute_position)
{
float lengthsScaleFactor = (units == GCodeTimeEstimator::Inches) ? INCHES_TO_MM : 1.0f;
if (lineG1.has(Slic3r::Axis(axis)))
{
float ret = lineG1.value(Slic3r::Axis(axis)) * lengthsScaleFactor;
return is_relative ? current_absolute_position + ret : ret;
}
else
return current_absolute_position;
}
void GCodeTimeEstimator::_processG1(const GCodeReader::GCodeLine& line)
{
PROFILE_FUNC();
increment_g1_line_id();
// updates axes positions from line
EUnits units = get_units();
float new_pos[Num_Axis];
for (unsigned char a = X; a < Num_Axis; ++a)
{
bool is_relative = (get_global_positioning_type() == Relative);
if (a == E)
is_relative |= (get_e_local_positioning_type() == Relative);
new_pos[a] = axis_absolute_position_from_G1_line((EAxis)a, line, units, is_relative, get_axis_position((EAxis)a));
}
// 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;
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, std::abs(block.delta_pos[a]));
}
// is it a move ?
if (max_abs_delta == 0.0f)
return;
// calculates block feedrate
m_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)
{
m_curr.axis_feedrate[a] = m_curr.feedrate * block.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 = 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 * std::abs(block.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, 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 (block.delta_pos[E] < 0.0f)
{
if ((block.delta_pos[X] != 0.0f) || (block.delta_pos[Y] != 0.0f) || (block.delta_pos[Z] != 0.0f))
block.move_type = Block::Move;
else
block.move_type = Block::Retract;
}
else if (block.delta_pos[E] > 0.0f)
{
if ((block.delta_pos[X] == 0.0f) && (block.delta_pos[Y] == 0.0f) && (block.delta_pos[Z] == 0.0f))
block.move_type = Block::Unretract;
else if ((block.delta_pos[X] != 0.0f) || (block.delta_pos[Y] != 0.0f))
block.move_type = Block::Extrude;
}
else if ((block.delta_pos[X] != 0.0f) || (block.delta_pos[Y] != 0.0f) || (block.delta_pos[Z] != 0.0f))
block.move_type = Block::Move;
#endif // ENABLE_MOVE_STATS
// adds block to blocks list
m_blocks.emplace_back(block);
m_g1_line_ids.emplace_back(G1LineIdToBlockIdMap::value_type(get_g1_line_id(), (unsigned int)m_blocks.size() - 1));
}
void GCodeTimeEstimator::_processG4(const GCodeReader::GCodeLine& line)
{
PROFILE_FUNC();
GCodeFlavor dialect = get_dialect();
float value;
if (line.has_value('P', value))
add_additional_time(value * MILLISEC_TO_SEC);
// see: http://reprap.org/wiki/G-code#G4:_Dwell
if ((dialect == gcfRepetier) ||
(dialect == gcfMarlin) ||
(dialect == gcfSmoothie) ||
(dialect == gcfRepRap))
{
if (line.has_value('S', value))
add_additional_time(value);
}
_simulate_st_synchronize();
}
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_position(X, line.x() * lengthsScaleFactor);
anyFound = true;
}
if (line.has_y())
{
set_axis_position(Y, line.y() * lengthsScaleFactor);
anyFound = true;
}
if (line.has_z())
{
set_axis_position(Z, line.z() * lengthsScaleFactor);
anyFound = true;
}
if (line.has_e())
{
set_axis_position(E, line.e() * lengthsScaleFactor);
anyFound = true;
}
else
_simulate_st_synchronize();
if (!anyFound)
{
for (unsigned char a = X; a < Num_Axis; ++a)
{
set_axis_position((EAxis)a, 0.0f);
}
}
}
void GCodeTimeEstimator::_processM1(const GCodeReader::GCodeLine& line)
{
PROFILE_FUNC();
_simulate_st_synchronize();
}
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 != gcfRepRap) && (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::_processM600(const GCodeReader::GCodeLine& line)
{
PROFILE_FUNC();
m_needs_color_times = true;
_calculate_time();
if (m_color_time_cache != 0.0f)
{
m_color_times.push_back(m_color_time_cache);
m_color_time_cache = 0.0f;
}
}
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.
add_additional_time(get_filament_unload_time(get_extruder_id()));
reset_extruder_id();
_simulate_st_synchronize();
}
}
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.
add_additional_time(get_filament_unload_time(get_extruder_id()));
set_extruder_id(id);
add_additional_time(get_filament_load_time(get_extruder_id()));
_simulate_st_synchronize();
}
}
}
void GCodeTimeEstimator::_simulate_st_synchronize()
{
PROFILE_FUNC();
_calculate_time();
}
void GCodeTimeEstimator::_forward_pass()
{
PROFILE_FUNC();
if (m_blocks.size() > 1)
{
for (int i = m_last_st_synchronized_block_id + 1; i < (int)m_blocks.size() - 1; ++i)
{
_planner_forward_pass_kernel(m_blocks[i], m_blocks[i + 1]);
}
}
}
void GCodeTimeEstimator::_reverse_pass()
{
PROFILE_FUNC();
if (m_blocks.size() > 1)
{
for (int i = (int)m_blocks.size() - 1; i >= m_last_st_synchronized_block_id + 2; --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.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 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()
{
PROFILE_FUNC();
Block* curr = nullptr;
Block* next = nullptr;
for (int i = m_last_st_synchronized_block_id + 1; i < (int)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_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
}