From 4e1caa39eb6d7e6ac7353cc7238d505b003cda42 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 5 Aug 2026 01:21:32 +0800 Subject: [PATCH] Flush the wipe tower planner queue with M400 on Klipper The wipe tower emitted G4 S0 to make the firmware finish its queued moves before commands that must not take effect early. Klipper's G4 reads only the P parameter, so that flush never happened there and a temperature change could land seconds ahead of the moves it was meant to follow. Klipper now gets M400 instead, through one helper shared by both wipe tower implementations. No change to any other firmware flavor's output, so no shipped profile or saved project is affected. --- src/libslic3r/GCode/WipeTower.cpp | 7 ++++++- src/libslic3r/GCode/WipeTower.hpp | 8 ++++++++ src/libslic3r/GCode/WipeTower2.cpp | 9 +++++---- tests/fff_print/CMakeLists.txt | 1 + tests/fff_print/test_wipe_tower.cpp | 27 +++++++++++++++++++++++++++ 5 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 tests/fff_print/test_wipe_tower.cpp diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index 3d7353eadf..cfa407ea73 100644 --- a/src/libslic3r/GCode/WipeTower.cpp +++ b/src/libslic3r/GCode/WipeTower.cpp @@ -617,6 +617,11 @@ 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"; +} + class WipeTowerWriter { public: @@ -1190,7 +1195,7 @@ public: WipeTowerWriter& flush_planner_queue() { - m_gcode += "G4 S0\n"; + m_gcode += flush_planner_queue_command(m_gcode_flavor); return *this; } diff --git a/src/libslic3r/GCode/WipeTower.hpp b/src/libslic3r/GCode/WipeTower.hpp index 66e8acf1c4..a083da1fb2 100644 --- a/src/libslic3r/GCode/WipeTower.hpp +++ b/src/libslic3r/GCode/WipeTower.hpp @@ -26,6 +26,14 @@ enum GCodeFlavor : unsigned char; Polylines construct_gap_for_skip_points( const Polygon& polygon, const std::vector& skip_points, float wt_width, float gap_length, Polygon& insert_skip_polygon); +// Returns the command that makes the firmware finish its queued moves, so a command or +// custom-G-code boundary right after (resetting the extruder position, entering +// [change_filament_gcode] / [filament_start_gcode]) is not reached early. Klipper acts on +// such commands the moment it parses them, and its G4 reads only P, so the zero dwell the +// other flavors use synchronizes nothing there — M400 does. Defined in WipeTower.cpp, shared +// by WipeTower and WipeTower2. +const char* flush_planner_queue_command(GCodeFlavor flavor); + class WipeTower { public: diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index 51ab155dd9..5dc78d4785 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -386,7 +386,8 @@ public: } WipeTowerWriter2& switch_filament_monitoring(bool enable) { - m_gcode += std::string("G4 S0\n") + "M591 " + (enable ? "R" : "S0") + "\n"; + m_gcode += flush_planner_queue_command(m_gcode_flavor); + m_gcode += std::string("M591 ") + (enable ? "R" : "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 + m_gcode += flush_planner_queue_command(m_gcode_flavor); m_gcode += "M" + std::to_string(wait ? 109 : 104) + " S" + std::to_string(temperature) + "\n"; 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; } diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index 70ab639faa..08f86de8a7 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -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") diff --git a/tests/fff_print/test_wipe_tower.cpp b/tests/fff_print/test_wipe_tower.cpp new file mode 100644 index 0000000000..de5836ba7a --- /dev/null +++ b/tests/fff_print/test_wipe_tower.cpp @@ -0,0 +1,27 @@ +#include + +#include + +#include "libslic3r/GCode/WipeTower.hpp" +#include "libslic3r/PrintConfig.hpp" + +using namespace Slic3r; + +// The wipe tower flushes the firmware's motion queue before a command or custom-G-code +// boundary that must not be reached early (an extruder-position reset, entering custom +// G-code). Klipper acts on those the moment it parses them, and its G4 reads only P, so the +// zero dwell every other flavor uses is not a flush there. +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(gcfMarlinLegacy, gcfRepRapFirmware, gcfRepetier, + gcfMarlinFirmware, gcfRepRapSprinter, gcfTeacup, + gcfMakerWare, gcfSailfish, gcfMach3, gcfMachinekit, + gcfSmoothie, gcfNoExtrusion); + INFO("gcode flavor enum value: " << int(flavor)); + CHECK(std::string(flush_planner_queue_command(flavor)) == "G4 S0\n"); +}