fix(gcode): use Klipper motion planning for time estimates (#15545)

Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
Co-authored-by: raistlin7447 <kris.austin@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-19 16:15:59 -03:00
1 parent 69193d5681
commit 5a36120c58
12 files changed
+602 -133

No files matched your search

-8
View File
@@ -6660,10 +6660,6 @@ LayerResult GCode::process_layer(
case CalibMode::Calib_Input_shaping_freq: {
if (m_layer_index == 1){
gcode += writer().set_input_shaping('A', print.calib_params().start, 0.f, print.calib_params().shaper_type);
if (m_writer.get_gcode_flavor() == gcfKlipper) {
// Disable minimum cruise ratio to ensure consistent motion for calibration
gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n";
}
} else {
if (print.calib_params().freqStartX == print.calib_params().freqStartY && print.calib_params().freqEndX == print.calib_params().freqEndY) {
gcode += writer().set_input_shaping('A', 0.f, this->interpolate_value_across_layers(print.calib_params().freqStartX, print.calib_params().freqEndX), "");
@@ -6676,10 +6672,6 @@ LayerResult GCode::process_layer(
}
case CalibMode::Calib_Input_shaping_damp: {
if (m_layer_index == 1){
if (m_writer.get_gcode_flavor() == gcfKlipper) {
// Disable minimum cruise ratio to ensure consistent motion for calibration
gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n";
}
gcode += writer().set_input_shaping('X', 0.f, print.calib_params().freqStartX, print.calib_params().shaper_type);
gcode += writer().set_input_shaping('Y', 0.f, print.calib_params().freqStartY, print.calib_params().shaper_type);
} else {
+293 -100
View File
@@ -24,6 +24,7 @@
#include <algorithm>
#include <array>
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/constants.hpp>
#include <boost/log/trivial.hpp>
@@ -378,6 +379,11 @@ void GCodeProcessor::TimeMachine::reset()
travel_acceleration = 0.0f;
max_travel_acceleration = 0.0f;
extrude_factor_override_percentage = 1.0f;
klipper = false;
minimum_cruise_ratio = 0.5f;
requested_accel_to_decel = -1.0f;
klipper_junction_flush = 1.0f;
klipper_queue_priming = true;
time = 0.0f;
stop_times = std::vector<StopTime>();
curr.reset();
@@ -493,16 +499,87 @@ GCodeProcessor::TimeMachine::AdditionalBuffer GCodeProcessor::TimeMachine::merge
return merged;
}
// Klipper toolhead.py LookAheadQueue.flush: the pseudo-acceleration pass
// reserves cruising distance across a sequence, not separately in each segment.
size_t GCodeProcessor::TimeMachine::plan_klipper(bool lazy)
{
klipper_junction_flush = requested_accel_to_decel >= 0.0f ? 0.250f : 0.150f;
klipper_queue_priming = false;
size_t flush_count = blocks.size();
bool update_flush_count = lazy;
float next_start_v2 = 0.0f;
float next_mcr_start_v2 = 0.0f;
float peak_cruise_v2 = 0.0f;
size_t pending = 0;
for (size_t i = blocks.size(); i-- > 0;) {
TimeBlock& block = blocks[i];
const float delta_v2 = 2.0f * block.distance * block.acceleration;
const float reachable_start_v2 = next_start_v2 + delta_v2;
const float start_v2 = std::min(sqr(block.max_entry_speed), reachable_start_v2);
const float reachable_mcr_v2 = next_mcr_start_v2 + block.mcr_delta_v2;
const float mcr_start_v2 = std::min(block.max_mcr_entry_speed_sqr, reachable_mcr_v2);
float cruise_v2 = -1.0f;
++pending;
if (mcr_start_v2 < reachable_mcr_v2) {
if (mcr_start_v2 + block.mcr_delta_v2 > next_mcr_start_v2 || pending > 1) {
// Stop at this block, not past it: the blocks above still have a cruise speed
// that depends on moves not yet seen, so only [0, i) may be committed.
if (update_flush_count && peak_cruise_v2 > 0.0f) {
flush_count = i;
update_flush_count = false;
}
peak_cruise_v2 = std::min(sqr(block.feedrate_profile.cruise),
0.5f * (mcr_start_v2 + reachable_mcr_v2));
}
cruise_v2 = std::min({0.5f * (start_v2 + reachable_start_v2),
sqr(block.feedrate_profile.cruise), peak_cruise_v2});
pending = 0;
}
// Until the forward pass, these fields hold v^2, not speeds; -1 marks
// an unassigned cruise speed. A lazy return leaves them in this state.
block.feedrate_profile.entry = start_v2;
block.feedrate_profile.exit = next_start_v2;
block.trapezoid.cruise_feedrate = cruise_v2;
block.peak_cruise_v2 = peak_cruise_v2;
next_start_v2 = start_v2;
next_mcr_start_v2 = mcr_start_v2;
}
if (update_flush_count)
return 0;
// A run of blocks that cannot accelerate is resolved here, once the peak cruise speed of the
// accelerating block that precedes it is known. Each one takes a running minimum with its own
// entry speed, seeded from that peak rather than from the setter's own clamped cruise speed.
float prev_cruise_v2 = 0.0f;
for (size_t i = 0; i < flush_count; ++i) {
TimeBlock& block = blocks[i];
float cruise_v2 = block.trapezoid.cruise_feedrate;
if (cruise_v2 < 0.0f) {
cruise_v2 = std::min(prev_cruise_v2, block.feedrate_profile.entry);
prev_cruise_v2 = cruise_v2;
} else
prev_cruise_v2 = block.peak_cruise_v2;
const float start_v2 = std::min(block.feedrate_profile.entry, cruise_v2);
const float end_v2 = std::min(block.feedrate_profile.exit, cruise_v2);
// A zero acceleration would put an infinity in the ramp distances; the Marlin helpers guard
// their divides the same way and fall back to a pure cruise.
const float inv_two_accel = block.acceleration > 0.0f ? 0.5f / block.acceleration : 0.0f;
block.feedrate_profile.entry = std::sqrt(start_v2);
block.feedrate_profile.exit = std::sqrt(end_v2);
block.trapezoid.cruise_feedrate = std::sqrt(cruise_v2);
block.trapezoid.accelerate_until = (cruise_v2 - start_v2) * inv_two_accel;
block.trapezoid.decelerate_after = block.distance - (cruise_v2 - end_v2) * inv_two_accel;
}
return flush_count;
}
void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, PrintEstimatedStatistics::ETimeMode mode, size_t keep_last_n_blocks, float additional_time, EMoveType target_move_type, bool is_final)
{
if (!enabled)
return;
// Orca: on the finalization pass, drain extra time that is still buffered (e.g. a
// trailing filament change) even with fewer than two blocks queued -- no later pass
// exists to attribute it on. Every other pass keeps the original >= 2 requirement,
// and an empty buffer keeps today's early-return unchanged (no behavior change).
// Drain buffered delays on the final pass, even with no motion left.
// A trailing filament change has no later move to receive its time.
const bool drain_final = is_final && !m_additional_time_buffer.empty();
if (blocks.size() < 2 && !drain_final) {
if ((klipper ? blocks.empty() : blocks.size() < 2) && !drain_final) {
// Not enough blocks to attribute the extra time to yet; buffer it so it is
// applied on a later pass instead of being dropped.
if (additional_time > 0.0f)
@@ -510,7 +587,7 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
return;
}
assert(keep_last_n_blocks <= blocks.size());
assert(klipper || keep_last_n_blocks <= blocks.size());
// Merge any previously buffered extra time with this call's extra time. Each
// entry is applied, in order, to the first block matching its target move type
@@ -520,23 +597,36 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
additional_buffer.emplace_back(target_move_type, additional_time);
additional_buffer = merge_adjacent_additional_time_blocks(additional_buffer);
// reverse_pass
for (int i = static_cast<int>(blocks.size()) - 1; i > 0; --i) {
planner_reverse_pass_kernel(blocks[i - 1], blocks[i]);
size_t n_blocks_process;
if (klipper) {
n_blocks_process = plan_klipper(keep_last_n_blocks != 0);
keep_last_n_blocks = blocks.size() - n_blocks_process;
} else {
for (int i = static_cast<int>(blocks.size()) - 1; i > 0; --i)
planner_reverse_pass_kernel(blocks[i - 1], blocks[i]);
for (size_t i = 0; i + 1 < blocks.size(); ++i)
planner_forward_pass_kernel(blocks[i], blocks[i + 1]);
recalculate_trapezoids(blocks);
n_blocks_process = blocks.size() - keep_last_n_blocks;
}
// forward_pass
for (size_t i = 0; i + 1 < blocks.size(); ++i) {
planner_forward_pass_kernel(blocks[i], blocks[i + 1]);
}
recalculate_trapezoids(blocks);
const size_t n_blocks_process = blocks.size() - keep_last_n_blocks;
size_t additional_buffer_idx = 0;
for (size_t i = 0; i < n_blocks_process; ++i) {
const TimeBlock& block = blocks[i];
float block_time = block.time();
double motion_time;
if (klipper && block.trapezoid.cruise_feedrate > 0.0f) {
// Integrate using the planned speeds. Reconstructing the exit speed
// from rounded deceleration distance loses precision near a stop.
const double cruise = block.trapezoid.cruise_feedrate;
motion_time = block.distance / cruise;
if (block.acceleration > 0.0f) {
const double accel_delta = cruise - block.feedrate_profile.entry;
const double decel_delta = cruise - block.feedrate_profile.exit;
motion_time += (sqr(accel_delta) + sqr(decel_delta)) / (2.0 * block.acceleration * cruise);
}
} else {
motion_time = block.time();
}
double block_time = motion_time;
if (additional_buffer_idx < additional_buffer.size()) {
const EMoveType buf_move_type = additional_buffer[additional_buffer_idx].first;
if (buf_move_type == EMoveType::Noop || buf_move_type == block.move_type) {
@@ -545,13 +635,13 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
}
}
time += double(block_time);
time += block_time;
// Orca: accumulate per-SkipType time spent inside SKIPPABLE regions, folded into this single
// calculate_time write site. Fires fleet-wide (the shipping time_lapse_gcode template stamps
// blocks stTimelapse), but writes only skippable_part_time, which has no g-code-emitting
// reader until the pre-heat injector consumes it, so output is byte-identical.
if (block.skippable_type != SkipType::stNone)
result.skippable_part_time[block.skippable_type] += block.time();
result.skippable_part_time[block.skippable_type] += motion_time;
result.moves[block.move_id].time[static_cast<size_t>(mode)] = block_time;
gcode_time.cache += block_time;
//BBS
@@ -689,14 +779,12 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
if (keep_last_n_blocks) {
blocks.erase(blocks.begin(), blocks.begin() + n_blocks_process);
// Ensure that the new first block's entry speed will be preserved to prevent discontinuity
// between the erased blocks' exit speed and the new first block's entry speed.
// Otherwise, the first block's entry speed could be recalculated on the next pass without
// considering that there are no more blocks before this first block. This could lead
// to discontinuity between the exit speed (of already processed blocks) and the entry
// speed of the first block.
TimeBlock &first_block = blocks.front();
first_block.max_entry_speed = first_block.feedrate_profile.entry;
// Preserve the boundary speed in the Marlin planner. Klipper only flushes
// moves whose cruise speed is already independent of future moves.
if (!klipper) {
TimeBlock& first_block = blocks.front();
first_block.max_entry_speed = first_block.feedrate_profile.entry;
}
} else {
blocks.clear();
}
@@ -3872,9 +3960,9 @@ void GCodeProcessor::finalize(bool post_process)
update_slice_warnings();
}
float GCodeProcessor::get_time(PrintEstimatedStatistics::ETimeMode mode) const
double GCodeProcessor::get_time(PrintEstimatedStatistics::ETimeMode mode) const
{
return (mode < PrintEstimatedStatistics::ETimeMode::Count) ? float(m_time_processor.machines[static_cast<size_t>(mode)].time) : 0.0f;
return (mode < PrintEstimatedStatistics::ETimeMode::Count) ? m_time_processor.machines[static_cast<size_t>(mode)].time : 0.0;
}
float GCodeProcessor::get_prepare_time(PrintEstimatedStatistics::ETimeMode mode) const
@@ -5031,6 +5119,14 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line, const std::o
if (line.has_e()) g1_axes[E] = (double)line.e();
std::optional<double> g1_feedrate = std::nullopt;
if (line.has_f()) g1_feedrate = (double)line.f();
if (m_flavor == gcfKlipper) {
// Preserve decimal coordinates for short segments without parsing twice.
for (unsigned char axis = X; axis <= E; ++axis)
if (line.has(static_cast<Axis>(axis)))
g1_axes[axis] = line.precise_value(static_cast<Axis>(axis));
if (line.has_f())
g1_feedrate = line.precise_value(F);
}
process_G1(g1_axes, g1_feedrate);
}
@@ -5228,7 +5324,7 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
//BBS: limite the cruise according to centripetal acceleration
//Only need to handle when both prev and curr segment has movement in x-y plane
if ((prev.exit_direction(0) != 0.0f || prev.exit_direction(1) != 0.0f) &&
if (m_flavor != gcfKlipper && (prev.exit_direction(0) != 0.0f || prev.exit_direction(1) != 0.0f) &&
(curr.enter_direction(0) != 0.0f || curr.enter_direction(1) != 0.0f)) {
Vec3f v1 = prev.exit_direction;
v1(2, 0) = 0.0f;
@@ -5252,6 +5348,15 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
}
}
if (m_flavor == gcfKlipper && !is_extrusion_only_move(delta_pos)) {
const float max_velocity = get_axis_max_feedrate(static_cast<PrintEstimatedStatistics::ETimeMode>(i), X, m_machine_config_idx);
if (max_velocity > 0.0f)
curr.feedrate = std::min(curr.feedrate, max_velocity);
}
// Klipper applies E limits to retractions and moves without XY motion,
// not to positive extrusion along a printing path.
const bool limit_extruder = m_flavor != gcfKlipper || delta_pos[E] < 0.0 ||
(delta_pos[X] == 0.0 && delta_pos[Y] == 0.0);
// calculates block cruise feedrate
float min_feedrate_factor = 1.0f;
for (unsigned char a = X; a <= E; ++a) {
@@ -5260,7 +5365,8 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
curr.axis_feedrate[a] *= machine.extrude_factor_override_percentage;
curr.abs_axis_feedrate[a] = std::abs(curr.axis_feedrate[a]);
if (curr.abs_axis_feedrate[a] != 0.0f) {
if (curr.abs_axis_feedrate[a] != 0.0f && (a != E || limit_extruder) &&
(m_flavor != gcfKlipper || a == Z || a == E)) {
float axis_max_feedrate = get_axis_max_feedrate(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a), m_machine_config_idx);
if (axis_max_feedrate != 0.0f) min_feedrate_factor = std::min<float>(min_feedrate_factor, axis_max_feedrate / curr.abs_axis_feedrate[a]);
}
@@ -5277,20 +5383,42 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
}
// calculates block acceleration
float acceleration =
(type == EMoveType::Travel) ? get_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
(is_extrusion_only_move(delta_pos) ?
get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)));
float acceleration;
if (m_flavor == gcfKlipper && is_extrusion_only_move(delta_pos)) {
// Klipper's max_extrude_only_accel. machine_max_acceleration_e is optional and some
// shipped profiles leave it at 0, so keep the non-zero retract fallback.
acceleration = get_axis_max_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), E, m_machine_config_idx);
if (acceleration <= 0.0f)
acceleration = get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i));
} else
acceleration =
(m_flavor == gcfKlipper) ? get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
(type == EMoveType::Travel) ? get_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
(is_extrusion_only_move(delta_pos) ?
get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)));
//BBS
for (unsigned char a = X; a <= E; ++a) {
if ((a == E && !limit_extruder) || (m_flavor == gcfKlipper && (a == X || a == Y)))
continue;
float axis_max_acceleration = get_axis_max_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a), m_machine_config_idx);
// An unset (0) limit means "no limit", as in the feedrate loop above. Only the Klipper
// planner divides by the block acceleration, so leave the Marlin path as it was.
if (m_flavor == gcfKlipper && axis_max_acceleration <= 0.0f)
continue;
if (acceleration * std::abs(delta_pos[a]) * inv_distance > axis_max_acceleration)
acceleration = axis_max_acceleration / (std::abs(delta_pos[a]) * inv_distance);
}
block.acceleration = acceleration;
if (m_flavor == gcfKlipper) {
block.junction_deviation = get_junction_deviation(static_cast<PrintEstimatedStatistics::ETimeMode>(i), acceleration);
const float cruise_acceleration = machine.requested_accel_to_decel >= 0.0f ?
std::min(machine.acceleration, machine.requested_accel_to_decel) :
machine.acceleration * (1.0f - machine.minimum_cruise_ratio);
block.mcr_delta_v2 = 2.0f * distance * std::min(acceleration, cruise_acceleration);
}
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
@@ -5401,6 +5529,12 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
block.feedrate_profile.entry = std::min(vmax_junction, v_allowable);
block.max_entry_speed = vmax_junction;
if (m_flavor == gcfKlipper && has_prev_move) {
const TimeBlock& previous = blocks.back();
block.max_entry_speed = std::min(block.max_entry_speed,
std::sqrt(sqr(previous.max_entry_speed) + 2.0f * previous.distance * previous.acceleration));
block.max_mcr_entry_speed_sqr = std::min(sqr(block.max_entry_speed), previous.max_mcr_entry_speed_sqr + previous.mcr_delta_v2);
}
block.flags.nominal_length = (block.feedrate_profile.cruise <= v_allowable);
block.flags.recalculate = true;
block.safe_feedrate = curr.safe_feedrate;
@@ -5412,9 +5546,12 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
prev = curr;
blocks.push_back(block);
if (m_flavor == gcfKlipper && blocks.size() > 1)
machine.klipper_junction_flush -= distance / curr.feedrate;
}
if (m_time_processor.machines[0].blocks.size() > TimeProcessor::Planner::refresh_threshold)
if (m_flavor == gcfKlipper ? m_time_processor.machines[0].klipper_junction_flush <= 0.0f :
m_time_processor.machines[0].blocks.size() > TimeProcessor::Planner::refresh_threshold)
calculate_time(m_result, TimeProcessor::Planner::queue_size);
const Vec3f plate_offset = {(float) m_x_offset, (float) m_y_offset, 0.0f};
@@ -6341,6 +6478,8 @@ void GCodeProcessor::process_M108(const GCodeReader::GCodeLine& line)
void GCodeProcessor::process_M109(const GCodeReader::GCodeLine& line)
{
if (m_flavor == gcfKlipper)
simulate_st_synchronize();
int filament_id = get_filament_id();
float new_temp;
if (line.has_value('R', new_temp)) {
@@ -6403,6 +6542,8 @@ void GCodeProcessor::process_M140(const GCodeReader::GCodeLine& line)
void GCodeProcessor::process_M190(const GCodeReader::GCodeLine& line)
{
if (m_flavor == gcfKlipper)
simulate_st_synchronize();
float new_temp;
if (line.has_value('S', new_temp))
m_highest_bed_temp = m_highest_bed_temp < (int)new_temp ? (int)new_temp : m_highest_bed_temp;
@@ -6480,6 +6621,21 @@ void GCodeProcessor::process_M203(const GCodeReader::GCodeLine& line)
void GCodeProcessor::process_M204(const GCodeReader::GCodeLine& line)
{
float value;
if (m_flavor == gcfKlipper) {
if (!line.has_value('S', value)) {
float travel;
if (!line.has_value('P', value) || !line.has_value('T', travel))
return;
value = std::min(value, travel);
}
// Klipper replaces its single live max_accel for print and travel; the profile-clamping
// setters would diverge from the firmware when custom G-code raises the limit.
if (value > 0.0f) {
for (auto& machine : m_time_processor.machines)
machine.acceleration = machine.travel_acceleration = value;
}
return;
}
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
if (static_cast<PrintEstimatedStatistics::ETimeMode>(i) == PrintEstimatedStatistics::ETimeMode::Normal ||
m_time_processor.machine_envelope_processing_enabled) {
@@ -6541,50 +6697,50 @@ void GCodeProcessor::process_M205(const GCodeReader::GCodeLine& line)
void GCodeProcessor::process_SET_VELOCITY_LIMIT(const GCodeReader::GCodeLine& line)
{
// handle SQUARE_CORNER_VELOCITY
static const std::regex square_corner_velocity_pattern("\\sSQUARE_CORNER_VELOCITY\\s*=\\s*([0-9]*\\.*[0-9]*)");
std::smatch matches;
if (std::regex_search(line.raw(), matches, square_corner_velocity_pattern) && matches.size() == 2) {
float _jerk = 0;
try
{
_jerk = std::stof(matches[1]);
static const std::regex parameter_pattern(
R"(\s(VELOCITY|ACCEL|ACCEL_TO_DECEL|SQUARE_CORNER_VELOCITY|MINIMUM_CRUISE_RATIO)\s*=\s*([+]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?)(?=\s|;|$))",
std::regex::icase);
const std::string command = line.raw().substr(0, line.raw().find(';'));
std::vector<std::pair<std::string, float>> parameters;
for (std::sregex_iterator it(command.begin(), command.end(), parameter_pattern), end; it != end; ++it) {
float value;
try {
value = std::stof((*it)[2].str());
} catch (const std::exception&) {
continue;
}
catch (...){}
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
set_option_value(m_time_processor.machine_limits.machine_max_jerk_x, i, _jerk);
set_option_value(m_time_processor.machine_limits.machine_max_jerk_y, i, _jerk);
if (std::isfinite(value))
parameters.emplace_back(boost::to_upper_copy((*it)[1].str()), value);
}
// Klipper 0.13 converts ACCEL_TO_DECEL only when the line has no MINIMUM_CRUISE_RATIO, and later
// releases ignore it, so a line carrying both (as Orca writes) is planned with the cruise ratio.
const bool has_cruise_ratio = std::any_of(parameters.begin(), parameters.end(),
[](const auto& parameter) { return parameter.first == "MINIMUM_CRUISE_RATIO" && parameter.second >= 0.0f && parameter.second < 1.0f; });
for (const auto& [name, value] : parameters) {
for (size_t i = 0; i < m_time_processor.machines.size(); ++i) {
TimeMachine& machine = m_time_processor.machines[i];
// Klipper replaces its single live max_accel for print and travel; the profile-clamping
// setters would diverge from the firmware when custom G-code raises the limit.
if (name == "ACCEL" && value > 0.0f)
machine.acceleration = machine.travel_acceleration = value;
else if (name == "MINIMUM_CRUISE_RATIO" && value >= 0.0f && value < 1.0f) {
if (machine.klipper_queue_priming && machine.requested_accel_to_decel >= 0.0f)
machine.klipper_junction_flush -= 1.0f;
machine.minimum_cruise_ratio = value;
machine.requested_accel_to_decel = -1.0f;
} else if (name == "ACCEL_TO_DECEL" && value > 0.0f && !has_cruise_ratio) {
if (machine.klipper_queue_priming && machine.requested_accel_to_decel < 0.0f)
machine.klipper_junction_flush += 1.0f;
machine.requested_accel_to_decel = value;
} else if (name == "SQUARE_CORNER_VELOCITY" && value >= 0.0f) {
set_option_value(m_time_processor.machine_limits.machine_max_jerk_x, i, value);
set_option_value(m_time_processor.machine_limits.machine_max_jerk_y, i, value);
} else if (name == "VELOCITY" && value > 0.0f) {
set_option_value(m_time_processor.machine_limits.machine_max_speed_x, i, value);
set_option_value(m_time_processor.machine_limits.machine_max_speed_y, i, value);
}
}
}
static const std::regex accel_pattern("\\sACCEL\\s*=\\s*([0-9]*\\.*[0-9]*)");
if (std::regex_search(line.raw(), matches, accel_pattern) && matches.size() == 2) {
float _accl = 0;
try
{
_accl = std::stof(matches[1]);
}
catch (...) {}
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
set_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), _accl);
set_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), _accl);
}
}
static const std::regex velocity_pattern("\\sVELOCITY\\s*=\\s*([0-9]*\\.*[0-9]*)");
if (std::regex_search(line.raw(), matches, velocity_pattern) && matches.size() == 2) {
float _speed = 0;
try
{
_speed = std::stof(matches[1]);
}
catch (...) {}
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
set_option_value(m_time_processor.machine_limits.machine_max_speed_x, i, _speed);
set_option_value(m_time_processor.machine_limits.machine_max_speed_y, i, _speed);
}
}
}
void GCodeProcessor::process_M221(const GCodeReader::GCodeLine& line)
@@ -6621,14 +6777,15 @@ void GCodeProcessor::process_M400(const GCodeReader::GCodeLine& line)
{
float value_s = 0.0;
float value_p = 0.0;
if (line.has_value('S', value_s) || line.has_value('P', value_p)) {
value_s += value_p * 0.001;
// Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see
// m_skip_end_gcode_delays). Only omits dwell time — no state is updated here.
if (m_skip_end_gcode_delays)
return;
simulate_st_synchronize(value_s);
}
const bool has_delay = line.has_value('S', value_s) || line.has_value('P', value_p);
if (m_flavor != gcfKlipper && !has_delay)
return;
value_s += value_p * 0.001;
// Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see
// m_skip_end_gcode_delays). Only omits dwell time — no state is updated here.
if (m_skip_end_gcode_delays && has_delay)
return;
simulate_st_synchronize(value_s);
}
void GCodeProcessor::process_M401(const GCodeReader::GCodeLine& line)
@@ -7460,12 +7617,12 @@ float GCodeProcessor::get_junction_deviation(PrintEstimatedStatistics::ETimeMode
{
const size_t id = static_cast<size_t>(mode);
// Klipper has no classic jerk: jd = scv^2 * (sqrt(2) - 1) / max_accel
// (toolhead.py::_calc_junction_deviation). Passing the block acceleration back in makes it cancel
// in calc_vmax_junction_deviation(), leaving the identity v == scv at a 90 degree corner.
// Klipper derives junction deviation from the live toolhead acceleration,
// before a move is constrained by Z or extruder limits.
if (m_flavor == gcfKlipper) {
// machine_max_jerk_x holds the square corner velocity; process_SET_VELOCITY_LIMIT() writes it.
const float scv = get_option_value(m_time_processor.machine_limits.machine_max_jerk_x, id);
acceleration = m_time_processor.machines[id].acceleration;
if (scv <= 0.0f || acceleration <= 0.0f)
return 0.0f;
return sqr(scv) * (std::sqrt(2.0f) - 1.0f) / acceleration;
@@ -7497,18 +7654,45 @@ float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const
const TimeMachine::State& curr, bool has_prev_move,
PrintEstimatedStatistics::ETimeMode mode) const
{
if (m_flavor == gcfKlipper) {
if (!has_prev_move || curr.enter_direction.isZero() || prev.exit_direction.isZero())
return 0.0f;
const TimeBlock& previous = m_time_processor.machines[static_cast<size_t>(mode)].blocks.back();
double limit = std::min(sqr(double(block.feedrate_profile.cruise)), sqr(double(prev.feedrate)));
const double extrude_ratio_change = std::abs(curr.axis_feedrate[E] / curr.feedrate -
prev.axis_feedrate[E] / prev.feedrate);
// Klipper's instantaneous_corner_velocity defaults to 1 mm/s.
// It is independent of machine_max_jerk_e; custom firmware values
// cannot be inferred from the slicer's jerk setting.
if (extrude_ratio_change > 0.0f)
limit = std::min(limit, sqr(1.0 / extrude_ratio_change));
// Recover directions from the double-precision axis velocities: float
// unit vectors lose the angle between nearly collinear short segments.
const Vec3d prev_direction = Vec3d(prev.axis_feedrate[X], prev.axis_feedrate[Y], prev.axis_feedrate[Z]).normalized();
const Vec3d curr_direction = Vec3d(curr.axis_feedrate[X], curr.axis_feedrate[Y], curr.axis_feedrate[Z]).normalized();
const double cosine = std::clamp(-prev_direction.dot(curr_direction), -1.0, 1.0);
const double sin_half = std::sqrt(0.5 * (1.0 - cosine));
const double cos_half = std::sqrt(0.5 * (1.0 + cosine));
if (sin_half < 1.0 && cos_half > 0.0) {
const double radius = sin_half / (1.0 - sin_half);
const double half_tan = 0.5 * sin_half / cos_half;
limit = std::min({limit,
radius * block.junction_deviation * block.acceleration,
radius * previous.junction_deviation * previous.acceleration,
half_tan * block.distance * block.acceleration,
half_tan * previous.distance * previous.acceleration});
}
return std::sqrt(limit);
}
const float junction_deviation = get_junction_deviation(mode, block.acceleration);
if (junction_deviation <= 0.0f)
return -1.0f; // classic jerk machine, the caller keeps its own computation
if (!has_prev_move)
return 0.0f; // starts from rest, the planner raises this on the reverse pass
// -1 for a straight continuation, +1 for a full reversal. Half angle identity, no acos()/sin().
// Both vectors are unit length over XYZE, so this really is a cosine: scaling by 1 / distance
// instead, as PrusaSlicer does, leaves an E term that makes extruding corners look straighter
// than they are. Marlin normalizes over XYZE for any extruding move (planner.cpp, esteps > 0)
// and Klipper keeps E out of the cosine entirely (toolhead.py::Move.calc_junction); both agree
// that the corner is planned by its geometry, and normalizing matches them to within 1e-5.
// Unit vectors over XYZE keep this a cosine. Scaling by 1 / XYZ distance
// leaves an E term that makes extruding corners appear straighter.
// Marlin normalizes over XYZE; the Klipper branch above uses XYZ only.
float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec);
if (junction_cos_theta > 0.999999f)
return 0.0f; // the path doubles back, the machine has to stop
@@ -7524,8 +7708,8 @@ float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const
float vmax_junction_sqr = (junction_acceleration * junction_deviation * sin_theta_d2) / (1.0f - sin_theta_d2);
// Marlin's JD_HANDLE_SMALL_SEGMENTS: a short move through a shallow corner is treated as an arc and
// capped by the centripetal acceleration it needs. Klipper has no equivalent.
if (m_flavor != gcfKlipper && block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
// capped by the centripetal acceleration it needs.
if (block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
// Fast acos(-t), max. error +-0.033rad. MinMax polynomial by W. Randolph Franklin:
// https://wrf.ecse.rpi.edu/Research/Short_Notes/arcsin/onlyelem.html
const float neg = junction_cos_theta < 0.0f ? -1.0f : 1.0f;
@@ -7700,6 +7884,7 @@ void GCodeProcessor::calculate_time(GCodeProcessorResult& result, size_t keep_la
std::vector<TimeMachine::ActualSpeedMove> actual_speed_moves;
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
TimeMachine& machine = m_time_processor.machines[i];
machine.klipper = m_flavor == gcfKlipper;
machine.calculate_time(m_result, static_cast<PrintEstimatedStatistics::ETimeMode>(i), keep_last_n_blocks, additional_time, target_move_type, is_final);
if (static_cast<PrintEstimatedStatistics::ETimeMode>(i) == PrintEstimatedStatistics::ETimeMode::Normal)
actual_speed_moves = std::move(machine.actual_speed_moves);
@@ -7778,6 +7963,14 @@ void GCodeProcessor::calculate_time(GCodeProcessorResult& result, size_t keep_la
void GCodeProcessor::simulate_st_synchronize(float additional_time, EMoveType target_move_type)
{
calculate_time(m_result, 0, additional_time, target_move_type);
if (m_flavor == gcfKlipper) {
// ToolHead overrides the normal look-ahead window with BUFFER_TIME_HIGH
// at startup and after synchronization: 2 s on legacy, 1 s on modern Klipper.
for (TimeMachine& machine : m_time_processor.machines) {
machine.klipper_junction_flush = machine.requested_accel_to_decel >= 0.0f ? 2.0f : 1.0f;
machine.klipper_queue_priming = true;
}
}
}
void GCodeProcessor::update_estimated_times_stats()
@@ -7812,7 +8005,7 @@ double GCodeProcessor::extract_absolute_position_on_axis(Axis axis, const GCodeR
is_relative |= (m_e_local_positioning_type == EPositioningType::Relative);
const double lengthsScaleFactor = (m_units == EUnits::Inches) ? double(INCHES_TO_MM) : 1.0;
double ret = line.value(Slic3r::Axis(axis)) * lengthsScaleFactor;
double ret = line.precise_value(Slic3r::Axis(axis)) * lengthsScaleFactor;
// if (axis == E && m_use_volumetric_e)
// ret /= area_filament_cross_section;
return is_relative ? m_start_position[axis] + ret : m_origin[axis] + ret;
+16 -3
View File
@@ -91,12 +91,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
struct Mode
{
float time;
double time;
float prepare_time;
std::vector<std::pair<CustomGCode::Type, std::pair<float, float>>> custom_gcode_times;
void reset() {
time = 0.0f;
time = 0.0;
prepare_time = 0.0f;
custom_gcode_times.clear();
custom_gcode_times.shrink_to_fit();
@@ -703,6 +703,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
float acceleration{ 0.0f }; // mm/s^2
float max_entry_speed{ 0.0f }; // mm/s
float safe_feedrate{ 0.0f }; // mm/s
float junction_deviation{ 0.0f };
float max_mcr_entry_speed_sqr{ 0.0f };
float mcr_delta_v2{ 0.0f };
// Peak cruise speed squared in effect when this block was planned. Only set on a block
// that can accelerate; the delayed blocks after it are seeded from its value.
float peak_cruise_v2{ 0.0f };
Flags flags;
FeedrateProfile feedrate_profile;
Trapezoid trapezoid;
@@ -782,6 +788,11 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
// hard limit for the travel acceleration, to which the firmware will clamp.
float max_travel_acceleration; // mm/s^2
float extrude_factor_override_percentage;
bool klipper{ false };
float minimum_cruise_ratio{ 0.5f };
float requested_accel_to_decel{ -1.0f };
float klipper_junction_flush{ 1.0f };
bool klipper_queue_priming{ true };
// We accumulate total print time in doubles to reduce the loss of precision
// while adding big floating numbers with small float numbers.
double time; // s
@@ -818,6 +829,8 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
// Merge adjacent buffer entries that target the same move type.
static AdditionalBuffer merge_adjacent_additional_time_blocks(const AdditionalBuffer& buffer);
size_t plan_klipper(bool lazy);
// additional_time is attributed to the first block matching target_move_type
// (EMoveType::Noop matches any block, i.e. the first processed block).
void calculate_time(GCodeProcessorResult& result, PrintEstimatedStatistics::ETimeMode mode, size_t keep_last_n_blocks = 0, float additional_time = 0.0f, EMoveType target_move_type = EMoveType::Noop, bool is_final = false);
@@ -1383,7 +1396,7 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
void process_buffer(const std::string& buffer);
void finalize(bool post_process);
float get_time(PrintEstimatedStatistics::ETimeMode mode) const;
double get_time(PrintEstimatedStatistics::ETimeMode mode) const;
float get_prepare_time(PrintEstimatedStatistics::ETimeMode mode) const;
std::string get_time_dhm(PrintEstimatedStatistics::ETimeMode mode) const;
std::vector<std::pair<CustomGCode::Type, std::pair<float, float>>> get_custom_gcode_times(PrintEstimatedStatistics::ETimeMode mode, bool include_remaining) const;
+5 -2
View File
@@ -26,6 +26,7 @@
#include "ClipperUtils.hpp"
#include "libslic3r/Line.hpp"
#include "LocalesUtils.hpp"
#include "libslic3r/GCodeWriter.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Point.hpp"
@@ -1528,8 +1529,10 @@ private:
gcode << "M204 P" << acceleration;
}
else if (m_gcode_flavor == gcfKlipper && m_accel_to_decel_enable) {
gcode << "SET_VELOCITY_LIMIT ACCEL_TO_DECEL=" << acceleration * m_accel_to_decel_factor / 100;
gcode << "\nM204 S" << acceleration;
// One line, so Klipper derives the limit from this acceleration rather than the previous one.
std::string limits = "SET_VELOCITY_LIMIT ACCEL=" + std::to_string(acceleration);
GCodeWriter::append_klipper_accel_to_decel(limits, acceleration, m_accel_to_decel_factor);
gcode << limits;
}
else {
// M204: Set default acceleration
+1 -1
View File
@@ -95,7 +95,7 @@ const char* GCodeReader::parse_line_internal(const char *ptr, const char *end, G
if (pend != c && is_end_of_word(*pend)) {
// The axis value has been parsed correctly.
if (axis != UNKNOWN_AXIS)
gline.m_axis[int(axis)] = float(v);
gline.m_axis[int(axis)] = v;
gline.m_mask |= 1 << int(axis);
c = pend;
} else
+11 -10
View File
@@ -35,7 +35,8 @@ public:
std::string_view axis_pos(char axis) const;
void clear() { m_raw.clear(); }
bool has(Axis axis) const { return (m_mask & (1 << int(axis))) != 0; }
float value(Axis axis) const { return m_axis[axis]; }
float value(Axis axis) const { return float(m_axis[axis]); }
double precise_value(Axis axis) const { return m_axis[axis]; }
bool has(char axis) const;
bool has_value(char axis, float &value) const;
// Parse value of an axis from raw string starting at axis_pos.
@@ -72,15 +73,15 @@ public:
bool has_p() const { return this->has(P); }
bool has_unknown_axis() const { return this->has(UNKNOWN_AXIS); }
float x() const { return m_axis[X]; }
float y() const { return m_axis[Y]; }
float z() const { return m_axis[Z]; }
float e() const { return m_axis[E]; }
float f() const { return m_axis[F]; }
float x() const { return float(m_axis[X]); }
float y() const { return float(m_axis[Y]); }
float z() const { return float(m_axis[Z]); }
float e() const { return float(m_axis[E]); }
float f() const { return float(m_axis[F]); }
// BBS: add I J P axis
float i() const { return m_axis[I]; }
float j() const { return m_axis[J]; }
float p() const { return m_axis[P]; }
float i() const { return float(m_axis[I]); }
float j() const { return float(m_axis[J]); }
float p() const { return float(m_axis[P]); }
static bool cmd_is(const std::string &gcode_line, const char *cmd_test) {
const char *cmd = GCodeReader::skip_whitespaces(gcode_line.c_str());
@@ -100,7 +101,7 @@ public:
}
private:
std::string m_raw;
float m_axis[NUM_AXES];
double m_axis[NUM_AXES];
uint32_t m_mask;
friend class GCodeReader;
};
+13 -8
View File
@@ -453,12 +453,11 @@ std::string GCodeWriter::set_acceleration_internal(Acceleration type, unsigned i
gcode += separate_travel ? "M204 T" : "M204 P";
gcode += value;
} else if (FLAVOR_IS(gcfKlipper)) {
gcode.reserve(96);
gcode.reserve(128);
gcode += "SET_VELOCITY_LIMIT ACCEL=";
gcode += value;
if (this->config.accel_to_decel_enable) {
gcode += " ACCEL_TO_DECEL=";
gcode += float_to_string_decimal_point(acceleration * this->config.accel_to_decel_factor / 100);
append_klipper_accel_to_decel(gcode, acceleration, this->config.accel_to_decel_factor.value);
if (GCodeWriter::full_gcode_comment)
gcode += " ; adjust ACCEL_TO_DECEL";
}
@@ -473,6 +472,14 @@ std::string GCodeWriter::set_acceleration_internal(Acceleration type, unsigned i
return gcode;
}
void GCodeWriter::append_klipper_accel_to_decel(std::string &gcode, unsigned int acceleration, double accel_to_decel_factor)
{
gcode += " ACCEL_TO_DECEL=";
gcode += float_to_string_decimal_point(acceleration * accel_to_decel_factor / 100);
gcode += " MINIMUM_CRUISE_RATIO=";
gcode += float_to_string_decimal_point(1. - accel_to_decel_factor / 100);
}
std::string GCodeWriter::set_jerk_xy(double jerk)
{
if (jerk < 0.01 || is_approx(jerk, m_last_jerk))
@@ -548,15 +555,13 @@ std::string GCodeWriter::set_accel_and_jerk(unsigned int acceleration, double je
return std::string();
std::string gcode;
gcode.reserve(96);
gcode.reserve(128);
gcode += "SET_VELOCITY_LIMIT";
if (set_acceleration) {
gcode += " ACCEL=";
gcode += std::to_string(acceleration);
if (this->config.accel_to_decel_enable) {
gcode += " ACCEL_TO_DECEL=";
gcode += float_to_string_decimal_point(acceleration * this->config.accel_to_decel_factor / 100);
}
if (this->config.accel_to_decel_enable)
append_klipper_accel_to_decel(gcode, acceleration, this->config.accel_to_decel_factor.value);
m_last_acceleration = acceleration;
}
if (set_jerk) {
+4
View File
@@ -71,6 +71,10 @@ public:
std::string set_jerk_xy(double jerk);
// Orca: set acceleration and jerk in one command for Klipper
std::string set_accel_and_jerk(unsigned int acceleration, double jerk);
// Orca: Klipper's accel_to_decel limit for `acceleration`, sent as the legacy ACCEL_TO_DECEL and as the
// equivalent MINIMUM_CRUISE_RATIO = 1 - ACCEL_TO_DECEL / ACCEL that replaced it. Klipper before 0.13 and
// vendor forks read only the first, current Klipper only the second, and 0.13 prefers the second.
static void append_klipper_accel_to_decel(std::string &gcode, unsigned int acceleration, double accel_to_decel_factor);
std::string set_junction_deviation(double junction_deviation);
std::string set_pressure_advance(double pa, int tool = -1) const;
std::string set_input_shaping(char axis, float damp, float freq, std::string type) const;
+6
View File
@@ -17866,6 +17866,9 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params)
filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false});
print_config->set_key_value("layer_height", new ConfigOptionFloat(0.2));
set_config_values<bool, ConfigOptionBools>(print_config, "enable_overhang_speed", false);
// A 100% factor sends Klipper MINIMUM_CRUISE_RATIO=0 with every acceleration change, for the whole test.
print_config->set_key_value("accel_to_decel_enable", new ConfigOptionBool(true));
print_config->set_key_value("accel_to_decel_factor", new ConfigOptionPercent(100));
print_config->set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
print_config->set_key_value("wall_loops", new ConfigOptionInt(1));
print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0));
@@ -17930,6 +17933,9 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params)
filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats { 0.0 });
filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false});
set_config_values<bool, ConfigOptionBools>(print_config, "enable_overhang_speed", false);
// A 100% factor sends Klipper MINIMUM_CRUISE_RATIO=0 with every acceleration change, for the whole test.
print_config->set_key_value("accel_to_decel_enable", new ConfigOptionBool(true));
print_config->set_key_value("accel_to_decel_factor", new ConfigOptionPercent(100));
print_config->set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
print_config->set_key_value("wall_loops", new ConfigOptionInt(1));
print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0));
+200
View File
@@ -2,15 +2,18 @@
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/catch_message.hpp>
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/MultiNozzleUtils.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include "libslic3r/Utils.hpp"
#include "test_utils.hpp"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
@@ -30,6 +33,7 @@ namespace Slic3r { enum ExtrusionRole : uint8_t; }
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
using Catch::Matchers::WithinRel;
// Regression coverage for filament/tool-change time being folded into the first
// pending motion block (an extrusion move) instead of the tool-change move, and
@@ -570,6 +574,33 @@ double planned_corner_speed(GCodeFlavor flavor, double corner_velocity, double j
} // namespace
TEST_CASE("Klipper extrusion ratio changes use its own corner velocity, not E jerk", "[GCodeTiming][JunctionDeviation]")
{
const double jerk_e = GENERATE(0.0, 100.0);
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
config.machine_max_jerk_e.values = {jerk_e, jerk_e};
GCodeProcessor processor;
run_processor(processor, config,
"G90\nM83\nSET_VELOCITY_LIMIT ACCEL=1000 MINIMUM_CRUISE_RATIO=0\n"
"G1 X20 Y60 F6000\nM400\n"
"G1 X60 Y60 E0.8 F6000\nG1 X100 Y60 E1.6 F6000\nM400\n");
// A 0.02 -> 0.04 extrusion ratio change is capped at 1 / 0.02 = 50 mm/s.
REQUIRE_THAT(corner_speed(processor.get_result()), Catch::Matchers::WithinAbs(50.0, 0.01));
}
TEST_CASE("Klipper legacy smoothing stays absolute across acceleration changes", "[GCodeTiming][Klipper]")
{
const bool modern = GENERATE(false, true);
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
const std::string gcode = std::string("G90\nSET_VELOCITY_LIMIT ACCEL=1000 ") +
(modern ? "MINIMUM_CRUISE_RATIO=0.75\n" : "ACCEL_TO_DECEL=250\n") +
"G1 X40 F30000\nM400\nM204 S2000\nG1 X0\nM400\n";
GCodeProcessor processor;
run_processor(processor, config, gcode.c_str());
const double expected = modern ? 0.5 + 0.25 * std::sqrt(2.0) : 0.95;
REQUIRE_THAT(processor.get_time(PrintEstimatedStatistics::ETimeMode::Normal), WithinAbs(expected, 1e-5));
}
TEST_CASE("Klipper corners are planned with junction deviation derived from the square corner velocity",
"[GCodeTiming][JunctionDeviation]")
{
@@ -669,3 +700,172 @@ TEST_CASE("How fast a corner is taken does not depend on how much is extruded th
Catch::Matchers::WithinRel(marlin, 0.02));
}
}
// Expected times are Klipper's own planner math (klippy/toolhead.py). For a move from rest to rest,
// LookAheadQueue.flush() reduces to cruise_v2 = min(vmax^2, d * accel, d * pseudo_accel) with
// pseudo_accel = accel * (1 - minimum_cruise_ratio), and Move.set_junction() to a trapezoid at accel;
// a move too short to reach its speed still spends that ratio of its length cruising.
namespace {
constexpr double klipper_accel = 1000.0; // make_junction_config's acceleration
constexpr double klipper_cruise_ratio = 0.5;
constexpr double klipper_pseudo_accel = klipper_accel * (1.0 - klipper_cruise_ratio);
// Time of a move that starts and ends at rest.
double rest_to_rest_time(double distance, double max_speed, double accel, double pseudo_accel)
{
const double cruise_v2 = std::min(sqr(max_speed), distance * pseudo_accel);
const double cruise = std::sqrt(cruise_v2);
return 2.0 * cruise / accel + (distance - cruise_v2 / accel) / cruise;
}
// Every run states the cruise ratio rather than relying on the estimator's built-in default.
void run_klipper(GCodeProcessor& proc, const std::string& gcode,
const FullPrintConfig& config = make_junction_config(gcfKlipper, 5.0, 0.0))
{
const std::string limits = "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=" + std::to_string(klipper_cruise_ratio) + "\n";
run_processor(proc, config, (limits + gcode).c_str());
}
double klipper_time(const std::string& gcode, const FullPrintConfig& config = make_junction_config(gcfKlipper, 5.0, 0.0))
{
GCodeProcessor proc;
run_klipper(proc, gcode, config);
return proc.get_time(PrintEstimatedStatistics::ETimeMode::Normal);
}
double time_after_retract(const GCodeProcessorResult& r)
{
double t = 0.0;
bool after = false;
for (const auto& mv : r.moves) {
if (mv.type == EMoveType::Retract)
after = true;
else if (after)
t += mv.time[NORMAL];
}
return t;
}
} // namespace
TEST_CASE("A straight line split into short segments is planned as one move", "[GCodeTiming][Klipper]")
{
// 100 mm at 50 mm/s queues 2 s of motion, so the look-ahead flushes lazily part-way along the
// line; at 10 mm/s^2 the ramps span many segments and the cruise ratio caps the speed.
const int segments = GENERATE(1, 100, 1000);
std::ostringstream gcode;
gcode << "SET_VELOCITY_LIMIT ACCEL=10\n" << std::fixed << std::setprecision(3);
for (int i = 1; i <= segments; ++i)
gcode << "G1 X" << 100.0 * i / segments << " F3000\n";
REQUIRE_THAT(klipper_time(gcode.str()),
WithinRel(rest_to_rest_time(100.0, 50.0, 10.0, 10.0 * (1.0 - klipper_cruise_ratio)), 1e-6));
}
TEST_CASE("Where the look-ahead flushes does not change the estimate", "[GCodeTiming][Klipper]")
{
// A retraction stops the toolhead, so the zig-zag after it is planned on its own; the straight run
// before it only shifts where the queue flushes lazily (1 s of motion after a stop, then every 0.15 s).
auto zigzag_time = [](int run_moves) {
std::string gcode = "M83\nG91\n";
for (int i = 0; i < run_moves; ++i)
gcode += "G1 X1 F6000\n";
gcode += "G1 E-1 F2400\n";
const char* moves[] = {"G1 X2 F3000\n", "G1 Y2 F9000\n", "G1 X2 F1200\n", "G1 Y-2 F9000\n"};
for (int i = 0; i < 24; ++i)
gcode += moves[i % 4];
GCodeProcessor proc;
run_klipper(proc, gcode);
return time_after_retract(proc.get_result());
};
const double reference = zigzag_time(0);
REQUIRE(reference > 0.0);
for (int run_moves : {30, 80, 105, 130}) {
INFO("run moves = " << run_moves);
REQUIRE_THAT(zigzag_time(run_moves), WithinAbs(reference, 1e-9));
}
}
TEST_CASE("A short move into a corner is limited by the centripetal term, not the corner velocity",
"[GCodeTiming][Klipper]")
{
// A 50 mm/s square corner velocity allows 50 mm/s through the right angle; the 1 mm move into it
// allows sqrt(0.5 * 1 * 1000 * tan(45 deg)) = 22.4 mm/s (Move.calc_junction's move_centripetal_v2).
GCodeProcessor proc;
run_klipper(proc, "G90\nG1 X59 Y60 F6000\nM400\nG1 X60 Y60 F9000\nG1 X60 Y100 F9000\n",
make_junction_config(gcfKlipper, 50.0, 0.0));
REQUIRE_THAT(corner_speed(proc.get_result()), WithinRel(std::sqrt(500.0), 1e-6));
}
TEST_CASE("A corner keeps the square corner velocity when the acceleration changes between its moves",
"[GCodeTiming][Klipper]")
{
GCodeProcessor proc;
run_klipper(proc, "G90\nG1 X20 Y60 F6000\nM400\nG1 X60 Y60 F9000\nM204 S4000\nG1 X60 Y100 F9000\n");
REQUIRE_THAT(corner_speed(proc.get_result()), WithinRel(5.0, 1e-6));
}
TEST_CASE("A retraction stops the toolhead and runs at the extruder's own limits", "[GCodeTiming][Klipper]")
{
// An E acceleration of 0 is unset, and the retraction then uses the retract acceleration.
const auto [e_accel, retract_accel] = GENERATE(table<double, double>({{20000.0, 20000.0}, {0.0, 3000.0}}));
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
config.machine_max_acceleration_e.values = {e_accel, e_accel};
config.machine_max_acceleration_retracting.values = {3000.0, 3000.0};
GCodeProcessor proc;
run_klipper(proc, "G90\nM83\nG1 X60 Y60 F9000\nG1 E-1 F2400\nG1 X100 Y60 F9000\n", config);
REQUIRE_THAT(corner_speed(proc.get_result()), WithinAbs(0.0, 1e-6));
const double expected = rest_to_rest_time(std::sqrt(2.0) * 60.0, 150.0, klipper_accel, klipper_pseudo_accel) +
rest_to_rest_time(1.0, 40.0, retract_accel, std::min(retract_accel, klipper_pseudo_accel)) +
rest_to_rest_time(40.0, 150.0, klipper_accel, klipper_pseudo_accel);
REQUIRE_THAT(proc.get_time(PrintEstimatedStatistics::ETimeMode::Normal), WithinRel(expected, 1e-6));
}
TEST_CASE("Extruder limits apply to retractions, not to extrusion along a path", "[GCodeTiming][Klipper]")
{
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
config.machine_max_speed_e.values = {10.0, 10.0};
config.machine_max_acceleration_e.values = {10.0, 10.0};
REQUIRE_THAT(klipper_time("M83\nG1 X40 E20 F30000\n", config),
WithinRel(rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
REQUIRE_THAT(klipper_time("M83\nG1 E-1 F2400\n", config),
WithinRel(rest_to_rest_time(1.0, 10.0, 10.0, 10.0), 1e-6));
}
TEST_CASE("A Z move is limited by the Z axis speed and acceleration", "[GCodeTiming][Klipper]")
{
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
config.machine_max_speed_z.values = {20.0, 20.0};
config.machine_max_acceleration_z.values = {100.0, 100.0};
REQUIRE_THAT(klipper_time("G1 Z10 F1200\n", config),
WithinRel(rest_to_rest_time(10.0, 20.0, 100.0, 100.0), 1e-6));
}
TEST_CASE("M204 sets the acceleration from S, or the smaller of P and T", "[GCodeTiming][Klipper]")
{
const char* m204 = GENERATE("M204 S500", "M204 P500 T2000", "M204 P2000 T500");
REQUIRE_THAT(klipper_time(std::string(m204) + "\nG1 X100 F30000\n"),
WithinRel(rest_to_rest_time(100.0, 500.0, 500.0, 500.0 * (1.0 - klipper_cruise_ratio)), 1e-6));
}
TEST_CASE("SET_VELOCITY_LIMIT applies every limit on the line and ignores the comment", "[GCodeTiming][Klipper]")
{
REQUIRE_THAT(klipper_time("SET_VELOCITY_LIMIT VELOCITY=80 ACCEL=500 MINIMUM_CRUISE_RATIO=0.2 ; ACCEL=9999\n"
"G1 X20 F30000\n"),
WithinRel(rest_to_rest_time(20.0, 80.0, 500.0, 500.0 * (1.0 - 0.2)), 1e-6));
}
TEST_CASE("ACCEL_TO_DECEL is ignored on a line that also sets the minimum cruise ratio", "[GCodeTiming][Klipper]")
{
// Klipper 0.13 prefers the cruise ratio and later releases no longer read ACCEL_TO_DECEL at all.
// ACCEL_TO_DECEL comes last, so applying the parameters in line order would let it win.
REQUIRE_THAT(klipper_time("SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=" + std::to_string(klipper_cruise_ratio) +
" ACCEL_TO_DECEL=100\nG1 X40 F30000\n"),
WithinRel(rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
}
TEST_CASE("M400 without a delay waits for the queue to drain", "[GCodeTiming][Klipper]")
{
REQUIRE_THAT(klipper_time("G90\nG1 X40 F30000\nM400\nG1 X80\n"),
WithinRel(2.0 * rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
}
+36 -1
View File
@@ -514,7 +514,7 @@ TEST_CASE("Acceleration and velocity limit commands print their values in genera
const Case c = GENERATE(values<Case>({
{gcfKlipper, Command::KlipperLimits, 2000000, 25. / 3., false,
{"SET_VELOCITY_LIMIT ACCEL=2000000 ", "ACCEL_TO_DECEL=1e+06 ", "SQUARE_CORNER_VELOCITY=8.33333\n"}, {}},
{gcfKlipper, Command::KlipperLimits, 12345, 0., false, {"ACCEL=12345 ", "ACCEL_TO_DECEL=6172.5\n"}, {"SQUARE_CORNER_VELOCITY"}},
{gcfKlipper, Command::KlipperLimits, 12345, 0., false, {"ACCEL=12345 ", "ACCEL_TO_DECEL=6172.5 ", "MINIMUM_CRUISE_RATIO=0.5\n"}, {"SQUARE_CORNER_VELOCITY"}},
{gcfKlipper, Command::KlipperLimits, 0, 0.25, true, {"SQUARE_CORNER_VELOCITY=0.25 ", "; adjust VELOCITY_LIMIT"}, {"ACCEL"}},
{gcfKlipper, Command::Print, 3001, 0., true, {"ACCEL=3001 ", "ACCEL_TO_DECEL=1500.5 ", "; adjust ACCEL_TO_DECEL", "; adjust acceleration"}, {}},
{gcfMarlinFirmware, Command::Print, 2500, 0., false, {"M204 P2500\n"}, {}},
@@ -1272,6 +1272,41 @@ TEST_CASE("Custom G-code motion limits are restored before generated moves", "[G
REQUIRE(gcode.find("M205 X8 Y8 ; adjust jerk", custom_gcode_pos) != std::string::npos);
}
TEST_CASE("Klipper acceleration commands send accel_to_decel both as ACCEL_TO_DECEL and as the cruise ratio", "[GCodeWriter]")
{
// Klipper 0.13 defines MINIMUM_CRUISE_RATIO = 1 - ACCEL_TO_DECEL / ACCEL; older releases read only the
// first parameter and current ones only the second, so both have to describe the same limit.
GCodeWriter writer;
writer.config.gcode_flavor.value = gcfKlipper;
writer.config.accel_to_decel_enable.value = true;
// The writer appends "; adjust ..." comments by default; keep them out so the line ends at the ratio.
struct CommentGuard
{
bool saved = GCodeWriter::full_gcode_comment;
~CommentGuard() { GCodeWriter::full_gcode_comment = saved; }
} comment_guard;
GCodeWriter::full_gcode_comment = false;
SECTION("a 25% factor") {
writer.config.accel_to_decel_factor.value = 25;
REQUIRE_THAT(writer.set_print_acceleration(2000),
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000 ACCEL_TO_DECEL=500 MINIMUM_CRUISE_RATIO=0.75"));
REQUIRE_THAT(writer.set_accel_and_jerk(3000, 0.0),
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=3000 ACCEL_TO_DECEL=750 MINIMUM_CRUISE_RATIO=0.75"));
}
SECTION("a 100% factor turns the cruise ratio off") {
writer.config.accel_to_decel_factor.value = 100;
REQUIRE_THAT(writer.set_print_acceleration(2000),
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000 ACCEL_TO_DECEL=2000 MINIMUM_CRUISE_RATIO=0\n"));
}
SECTION("neither is sent while the option is off") {
writer.config.accel_to_decel_enable.value = false;
const std::string line = writer.set_print_acceleration(2000);
REQUIRE_THAT(line, Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000"));
REQUIRE_THAT(line, !Catch::Matchers::ContainsSubstring("ACCEL_TO_DECEL") && !Catch::Matchers::ContainsSubstring("MINIMUM_CRUISE_RATIO"));
}
}
// Regression test for the belt-printer "illegal gantry move at print start" bug.
//
// On a belt printer the layer-change z-hop is deferred (lazy_lift) and consumed
+17
View File
@@ -202,6 +202,23 @@ TEST_CASE("The wipe tower's toolchange planner flush follows the gcode flavor",
}
}
TEST_CASE("The wipe tower sets Klipper's acceleration and accel_to_decel on one line", "[WipeTower]")
{
// On separate lines, current Klipper finds nothing it reads on the ACCEL_TO_DECEL one and 0.13
// derives the cruise ratio from the acceleration before the change.
DynamicPrintConfig config = wipe_tower_toolchange_config("klipper");
// Type 2 towers leave the acceleration to the main G-code writer.
config.set_deserialize_strict({ { "wipe_tower_type", "type1" },
{ "accel_to_decel_enable", "1" }, { "accel_to_decel_factor", "50%" },
{ "default_acceleration", "1000" }, { "travel_acceleration", "2000" },
{ "machine_max_acceleration_extruding", "5000" } });
const std::string tower = wipe_tower_regions(slice_with_prime_tower(config));
REQUIRE_THAT(tower, Catch::Matchers::ContainsSubstring("SET_VELOCITY_LIMIT ACCEL="));
CHECK_THAT(tower, Catch::Matchers::ContainsSubstring(" MINIMUM_CRUISE_RATIO=0.5\n"));
CHECK_THAT(tower, !Catch::Matchers::ContainsSubstring("SET_VELOCITY_LIMIT ACCEL_TO_DECEL") &&
!Catch::Matchers::ContainsSubstring("M204"));
}
// What Print feeds the shared estimate. The libslic3r WipeTowerEstimate cases cannot see this:
// they call the estimator directly. The estimate counts the filaments the print really uses,
// so the two-filament shape gives the outer wall the second one.