From ed66f498eb713f59f14d000f8bfa550b34ce8528 Mon Sep 17 00:00:00 2001
From: Scott Lahteine <thinkyhead@users.noreply.github.com>
Date: Tue, 16 May 2023 02:38:24 -0500
Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=B8=20Fixed-Time=20Motion=20EEPROM=20a?=
 =?UTF-8?q?nd=20Menu=20(#25835)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Marlin/Configuration_adv.h                    |   2 +
 Marlin/src/core/language.h                    |   1 +
 Marlin/src/gcode/feature/ft_motion/M493.cpp   | 290 +++++++++++-------
 Marlin/src/gcode/gcode.h                      |   1 +
 .../ftdi_eve_touch_ui/language/language_en.h  |   1 -
 Marlin/src/lcd/language/language_en.h         |  17 +
 Marlin/src/lcd/menu/menu_motion.cpp           | 136 ++++++++
 Marlin/src/module/ft_motion.cpp               |  68 ++--
 Marlin/src/module/ft_motion.h                 |  68 +++-
 Marlin/src/module/ft_types.h                  |   4 +-
 Marlin/src/module/planner.cpp                 |   8 +-
 Marlin/src/module/settings.cpp                |  47 ++-
 Marlin/src/module/stepper.cpp                 |   2 +-
 buildroot/tests/STM32F103RC_btt               |   2 +-
 14 files changed, 474 insertions(+), 173 deletions(-)

diff --git a/Marlin/Configuration_adv.h b/Marlin/Configuration_adv.h
index 587a368301d..b68557cb289 100644
--- a/Marlin/Configuration_adv.h
+++ b/Marlin/Configuration_adv.h
@@ -1148,6 +1148,8 @@
   // This value may be configured to adjust duration to consume the command buffer.
   // Try increasing this value if stepper motion is not smooth.
   #define FTM_STEPPERCMD_BUFF_SIZE 1000                 // Size of the stepper command buffers.
+
+  //#define FT_MOTION_MENU                              // Provide a MarlinUI menu to set M493 parameters.
 #endif
 
 /**
diff --git a/Marlin/src/core/language.h b/Marlin/src/core/language.h
index c951012b6df..388e7cd820e 100644
--- a/Marlin/src/core/language.h
+++ b/Marlin/src/core/language.h
@@ -277,6 +277,7 @@
 // Settings Report Strings
 #define STR_Z_AUTO_ALIGN                    "Z Auto-Align"
 #define STR_BACKLASH_COMPENSATION           "Backlash compensation"
+#define STR_FT_MOTION                       "Fixed-Time Motion"
 #define STR_S_SEG_PER_SEC                   "S<seg-per-sec>"
 #define STR_DELTA_SETTINGS                  "Delta (L<diagonal-rod> R<radius> H<height> S<seg-per-sec> XYZ<tower-angle-trim> ABC<rod-trim>)"
 #define STR_SCARA_SETTINGS                  "SCARA"
diff --git a/Marlin/src/gcode/feature/ft_motion/M493.cpp b/Marlin/src/gcode/feature/ft_motion/M493.cpp
index f7b8f1e752c..83c6a9a5dac 100644
--- a/Marlin/src/gcode/feature/ft_motion/M493.cpp
+++ b/Marlin/src/gcode/feature/ft_motion/M493.cpp
@@ -28,26 +28,109 @@
 #include "../../../module/ft_motion.h"
 
 void say_shaping() {
-  SERIAL_ECHO_TERNARY(fxdTiCtrl.cfg_mode, "Fixed time controller ", "en", "dis", "abled");
-  if (fxdTiCtrl.cfg_mode == ftMotionMode_DISABLED || fxdTiCtrl.cfg_mode == ftMotionMode_ENABLED) {
-    SERIAL_ECHOLNPGM(".");
-    return;
-  }
+  // FT Enabled
+  SERIAL_ECHO_TERNARY(fxdTiCtrl.cfg.mode, "Fixed-Time Motion ", "en", "dis", "abled");
+
+  // FT Shaping
   #if HAS_X_AXIS
-    SERIAL_ECHOPGM(" with ");
-    switch (fxdTiCtrl.cfg_mode) {
-      default: break;
-      //case ftMotionMode_ULENDO_FBS: SERIAL_ECHOLNPGM("Ulendo FBS."); return;
-      case ftMotionMode_ZV: SERIAL_ECHOLNPGM("ZV"); break;
-      case ftMotionMode_ZVD: SERIAL_ECHOLNPGM("ZVD"); break;
-      case ftMotionMode_EI: SERIAL_ECHOLNPGM("EI"); break;
-      case ftMotionMode_2HEI: SERIAL_ECHOLNPGM("2 Hump EI"); break;
-      case ftMotionMode_3HEI: SERIAL_ECHOLNPGM("3 Hump EI"); break;
-      case ftMotionMode_MZV: SERIAL_ECHOLNPGM("MZV"); break;
-      //case ftMotionMode_DISCTF: SERIAL_ECHOLNPGM("discrete transfer functions"); break;
+    if (fxdTiCtrl.cfg.mode > ftMotionMode_ENABLED) {
+      SERIAL_ECHOPGM(" with ");
+      switch (fxdTiCtrl.cfg.mode) {
+        default: break;
+        case ftMotionMode_ZV:   SERIAL_ECHOPGM("ZV");        break;
+        case ftMotionMode_ZVD:  SERIAL_ECHOPGM("ZVD");       break;
+        case ftMotionMode_EI:   SERIAL_ECHOPGM("EI");        break;
+        case ftMotionMode_2HEI: SERIAL_ECHOPGM("2 Hump EI"); break;
+        case ftMotionMode_3HEI: SERIAL_ECHOPGM("3 Hump EI"); break;
+        case ftMotionMode_MZV:  SERIAL_ECHOPGM("MZV");       break;
+        //case ftMotionMode_DISCTF: SERIAL_ECHOPGM("discrete transfer functions"); break;
+        //case ftMotionMode_ULENDO_FBS: SERIAL_ECHOPGM("Ulendo FBS."); return;
+      }
+      SERIAL_ECHOPGM(" shaping");
     }
-    SERIAL_ECHOLNPGM(" shaping.");
   #endif
+  SERIAL_ECHOLNPGM(".");
+
+  const bool z_based = TERN0(HAS_DYNAMIC_FREQ_MM, fxdTiCtrl.cfg.dynFreqMode == dynFreqMode_Z_BASED),
+             g_based = TERN0(HAS_DYNAMIC_FREQ_G,  fxdTiCtrl.cfg.dynFreqMode == dynFreqMode_MASS_BASED),
+             dynamic = z_based || g_based;
+
+  // FT Dynamic Frequency Mode
+  if (fxdTiCtrl.cfg.modeHasShaper()) {
+    #if HAS_DYNAMIC_FREQ
+      SERIAL_ECHOPGM("Dynamic Frequency Mode ");
+      switch (fxdTiCtrl.cfg.dynFreqMode) {
+        default:
+        case dynFreqMode_DISABLED: SERIAL_ECHOPGM("disabled"); break;
+        #if HAS_DYNAMIC_FREQ_MM
+          case dynFreqMode_Z_BASED: SERIAL_ECHOPGM("Z-based"); break;
+        #endif
+        #if HAS_DYNAMIC_FREQ_G
+          case dynFreqMode_MASS_BASED: SERIAL_ECHOPGM("Mass-based"); break;
+        #endif
+      }
+      SERIAL_ECHOLNPGM(".");
+    #endif
+
+    #if HAS_X_AXIS
+      SERIAL_ECHO_TERNARY(dynamic, "X/A ", "base dynamic", "static", " compensator frequency: ");
+      SERIAL_ECHO_F(fxdTiCtrl.cfg.baseFreq[X_AXIS], 2);
+      SERIAL_ECHOPGM("Hz");
+      #if HAS_DYNAMIC_FREQ
+        if (dynamic) {
+          SERIAL_ECHOPGM(" scaling: ");
+          SERIAL_ECHO_F(fxdTiCtrl.cfg.dynFreqK[X_AXIS], 8);
+          serial_ternary(F("Hz/"), z_based, F("mm"), F("g"));
+        }
+      #endif
+      SERIAL_EOL();
+    #endif
+
+    #if HAS_Y_AXIS
+      SERIAL_ECHO_TERNARY(dynamic, "Y/B ", "base dynamic", "static", " compensator frequency: ");
+      SERIAL_ECHO_F(fxdTiCtrl.cfg.baseFreq[Y_AXIS], 2);
+      SERIAL_ECHOLNPGM(" Hz");
+      #if HAS_DYNAMIC_FREQ
+        if (dynamic) {
+          SERIAL_ECHOPGM(" scaling: ");
+          SERIAL_ECHO_F(fxdTiCtrl.cfg.dynFreqK[Y_AXIS], 8);
+          serial_ternary(F("Hz/"), z_based, F("mm"), F("g"));
+        }
+      #endif
+      SERIAL_EOL();
+    #endif
+  }
+
+  #if HAS_EXTRUDERS
+    SERIAL_ECHO_TERNARY(fxdTiCtrl.cfg.linearAdvEna, "Linear Advance ", "en", "dis", "abled");
+    SERIAL_ECHOLNPGM(". Gain: "); SERIAL_ECHO_F(fxdTiCtrl.cfg.linearAdvK, 5);
+  #endif
+
+}
+
+void GcodeSuite::M493_report(const bool forReplay/*=true*/) {
+  report_heading_etc(forReplay, F(STR_FT_MOTION));
+  const ft_config_t &c = fxdTiCtrl.cfg;
+  SERIAL_ECHOPGM("  M493 S", c.mode);
+  #if HAS_X_AXIS
+    SERIAL_ECHOPGM(" A", c.baseFreq[X_AXIS]);
+    #if HAS_Y_AXIS
+      SERIAL_ECHOPGM(" B", c.baseFreq[Y_AXIS]);
+    #endif
+  #endif
+  #if HAS_DYNAMIC_FREQ
+    SERIAL_ECHOPGM(" D", c.dynFreqMode);
+    #if HAS_X_AXIS
+      SERIAL_ECHOPGM(" F", c.dynFreqK[X_AXIS]);
+      #if HAS_Y_AXIS
+        SERIAL_ECHOPGM(" H", c.dynFreqK[Y_AXIS]);
+      #endif
+    #endif
+  #endif
+  #if HAS_EXTRUDERS
+    SERIAL_ECHOPGM(" P", c.linearAdvEna, " K", c.linearAdvK);
+  #endif
+  SERIAL_EOL();
 }
 
 /**
@@ -79,29 +162,36 @@ void say_shaping() {
  *    H<Hz> Set frequency scaling for the Y axis
  */
 void GcodeSuite::M493() {
+  struct { bool update_n:1, update_a:1, reset_ft:1, report_h:1; } flag = { false };
+
+  if (!parser.seen_any()) flag.report_h = true;
+
   // Parse 'S' mode parameter.
   if (parser.seenval('S')) {
-    const ftMotionMode_t val = (ftMotionMode_t)parser.value_byte();
-    switch (val) {
-      case ftMotionMode_DISABLED:
-      case ftMotionMode_ENABLED:
+    const ftMotionMode_t oldmm = fxdTiCtrl.cfg.mode,
+                         newmm = (ftMotionMode_t)parser.value_byte();
+    switch (newmm) {
       #if HAS_X_AXIS
+        case ftMotionMode_ZV:
         case ftMotionMode_ZVD:
         case ftMotionMode_2HEI:
         case ftMotionMode_3HEI:
         case ftMotionMode_MZV:
         //case ftMotionMode_ULENDO_FBS:
         //case ftMotionMode_DISCTF:
-          fxdTiCtrl.cfg_mode = val;
-          say_shaping();
-          break;
       #endif
+      case ftMotionMode_DISABLED:
+      case ftMotionMode_ENABLED:
+        fxdTiCtrl.cfg.mode = newmm;
+        flag.report_h = true;
+        break;
       default:
         SERIAL_ECHOLNPGM("?Invalid control mode [M] value.");
         return;
     }
 
-    switch (val) {
+    if (fxdTiCtrl.cfg.mode != oldmm) switch (newmm) {
+      default: break;
       #if HAS_X_AXIS
         //case ftMotionMode_ULENDO_FBS:
         //case ftMotionMode_DISCTF:
@@ -112,15 +202,11 @@ void GcodeSuite::M493() {
         case ftMotionMode_2HEI:
         case ftMotionMode_3HEI:
         case ftMotionMode_MZV:
-          fxdTiCtrl.updateShapingN(fxdTiCtrl.cfg_baseFreq[0] OPTARG(HAS_Y_AXIS, fxdTiCtrl.cfg_baseFreq[1]));
-          fxdTiCtrl.updateShapingA();
-          fxdTiCtrl.reset();
-          break;
+          flag.update_n = flag.update_a = true;
       #endif
       case ftMotionMode_ENABLED:
-        fxdTiCtrl.reset();
+        flag.reset_ft = true;
         break;
-      default: break;
     }
   }
 
@@ -129,47 +215,44 @@ void GcodeSuite::M493() {
     // Pressure control (linear advance) parameter.
     if (parser.seen('P')) {
       const bool val = parser.value_bool();
-      fxdTiCtrl.cfg_linearAdvEna = val;
-      SERIAL_ECHO_TERNARY(val, "Pressure control: Linear Advance ", "en", "dis", "abled.\n");
+      fxdTiCtrl.cfg.linearAdvEna = val;
+      SERIAL_ECHO_TERNARY(val, "Linear Advance ", "en", "dis", "abled.\n");
     }
 
     // Pressure control (linear advance) gain parameter.
     if (parser.seenval('K')) {
       const float val = parser.value_float();
       if (val >= 0.0f) {
-        fxdTiCtrl.cfg_linearAdvK = val;
-        SERIAL_ECHOPGM("Pressure control: Linear Advance gain set to: ");
-        SERIAL_ECHO_F(val, 5);
-        SERIAL_ECHOLNPGM(".");
-      }
-      else { // Value out of range.
-        SERIAL_ECHOLNPGM("Pressure control: Linear Advance gain out of range.");
+        fxdTiCtrl.cfg.linearAdvK = val;
+        flag.report_h = true;
       }
+      else // Value out of range.
+        SERIAL_ECHOLNPGM("Linear Advance gain out of range.");
     }
 
   #endif // HAS_EXTRUDERS
 
-  #if HAS_Z_AXIS || HAS_EXTRUDERS
+  #if HAS_DYNAMIC_FREQ
 
     // Dynamic frequency mode parameter.
     if (parser.seenval('D')) {
-      if (WITHIN(fxdTiCtrl.cfg_mode, 10U, 19U)) {
+      if (fxdTiCtrl.cfg.modeHasShaper()) {
         const dynFreqMode_t val = dynFreqMode_t(parser.value_byte());
         switch (val) {
           case dynFreqMode_DISABLED:
-            fxdTiCtrl.cfg_dynFreqMode = val;
-            SERIAL_ECHOLNPGM("Dynamic frequency mode disabled.");
+            fxdTiCtrl.cfg.dynFreqMode = val;
+            flag.report_h = true;
             break;
-          #if HAS_Z_AXIS
+          #if HAS_DYNAMIC_FREQ_MM
             case dynFreqMode_Z_BASED:
-              fxdTiCtrl.cfg_dynFreqMode = val;
-              SERIAL_ECHOLNPGM("Z-based Dynamic Frequency Mode.");
+              fxdTiCtrl.cfg.dynFreqMode = val;
+              flag.report_h = true;
               break;
           #endif
-          #if HAS_EXTRUDERS
+          #if HAS_DYNAMIC_FREQ_G
             case dynFreqMode_MASS_BASED:
-              fxdTiCtrl.cfg_dynFreqMode = val;
-              SERIAL_ECHOLNPGM("Mass-based Dynamic Frequency Mode.");
+              fxdTiCtrl.cfg.dynFreqMode = val;
+              flag.report_h = true;
               break;
           #endif
           default:
@@ -178,58 +261,46 @@ void GcodeSuite::M493() {
         }
       }
       else {
-        SERIAL_ECHOLNPGM("Incompatible shaper for [D] Dynamic Frequency mode.");
+        SERIAL_ECHOLNPGM("?Wrong shaper for [D] Dynamic Frequency mode.");
       }
     }
 
-  #endif // HAS_Z_AXIS || HAS_EXTRUDERS
+    const bool modeUsesDynFreq = (
+         TERN0(HAS_DYNAMIC_FREQ_MM, fxdTiCtrl.cfg.dynFreqMode == dynFreqMode_Z_BASED)
+      || TERN0(HAS_DYNAMIC_FREQ_G,  fxdTiCtrl.cfg.dynFreqMode == dynFreqMode_MASS_BASED)
+    );
+
+  #endif // HAS_DYNAMIC_FREQ
 
   #if HAS_X_AXIS
 
     // Parse frequency parameter (X axis).
     if (parser.seenval('A')) {
-      if (WITHIN(fxdTiCtrl.cfg_mode, 10U, 19U)) {
+      if (fxdTiCtrl.cfg.modeHasShaper()) {
         const float val = parser.value_float();
-        const bool frequencyInRange = WITHIN(val, FTM_MIN_SHAPE_FREQ, (FTM_FS) / 2);
         // TODO: Frequency minimum is dependent on the shaper used; the above check isn't always correct.
-        if (frequencyInRange) {
-          fxdTiCtrl.cfg_baseFreq[0] = val;
-          fxdTiCtrl.updateShapingN(fxdTiCtrl.cfg_baseFreq[0] OPTARG(HAS_Y_AXIS, fxdTiCtrl.cfg_baseFreq[1]));
-          fxdTiCtrl.reset();
-          if (fxdTiCtrl.cfg_dynFreqMode) { SERIAL_ECHOPGM("Compensator base dynamic frequency (X/A axis) set to:"); }
-          else { SERIAL_ECHOPGM("Compensator static frequency (X/A axis) set to: "); }
-          SERIAL_ECHO_F(fxdTiCtrl.cfg_baseFreq[0], 2);
-          SERIAL_ECHOLNPGM(".");
-        }
-        else { // Frequency out of range.
-          SERIAL_ECHOLNPGM("Invalid [A] frequency value.");
+        if (WITHIN(val, FTM_MIN_SHAPE_FREQ, (FTM_FS) / 2)) {
+          fxdTiCtrl.cfg.baseFreq[X_AXIS] = val;
+          flag.update_n = flag.reset_ft = flag.report_h = true;
         }
+        else // Frequency out of range.
+          SERIAL_ECHOLNPGM("Invalid [", AS_CHAR('A'), "] frequency value.");
       }
-      else { // Mode doesn't use frequency.
-        SERIAL_ECHOLNPGM("Incompatible mode for [A] frequency.");
-      }
+      else // Mode doesn't use frequency.
+        SERIAL_ECHOLNPGM("Wrong mode for [", AS_CHAR('A'), "] frequency.");
     }
 
-    #if HAS_Z_AXIS || HAS_EXTRUDERS
+    #if HAS_DYNAMIC_FREQ
       // Parse frequency scaling parameter (X axis).
       if (parser.seenval('F')) {
-        const bool modeUsesDynFreq = (
-             TERN0(HAS_Z_AXIS,    fxdTiCtrl.cfg_dynFreqMode == dynFreqMode_Z_BASED)
-          || TERN0(HAS_EXTRUDERS, fxdTiCtrl.cfg_dynFreqMode == dynFreqMode_MASS_BASED)
-        );
-
         if (modeUsesDynFreq) {
-          const float val = parser.value_float();
-          fxdTiCtrl.cfg_dynFreqK[0] = val;
-          SERIAL_ECHOPGM("Frequency scaling (X/A axis) set to: ");
-          SERIAL_ECHO_F(fxdTiCtrl.cfg_dynFreqK[0], 8);
-          SERIAL_ECHOLNPGM(".");
-        }
-        else {
-          SERIAL_ECHOLNPGM("Incompatible mode for [F] frequency scaling.");
+          fxdTiCtrl.cfg.dynFreqK[X_AXIS] = parser.value_float();
+          flag.report_h = true;
         }
+        else
+          SERIAL_ECHOLNPGM("Wrong mode for [", AS_CHAR('F'), "] frequency scaling.");
       }
-    #endif // HAS_Z_AXIS || HAS_EXTRUDERS
+    #endif
 
   #endif // HAS_X_AXIS
 
@@ -237,49 +308,40 @@ void GcodeSuite::M493() {
 
     // Parse frequency parameter (Y axis).
     if (parser.seenval('B')) {
-      if (WITHIN(fxdTiCtrl.cfg_mode, 10U, 19U)) {
+      if (fxdTiCtrl.cfg.modeHasShaper()) {
         const float val = parser.value_float();
-        const bool frequencyInRange = WITHIN(val, FTM_MIN_SHAPE_FREQ, (FTM_FS) / 2);
-        if (frequencyInRange) {
-          fxdTiCtrl.cfg_baseFreq[1] = val;
-          fxdTiCtrl.updateShapingN(fxdTiCtrl.cfg_baseFreq[0] OPTARG(HAS_Y_AXIS, fxdTiCtrl.cfg_baseFreq[1]));
-          fxdTiCtrl.reset();
-          if (fxdTiCtrl.cfg_dynFreqMode) { SERIAL_ECHOPGM("Compensator base dynamic frequency (Y/B axis) set to:"); }
-          else { SERIAL_ECHOPGM("Compensator static frequency (Y/B axis) set to: "); }
-          SERIAL_ECHO_F(fxdTiCtrl.cfg_baseFreq[1], 2);
-          SERIAL_ECHOLNPGM(".");
-        }
-        else { // Frequency out of range.
-          SERIAL_ECHOLNPGM("Invalid frequency [B] value.");
+        if (WITHIN(val, FTM_MIN_SHAPE_FREQ, (FTM_FS) / 2)) {
+          fxdTiCtrl.cfg.baseFreq[Y_AXIS] = val;
+          flag.update_n = flag.reset_ft = flag.report_h = true;
         }
+        else // Frequency out of range.
+          SERIAL_ECHOLNPGM("Invalid frequency [", AS_CHAR('B'), "] value.");
       }
-      else { // Mode doesn't use frequency.
-        SERIAL_ECHOLNPGM("Incompatible mode for [B] frequency.");
-      }
+      else // Mode doesn't use frequency.
+        SERIAL_ECHOLNPGM("Wrong mode for [", AS_CHAR('B'), "] frequency.");
     }
 
-    #if HAS_Z_AXIS || HAS_EXTRUDERS
+    #if HAS_DYNAMIC_FREQ
       // Parse frequency scaling parameter (Y axis).
       if (parser.seenval('H')) {
-        const bool modeUsesDynFreq = (
-             TERN0(HAS_Z_AXIS,    fxdTiCtrl.cfg_dynFreqMode == dynFreqMode_Z_BASED)
-          || TERN0(HAS_EXTRUDERS, fxdTiCtrl.cfg_dynFreqMode == dynFreqMode_MASS_BASED)
-        );
-
         if (modeUsesDynFreq) {
-          const float val = parser.value_float();
-          fxdTiCtrl.cfg_dynFreqK[1] = val;
-          SERIAL_ECHOPGM("Frequency scaling (Y/B axis) set to: ");
-          SERIAL_ECHO_F(val, 8);
-          SERIAL_ECHOLNPGM(".");
-        }
-        else {
-          SERIAL_ECHOLNPGM("Incompatible mode for [H] frequency scaling.");
+          fxdTiCtrl.cfg.dynFreqK[Y_AXIS] = parser.value_float();
+          flag.report_h = true;
         }
+        else
+          SERIAL_ECHOLNPGM("Wrong mode for [", AS_CHAR('H'), "] frequency scaling.");
       }
-    #endif // HAS_Z_AXIS || HAS_EXTRUDERS
+    #endif
 
   #endif // HAS_Y_AXIS
+
+  #if HAS_X_AXIS
+    if (flag.update_n) fxdTiCtrl.refreshShapingN();
+    if (flag.update_a) fxdTiCtrl.updateShapingA();
+  #endif
+  if (flag.reset_ft) fxdTiCtrl.reset();
+  if (flag.report_h) say_shaping();
+
 }
 
 #endif // FT_MOTION
diff --git a/Marlin/src/gcode/gcode.h b/Marlin/src/gcode/gcode.h
index eb465ea7c39..9283a92e396 100644
--- a/Marlin/src/gcode/gcode.h
+++ b/Marlin/src/gcode/gcode.h
@@ -1047,6 +1047,7 @@ private:
 
   #if ENABLED(FT_MOTION)
     static void M493();
+    static void M493_report(const bool forReplay=true);
   #endif
 
   static void M500();
diff --git a/Marlin/src/lcd/extui/ftdi_eve_touch_ui/language/language_en.h b/Marlin/src/lcd/extui/ftdi_eve_touch_ui/language/language_en.h
index 05e625842ad..9c069d9d246 100644
--- a/Marlin/src/lcd/extui/ftdi_eve_touch_ui/language/language_en.h
+++ b/Marlin/src/lcd/extui/ftdi_eve_touch_ui/language/language_en.h
@@ -91,7 +91,6 @@ namespace Language_en {
   LSTR MSG_IDLE                     = u8"idle";
   LSTR MSG_SET_MAXIMUM              = u8"Set Maximum";
   LSTR MSG_PRINT_SPEED              = u8"Print Speed";
-  LSTR MSG_LINEAR_ADVANCE           = u8"Linear Advance";
   LSTR MSG_LINEAR_ADVANCE_K         = u8"K";
   LSTR MSG_LINEAR_ADVANCE_K1        = u8"K E1";
   LSTR MSG_LINEAR_ADVANCE_K2        = u8"K E2";
diff --git a/Marlin/src/lcd/language/language_en.h b/Marlin/src/lcd/language/language_en.h
index 0cfd2f8beae..bc97407fc22 100644
--- a/Marlin/src/lcd/language/language_en.h
+++ b/Marlin/src/lcd/language/language_en.h
@@ -446,6 +446,7 @@ namespace Language_en {
   LSTR MSG_DRAW_MIN_Y                     = _UxGT("Draw Min Y");
   LSTR MSG_DRAW_MAX_Y                     = _UxGT("Draw Max Y");
   LSTR MSG_MAX_BELT_LEN                   = _UxGT("Max Belt Len");
+  LSTR MSG_LINEAR_ADVANCE                 = _UxGT("Linear Advance");
   LSTR MSG_ADVANCE_K                      = _UxGT("Advance K");
   LSTR MSG_ADVANCE_K_E                    = _UxGT("Advance K *");
   LSTR MSG_CONTRAST                       = _UxGT("LCD Contrast");
@@ -836,6 +837,22 @@ namespace Language_en {
   LSTR MSG_BACKLASH_CORRECTION            = _UxGT("Correction");
   LSTR MSG_BACKLASH_SMOOTHING             = _UxGT("Smoothing");
 
+  LSTR MSG_FIXED_TIME_MOTION              = _UxGT("Fixed-Time Motion");
+  LSTR MSG_FTM_MODE                       = _UxGT("Motion Mode:");
+  LSTR MSG_FTM_ZV                         = _UxGT("ZV");
+  LSTR MSG_FTM_ZVD                        = _UxGT("ZVD");
+  LSTR MSG_FTM_EI                         = _UxGT("EI");
+  LSTR MSG_FTM_2HEI                       = _UxGT("2HEI");
+  LSTR MSG_FTM_3HEI                       = _UxGT("3HEI");
+  LSTR MSG_FTM_MZV                        = _UxGT("MZV");
+  //LSTR MSG_FTM_ULENDO_FBS               = _UxGT("Ulendo FBS");
+  //LSTR MSG_FTM_DISCTF                   = _UxGT("DISCTF");
+  LSTR MSG_FTM_DYN_MODE                   = _UxGT("DF Mode:");
+  LSTR MSG_FTM_Z_BASED                    = _UxGT("Z-based");
+  LSTR MSG_FTM_MASS_BASED                 = _UxGT("Mass-based");
+  LSTR MSG_FTM_BASE_FREQ_N                = _UxGT("@ Base Freq.");
+  LSTR MSG_FTM_DFREQ_K_N                  = _UxGT("@ Dyn. Freq.");
+
   LSTR MSG_LEVEL_X_AXIS                   = _UxGT("Level X Axis");
   LSTR MSG_AUTO_CALIBRATE                 = _UxGT("Auto Calibrate");
   #if ENABLED(TOUCH_UI_FTDI_EVE)
diff --git a/Marlin/src/lcd/menu/menu_motion.cpp b/Marlin/src/lcd/menu/menu_motion.cpp
index 8caa1e52640..bab03db6062 100644
--- a/Marlin/src/lcd/menu/menu_motion.cpp
+++ b/Marlin/src/lcd/menu/menu_motion.cpp
@@ -313,7 +313,136 @@ void menu_move() {
   void goto_tramming_wizard();
 #endif
 
+#if ENABLED(FT_MOTION_MENU)
+
+  #include "../../module/ft_motion.h"
+  #include "../../gcode/gcode.h"
+
+  void _M493_S(const ftMotionMode_t s) {
+    char cmd[10];
+    sprintf_P(cmd, PSTR("M493S%i"), int(s));
+    gcode.process_subcommands_now(cmd);
+    ui.go_back();
+  }
+
+  inline void menu_ftm_mode() {
+    const ftMotionMode_t mode = fxdTiCtrl.cfg.mode;
+
+    START_MENU();
+    BACK_ITEM(MSG_FIXED_TIME_MOTION);
+
+    if (mode != ftMotionMode_DISABLED) ACTION_ITEM(MSG_LCD_OFF,  []{ _M493_S(ftMotionMode_DISABLED); });
+    if (mode != ftMotionMode_ENABLED)  ACTION_ITEM(MSG_LCD_ON,   []{ _M493_S(ftMotionMode_ENABLED); });
+    #if HAS_X_AXIS
+      if (mode != ftMotionMode_ZV)     ACTION_ITEM(MSG_FTM_ZV,   []{ _M493_S(ftMotionMode_ZV); });
+      if (mode != ftMotionMode_ZVD)    ACTION_ITEM(MSG_FTM_ZVD,  []{ _M493_S(ftMotionMode_ZVD); });
+      if (mode != ftMotionMode_EI)     ACTION_ITEM(MSG_FTM_EI,   []{ _M493_S(ftMotionMode_EI); });
+      if (mode != ftMotionMode_2HEI)   ACTION_ITEM(MSG_FTM_2HEI, []{ _M493_S(ftMotionMode_2HEI); });
+      if (mode != ftMotionMode_3HEI)   ACTION_ITEM(MSG_FTM_3HEI, []{ _M493_S(ftMotionMode_3HEI); });
+      if (mode != ftMotionMode_MZV)    ACTION_ITEM(MSG_FTM_MZV,  []{ _M493_S(ftMotionMode_MZV); });
+      //if (mode != ftMotionMode_ULENDO_FBS) ACTION_ITEM(MSG_FTM_ULENDO_FBS, []{ _M493_S(ftMotionMode_ULENDO_FBS); });
+      //if (mode != ftMotionMode_DISCTF)     ACTION_ITEM(MSG_FTM_DISCTF,     []{ _M493_S(ftMotionMode_DISCTF); });
+    #endif
+
+    END_MENU();
+  }
+
+  #if HAS_DYNAMIC_FREQ
+
+    void _M493_D(const dynFreqMode_t d) {
+      char cmd[10];
+      sprintf_P(cmd, PSTR("M493D%i"), int(d));
+      gcode.process_subcommands_now(cmd);
+      ui.go_back();
+    }
+
+    inline void menu_ftm_dyn_mode() {
+      const dynFreqMode_t dmode = fxdTiCtrl.cfg.dynFreqMode;
+
+      START_MENU();
+      BACK_ITEM(MSG_FIXED_TIME_MOTION);
+
+      if (dmode != dynFreqMode_DISABLED) ACTION_ITEM(MSG_LCD_OFF, []{ _M493_D(dynFreqMode_DISABLED); });
+      #if HAS_DYNAMIC_FREQ_MM
+        if (dmode != dynFreqMode_Z_BASED) ACTION_ITEM(MSG_FTM_Z_BASED, []{ _M493_D(dynFreqMode_Z_BASED); });
+      #endif
+      #if HAS_DYNAMIC_FREQ_G
+        if (dmode != dynFreqMode_MASS_BASED) ACTION_ITEM(MSG_FTM_MASS_BASED, []{ _M493_D(dynFreqMode_MASS_BASED); });
+      #endif
+
+      END_MENU();
+    }
+
+  #endif // HAS_DYNAMIC_FREQ
+
+  void menu_ft_motion() {
+    ft_config_t &c = fxdTiCtrl.cfg;
+
+    FSTR_P ftmode;
+    switch (c.mode) {
+      default:
+      case ftMotionMode_DISABLED: ftmode = GET_TEXT_F(MSG_LCD_OFF);  break;
+      case ftMotionMode_ENABLED:  ftmode = GET_TEXT_F(MSG_LCD_ON);   break;
+      case ftMotionMode_ZV:       ftmode = GET_TEXT_F(MSG_FTM_ZV);   break;
+      case ftMotionMode_ZVD:      ftmode = GET_TEXT_F(MSG_FTM_ZVD);  break;
+      case ftMotionMode_EI:       ftmode = GET_TEXT_F(MSG_FTM_EI);   break;
+      case ftMotionMode_2HEI:     ftmode = GET_TEXT_F(MSG_FTM_2HEI); break;
+      case ftMotionMode_3HEI:     ftmode = GET_TEXT_F(MSG_FTM_3HEI); break;
+      case ftMotionMode_MZV:      ftmode = GET_TEXT_F(MSG_FTM_MZV);  break;
+      //case ftMotionMode_ULENDO_FBS: ftmode = GET_TEXT_F(MSG_FTM_ULENDO_FBS); break;
+      //case ftMotionMode_DISCTF:     ftmode = GET_TEXT_F(MSG_FTM_DISCTF);     break;
+    }
+
+    #if HAS_DYNAMIC_FREQ
+      FSTR_P dmode;
+      switch (c.dynFreqMode) {
+        default:
+        case dynFreqMode_DISABLED:   dmode = GET_TEXT_F(MSG_LCD_OFF);        break;
+        case dynFreqMode_Z_BASED:    dmode = GET_TEXT_F(MSG_FTM_Z_BASED);    break;
+        case dynFreqMode_MASS_BASED: dmode = GET_TEXT_F(MSG_FTM_MASS_BASED); break;
+      }
+    #endif
+
+    START_MENU();
+    BACK_ITEM(MSG_ADVANCED_SETTINGS);
+
+    SUBMENU(MSG_FTM_MODE, menu_ftm_mode);
+    MENU_ITEM_ADDON_START_RJ(5); lcd_put_u8str(ftmode); MENU_ITEM_ADDON_END();
+
+    #if HAS_X_AXIS
+      EDIT_ITEM_FAST_N(float42_52, X_AXIS, MSG_FTM_BASE_FREQ_N, &c.baseFreq[X_AXIS], FTM_MIN_SHAPE_FREQ, (FTM_FS) / 2, fxdTiCtrl.refreshShapingN);
+    #endif
+    #if HAS_Y_AXIS
+      EDIT_ITEM_FAST_N(float42_52, Y_AXIS, MSG_FTM_BASE_FREQ_N, &c.baseFreq[Y_AXIS], FTM_MIN_SHAPE_FREQ, (FTM_FS) / 2, fxdTiCtrl.refreshShapingN);
+    #endif
+
+    #if HAS_DYNAMIC_FREQ
+      if (c.modeHasShaper()) {
+        SUBMENU(MSG_FTM_DYN_MODE, menu_ftm_dyn_mode);
+        MENU_ITEM_ADDON_START_RJ(11); lcd_put_u8str(dmode); MENU_ITEM_ADDON_END();
+        #if HAS_X_AXIS
+          EDIT_ITEM_FAST_N(float42_52, X_AXIS, MSG_FTM_DFREQ_K_N, &c.dynFreqK[X_AXIS], 0.0f, 20.0f);
+        #endif
+        #if HAS_Y_AXIS
+          EDIT_ITEM_FAST_N(float42_52, Y_AXIS, MSG_FTM_DFREQ_K_N, &c.dynFreqK[Y_AXIS], 0.0f, 20.0f);
+        #endif
+      }
+    #endif
+    #if HAS_EXTRUDERS
+      EDIT_ITEM(bool, MSG_LINEAR_ADVANCE, &c.linearAdvEna);
+      EDIT_ITEM(float42_52, MSG_ADVANCE_K, &c.linearAdvK, 0, 10);
+    #endif
+
+    END_MENU();
+  }
+
+#endif // FT_MOTION_MENU
+
 void menu_motion() {
+  #if ENABLED(FT_MOTION_MENU)
+    const bool is_busy = printer_busy();
+  #endif
+
   START_MENU();
 
   //
@@ -339,6 +468,13 @@ void menu_motion() {
     #endif
   #endif
 
+  //
+  // M493 - Fixed-Time Motion
+  //
+  #if ENABLED(FT_MOTION_MENU)
+    if (!is_busy) SUBMENU(MSG_FIXED_TIME_MOTION, menu_ft_motion);
+  #endif
+
   //
   // Pen up/down menu
   //
diff --git a/Marlin/src/module/ft_motion.cpp b/Marlin/src/module/ft_motion.cpp
index 407296f0303..d6c834cbc48 100644
--- a/Marlin/src/module/ft_motion.cpp
+++ b/Marlin/src/module/ft_motion.cpp
@@ -29,32 +29,28 @@
 
 FxdTiCtrl fxdTiCtrl;
 
+#if !HAS_X_AXIS
+  static_assert(FTM_DEFAULT_MODE == ftMotionMode_ZV, "ftMotionMode_ZV requires at least one linear axis.");
+  static_assert(FTM_DEFAULT_MODE == ftMotionMode_ZVD, "ftMotionMode_ZVD requires at least one linear axis.");
+  static_assert(FTM_DEFAULT_MODE == ftMotionMode_EI, "ftMotionMode_EI requires at least one linear axis.");
+  static_assert(FTM_DEFAULT_MODE == ftMotionMode_2HEI, "ftMotionMode_2HEI requires at least one linear axis.");
+  static_assert(FTM_DEFAULT_MODE == ftMotionMode_3HEI, "ftMotionMode_3HEI requires at least one linear axis.");
+  static_assert(FTM_DEFAULT_MODE == ftMotionMode_MZV, "ftMotionMode_MZV requires at least one linear axis.");
+#endif
+#if !HAS_DYNAMIC_FREQ_MM
+  static_assert(FTM_DEFAULT_DYNFREQ_MODE != dynFreqMode_Z_BASED, "dynFreqMode_Z_BASED requires a Z axis.");
+#endif
+#if !HAS_DYNAMIC_FREQ_G
+  static_assert(FTM_DEFAULT_DYNFREQ_MODE != dynFreqMode_MASS_BASED, "dynFreqMode_MASS_BASED requires an X axis and an extruder.");
+#endif
+
 //-----------------------------------------------------------------//
 // Variables.
 //-----------------------------------------------------------------//
 
 // Public variables.
-ftMotionMode_t FxdTiCtrl::cfg_mode = FTM_DEFAULT_MODE;                // Mode / active compensation mode configuration.
-
-#if HAS_EXTRUDERS
-  bool FxdTiCtrl::cfg_linearAdvEna = FTM_LINEAR_ADV_DEFAULT_ENA;      // Linear advance enable configuration.
-  float FxdTiCtrl::cfg_linearAdvK = FTM_LINEAR_ADV_DEFAULT_K;         // Linear advance gain.
-#endif
-
-dynFreqMode_t FxdTiCtrl::cfg_dynFreqMode = FTM_DEFAULT_DYNFREQ_MODE;  // Dynamic frequency mode configuration.
-#if !HAS_Z_AXIS
-  static_assert(FTM_DEFAULT_DYNFREQ_MODE != dynFreqMode_Z_BASED, "dynFreqMode_Z_BASED requires a Z axis.");
-#endif
-#if !(HAS_X_AXIS && HAS_EXTRUDERS)
-  static_assert(FTM_DEFAULT_DYNFREQ_MODE != dynFreqMode_MASS_BASED, "dynFreqMode_MASS_BASED requires an X axis and an extruder.");
-#endif
-
-#if HAS_X_AXIS
-  float FxdTiCtrl::cfg_baseFreq[] = {  FTM_SHAPING_DEFAULT_X_FREQ     // Base frequency. [Hz]
-                    OPTARG(HAS_Y_AXIS, FTM_SHAPING_DEFAULT_Y_FREQ) };
-  float FxdTiCtrl::cfg_dynFreqK[] = { 0.0f OPTARG(HAS_Y_AXIS, 0.0f) };      // Scaling / gain for dynamic frequency. [Hz/mm] or [Hz/g]
-#endif
 
+ft_config_t FxdTiCtrl::cfg;
 ft_command_t FxdTiCtrl::stepperCmdBuff[FTM_STEPPERCMD_BUFF_SIZE] = {0U};                // Buffer of stepper commands.
 hal_timer_t FxdTiCtrl::stepperCmdBuff_StepRelativeTi[FTM_STEPPERCMD_BUFF_SIZE] = {0U};  // Buffer of the stepper command timing.
 uint8_t FxdTiCtrl::stepperCmdBuff_ApplyDir[FTM_STEPPERCMD_DIR_SIZE] = {0U};             // Buffer of whether DIR needs to be updated.
@@ -209,7 +205,7 @@ void FxdTiCtrl::runoutBlock() {
 // Controller main, to be invoked from non-isr task.
 void FxdTiCtrl::loop() {
 
-  if (!cfg_mode) return;
+  if (!cfg.mode) return;
 
   // Handle block abort with the following sequence:
   // 1. Zero out commands in stepper ISR.
@@ -291,7 +287,7 @@ void FxdTiCtrl::loop() {
     const float K = exp( -zeta * M_PI / sqrt(1.0f - sq(zeta)) ),
                 K2 = sq(K);
 
-    switch (cfg_mode) {
+    switch (cfg.mode) {
 
       case ftMotionMode_ZV:
         xy_max_i = 1U;
@@ -363,7 +359,7 @@ void FxdTiCtrl::loop() {
 
     const float df = sqrt(1.0f - sq(zeta));
 
-    switch (cfg_mode) {
+    switch (cfg.mode) {
       case ftMotionMode_ZV:
         x_Ni[1] = round((0.5f / xf / df) * (FTM_FS));
         #if HAS_Y_AXIS
@@ -472,8 +468,8 @@ uint32_t FxdTiCtrl::stepperCmdBuffItems() {
 // Initializes storage variables before startup.
 void FxdTiCtrl::init() {
   #if HAS_X_AXIS
-    updateShapingN(cfg_baseFreq[0] OPTARG(HAS_Y_AXIS, cfg_baseFreq[1]));
-    updateShapingA(FTM_SHAPING_ZETA, FTM_SHAPING_V_TOL);
+    refreshShapingN();
+    updateShapingA();
   #endif
   reset(); // Precautionary.
 }
@@ -606,9 +602,9 @@ void FxdTiCtrl::makeVector() {
 
   #if HAS_EXTRUDERS
     const float new_raw_z1 = e_startPosn + e_Ratio * dist;
-    if (cfg_linearAdvEna) {
+    if (cfg.linearAdvEna) {
       float dedt_adj = (new_raw_z1 - e_raw_z1) * (FTM_FS);
-      if (e_Ratio > 0.0f) dedt_adj += accel_k * cfg_linearAdvK;
+      if (e_Ratio > 0.0f) dedt_adj += accel_k * cfg.linearAdvK;
 
       e_advanced_z1 += dedt_adj * (FTM_TS);
       ed[makeVector_batchIdx] = e_advanced_z1;
@@ -622,28 +618,28 @@ void FxdTiCtrl::makeVector() {
   #endif
 
   // Update shaping parameters if needed.
-  #if HAS_Z_AXIS
+  #if HAS_DYNAMIC_FREQ_MM
     static float zd_z1 = 0.0f;
   #endif
-  switch (cfg_dynFreqMode) {
+  switch (cfg.dynFreqMode) {
 
-    #if HAS_Z_AXIS
+    #if HAS_DYNAMIC_FREQ_MM
       case dynFreqMode_Z_BASED:
         if (zd[makeVector_batchIdx] != zd_z1) { // Only update if Z changed.
-          const float xf = cfg_baseFreq[0] + cfg_dynFreqK[0] * zd[makeVector_batchIdx],
-                      yf = cfg_baseFreq[1] + cfg_dynFreqK[1] * zd[makeVector_batchIdx];
+          const float xf = cfg.baseFreq[X_AXIS] + cfg.dynFreqK[X_AXIS] * zd[makeVector_batchIdx],
+                      yf = cfg.baseFreq[Y_AXIS] + cfg.dynFreqK[Y_AXIS] * zd[makeVector_batchIdx];
           updateShapingN(_MAX(xf, FTM_MIN_SHAPE_FREQ), _MAX(yf, FTM_MIN_SHAPE_FREQ));
           zd_z1 = zd[makeVector_batchIdx];
         }
         break;
     #endif
 
-    #if HAS_X_AXIS && HAS_EXTRUDERS
+    #if HAS_DYNAMIC_FREQ_G
       case dynFreqMode_MASS_BASED:
         // Update constantly. The optimization done for Z value makes
         // less sense for E, as E is expected to constantly change.
-        updateShapingN(      cfg_baseFreq[0] + cfg_dynFreqK[0] * ed[makeVector_batchIdx]
-          OPTARG(HAS_Y_AXIS, cfg_baseFreq[1] + cfg_dynFreqK[1] * ed[makeVector_batchIdx]) );
+        updateShapingN(      cfg.baseFreq[X_AXIS] + cfg.dynFreqK[X_AXIS] * ed[makeVector_batchIdx]
+          OPTARG(HAS_Y_AXIS, cfg.baseFreq[Y_AXIS] + cfg.dynFreqK[Y_AXIS] * ed[makeVector_batchIdx]) );
         break;
     #endif
 
@@ -652,7 +648,7 @@ void FxdTiCtrl::makeVector() {
 
   // Apply shaping if in mode.
   #if HAS_X_AXIS
-    if (WITHIN(cfg_mode, 10U, 19U)) {
+    if (WITHIN(cfg.mode, 10U, 19U)) {
       xd_zi[xy_zi_idx] = xd[makeVector_batchIdx];
       xd[makeVector_batchIdx] *= x_Ai[0];
       #if HAS_Y_AXIS
diff --git a/Marlin/src/module/ft_motion.h b/Marlin/src/module/ft_motion.h
index a277f3ac26e..232d191cda6 100644
--- a/Marlin/src/module/ft_motion.h
+++ b/Marlin/src/module/ft_motion.h
@@ -28,20 +28,69 @@
 
 #define FTM_STEPPERCMD_DIR_SIZE ((FTM_STEPPERCMD_BUFF_SIZE + 7) / 8)
 
+#if HAS_X_AXIS && (HAS_Z_AXIS || HAS_EXTRUDERS)
+  #define HAS_DYNAMIC_FREQ 1
+  #if HAS_Z_AXIS
+    #define HAS_DYNAMIC_FREQ_MM 1
+  #endif
+  #if HAS_EXTRUDERS
+    #define HAS_DYNAMIC_FREQ_G 1
+  #endif
+#endif
+
+typedef struct FTConfig {
+  ftMotionMode_t mode = FTM_DEFAULT_MODE;                   // Mode / active compensation mode configuration.
+
+  bool modeHasShaper() { return WITHIN(mode, 10U, 19U); }
+
+  #if HAS_X_AXIS
+    float baseFreq[1 + ENABLED(HAS_Y_AXIS)] =               // Base frequency. [Hz]
+      { FTM_SHAPING_DEFAULT_X_FREQ OPTARG(HAS_Y_AXIS, FTM_SHAPING_DEFAULT_Y_FREQ) };
+  #endif
+
+  #if HAS_DYNAMIC_FREQ
+    dynFreqMode_t dynFreqMode = FTM_DEFAULT_DYNFREQ_MODE;   // Dynamic frequency mode configuration.
+    float dynFreqK[1 + ENABLED(HAS_Y_AXIS)] = { 0.0f };     // Scaling / gain for dynamic frequency. [Hz/mm] or [Hz/g]
+  #else
+    static constexpr dynFreqMode_t dynFreqMode = dynFreqMode_DISABLED;
+  #endif
+
+  #if HAS_EXTRUDERS
+    bool linearAdvEna = FTM_LINEAR_ADV_DEFAULT_ENA;         // Linear advance enable configuration.
+    float linearAdvK = FTM_LINEAR_ADV_DEFAULT_K;            // Linear advance gain.
+  #endif
+} ft_config_t;
+
 class FxdTiCtrl {
 
   public:
 
     // Public variables
-    static ftMotionMode_t cfg_mode;                         // Mode / active compensation mode configuration.
-    static bool cfg_linearAdvEna;                           // Linear advance enable configuration.
-    static float cfg_linearAdvK;                            // Linear advance gain.
-    static dynFreqMode_t cfg_dynFreqMode;                   // Dynamic frequency mode configuration.
+    static ft_config_t cfg;
 
-    #if HAS_X_AXIS
-      static float cfg_baseFreq[1 + ENABLED(HAS_Y_AXIS)];   // Base frequency. [Hz]
-      static float cfg_dynFreqK[1 + ENABLED(HAS_Y_AXIS)];   // Scaling / gain for dynamic frequency. [Hz/mm] or [Hz/g]
-    #endif
+    static void set_defaults() {
+      cfg.mode = FTM_DEFAULT_MODE;
+
+      TERN_(HAS_X_AXIS, cfg.baseFreq[X_AXIS] = FTM_SHAPING_DEFAULT_X_FREQ);
+      TERN_(HAS_Y_AXIS, cfg.baseFreq[Y_AXIS] = FTM_SHAPING_DEFAULT_Y_FREQ);
+
+      #if HAS_DYNAMIC_FREQ
+        cfg.dynFreqMode = FTM_DEFAULT_DYNFREQ_MODE;
+        cfg.dynFreqK[X_AXIS] = TERN_(HAS_Y_AXIS, cfg.dynFreqK[Y_AXIS]) = 0.0f;
+      #endif
+
+      #if HAS_EXTRUDERS
+        cfg.linearAdvEna = FTM_LINEAR_ADV_DEFAULT_ENA;
+        cfg.linearAdvK = FTM_LINEAR_ADV_DEFAULT_K;
+      #endif
+
+      #if HAS_X_AXIS
+        refreshShapingN();
+        updateShapingA();
+      #endif
+
+      reset();
+    }
 
     static ft_command_t stepperCmdBuff[FTM_STEPPERCMD_BUFF_SIZE];               // Buffer of stepper commands.
     static hal_timer_t stepperCmdBuff_StepRelativeTi[FTM_STEPPERCMD_BUFF_SIZE]; // Buffer of the stepper command timing.
@@ -68,6 +117,9 @@ class FxdTiCtrl {
       // Refresh the indices used by shaping functions.
       // To be called when frequencies change.
       static void updateShapingN(const_float_t xf OPTARG(HAS_Y_AXIS, const_float_t yf), const_float_t zeta=FTM_SHAPING_ZETA);
+
+      static void refreshShapingN() { updateShapingN(cfg.baseFreq[X_AXIS] OPTARG(HAS_Y_AXIS, cfg.baseFreq[Y_AXIS])); }
+
     #endif
 
     static void reset();                                    // Resets all states of the fixed time conversion to defaults.
diff --git a/Marlin/src/module/ft_types.h b/Marlin/src/module/ft_types.h
index 613e177a391..a3239a92464 100644
--- a/Marlin/src/module/ft_types.h
+++ b/Marlin/src/module/ft_types.h
@@ -26,14 +26,14 @@
 typedef enum FXDTICtrlMode : uint8_t {
   ftMotionMode_DISABLED   =  0U,
   ftMotionMode_ENABLED    =  1U,
-  ftMotionMode_ULENDO_FBS =  2U,
+  //ftMotionMode_ULENDO_FBS = 2U,
   ftMotionMode_ZV         = 10U,
   ftMotionMode_ZVD        = 11U,
   ftMotionMode_EI         = 12U,
   ftMotionMode_2HEI       = 13U,
   ftMotionMode_3HEI       = 14U,
   ftMotionMode_MZV        = 15U,
-  ftMotionMode_DISCTF     = 20U
+  //ftMotionMode_DISCTF   = 20U
 } ftMotionMode_t;
 
 enum dynFreqMode_t : uint8_t {
diff --git a/Marlin/src/module/planner.cpp b/Marlin/src/module/planner.cpp
index b0d0b3e353c..02a7d05cae3 100644
--- a/Marlin/src/module/planner.cpp
+++ b/Marlin/src/module/planner.cpp
@@ -1692,7 +1692,7 @@ void Planner::quick_stop() {
   // Restart the block delay for the first movement - As the queue was
   // forced to empty, there's no risk the ISR will touch this.
 
-  delay_before_delivering = TERN_(FT_MOTION, fxdTiCtrl.cfg_mode ? BLOCK_DELAY_NONE :) BLOCK_DELAY_FOR_1ST_MOVE;
+  delay_before_delivering = TERN_(FT_MOTION, fxdTiCtrl.cfg.mode ? BLOCK_DELAY_NONE :) BLOCK_DELAY_FOR_1ST_MOVE;
 
   TERN_(HAS_WIRED_LCD, clear_block_buffer_runtime()); // Clear the accumulated runtime
 
@@ -1851,7 +1851,7 @@ bool Planner::_buffer_steps(const xyze_long_t &target
     // As there are no queued movements, the Stepper ISR will not touch this
     // variable, so there is no risk setting this here (but it MUST be done
     // before the following line!!)
-    delay_before_delivering = TERN_(FT_MOTION, fxdTiCtrl.cfg_mode ? BLOCK_DELAY_NONE :) BLOCK_DELAY_FOR_1ST_MOVE;
+    delay_before_delivering = TERN_(FT_MOTION, fxdTiCtrl.cfg.mode ? BLOCK_DELAY_NONE :) BLOCK_DELAY_FOR_1ST_MOVE;
   }
 
   // Move buffer head
@@ -2924,7 +2924,7 @@ void Planner::buffer_sync_block(const BlockFlagBit sync_flag/*=BLOCK_BIT_SYNC_PO
     // As there are no queued movements, the Stepper ISR will not touch this
     // variable, so there is no risk setting this here (but it MUST be done
     // before the following line!!)
-    delay_before_delivering = TERN_(FT_MOTION, fxdTiCtrl.cfg_mode ? BLOCK_DELAY_NONE :) BLOCK_DELAY_FOR_1ST_MOVE;
+    delay_before_delivering = TERN_(FT_MOTION, fxdTiCtrl.cfg.mode ? BLOCK_DELAY_NONE :) BLOCK_DELAY_FOR_1ST_MOVE;
   }
 
   block_buffer_head = next_buffer_head;
@@ -3217,7 +3217,7 @@ bool Planner::buffer_line(const xyze_pos_t &cart, const_feedRate_t fr_mm_s
       // As there are no queued movements, the Stepper ISR will not touch this
       // variable, so there is no risk setting this here (but it MUST be done
       // before the following line!!)
-      delay_before_delivering = TERN_(FT_MOTION, fxdTiCtrl.cfg_mode ? BLOCK_DELAY_NONE :) BLOCK_DELAY_FOR_1ST_MOVE;
+      delay_before_delivering = TERN_(FT_MOTION, fxdTiCtrl.cfg.mode ? BLOCK_DELAY_NONE :) BLOCK_DELAY_FOR_1ST_MOVE;
     }
 
     // Move buffer head
diff --git a/Marlin/src/module/settings.cpp b/Marlin/src/module/settings.cpp
index 53e98a46265..b6e605e853e 100644
--- a/Marlin/src/module/settings.cpp
+++ b/Marlin/src/module/settings.cpp
@@ -111,6 +111,10 @@
   #include "../feature/backlash.h"
 #endif
 
+#if ENABLED(FT_MOTION)
+  #include "../module/ft_motion.h"
+#endif
+
 #if HAS_FILAMENT_SENSOR
   #include "../feature/runout.h"
   #ifndef FIL_RUNOUT_ENABLED_DEFAULT
@@ -594,16 +598,23 @@ typedef struct SettingsDataStruct {
     MPC_t mpc_constants[HOTENDS];                       // M306
   #endif
 
+  //
+  // Fixed-Time Motion
+  //
+  #if ENABLED(FT_MOTION)
+    ft_config_t fxdTiCtrl_cfg;                          // M493
+  #endif
+
   //
   // Input Shaping
   //
   #if ENABLED(INPUT_SHAPING_X)
-    float shaping_x_frequency, // M593 X F
-          shaping_x_zeta;      // M593 X D
+    float shaping_x_frequency,                          // M593 X F
+          shaping_x_zeta;                               // M593 X D
   #endif
   #if ENABLED(INPUT_SHAPING_Y)
-    float shaping_y_frequency, // M593 Y F
-          shaping_y_zeta;      // M593 Y D
+    float shaping_y_frequency,                          // M593 Y F
+          shaping_y_zeta;                               // M593 Y D
   #endif
 
 } SettingsData;
@@ -1648,6 +1659,14 @@ void MarlinSettings::postprocess() {
       HOTEND_LOOP() EEPROM_WRITE(thermalManager.temp_hotend[e].mpc);
     #endif
 
+    //
+    // Fixed-Time Motion
+    //
+    #if ENABLED(FT_MOTION)
+      _FIELD_TEST(fxdTiCtrl_cfg);
+      EEPROM_WRITE(fxdTiCtrl.cfg);
+    #endif
+
     //
     // Input Shaping
     ///
@@ -2646,9 +2665,15 @@ void MarlinSettings::postprocess() {
       // Model predictive control
       //
       #if ENABLED(MPCTEMP)
-      {
         HOTEND_LOOP() EEPROM_READ(thermalManager.temp_hotend[e].mpc);
-      }
+      #endif
+
+      //
+      // Fixed-Time Motion
+      //
+      #if ENABLED(FT_MOTION)
+        _FIELD_TEST(fxdTiCtrl_cfg);
+        EEPROM_READ(fxdTiCtrl.cfg);
       #endif
 
       //
@@ -3445,6 +3470,11 @@ void MarlinSettings::reset() {
     }
   #endif
 
+  //
+  // Fixed-Time Motion
+  //
+  TERN_(FT_MOTION, fxdTiCtrl.set_defaults());
+
   //
   // Input Shaping
   //
@@ -3706,6 +3736,11 @@ void MarlinSettings::reset() {
     //
     TERN_(HAS_STEALTHCHOP, gcode.M569_report(forReplay));
 
+    //
+    // Fixed-Time Motion
+    //
+    TERN_(FT_MOTION, gcode.M493_report(forReplay));
+
     //
     // Input Shaping
     //
diff --git a/Marlin/src/module/stepper.cpp b/Marlin/src/module/stepper.cpp
index b23667b8201..3188f77da8b 100644
--- a/Marlin/src/module/stepper.cpp
+++ b/Marlin/src/module/stepper.cpp
@@ -1497,7 +1497,7 @@ void Stepper::isr() {
     #if ENABLED(FT_MOTION)
 
       // NOTE STEPPER_TIMER_RATE is equal to 2000000, not what VSCode shows
-      const bool using_fxtictrl = fxdTiCtrl.cfg_mode;
+      const bool using_fxtictrl = fxdTiCtrl.cfg.mode;
       if (using_fxtictrl) {
         if (!nextMainISR) {
           if (abort_current_block) {
diff --git a/buildroot/tests/STM32F103RC_btt b/buildroot/tests/STM32F103RC_btt
index 95a18c615ff..d0da6305e53 100755
--- a/buildroot/tests/STM32F103RC_btt
+++ b/buildroot/tests/STM32F103RC_btt
@@ -12,7 +12,7 @@ set -e
 restore_configs
 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
-opt_enable PINS_DEBUGGING Z_IDLE_HEIGHT FT_MOTION
+opt_enable CR10_STOCKDISPLAY PINS_DEBUGGING Z_IDLE_HEIGHT FT_MOTION FT_MOTION_MENU
 exec_test $1 $2 "BigTreeTech SKR Mini E3 1.0 - TMC2209 HW Serial, FT_MOTION" "$3"
 
 # clean up