Nonlinear Extrusion Control (M592) (#26127)

This commit is contained in:
Andrew Bortz 2023-10-10 20:24:48 -07:00 committed by GitHub
parent 6d301a282e
commit e9b9d634c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 190 additions and 8 deletions

View file

@ -2275,6 +2275,14 @@
//#define EXPERIMENTAL_I2S_LA // Allow I2S_STEPPER_STREAM to be used with LA. Performance degrades as the LA step rate reaches ~20kHz. //#define EXPERIMENTAL_I2S_LA // Allow I2S_STEPPER_STREAM to be used with LA. Performance degrades as the LA step rate reaches ~20kHz.
#endif #endif
/**
* Nonlinear Extrusion Control
*
* Control extrusion rate based on instantaneous extruder velocity. Can be used to correct for
* underextrusion at high extruder speeds that are otherwise well-behaved (i.e., not skipping).
*/
//#define NONLINEAR_EXTRUSION
// @section leveling // @section leveling
/** /**

View file

@ -301,6 +301,7 @@
#define STR_CHAMBER_PID "Chamber PID" #define STR_CHAMBER_PID "Chamber PID"
#define STR_STEPS_PER_UNIT "Steps per unit" #define STR_STEPS_PER_UNIT "Steps per unit"
#define STR_LINEAR_ADVANCE "Linear Advance" #define STR_LINEAR_ADVANCE "Linear Advance"
#define STR_NONLINEAR_EXTRUSION "Nonlinear Extrusion"
#define STR_CONTROLLER_FAN "Controller Fan" #define STR_CONTROLLER_FAN "Controller Fan"
#define STR_STEPPER_MOTOR_CURRENTS "Stepper motor currents" #define STR_STEPPER_MOTOR_CURRENTS "Stepper motor currents"
#define STR_RETRACT_S_F_Z "Retract (S<length> F<feedrate> Z<lift>)" #define STR_RETRACT_S_F_Z "Retract (S<length> F<feedrate> Z<lift>)"

View file

@ -0,0 +1,51 @@
/**
* Marlin 3D Printer Firmware
* Copyright (c) 2023 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
*
* Based on Sprinter and grbl.
* Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#include "../../../inc/MarlinConfig.h"
#if ENABLED(NONLINEAR_EXTRUSION)
#include "../../gcode.h"
#include "../../../module/stepper.h"
void GcodeSuite::M592_report(const bool forReplay/*=true*/) {
report_heading(forReplay, F(STR_NONLINEAR_EXTRUSION));
SERIAL_ECHOLNPGM(" M593 A", stepper.ne.A, " B", stepper.ne.B, " C", stepper.ne.C);
}
/**
* M592: Get or set nonlinear extrusion parameters
* A<factor> Linear coefficient (default 0.0)
* B<factor> Quadratic coefficient (default 0.0)
* C<factor> Constant coefficient (default 1.0)
*
* Adjusts the amount of extrusion based on the instantaneous velocity of extrusion, as a multiplier.
* The amount of extrusion is multiplied by max(C, C + A*v + B*v^2) where v is extruder velocity in mm/s.
* Only adjusts forward extrusions, since those are the ones affected by backpressure.
*/
void GcodeSuite::M592() {
if (parser.seenval('A')) stepper.ne.A = parser.value_float();
if (parser.seenval('B')) stepper.ne.B = parser.value_float();
if (parser.seenval('C')) stepper.ne.C = parser.value_float();
}
#endif // NONLINEAR_EXTRUSION

View file

