🐛 Fix Tool Change priming (#21142)

This commit is contained in:
Robert Brenckman 2022-04-10 01:24:07 -04:00 committed by Scott Lahteine
parent 2086cc9f4e
commit b2b5b85045
7 changed files with 199 additions and 112 deletions

View file

@ -2442,12 +2442,16 @@
#define TOOLCHANGE_FS_FAN_SPEED 255 // 0-255
#define TOOLCHANGE_FS_FAN_TIME 10 // (seconds)
// Swap uninitialized extruder (using TOOLCHANGE_FS_PRIME_SPEED feedrate)
// (May break filament if not retracted beforehand.)
//#define TOOLCHANGE_FS_INIT_BEFORE_SWAP
// Use TOOLCHANGE_FS_PRIME_SPEED feedrate the first time each extruder is primed
//#define TOOLCHANGE_FS_SLOW_FIRST_PRIME
// Prime on the first T0 (For other tools use TOOLCHANGE_FS_INIT_BEFORE_SWAP)
// Enable with M217 V1 before printing to avoid unwanted priming on host connect
/**
* Prime T0 the first time T0 is sent to the printer:
* [ Power-On -> T0 { Activate & Prime T0 } -> T1 { Retract T0, Activate & Prime T1 } ]
* If disabled, no priming on T0 until switching back to T0 from another extruder:
* [ Power-On -> T0 { T0 Activated } -> T1 { Activate & Prime T1 } -> T0 { Retract T1, Activate & Prime T0 } ]
* Enable with M217 V1 before printing to avoid unwanted priming on host connect.
*/
//#define TOOLCHANGE_FS_PRIME_FIRST_USED
/**

View file

@ -34,28 +34,28 @@
#include "../../MarlinCore.h" // for SP_X_STR, etc.
/**
* M217 - Set SINGLENOZZLE toolchange parameters
* M217 - Set toolchange parameters
*
* // Tool change command
* Q Prime active tool and exit
*
* // Tool change settings
* S[linear] Swap length
* B[linear] Extra Swap length
* E[linear] Prime length
* P[linear/m] Prime speed
* R[linear/m] Retract speed
* U[linear/m] UnRetract speed
* V[linear] 0/1 Enable auto prime first extruder used
* W[linear] 0/1 Enable park & Z Raise
* X[linear] Park X (Requires TOOLCHANGE_PARK)
* Y[linear] Park Y (Requires TOOLCHANGE_PARK)
* I[linear] Park I (Requires TOOLCHANGE_PARK and LINEAR_AXES >= 4)
* J[linear] Park J (Requires TOOLCHANGE_PARK and LINEAR_AXES >= 5)
* K[linear] Park K (Requires TOOLCHANGE_PARK and LINEAR_AXES >= 6)
* Z[linear] Z Raise
* F[linear] Fan Speed 0-255
* G[linear/s] Fan time
* S[linear] Swap length
* B[linear] Extra Swap resume length
* E[linear] Extra Prime length (as used by M217 Q)
* P[linear/min] Prime speed
* R[linear/min] Retract speed
* U[linear/min] UnRetract speed
* V[linear] 0/1 Enable auto prime first extruder used
* W[linear] 0/1 Enable park & Z Raise
* X[linear] Park X (Requires TOOLCHANGE_PARK)
* Y[linear] Park Y (Requires TOOLCHANGE_PARK)
* I[linear] Park I (Requires TOOLCHANGE_PARK and NUM_AXES >= 4)
* J[linear] Park J (Requires TOOLCHANGE_PARK and NUM_AXES >= 5)
* K[linear] Park K (Requires TOOLCHANGE_PARK and NUM_AXES >= 6)
* Z[linear] Z Raise
* F[speed] Fan Speed 0-255
* D[seconds] Fan time
*
* Tool migration settings
* A[0|1] Enable auto-migration on runout
@ -79,8 +79,8 @@ void GcodeSuite::M217() {
if (parser.seenval('R')) { const int16_t v = parser.value_linear_units(); toolchange_settings.retract_speed = constrain(v, 10, 5400); }
if (parser.seenval('U')) { const int16_t v = parser.value_linear_units(); toolchange_settings.unretract_speed = constrain(v, 10, 5400); }
#if TOOLCHANGE_FS_FAN >= 0 && HAS_FAN
if (parser.seenval('F')) { const int16_t v = parser.value_linear_units(); toolchange_settings.fan_speed = constrain(v, 0, 255); }
if (parser.seenval('G')) { const int16_t v = parser.value_linear_units(); toolchange_settings.fan_time = constrain(v, 1, 30); }
if (parser.seenval('F')) { const uint16_t v = parser.value_ushort(); toolchange_settings.fan_speed = constrain(v, 0, 255); }
if (parser.seenval('D')) { const uint16_t v = parser.value_ushort(); toolchange_settings.fan_time = constrain(v, 1, 30); }
#endif
#endif
@ -159,7 +159,7 @@ void GcodeSuite::M217_report(const bool forReplay/*=true*/) {
SERIAL_ECHOPGM(" R", LINEAR_UNIT(toolchange_settings.retract_speed),
" U", LINEAR_UNIT(toolchange_settings.unretract_speed),
" F", toolchange_settings.fan_speed,
" G", toolchange_settings.fan_time);
" D", toolchange_settings.fan_time);
#if ENABLED(TOOLCHANGE_MIGRATION_FEATURE)
SERIAL_ECHOPGM(" A", migration.automode);

