mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-05 17:17:42 +00:00
Make the wipe tower's planner flush and dwell work on Klipper (#15133)
# Description On Klipper the wipe tower's motion-queue synchronization silently did nothing. Klipper acts on commands the moment it parses them, and its `G4` reads only `P` in milliseconds — it ignores `S` — so the `G4 S0` the tower used to flush the queue before a temperature change never synchronized anything, and the cooling delay after a filament's cooling moves passed instantly instead of waiting. The tower now emits `M400` for the flush and `G4 P<ms>` for the dwell when the flavor is Klipper. Only `gcode_flavor = klipper` is affected; G-code for every other flavor is byte-identical, so no shipped profile or existing project file changes. # Screenshots/Recordings/Graphs <!-- > Please attach relevant screenshots to showcase the UI changes. > Please attach images that can help explain the changes. --> ## Tests <!-- > Please describe the tests that you have conducted to verify the changes made in this PR. --> <!-- > A guide for users on how to download the artifacts from this PR. --> [How to Download Pull Requests Artifacts for Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
This commit is contained in:
@@ -617,6 +617,18 @@ Polygon generate_rectange_polygon(const Vec2f &wt_box_min ,const Vec2f & wt_box_
|
||||
return res;
|
||||
}
|
||||
|
||||
const char* flush_planner_queue_command(GCodeFlavor flavor)
|
||||
{
|
||||
return flavor == gcfKlipper ? "M400\n" : "G4 S0\n";
|
||||
}
|
||||
|
||||
std::string wait_command(GCodeFlavor flavor, float seconds)
|
||||
{
|
||||
if (flavor == gcfKlipper)
|
||||
return "G4 P" + std::to_string(std::lround(seconds * 1000.f)) + "\n";
|
||||
return "G4 S" + Slic3r::float_to_string_decimal_point(seconds, 3) + "\n";
|
||||
}
|
||||
|
||||
class WipeTowerWriter
|
||||
{
|
||||
public:
|
||||
@@ -1145,7 +1157,7 @@ public:
|
||||
{
|
||||
if (time==0.f)
|
||||
return *this;
|
||||
m_gcode += "G4 S" + Slic3r::float_to_string_decimal_point(time, 3) + "\n";
|
||||
m_gcode += wait_command(m_gcode_flavor, time);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -1190,7 +1202,7 @@ public:
|
||||
|
||||
WipeTowerWriter& flush_planner_queue()
|
||||
{
|
||||
m_gcode += "G4 S0\n";
|
||||
m_gcode += flush_planner_queue_command(m_gcode_flavor);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -1333,6 +1345,8 @@ public:
|
||||
{
|
||||
std::string buffer;
|
||||
if (wait_for_moves)
|
||||
// Not flush_planner_queue_command(): this BBL precool path wants M400, which every
|
||||
// flavor it reaches understands, not the zero dwell the other flavors flush with.
|
||||
buffer += "M400\n";
|
||||
buffer += "M104";
|
||||
if (target_extruder != -1)
|
||||
|
||||
@@ -26,6 +26,12 @@ enum GCodeFlavor : unsigned char;
|
||||
Polylines construct_gap_for_skip_points(
|
||||
const Polygon& polygon, const std::vector<Vec2f>& skip_points, float wt_width, float gap_length, Polygon& insert_skip_polygon);
|
||||
|
||||
// Klipper acts on commands the instant it parses them, and its G4 reads only P (milliseconds),
|
||||
// so the zero-second and seconds-valued dwells every other flavor uses neither synchronize nor
|
||||
// pause there. Both defined in WipeTower.cpp, shared by WipeTower and WipeTower2.
|
||||
const char* flush_planner_queue_command(GCodeFlavor flavor); // finish queued moves, e.g. around M104/M109
|
||||
std::string wait_command(GCodeFlavor flavor, float seconds); // pause for `seconds`
|
||||
|
||||
class WipeTower
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -386,7 +386,8 @@ public:
|
||||
}
|
||||
|
||||
WipeTowerWriter2& switch_filament_monitoring(bool enable) {
|
||||
m_gcode += std::string("G4 S0\n") + "M591 " + (enable ? "R" : "S0") + "\n";
|
||||
flush_planner_queue();
|
||||
m_gcode += enable ? "M591 R\n" : "M591 S0\n";
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -625,7 +626,7 @@ public:
|
||||
// Set extruder temperature, don't wait by default.
|
||||
WipeTowerWriter2& set_extruder_temp(int temperature, bool wait = false)
|
||||
{
|
||||
m_gcode += "G4 S0\n"; // to flush planner queue
|
||||
flush_planner_queue();
|
||||
m_gcode += "M" + std::to_string(wait ? 109 : 104) + " S" + std::to_string(temperature) + "\n";
|
||||
return *this;
|
||||
}
|
||||
@@ -635,7 +636,7 @@ public:
|
||||
{
|
||||
if (time==0.f)
|
||||
return *this;
|
||||
m_gcode += "G4 S" + Slic3r::float_to_string_decimal_point(time, 3) + "\n";
|
||||
m_gcode += wait_command(m_gcode_flavor, time);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -677,8 +678,8 @@ public:
|
||||
}
|
||||
|
||||
WipeTowerWriter2& flush_planner_queue()
|
||||
{
|
||||
m_gcode += "G4 S0\n";
|
||||
{
|
||||
m_gcode += flush_planner_queue_command(m_gcode_flavor);
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_slicing_pipeline_hook.cpp
|
||||
test_support_material.cpp
|
||||
test_trianglemesh.cpp
|
||||
test_wipe_tower.cpp
|
||||
)
|
||||
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r Catch2::Catch2WithMain)
|
||||
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
|
||||
|
||||
123
tests/fff_print/test_wipe_tower.cpp
Normal file
123
tests/fff_print/test_wipe_tower.cpp
Normal file
@@ -0,0 +1,123 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/WipeTower.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::Test;
|
||||
|
||||
// Taken from the config enum map rather than hand-listed, so a flavor added to GCodeFlavor later
|
||||
// is covered here without editing this file.
|
||||
static std::vector<GCodeFlavor> non_klipper_flavors()
|
||||
{
|
||||
std::vector<GCodeFlavor> flavors;
|
||||
for (const auto &[name, value] : ConfigOptionEnum<GCodeFlavor>::get_enum_values())
|
||||
if (GCodeFlavor(value) != gcfKlipper)
|
||||
flavors.push_back(GCodeFlavor(value));
|
||||
return flavors;
|
||||
}
|
||||
|
||||
static std::string flavor_name(GCodeFlavor flavor)
|
||||
{
|
||||
return ConfigOptionEnum<GCodeFlavor>::get_enum_names()[int(flavor)];
|
||||
}
|
||||
|
||||
TEST_CASE("Klipper flushes the wipe tower planner queue with M400", "[WipeTower]")
|
||||
{
|
||||
CHECK(std::string(flush_planner_queue_command(gcfKlipper)) == "M400\n");
|
||||
}
|
||||
|
||||
TEST_CASE("Other flavors flush the wipe tower planner queue with a zero dwell", "[WipeTower]")
|
||||
{
|
||||
const GCodeFlavor flavor = GENERATE(from_range(non_klipper_flavors()));
|
||||
INFO("gcode flavor: " << flavor_name(flavor));
|
||||
CHECK(std::string(flush_planner_queue_command(flavor)) == "G4 S0\n");
|
||||
}
|
||||
|
||||
// 1.5s is exactly representable as a float, so neither form can drift when rounded.
|
||||
TEST_CASE("Klipper waits in the wipe tower with a millisecond dwell", "[WipeTower]")
|
||||
{
|
||||
CHECK(wait_command(gcfKlipper, 1.5f) == "G4 P1500\n");
|
||||
}
|
||||
|
||||
TEST_CASE("Other flavors wait in the wipe tower with a seconds dwell", "[WipeTower]")
|
||||
{
|
||||
const GCodeFlavor flavor = GENERATE(from_range(non_klipper_flavors()));
|
||||
INFO("gcode flavor: " << flavor_name(flavor));
|
||||
CHECK(wait_command(flavor, 1.5f) == "G4 S1.500\n");
|
||||
}
|
||||
|
||||
// The cases above only exercise the helpers in isolation. The one below slices a real
|
||||
// two-filament print, so it also covers the binding constraint of both changes: that the
|
||||
// configured `gcode_flavor` reaches the wipe tower writer and lands in the exported G-code.
|
||||
|
||||
// The G-code inside each WIPE_TOWER_START/WIPE_TOWER_END pair, concatenated, so an M400 emitted
|
||||
// outside the tower (e.g. GCodeProcessor's pre-heat injector) cannot create a false match.
|
||||
static std::string wipe_tower_regions(const std::string &gcode)
|
||||
{
|
||||
const std::string &start_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_Start);
|
||||
const std::string &end_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_End);
|
||||
std::string regions;
|
||||
size_t pos = 0;
|
||||
while (true) {
|
||||
size_t start = gcode.find(start_tag, pos);
|
||||
if (start == std::string::npos)
|
||||
break;
|
||||
size_t end = gcode.find(end_tag, start);
|
||||
if (end == std::string::npos)
|
||||
break;
|
||||
regions.append(gcode, start, end - start);
|
||||
pos = end + 1;
|
||||
}
|
||||
return regions;
|
||||
}
|
||||
|
||||
// A per-layer toolchange between the wall and infill filaments, same shape as
|
||||
// test_multifilament.cpp's "Each feature prints with its assigned filament", so the wipe tower
|
||||
// runs its toolchange path (and so `flush_planner_queue()`) on every layer.
|
||||
static DynamicPrintConfig wipe_tower_toolchange_config(const std::string &gcode_flavor)
|
||||
{
|
||||
return multifilament_config(2, {
|
||||
{ "sparse_infill_filament_id", 1 },
|
||||
{ "internal_solid_filament_id", 1 },
|
||||
{ "top_surface_filament_id", 1 },
|
||||
{ "bottom_surface_filament_id", 1 },
|
||||
{ "outer_wall_filament_id", 2 },
|
||||
{ "inner_wall_filament_id", 2 },
|
||||
{ "enable_prime_tower", true },
|
||||
{ "layer_height", 0.3 },
|
||||
{ "gcode_flavor", gcode_flavor },
|
||||
});
|
||||
}
|
||||
|
||||
// Slices a 10mm cube under `config`. Not plain Test::slice: a brand-new Print's first `apply()`
|
||||
// counts one filament in use, and DynamicPrintConfig::normalize_fdm_2's single-filament rule then
|
||||
// clears `enable_prime_tower`. A second apply, once init_print's regions have settled, sees both
|
||||
// filaments and the tower survives.
|
||||
static std::string slice_with_prime_tower(const DynamicPrintConfig &config)
|
||||
{
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({ cube(10) }, print, model, config);
|
||||
print.apply(model, config);
|
||||
return gcode(print);
|
||||
}
|
||||
|
||||
TEST_CASE("The wipe tower's toolchange planner flush follows the gcode flavor", "[WipeTower]")
|
||||
{
|
||||
auto [flavor, expected, unexpected] = GENERATE(table<std::string, std::string, std::string>({
|
||||
{ "klipper", "M400", "G4 S0" },
|
||||
{ "marlin", "G4 S0", "M400" } }));
|
||||
DYNAMIC_SECTION(flavor) {
|
||||
const std::string tower = wipe_tower_regions(slice_with_prime_tower(wipe_tower_toolchange_config(flavor)));
|
||||
REQUIRE_FALSE(tower.empty());
|
||||
CHECK_THAT(tower, Catch::Matchers::ContainsSubstring(expected));
|
||||
CHECK_THAT(tower, !Catch::Matchers::ContainsSubstring(unexpected));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user