@ -935,6 +935,10 @@ void GcodeSuite::process_parsed_command(const bool no_ok/*=false*/) {
case 575: M575(); break; // M575: Set serial baudrate case 575: M575(); break; // M575: Set serial baudrate
#endif #endif
#if ENABLED(NONLINEAR_EXTRUSION)
case 592: M592(); break; // M592: Nonlinear Extrusion control
#endif
#if HAS_ZV_SHAPING #if HAS_ZV_SHAPING
case 593: M593(); break; // M593: Input Shaping control case 593: M593(); break; // M593: Input Shaping control
#endif #endif

View file

@ -259,6 +259,7 @@
* M554 - Get or set IP gateway. (Requires enabled Ethernet port) * M554 - Get or set IP gateway. (Requires enabled Ethernet port)
* M569 - Enable stealthChop on an axis. (Requires at least one _DRIVER_TYPE to be TMC2130/2160/2208/2209/5130/5160) * M569 - Enable stealthChop on an axis. (Requires at least one _DRIVER_TYPE to be TMC2130/2160/2208/2209/5130/5160)
* M575 - Change the serial baud rate. (Requires BAUD_RATE_GCODE) * M575 - Change the serial baud rate. (Requires BAUD_RATE_GCODE)
* M592 - Get or set nonlinear extrusion parameters. (Requires NONLINEAR_EXTRUSION)
* M593 - Get or set input shaping parameters. (Requires INPUT_SHAPING_[XY]) * M593 - Get or set input shaping parameters. (Requires INPUT_SHAPING_[XY])
* M600 - Pause for filament change: "M600 X<pos> Y<pos> Z<raise> E<first_retract> L<later_retract>". (Requires ADVANCED_PAUSE_FEATURE) * M600 - Pause for filament change: "M600 X<pos> Y<pos> Z<raise> E<first_retract> L<later_retract>". (Requires ADVANCED_PAUSE_FEATURE)
* M603 - Configure filament change: "M603 T<tool> U<unload_length> L<load_length>". (Requires ADVANCED_PAUSE_FEATURE) * M603 - Configure filament change: "M603 T<tool> U<unload_length> L<load_length>". (Requires ADVANCED_PAUSE_FEATURE)
@ -1106,6 +1107,11 @@ private:
static void M575(); static void M575();
#endif #endif
#if ENABLED(NONLINEAR_EXTRUSION)
static void M592();
static void M592_report(const bool forReplay=true);
#endif
#if HAS_ZV_SHAPING #if HAS_ZV_SHAPING
static void M593(); static void M593();
static void M593_report(const bool forReplay=true); static void M593_report(const bool forReplay=true);

View file

@ -858,6 +858,19 @@ static_assert(COUNT(arm) == LOGICAL_AXES, "AXIS_RELATIVE_MODES must contain " _L
#endif #endif
#endif #endif
/**
* Nonlinear Extrusion requirements
*/
#if ENABLED(NONLINEAR_EXTRUSION)
#if DISABLED(ADAPTIVE_STEP_SMOOTHING)
#error "ADAPTIVE_STEP_SMOOTHING is required for NONLINEAR_EXTRUSION."
#elif HAS_MULTI_EXTRUDER
#error "NONLINEAR_EXTRUSION doesn't currently support multi-extruder setups."
#elif DISABLED(CPU_32_BIT)
#error "NONLINEAR_EXTRUSION requires a 32-bit CPU."
#endif
#endif
/** /**
* Special tool-changing options * Special tool-changing options
*/ */

View file

@ -2005,7 +2005,7 @@ bool Planner::_populate_block(
#if HAS_EXTRUDERS #if HAS_EXTRUDERS
dm.e = (dist.e > 0); dm.e = (dist.e > 0);
const float esteps_float = dist.e * e_factor[extruder]; const float esteps_float = dist.e * e_factor[extruder];
const uint32_t esteps = ABS(esteps_float) + 0.5f; const uint32_t esteps = ABS(esteps_float);
#else #else
constexpr uint32_t esteps = 0; constexpr uint32_t esteps = 0;
#endif #endif

View file

@ -36,7 +36,7 @@
*/ */
// Change EEPROM version if the structure changes // Change EEPROM version if the structure changes
#define EEPROM_VERSION "V88" #define EEPROM_VERSION "V89"
#define EEPROM_OFFSET 100 #define EEPROM_OFFSET 100
// Check the integrity of data offsets. // Check the integrity of data offsets.
@ -634,6 +634,13 @@ typedef struct SettingsDataStruct {
hotend_idle_settings_t hotend_idle_config; // M86 S T E B hotend_idle_settings_t hotend_idle_config; // M86 S T E B
#endif #endif
//
// Nonlinear Extrusion
//
#if ENABLED(NONLINEAR_EXTRUSION)
ne_coeff_t stepper_ne; // M592 A B C
#endif
} SettingsData; } SettingsData;
//static_assert(sizeof(SettingsData) <= MARLIN_EEPROM_SIZE, "EEPROM too small to contain SettingsData!"); //static_assert(sizeof(SettingsData) <= MARLIN_EEPROM_SIZE, "EEPROM too small to contain SettingsData!");
@ -1729,6 +1736,13 @@ void MarlinSettings::postprocess() {
EEPROM_WRITE(hotend_idle.cfg); EEPROM_WRITE(hotend_idle.cfg);
#endif #endif
//
// Nonlinear Extrusion
//
#if ENABLED(NONLINEAR_EXTRUSION)
EEPROM_WRITE(stepper.ne);
#endif
// //
// Report final CRC and Data Size // Report final CRC and Data Size
// //
@ -2803,6 +2817,13 @@ void MarlinSettings::postprocess() {
EEPROM_READ(hotend_idle.cfg); EEPROM_READ(hotend_idle.cfg);
#endif #endif
//
// Nonlinear Extrusion
//
#if ENABLED(NONLINEAR_EXTRUSION)
EEPROM_READ(stepper.ne);
#endif
// //
// Validate Final Size and CRC // Validate Final Size and CRC
// //
@ -3396,7 +3417,6 @@ void MarlinSettings::reset() {
// //
// Heated Bed PID // Heated Bed PID
// //
#if ENABLED(PIDTEMPBED) #if ENABLED(PIDTEMPBED)
thermalManager.temp_bed.pid.set(DEFAULT_bedKp, DEFAULT_bedKi, DEFAULT_bedKd); thermalManager.temp_bed.pid.set(DEFAULT_bedKp, DEFAULT_bedKi, DEFAULT_bedKd);
#endif #endif
@ -3404,7 +3424,6 @@ void MarlinSettings::reset() {
// //
// Heated Chamber PID // Heated Chamber PID
// //
#if ENABLED(PIDTEMPCHAMBER) #if ENABLED(PIDTEMPCHAMBER)
thermalManager.temp_chamber.pid.set(DEFAULT_chamberKp, DEFAULT_chamberKi, DEFAULT_chamberKd); thermalManager.temp_chamber.pid.set(DEFAULT_chamberKp, DEFAULT_chamberKi, DEFAULT_chamberKd);
#endif #endif
@ -3456,7 +3475,6 @@ void MarlinSettings::reset() {
// //
// Volumetric & Filament Size // Volumetric & Filament Size
// //
#if DISABLED(NO_VOLUMETRICS) #if DISABLED(NO_VOLUMETRICS)
parser.volumetric_enabled = ENABLED(VOLUMETRIC_DEFAULT_ON); parser.volumetric_enabled = ENABLED(VOLUMETRIC_DEFAULT_ON);
for (uint8_t q = 0; q < COUNT(planner.filament_size); ++q) for (uint8_t q = 0; q < COUNT(planner.filament_size); ++q)
@ -3598,6 +3616,11 @@ void MarlinSettings::reset() {
// //
TERN_(FT_MOTION, fxdTiCtrl.set_defaults()); TERN_(FT_MOTION, fxdTiCtrl.set_defaults());
//
// Nonlinear Extrusion
//
TERN_(NONLINEAR_EXTRUSION, stepper.ne.reset());
// //
// Input Shaping // Input Shaping
// //
@ -3867,6 +3890,11 @@ void MarlinSettings::reset() {
// //
TERN_(FT_MOTION, gcode.M493_report(forReplay)); TERN_(FT_MOTION, gcode.M493_report(forReplay));
//
// Nonlinear Extrusion
//
TERN_(NONLINEAR_EXTRUSION, gcode.M592_report(forReplay));
// //
// Input Shaping // Input Shaping
// //

View file

@ -245,6 +245,13 @@ uint32_t Stepper::advance_divisor = 0,
bool Stepper::la_active = false; bool Stepper::la_active = false;
#endif #endif
#if ENABLED(NONLINEAR_EXTRUSION)
ne_coeff_t Stepper::ne;
ne_fix_t Stepper::ne_fix;
int32_t Stepper::ne_edividend;
uint32_t Stepper::ne_scale;
#endif
#if HAS_ZV_SHAPING #if HAS_ZV_SHAPING
shaping_time_t ShapingQueue::now = 0; shaping_time_t ShapingQueue::now = 0;
#if ANY(MCU_LPC1768, MCU_LPC1769) && DISABLED(NO_LPC_ETHERNET_BUFFER) #if ANY(MCU_LPC1768, MCU_LPC1769) && DISABLED(NO_LPC_ETHERNET_BUFFER)
@ -2191,6 +2198,16 @@ hal_timer_t Stepper::calc_timer_interval(uint32_t step_rate) {
#endif // !CPU_32_BIT #endif // !CPU_32_BIT
} }
#if ENABLED(NONLINEAR_EXTRUSION)
void Stepper::calc_nonlinear_e(uint32_t step_rate) {
const uint32_t velocity = ne_scale * step_rate; // Scale step_rate first so all intermediate values stay in range of 8.24 fixed point math
int32_t vd = (((int64_t)ne_fix.A * velocity) >> 24) + (((((int64_t)ne_fix.B * velocity) >> 24) * velocity) >> 24);
NOLESS(vd, 0);
advance_dividend.e = (uint64_t(ne_fix.C + vd) * ne_edividend) >> 24;
}
#endif
// Get the timer interval and the number of loops to perform per tick // Get the timer interval and the number of loops to perform per tick
hal_timer_t Stepper::calc_multistep_timer_interval(uint32_t step_rate) { hal_timer_t Stepper::calc_multistep_timer_interval(uint32_t step_rate) {
@ -2318,6 +2335,10 @@ hal_timer_t Stepper::block_phase_isr() {
interval = calc_multistep_timer_interval(acc_step_rate << oversampling_factor); interval = calc_multistep_timer_interval(acc_step_rate << oversampling_factor);
acceleration_time += interval; acceleration_time += interval;
#if ENABLED(NONLINEAR_EXTRUSION)
calc_nonlinear_e(acc_step_rate << oversampling_factor);
#endif
#if ENABLED(LIN_ADVANCE) #if ENABLED(LIN_ADVANCE)
if (la_active) { if (la_active) {
const uint32_t la_step_rate = la_advance_steps < current_block->max_adv_steps ? current_block->la_advance_rate : 0; const uint32_t la_step_rate = la_advance_steps < current_block->max_adv_steps ? current_block->la_advance_rate : 0;
@ -2388,6 +2409,10 @@ hal_timer_t Stepper::block_phase_isr() {
interval = calc_multistep_timer_interval(step_rate << oversampling_factor); interval = calc_multistep_timer_interval(step_rate << oversampling_factor);
deceleration_time += interval; deceleration_time += interval;
#if ENABLED(NONLINEAR_EXTRUSION)
calc_nonlinear_e(step_rate << oversampling_factor);
#endif
#if ENABLED(LIN_ADVANCE) #if ENABLED(LIN_ADVANCE)
if (la_active) { if (la_active) {
const uint32_t la_step_rate = la_advance_steps > current_block->final_adv_steps ? current_block->la_advance_rate : 0; const uint32_t la_step_rate = la_advance_steps > current_block->final_adv_steps ? current_block->la_advance_rate : 0;
@ -2436,6 +2461,10 @@ hal_timer_t Stepper::block_phase_isr() {
// step_rate to timer interval and loops for the nominal speed // step_rate to timer interval and loops for the nominal speed
ticks_nominal = calc_multistep_timer_interval(current_block->nominal_rate << oversampling_factor); ticks_nominal = calc_multistep_timer_interval(current_block->nominal_rate << oversampling_factor);
#if ENABLED(NONLINEAR_EXTRUSION)
calc_nonlinear_e(current_block->nominal_rate << oversampling_factor);
#endif
#if ENABLED(LIN_ADVANCE) #if ENABLED(LIN_ADVANCE)
if (la_active) if (la_active)
la_interval = calc_timer_interval(current_block->nominal_rate >> current_block->la_scaling); la_interval = calc_timer_interval(current_block->nominal_rate >> current_block->la_scaling);
@ -2636,10 +2665,13 @@ hal_timer_t Stepper::block_phase_isr() {
acceleration_time = deceleration_time = 0; acceleration_time = deceleration_time = 0;
#if ENABLED(ADAPTIVE_STEP_SMOOTHING) #if ENABLED(ADAPTIVE_STEP_SMOOTHING)
oversampling_factor = 0; // Assume no axis smoothing (via oversampling) // Nonlinear Extrusion needs at least 2x oversampling to permit increase of E step rate
// Otherwise assume no axis smoothing (via oversampling)
oversampling_factor = TERN(NONLINEAR_EXTRUSION, 1, 0);
// Decide if axis smoothing is possible // Decide if axis smoothing is possible
uint32_t max_rate = current_block->nominal_rate; // Get the step event rate
if (TERN1(DWIN_LCD_PROUI, hmiData.adaptiveStepSmoothing)) { if (TERN1(DWIN_LCD_PROUI, hmiData.adaptiveStepSmoothing)) {
uint32_t max_rate = current_block->nominal_rate; // Get the step event rate
while (max_rate < MIN_STEP_ISR_FREQUENCY) { // As long as more ISRs are possible... while (max_rate < MIN_STEP_ISR_FREQUENCY) { // As long as more ISRs are possible...
max_rate <<= 1; // Try to double the rate max_rate <<= 1; // Try to double the rate
if (max_rate < MIN_STEP_ISR_FREQUENCY) // Don't exceed the estimated ISR limit if (max_rate < MIN_STEP_ISR_FREQUENCY) // Don't exceed the estimated ISR limit
@ -2755,10 +2787,29 @@ hal_timer_t Stepper::block_phase_isr() {
acc_step_rate = current_block->initial_rate; acc_step_rate = current_block->initial_rate;
#endif #endif
#if ENABLED(NONLINEAR_EXTRUSION)
ne_edividend = advance_dividend.e;
const float scale = (float(ne_edividend) / advance_divisor) * planner.mm_per_step[E_AXIS_N(current_block->extruder)];
ne_scale = (1L << 24) * scale;
if (current_block->direction_bits.e) {
ne_fix.A = (1L << 24) * ne.A;
ne_fix.B = (1L << 24) * ne.B;
ne_fix.C = (1L << 24) * ne.C;
}
else {
ne_fix.A = ne_fix.B = 0;
ne_fix.C = (1L << 24);
}
#endif
// Calculate the initial timer interval // Calculate the initial timer interval
interval = calc_multistep_timer_interval(current_block->initial_rate << oversampling_factor); interval = calc_multistep_timer_interval(current_block->initial_rate << oversampling_factor);
acceleration_time += interval; acceleration_time += interval;
#if ENABLED(NONLINEAR_EXTRUSION)
calc_nonlinear_e(current_block->initial_rate << oversampling_factor);
#endif
#if ENABLED(LIN_ADVANCE) #if ENABLED(LIN_ADVANCE)
if (la_active) { if (la_active) {
const uint32_t la_step_rate = la_advance_steps < current_block->max_adv_steps ? current_block->la_advance_rate : 0; const uint32_t la_step_rate = la_advance_steps < current_block->max_adv_steps ? current_block->la_advance_rate : 0;

View file

@ -284,6 +284,11 @@ constexpr ena_mask_t enable_overlap[] = {
#endif // HAS_ZV_SHAPING #endif // HAS_ZV_SHAPING
#if ENABLED(NONLINEAR_EXTRUSION)
typedef struct { float A, B, C; void reset() { A = B = 0.0f; C = 1.0f; } } ne_coeff_t;
typedef struct { int32_t A, B, C; } ne_fix_t;
#endif
// //
// Stepper class definition // Stepper class definition
// //
@ -326,6 +331,10 @@ class Stepper {
static bool frozen; // Set this flag to instantly freeze motion static bool frozen; // Set this flag to instantly freeze motion
#endif #endif
#if ENABLED(NONLINEAR_EXTRUSION)
static ne_coeff_t ne;
#endif
private: private:
static block_t* current_block; // A pointer to the block currently being traced static block_t* current_block; // A pointer to the block currently being traced
@ -416,6 +425,12 @@ class Stepper {
static bool la_active; // Whether linear advance is used on the present segment. static bool la_active; // Whether linear advance is used on the present segment.
#endif #endif
#if ENABLED(NONLINEAR_EXTRUSION)
static int32_t ne_edividend;
static uint32_t ne_scale;
static ne_fix_t ne_fix;
#endif
#if ENABLED(BABYSTEPPING) #if ENABLED(BABYSTEPPING)
static constexpr hal_timer_t BABYSTEP_NEVER = HAL_TIMER_TYPE_MAX; static constexpr hal_timer_t BABYSTEP_NEVER = HAL_TIMER_TYPE_MAX;
static hal_timer_t nextBabystepISR; static hal_timer_t nextBabystepISR;
@ -660,6 +675,10 @@ class Stepper {
// Calculate timing interval and steps-per-ISR for the given step rate // Calculate timing interval and steps-per-ISR for the given step rate
static hal_timer_t calc_multistep_timer_interval(uint32_t step_rate); static hal_timer_t calc_multistep_timer_interval(uint32_t step_rate);
#if ENABLED(NONLINEAR_EXTRUSION)
static void calc_nonlinear_e(uint32_t step_rate);
#endif
#if ENABLED(S_CURVE_ACCELERATION) #if ENABLED(S_CURVE_ACCELERATION)
static void _calc_bezier_curve_coeffs(const int32_t v0, const int32_t v1, const uint32_t av); static void _calc_bezier_curve_coeffs(const int32_t v0, const int32_t v1, const uint32_t av);
static int32_t _eval_bezier_curve(const uint32_t curr_step); static int32_t _eval_bezier_curve(const uint32_t curr_step);

View file

@ -12,7 +12,7 @@ set -e
restore_configs restore_configs
opt_set MOTHERBOARD BOARD_BTT_SKR_MINI_E3_V1_0 SERIAL_PORT 1 SERIAL_PORT_2 -1 \ opt_set MOTHERBOARD BOARD_BTT_SKR_MINI_E3_V1_0 SERIAL_PORT 1 SERIAL_PORT_2 -1 \
X_DRIVER_TYPE TMC2209 Y_DRIVER_TYPE TMC2209 Z_DRIVER_TYPE TMC2209 E0_DRIVER_TYPE TMC2209 X_DRIVER_TYPE TMC2209 Y_DRIVER_TYPE TMC2209 Z_DRIVER_TYPE TMC2209 E0_DRIVER_TYPE TMC2209
opt_enable CR10_STOCKDISPLAY PINS_DEBUGGING Z_IDLE_HEIGHT FT_MOTION FT_MOTION_MENU opt_enable CR10_STOCKDISPLAY PINS_DEBUGGING Z_IDLE_HEIGHT FT_MOTION FT_MOTION_MENU ADAPTIVE_STEP_SMOOTHING NONLINEAR_EXTRUSION
exec_test $1 $2 "BigTreeTech SKR Mini E3 1.0 - TMC2209 HW Serial, FT_MOTION" "$3" exec_test $1 $2 "BigTreeTech SKR Mini E3 1.0 - TMC2209 HW Serial, FT_MOTION" "$3"
# clean up # clean up

View file

@ -312,6 +312,7 @@ CONTROLLER_FAN_EDITABLE = build_src_filter=+<src/gcode/feature/co
HAS_ZV_SHAPING = build_src_filter=+<src/gcode/feature/input_shaping> HAS_ZV_SHAPING = build_src_filter=+<src/gcode/feature/input_shaping>
GCODE_MACROS = build_src_filter=+<src/gcode/feature/macro> GCODE_MACROS = build_src_filter=+<src/gcode/feature/macro>
GRADIENT_MIX = build_src_filter=+<src/gcode/feature/mixing/M166.cpp> GRADIENT_MIX = build_src_filter=+<src/gcode/feature/mixing/M166.cpp>
NONLINEAR_EXTRUSION = build_src_filter=+<src/gcode/feature/nonlinear>
OTA_FIRMWARE_UPDATE = build_src_filter=+<src/gcode/feature/ota> OTA_FIRMWARE_UPDATE = build_src_filter=+<src/gcode/feature/ota>
HAS_SAVED_POSITIONS = build_src_filter=+<src/gcode/feature/pause/G60.cpp> +<src/gcode/feature/pause/G61.cpp> HAS_SAVED_POSITIONS = build_src_filter=+<src/gcode/feature/pause/G60.cpp> +<src/gcode/feature/pause/G61.cpp>
PARK_HEAD_ON_PAUSE = build_src_filter=+<src/gcode/feature/pause/M125.cpp> PARK_HEAD_ON_PAUSE = build_src_filter=+<src/gcode/feature/pause/M125.cpp>