View file

@ -390,6 +390,8 @@
#error "ENDSTOP_NOISE_FILTER is now ENDSTOP_NOISE_THRESHOLD [2-7]."
#elif defined(RETRACT_ZLIFT)
#error "RETRACT_ZLIFT is now RETRACT_ZRAISE."
#elif defined(TOOLCHANGE_FS_INIT_BEFORE_SWAP)
#error "TOOLCHANGE_FS_INIT_BEFORE_SWAP is now TOOLCHANGE_FS_SLOW_FIRST_PRIME."
#elif defined(TOOLCHANGE_PARK_ZLIFT) || defined(TOOLCHANGE_UNPARK_ZLIFT)
#error "TOOLCHANGE_PARK_ZLIFT and TOOLCHANGE_UNPARK_ZLIFT are now TOOLCHANGE_ZRAISE."
#elif defined(SINGLENOZZLE_TOOLCHANGE_ZRAISE)

View file

@ -124,8 +124,8 @@ void menu_advanced_settings();
EDIT_ITEM_FAST(int4, MSG_SINGLENOZZLE_UNRETRACT_SPEED, &toolchange_settings.unretract_speed, 10, 5400);
EDIT_ITEM(float3, MSG_FILAMENT_PURGE_LENGTH, &toolchange_settings.extra_prime, 0, max_extrude);
EDIT_ITEM_FAST(int4, MSG_SINGLENOZZLE_PRIME_SPEED, &toolchange_settings.prime_speed, 10, 5400);
EDIT_ITEM_FAST(int4, MSG_SINGLENOZZLE_FAN_SPEED, &toolchange_settings.fan_speed, 0, 255);
EDIT_ITEM_FAST(int4, MSG_SINGLENOZZLE_FAN_TIME, &toolchange_settings.fan_time, 1, 30);
EDIT_ITEM_FAST(uint8, MSG_SINGLENOZZLE_FAN_SPEED, &toolchange_settings.fan_speed, 0, 255);
EDIT_ITEM_FAST(uint8, MSG_SINGLENOZZLE_FAN_TIME, &toolchange_settings.fan_time, 1, 30);
#endif
EDIT_ITEM(float3, MSG_TOOL_CHANGE_ZLIFT, &toolchange_settings.z_raise, 0, 10);
END_MENU();

View file

