mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-19 21:51:06 +00:00
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:
12 files changed
+602
-133
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user