@ -32,6 +32,7 @@
#include "../MarlinCore.h"
//#define DEBUG_TOOL_CHANGE
//#define DEBUG_TOOLCHANGE_FILAMENT_SWAP
#define DEBUG_OUT ENABLED(DEBUG_TOOL_CHANGE)
#include "../core/debug_out.h"
@ -42,7 +43,6 @@
#if ENABLED(TOOLCHANGE_MIGRATION_FEATURE)
migration_settings_t migration = migration_defaults;
bool enable_first_prime;
#endif
#if ENABLED(TOOLCHANGE_FS_INIT_BEFORE_SWAP)
@ -150,6 +150,7 @@
#endif // SWITCHING_NOZZLE
// Move to position routines
void _line_to_current(const AxisEnum fr_axis, const float fscale=1) {
line_to_current_position(planner.settings.max_feedrate_mm_s[fr_axis] * fscale);
}
@ -899,10 +900,135 @@ void fast_line_to_current(const AxisEnum fr_axis) { _line_to_current(fr_axis, 0.
*/
#if ENABLED(TOOLCHANGE_FILAMENT_SWAP)
#ifdef DEBUG_TOOLCHANGE_FILAMENT_SWAP
#define FS_DEBUG(V...) SERIAL_ECHOLNPGM("DEBUG: " V)
#else
#define FS_DEBUG(...) NOOP
#endif
// Define any variables required
static Flags<EXTRUDERS> extruder_was_primed; // Extruders primed status
#if ENABLED(TOOLCHANGE_FS_PRIME_FIRST_USED)
bool enable_first_prime; // As set by M217 V
#endif
// Cool down with fan
inline void filament_swap_cooling() {
#if HAS_FAN && TOOLCHANGE_FS_FAN >= 0
thermalManager.fan_speed[TOOLCHANGE_FS_FAN] = toolchange_settings.fan_speed;
gcode.dwell(SEC_TO_MS(toolchange_settings.fan_time));
thermalManager.fan_speed[TOOLCHANGE_FS_FAN] = 0;
#endif
}
/**
* Check if too cold to move the specified tool
*
* Returns TRUE if too cold to move (also echos message: STR_ERR_HOTEND_TOO_COLD)
* Returns FALSE if able to move.
*/
bool too_cold(uint8_t toolID){
if (TERN0(PREVENT_COLD_EXTRUSION, !DEBUGGING(DRYRUN) && thermalManager.targetTooColdToExtrude(toolID))) {
SERIAL_ECHO_MSG(STR_ERR_HOTEND_TOO_COLD);
return true;
}
return false;
}
/**
* Cutting recovery -- Recover from cutting retraction that occurs at the end of nozzle priming
*
* If the active_extruder is up to temp (!too_cold):
* Extrude filament distance = toolchange_settings.extra_resume + TOOLCHANGE_FS_WIPE_RETRACT
* current_position.e = e;
* sync_plan_position_e();
*/
void extruder_cutting_recover(const_float_t e) {
if (!too_cold(active_extruder)) {
const float dist = toolchange_settings.extra_resume + (TOOLCHANGE_FS_WIPE_RETRACT);
FS_DEBUG("Performing Cutting Recover | Distance: ", dist, " | Speed: ", MMM_TO_MMS(toolchange_settings.unretract_speed), "mm/s");
unscaled_e_move(dist, MMM_TO_MMS(toolchange_settings.unretract_speed));
planner.synchronize();
FS_DEBUG("Set position to: ", e);
current_position.e = e;
sync_plan_position_e(); // Resume new E Position
}
}
/**
* Prime the currently selected extruder (Filament loading only)
*
* If too_cold(toolID) returns TRUE -> returns without moving extruder.
* Extruders filament = swap_length + extra prime, then performs cutting retraction if enabled.
* If cooling fan is enabled, calls filament_swap_cooling();
*/
void extruder_prime() {
if (too_cold(active_extruder)) {
FS_DEBUG("Priming Aborted - Nozzle Too Cold!");
return; // Extruder too cold to prime
}
float fr = toolchange_settings.unretract_speed; // Set default speed for unretract
#if ENABLED(TOOLCHANGE_FS_SLOW_FIRST_PRIME)
/*
* Perform first unretract movement at the slower Prime_Speed to avoid breakage on first prime
*/
static Flags<EXTRUDERS> extruder_did_first_prime; // Extruders first priming status
if (!extruder_did_first_prime[active_extruder]) {
extruder_did_first_prime.set(active_extruder); // Log first prime complete
// new nozzle - prime at user-specified speed.
FS_DEBUG("First time priming T", active_extruder, ", reducing speed from ", MMM_TO_MMS(fr), " to ", MMM_TO_MMS(toolchange_settings.prime_speed), "mm/s");
fr = toolchange_settings.prime_speed;
unscaled_e_move(0, MMM_TO_MMS(fr)); // Init planner with 0 length move
}
#endif
//Calculate and perform the priming distance
if (toolchange_settings.extra_prime >= 0) {
// Positive extra_prime value
// - Return filament at speed (fr) then extra_prime at prime speed
FS_DEBUG("Loading Filament for T", active_extruder, " | Distance: ", toolchange_settings.swap_length, " | Speed: ", MMM_TO_MMS(fr), "mm/s");
unscaled_e_move(toolchange_settings.swap_length, MMM_TO_MMS(fr)); // Prime (Unretract) filament by extruding equal to Swap Length (Unretract)
if (toolchange_settings.extra_prime > 0) {
FS_DEBUG("Performing Extra Priming for T", active_extruder, " | Distance: ", toolchange_settings.extra_prime, " | Speed: ", MMM_TO_MMS(toolchange_settings.prime_speed), "mm/s");
unscaled_e_move(toolchange_settings.extra_prime, MMM_TO_MMS(toolchange_settings.prime_speed)); // Extra Prime Distance
}
}
else {
// Negative extra_prime value
// - Unretract distance (swap length) is reduced by the value of extra_prime
const float eswap = toolchange_settings.swap_length + toolchange_settings.extra_prime;
FS_DEBUG("Negative ExtraPrime value - Swap Return Length has been reduced from ", toolchange_settings.swap_length, " to ", eswap);
FS_DEBUG("Loading Filament for T", active_extruder, " | Distance: ", eswap, " | Speed: ", MMM_TO_MMS(fr), "mm/s");
unscaled_e_move(eswap, MMM_TO_MMS(fr));
}
extruder_was_primed.set(active_extruder); // Log that this extruder has been primed
// Cutting retraction
#if TOOLCHANGE_FS_WIPE_RETRACT
FS_DEBUG("Performing Cutting Retraction | Distance: ", -(TOOLCHANGE_FS_WIPE_RETRACT), " | Speed: ", MMM_TO_MMS(toolchange_settings.retract_speed), "mm/s");
unscaled_e_move(-(TOOLCHANGE_FS_WIPE_RETRACT), MMM_TO_MMS(toolchange_settings.retract_speed));
#endif
// Cool down with fan
filament_swap_cooling();
}
/**
* Sequence to Prime the currently selected extruder
* Raise Z, move the ToolChange_Park if enabled, prime the extruder, move back.
*/
void tool_change_prime() {
if (toolchange_settings.extra_prime > 0
&& TERN(PREVENT_COLD_EXTRUSION, !thermalManager.targetTooColdToExtrude(active_extruder), 1)
) {
FS_DEBUG(">>> tool_change_prime()");
if (!too_cold(active_extruder)) {
destination = current_position; // Remember the old position
const bool ok = TERN1(TOOLCHANGE_PARK, all_axes_homed() && toolchange_settings.enable_park);
@ -931,20 +1057,7 @@ void fast_line_to_current(const AxisEnum fr_axis) { _line_to_current(fr_axis, 0.
}
#endif
// Prime (All distances are added and slowed down to ensure secure priming in all circumstances)
unscaled_e_move(toolchange_settings.swap_length + toolchange_settings.extra_prime, MMM_TO_MMS(toolchange_settings.prime_speed));
// Cutting retraction
#if TOOLCHANGE_FS_WIPE_RETRACT
unscaled_e_move(-(TOOLCHANGE_FS_WIPE_RETRACT), MMM_TO_MMS(toolchange_settings.retract_speed));
#endif
// Cool down with fan
#if HAS_FAN && TOOLCHANGE_FS_FAN >= 0
thermalManager.fan_speed[TOOLCHANGE_FS_FAN] = toolchange_settings.fan_speed;
gcode.dwell(SEC_TO_MS(toolchange_settings.fan_time));
thermalManager.fan_speed[TOOLCHANGE_FS_FAN] = 0;
#endif
extruder_prime();
// Move back
#if ENABLED(TOOLCHANGE_PARK)
@ -958,13 +1071,11 @@ void fast_line_to_current(const AxisEnum fr_axis) { _line_to_current(fr_axis, 0.
}
#endif
// Cutting recover
unscaled_e_move(toolchange_settings.extra_resume + TOOLCHANGE_FS_WIPE_RETRACT, MMM_TO_MMS(toolchange_settings.unretract_speed));
// Resume at the old E position
current_position.e = destination.e;
sync_plan_position_e();
extruder_cutting_recover(destination.e); // Cutting recover
}
FS_DEBUG("<<< tool_change_prime");
}
#endif // TOOLCHANGE_FILAMENT_SWAP
@ -1041,12 +1152,10 @@ void tool_change(const uint8_t new_tool, bool no_move/*=false*/) {
TEMPORARY_BED_LEVELING_STATE(false);
#endif
// First tool priming. To prime again, reboot the machine.
// First tool priming. To prime again, reboot the machine. -- Should only occur for first T0 after powerup!
#if ENABLED(TOOLCHANGE_FS_PRIME_FIRST_USED)
static bool first_tool_is_primed = false;
if (new_tool == old_tool && !first_tool_is_primed && enable_first_prime) {
if (enable_first_prime && old_tool == 0 && new_tool == 0 && !extruder_was_primed[0]) {
tool_change_prime();
first_tool_is_primed = true;
TERN_(TOOLCHANGE_FS_INIT_BEFORE_SWAP, toolchange_extruder_ready.set(old_tool)); // Primed and initialized
}
#endif
@ -1072,20 +1181,17 @@ void tool_change(const uint8_t new_tool, bool no_move/*=false*/) {
// Unload / Retract
#if ENABLED(TOOLCHANGE_FILAMENT_SWAP)
const bool should_swap = can_move_away && toolchange_settings.swap_length,
too_cold = TERN0(PREVENT_COLD_EXTRUSION,
!DEBUGGING(DRYRUN) && (thermalManager.targetTooColdToExtrude(old_tool) || thermalManager.targetTooColdToExtrude(new_tool))
);
const bool should_swap = can_move_away && toolchange_settings.swap_length;
if (should_swap) {
if (too_cold) {
SERIAL_ECHO_MSG(STR_ERR_HOTEND_TOO_COLD);
if (too_cold(old_tool)) {
// If SingleNozzle setup is too cold, unable to perform tool_change.
if (ENABLED(SINGLENOZZLE)) { active_extruder = new_tool; return; }
}
else {
// For first new tool, change without unloading the old. 'Just prime/init the new'
if (TERN1(TOOLCHANGE_FS_PRIME_FIRST_USED, first_tool_is_primed))
unscaled_e_move(-toolchange_settings.swap_length, MMM_TO_MMS(toolchange_settings.retract_speed));
TERN_(TOOLCHANGE_FS_PRIME_FIRST_USED, first_tool_is_primed = true); // The first new tool will be primed by toolchanging
else if (extruder_was_primed[old_tool]) {
// Retract the old extruder if it was previously primed
// To-Do: Should SingleNozzle always retract?
FS_DEBUG("Retracting Filament for T", old_tool, ". | Distance: ", toolchange_settings.swap_length, " | Speed: ", MMM_TO_MMS(toolchange_settings.retract_speed), "mm/s");
unscaled_e_move(-toolchange_settings.swap_length, MMM_TO_MMS(toolchange_settings.retract_speed));
}
}
#endif
@ -1190,36 +1296,8 @@ void tool_change(const uint8_t new_tool, bool no_move/*=false*/) {
#endif
#if ENABLED(TOOLCHANGE_FILAMENT_SWAP)
if (should_swap && !too_cold) {
float fr = toolchange_settings.unretract_speed;
#if ENABLED(TOOLCHANGE_FS_INIT_BEFORE_SWAP)
if (!toolchange_extruder_ready[new_tool]) {
toolchange_extruder_ready.set(new_tool);
fr = toolchange_settings.prime_speed; // Next move is a prime
unscaled_e_move(0, MMM_TO_MMS(fr)); // Init planner with 0 length move
}
#endif
// Unretract (or Prime)
unscaled_e_move(toolchange_settings.swap_length, MMM_TO_MMS(fr));
// Extra Prime
unscaled_e_move(toolchange_settings.extra_prime, MMM_TO_MMS(toolchange_settings.prime_speed));
// Cutting retraction
#if TOOLCHANGE_FS_WIPE_RETRACT
unscaled_e_move(-(TOOLCHANGE_FS_WIPE_RETRACT), MMM_TO_MMS(toolchange_settings.retract_speed));
#endif
// Cool down with fan
#if HAS_FAN && TOOLCHANGE_FS_FAN >= 0
thermalManager.fan_speed[TOOLCHANGE_FS_FAN] = toolchange_settings.fan_speed;
gcode.dwell(SEC_TO_MS(toolchange_settings.fan_time));
thermalManager.fan_speed[TOOLCHANGE_FS_FAN] = 0;
#endif
}
if (should_swap && !too_cold(active_extruder))
extruder_prime(); // Prime selected Extruder
#endif
// Prevent a move outside physical bounds
@ -1260,11 +1338,8 @@ void tool_change(const uint8_t new_tool, bool no_move/*=false*/) {
else DEBUG_ECHOLNPGM("Move back skipped");
#if ENABLED(TOOLCHANGE_FILAMENT_SWAP)
if (should_swap && !too_cold) {
// Cutting recover
unscaled_e_move(toolchange_settings.extra_resume + TOOLCHANGE_FS_WIPE_RETRACT, MMM_TO_MMS(toolchange_settings.unretract_speed));
current_position.e = 0;
sync_plan_position_e(); // New extruder primed and set to 0
if (should_swap && !too_cold(active_extruder)) {
extruder_cutting_recover(0); // New extruder primed and set to 0
// Restart Fan
#if HAS_FAN && TOOLCHANGE_FS_FAN >= 0
@ -1322,7 +1397,7 @@ void tool_change(const uint8_t new_tool, bool no_move/*=false*/) {
#endif
}
SERIAL_ECHO_MSG(STR_ACTIVE_EXTRUDER, active_extruder);
SERIAL_ECHOLNPGM(STR_ACTIVE_EXTRUDER, active_extruder);
#endif // HAS_MULTI_EXTRUDER
}

View file

@ -29,24 +29,30 @@
typedef struct {
#if ENABLED(TOOLCHANGE_FILAMENT_SWAP)
float swap_length, extra_prime, extra_resume;
int16_t prime_speed, retract_speed, unretract_speed, fan, fan_speed, fan_time;
float swap_length; // M217 S
float extra_prime; // M217 E
float extra_resume; // M217 B
int16_t prime_speed; // M217 P
int16_t retract_speed; // M217 R
int16_t unretract_speed; // M217 U
uint8_t fan_speed; // M217 F
uint8_t fan_time; // M217 D
#endif
#if ENABLED(TOOLCHANGE_PARK)
bool enable_park;
xy_pos_t change_point;
bool enable_park; // M217 W
xyz_pos_t change_point; // M217 X Y I J K
#endif
float z_raise;
float z_raise; // M217 Z
} toolchange_settings_t;
extern toolchange_settings_t toolchange_settings;
#if ENABLED(TOOLCHANGE_FILAMENT_SWAP)
void tool_change_prime();
#if ENABLED(TOOLCHANGE_FS_PRIME_FIRST_USED)
extern bool enable_first_prime; // M217 V
#endif
#if ENABLED(TOOLCHANGE_FS_PRIME_FIRST_USED)
extern bool enable_first_prime;
#if ENABLED(TOOLCHANGE_FILAMENT_SWAP)
void tool_change_prime(); // Prime the currently selected extruder
#endif
#if ENABLED(TOOLCHANGE_FS_INIT_BEFORE_SWAP)

View file

@ -24,7 +24,7 @@ opt_set MOTHERBOARD BOARD_BTT_GTR_V1_0 SERIAL_PORT -1 \
EXTRUDERS 5 TEMP_SENSOR_1 1 TEMP_SENSOR_2 1 TEMP_SENSOR_3 1 TEMP_SENSOR_4 1 \
NUM_Z_STEPPER_DRIVERS 4 \
DEFAULT_Kp_LIST '{ 22.2, 20.0, 21.0, 19.0, 18.0 }' DEFAULT_Ki_LIST '{ 1.08 }' DEFAULT_Kd_LIST '{ 114.0, 112.0, 110.0, 108.0 }'
opt_enable TOOLCHANGE_FILAMENT_SWAP TOOLCHANGE_MIGRATION_FEATURE TOOLCHANGE_FS_INIT_BEFORE_SWAP TOOLCHANGE_FS_PRIME_FIRST_USED \
opt_enable TOOLCHANGE_FILAMENT_SWAP TOOLCHANGE_MIGRATION_FEATURE TOOLCHANGE_FS_SLOW_FIRST_PRIME TOOLCHANGE_FS_PRIME_FIRST_USED \
PID_PARAMS_PER_HOTEND Z_MULTI_ENDSTOPS
exec_test $1 $2 "BigTreeTech GTR | 6 Extruders | Quad Z + Endstops" "$3"