From 3da29b4a04691d4e8dc8691b006cf51c0bf46fc9 Mon Sep 17 00:00:00 2001
From: tombrazier <68918209+tombrazier@users.noreply.github.com>
Date: Sun, 10 Apr 2022 07:20:05 +0100
Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=B8=20Improve=20MPC=20tuning,=20add=20?=
 =?UTF-8?q?menu=20items=20(#23984)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Scott Lahteine <thinkyhead@users.noreply.github.com>
---
 Marlin/Configuration.h                |   4 +
 Marlin/src/core/language.h            |   8 ++
 Marlin/src/gcode/temp/M306.cpp        |  12 +--
 Marlin/src/inc/SanityCheck.h          |   4 +
 Marlin/src/lcd/language/language_en.h |   8 ++
 Marlin/src/lcd/menu/menu_advanced.cpp |  51 ++++++++-
 Marlin/src/module/temperature.cpp     | 145 ++++++++++++++++----------
 buildroot/tests/BIGTREE_GTR_V1_0      |  12 ++-
 buildroot/tests/STM32F103RC_btt       |   1 -
 buildroot/tests/STM32F103RC_btt_maple |   1 -
 buildroot/tests/mega2560              |   5 +-
 11 files changed, 177 insertions(+), 74 deletions(-)

diff --git a/Marlin/Configuration.h b/Marlin/Configuration.h
index 1adf9e9442a..e2d99a44ecc 100644
--- a/Marlin/Configuration.h
+++ b/Marlin/Configuration.h
@@ -643,6 +643,9 @@
  * for PID_EXTRUSION_SCALING and PID_FAN_SCALING. Use M306 to autotune the model.
  */
 #if ENABLED(MPCTEMP)
+  //#define MPC_EDIT_MENU                             // Add MPC editing to the "Advanced Settings" menu. (~1300 bytes of flash)
+  //#define MPC_AUTOTUNE_MENU                         // Add MPC auto-tuning to the "Advanced Settings" menu. (~350 bytes of flash)
+
   #define MPC_MAX BANG_MAX                            // (0..255) Current to nozzle while MPC is active.
   #define MPC_HEATER_POWER { 40.0f }                  // (W) Heat cartridge powers.
 
@@ -671,6 +674,7 @@
   #define MPC_STEADYSTATE 0.5f                        // (K/s) Temperature change rate for steady state logic to be enforced.
 
   #define MPC_TUNING_POS { X_CENTER, Y_CENTER, 1.0f } // (mm) M306 Autotuning position, ideally bed center just above the surface.
+  #define MPC_TUNING_END_Z 10.0f                      // (mm) M306 Autotuning final Z position.
 #endif
 
 //===========================================================================
diff --git a/Marlin/src/core/language.h b/Marlin/src/core/language.h
index 5dc8a77a318..16d7b1bf663 100644
--- a/Marlin/src/core/language.h
+++ b/Marlin/src/core/language.h
@@ -232,6 +232,14 @@
 #define STR_PID_DEBUG_DTERM                 " dTerm "
 #define STR_PID_DEBUG_CTERM                 " cTerm "
 #define STR_INVALID_EXTRUDER_NUM            " - Invalid extruder number !"
+#define STR_MPC_AUTOTUNE                    "MPC Autotune"
+#define STR_MPC_AUTOTUNE_START              " start for " STR_E
+#define STR_MPC_AUTOTUNE_INTERRUPTED        " interrupted!"
+#define STR_MPC_AUTOTUNE_FINISHED           " finished! Put the constants below into Configuration.h"
+#define STR_MPC_COOLING_TO_AMBIENT          "Cooling to ambient"
+#define STR_MPC_HEATING_PAST_200            "Heating to over 200C"
+#define STR_MPC_MEASURING_AMBIENT           "Measuring ambient heatloss at "
+#define STR_MPC_TEMPERATURE_ERROR           "Temperature error"
 
 #define STR_HEATER_BED                      "bed"
 #define STR_HEATER_CHAMBER                  "chamber"
diff --git a/Marlin/src/gcode/temp/M306.cpp b/Marlin/src/gcode/temp/M306.cpp
index 9e1a8dd8ef0..307972eec1d 100644
--- a/Marlin/src/gcode/temp/M306.cpp
+++ b/Marlin/src/gcode/temp/M306.cpp
@@ -56,17 +56,7 @@ void GcodeSuite::M306() {
     return;
   }
 
-  HOTEND_LOOP() {
-    SERIAL_ECHOLNPGM("MPC constants for hotend ", e);
-    MPC_t& constants = thermalManager.temp_hotend[e].constants;
-    SERIAL_ECHOLNPGM("Heater power: ", constants.heater_power);
-    SERIAL_ECHOLNPGM("Heatblock heat capacity: ", constants.block_heat_capacity);
-    SERIAL_ECHOLNPAIR_F("Sensor responsivness: ", constants.sensor_responsiveness, 4);
-    SERIAL_ECHOLNPAIR_F("Ambient heat transfer coeff. (no fan): ", constants.ambient_xfer_coeff_fan0, 4);
-    #if ENABLED(MPC_INCLUDE_FAN)
-      SERIAL_ECHOLNPAIR_F("Ambient heat transfer coeff. (full fan): ", constants.ambient_xfer_coeff_fan0 + constants.fan255_adjustment, 4);
-    #endif
-  }
+  M306_report(true);
 }
 
 void GcodeSuite::M306_report(const bool forReplay/*=true*/) {
diff --git a/Marlin/src/inc/SanityCheck.h b/Marlin/src/inc/SanityCheck.h
index 1da0bf4a891..59effff1a31 100644
--- a/Marlin/src/inc/SanityCheck.h
+++ b/Marlin/src/inc/SanityCheck.h
@@ -2964,6 +2964,8 @@ static_assert(Y_MAX_LENGTH >= Y_BED_SIZE, "Movement bounds (Y_MIN_POS, Y_MAX_POS
     #error "DWIN_CREALITY_LCD requires SDSUPPORT to be enabled."
   #elif EITHER(PID_EDIT_MENU, PID_AUTOTUNE_MENU)
     #error "DWIN_CREALITY_LCD does not support PID_EDIT_MENU or PID_AUTOTUNE_MENU."
+  #elif EITHER(MPC_EDIT_MENU, MPC_AUTOTUNE_MENU)
+    #error "DWIN_CREALITY_LCD does not support MPC_EDIT_MENU or MPC_AUTOTUNE_MENU."
   #elif ENABLED(LEVEL_BED_CORNERS)
     #error "DWIN_CREALITY_LCD does not support LEVEL_BED_CORNERS."
   #elif BOTH(LCD_BED_LEVELING, PROBE_MANUALLY)
@@ -2974,6 +2976,8 @@ static_assert(Y_MAX_LENGTH >= Y_BED_SIZE, "Movement bounds (Y_MIN_POS, Y_MAX_POS
     #error "DWIN_LCD_PROUI requires SDSUPPORT to be enabled."
   #elif EITHER(PID_EDIT_MENU, PID_AUTOTUNE_MENU)
     #error "DWIN_LCD_PROUI does not support PID_EDIT_MENU or PID_AUTOTUNE_MENU."
+  #elif EITHER(MPC_EDIT_MENU, MPC_AUTOTUNE_MENU)
+    #error "DWIN_LCD_PROUI does not support MPC_EDIT_MENU or MPC_AUTOTUNE_MENU."
   #elif ENABLED(LEVEL_BED_CORNERS)
     #error "DWIN_LCD_PROUI does not support LEVEL_BED_CORNERS."
   #elif BOTH(LCD_BED_LEVELING, PROBE_MANUALLY)
diff --git a/Marlin/src/lcd/language/language_en.h b/Marlin/src/lcd/language/language_en.h
index 01470f80073..6ab0cb5a401 100644
--- a/Marlin/src/lcd/language/language_en.h
+++ b/Marlin/src/lcd/language/language_en.h
@@ -350,6 +350,14 @@ namespace Language_en {
   LSTR MSG_PID_BAD_EXTRUDER_NUM           = _UxGT("Autotune failed. Bad extruder.");
   LSTR MSG_PID_TEMP_TOO_HIGH              = _UxGT("Autotune failed. Temperature too high.");
   LSTR MSG_PID_TIMEOUT                    = _UxGT("Autotune failed! Timeout.");
+  LSTR MSG_MPC_MEASURING_AMBIENT          = _UxGT("Testing heat loss");
+  LSTR MSG_MPC_AUTOTUNE                   = _UxGT("MPC Autotune");
+  LSTR MSG_MPC_EDIT                       = _UxGT("Edit * MPC");
+  LSTR MSG_MPC_POWER_E                    = _UxGT("Power *");
+  LSTR MSG_MPC_BLOCK_HEAT_CAPACITY_E      = _UxGT("Block C *");
+  LSTR MSG_SENSOR_RESPONSIVENESS_E        = _UxGT("Sensor res *");
+  LSTR MSG_MPC_AMBIENT_XFER_COEFF_E       = _UxGT("Ambient h *");
+  LSTR MSG_MPC_AMBIENT_XFER_COEFF_FAN255_E= _UxGT("Amb. h fan *");
   LSTR MSG_SELECT                         = _UxGT("Select");
   LSTR MSG_SELECT_E                       = _UxGT("Select *");
   LSTR MSG_ACC                            = _UxGT("Accel");
diff --git a/Marlin/src/lcd/menu/menu_advanced.cpp b/Marlin/src/lcd/menu/menu_advanced.cpp
index a9fb58bb0b1..ea81003f23d 100644
--- a/Marlin/src/lcd/menu/menu_advanced.cpp
+++ b/Marlin/src/lcd/menu/menu_advanced.cpp
@@ -244,7 +244,7 @@ void menu_backlash();
   }
 #endif
 
-#if BOTH(AUTOTEMP, HAS_TEMP_HOTEND) || EITHER(PID_AUTOTUNE_MENU, PID_EDIT_MENU)
+#if BOTH(AUTOTEMP, HAS_TEMP_HOTEND) || ANY(PID_AUTOTUNE_MENU, PID_EDIT_MENU, MPC_AUTOTUNE_MENU, MPC_EDIT_MENU)
   #define SHOW_MENU_ADVANCED_TEMPERATURE 1
 #endif
 
@@ -253,7 +253,17 @@ void menu_backlash();
 //
 #if SHOW_MENU_ADVANCED_TEMPERATURE
 
+  #if ENABLED(MPC_EDIT_MENU)
+    #define MPC_EDIT_DEFS(N) \
+      MPC_t &c = thermalManager.temp_hotend[N].constants; \
+      TERN(MPC_INCLUDE_FAN, editable.decimal = c.ambient_xfer_coeff_fan0 + c.fan255_adjustment)
+  #endif
+
   void menu_advanced_temperature() {
+    #if ENABLED(MPC_EDIT_MENU) && !HAS_MULTI_HOTEND
+      MPC_EDIT_DEFS(0);
+    #endif
+
     START_MENU();
     BACK_ITEM(MSG_ADVANCED_SETTINGS);
 
@@ -330,6 +340,45 @@ void menu_backlash();
       #endif
     #endif
 
+    #if ENABLED(MPC_EDIT_MENU)
+
+      #define _MPC_EDIT_ITEMS(N) \
+        EDIT_ITEM_FAST_N(float31sign, N, MSG_MPC_POWER_E, &c.heater_power, 1, 200); \
+        EDIT_ITEM_FAST_N(float31sign, N, MSG_MPC_BLOCK_HEAT_CAPACITY_E, &c.block_heat_capacity, 0, 40); \
+        EDIT_ITEM_FAST_N(float43, N, MSG_SENSOR_RESPONSIVENESS_E, &c.sensor_responsiveness, 0, 1); \
+        EDIT_ITEM_FAST_N(float43, N, MSG_MPC_AMBIENT_XFER_COEFF_E, &c.ambient_xfer_coeff_fan0, 0, 1)
+
+      #if ENABLED(MPC_INCLUDE_FAN)
+        #define MPC_EDIT_ITEMS(N) \
+          _MPC_EDIT_ITEMS(N); \
+          EDIT_ITEM_FAST_N(float43, N, MSG_MPC_AMBIENT_XFER_COEFF_FAN255_E, &editable.decimal, 0, 1, []{ \
+            c.fan255_adjustment = editable.decimal - c.ambient_xfer_coeff_fan0; \
+          })
+      #else
+        #define MPC_EDIT_ITEMS _MPC_EDIT_ITEMS
+      #endif
+
+      #if HAS_MULTI_HOTEND
+        auto mpc_edit_hotend = [&](const uint8_t e) {
+          MPC_EDIT_DEFS(e);
+          START_MENU();
+          BACK_ITEM(MSG_TEMPERATURE);
+          MPC_EDIT_ITEMS(e);
+          END_MENU();
+        };
+        #define MPC_ENTRY(N) SUBMENU_N(N, MSG_MPC_EDIT, []{ mpc_edit_hotend(MenuItemBase::itemIndex); });
+      #else
+        #define MPC_ENTRY MPC_EDIT_ITEMS
+      #endif
+
+      REPEAT(HOTENDS, MPC_ENTRY);
+
+    #endif // MPC_EDIT_MENU
+
+    #if ENABLED(MPC_AUTOTUNE_MENU)
+      ACTION_ITEM(MSG_MPC_AUTOTUNE, []{ queue.inject(F("M306 T")); ui.return_to_status(); });
+    #endif
+
     #if ENABLED(PIDTEMPBED)
       #if ENABLED(PID_EDIT_MENU)
         _PID_EDIT_ITEMS_TMPL(H_BED, thermalManager.temp_bed);
diff --git a/Marlin/src/module/temperature.cpp b/Marlin/src/module/temperature.cpp
index c82b76579cb..58e6c816e8d 100644
--- a/Marlin/src/module/temperature.cpp
+++ b/Marlin/src/module/temperature.cpp
@@ -141,8 +141,12 @@
   #endif
 #endif
 
-#if EITHER(MPCTEMP, PID_EXTRUSION_SCALING)
+#if ENABLED(MPCTEMP)
   #include <math.h>
+  #include "probe.h"
+#endif
+
+#if EITHER(MPCTEMP, PID_EXTRUSION_SCALING)
   #include "stepper.h"
 #endif
 
@@ -864,33 +868,65 @@ volatile bool Temperature::raw_temps_ready = false;
 
       if (ELAPSED(ms, next_report_ms)) {
         next_report_ms += 1000UL;
-        SERIAL_ECHOLNPGM("Temperature ", current_temp);
+
+        print_heater_states(active_extruder);
+        SERIAL_EOL();
       }
 
       hal.idletask();
+      TERN(DWIN_CREALITY_LCD, DWIN_Update(), ui.update());
+
+      if (!wait_for_heatup) {
+        SERIAL_ECHOPGM(STR_MPC_AUTOTUNE);
+        SERIAL_ECHOLNPGM(STR_MPC_AUTOTUNE_INTERRUPTED);
+        return false;
+      }
+
+      return true;
     };
 
-    SERIAL_ECHOLNPGM("Measuring MPC constants for E", active_extruder);
-    MPCHeaterInfo& hotend = temp_hotend[active_extruder];
-    MPC_t& constants = hotend.constants;
+    struct OnExit {
+      ~OnExit() {
+        wait_for_heatup = false;
 
-    // move to center of bed, just above bed height and cool with max fan
-    TERN_(HAS_FAN, zero_fan_speeds());
+        ui.reset_status();
+
+        temp_hotend[active_extruder].target = 0.0f;
+        temp_hotend[active_extruder].soft_pwm_amount = 0;
+        #if HAS_FAN
+          set_fan_speed(ANY(MPC_FAN_0_ALL_HOTENDS, MPC_FAN_0_ACTIVE_HOTEND) ? 0 : active_extruder, 0);
+          planner.sync_fan_speeds(fan_speed);
+        #endif
+
+        do_z_clearance(MPC_TUNING_END_Z);
+      }
+    } on_exit;
+
+    SERIAL_ECHOPGM(STR_MPC_AUTOTUNE);
+    SERIAL_ECHOLNPGM(STR_MPC_AUTOTUNE_START, active_extruder);
+    MPCHeaterInfo &hotend = temp_hotend[active_extruder];
+    MPC_t &constants = hotend.constants;
+
+    // Move to center of bed, just above bed height and cool with max fan
     disable_all_heaters();
-    TERN_(HAS_FAN, set_fan_speed(ANY(MPC_FAN_0_ALL_HOTENDS, MPC_FAN_0_ACTIVE_HOTEND) ? 0 : active_extruder, 255));
-    TERN_(HAS_FAN, planner.sync_fan_speeds(fan_speed));
+    #if HAS_FAN
+      zero_fan_speeds();
+      set_fan_speed(ANY(MPC_FAN_0_ALL_HOTENDS, MPC_FAN_0_ACTIVE_HOTEND) ? 0 : active_extruder, 255);
+      planner.sync_fan_speeds(fan_speed);
+    #endif
     gcode.home_all_axes(true);
     const xyz_pos_t tuningpos = MPC_TUNING_POS;
     do_blocking_move_to(tuningpos);
 
-    SERIAL_ECHOLNPGM("Cooling to ambient");
+    SERIAL_ECHOLNPGM(STR_MPC_COOLING_TO_AMBIENT);
+    LCD_MESSAGE(MSG_COOLING);
     millis_t ms = millis(), next_report_ms = ms, next_test_ms = ms + 10000UL;
     celsius_float_t current_temp = degHotend(active_extruder),
                     ambient_temp = current_temp;
 
-    wait_for_heatup = true; // Can be interrupted with M108
-    while (wait_for_heatup) {
-      housekeeping(ms, current_temp, next_report_ms);
+    wait_for_heatup = true;
+    for (;;) { // Can be interrupted with M108
+      if (!housekeeping(ms, current_temp, next_report_ms)) return;
 
       if (ELAPSED(ms, next_test_ms)) {
         if (current_temp >= ambient_temp) {
@@ -902,12 +938,16 @@ volatile bool Temperature::raw_temps_ready = false;
       }
     }
 
-    TERN_(HAS_FAN, set_fan_speed(ANY(MPC_FAN_0_ALL_HOTENDS, MPC_FAN_0_ACTIVE_HOTEND) ? 0 : active_extruder, 0));
-    TERN_(HAS_FAN, planner.sync_fan_speeds(fan_speed));
+    #if HAS_FAN
+      set_fan_speed(ANY(MPC_FAN_0_ALL_HOTENDS, MPC_FAN_0_ACTIVE_HOTEND) ? 0 : active_extruder, 0);
+      planner.sync_fan_speeds(fan_speed);
+    #endif
 
     hotend.modeled_ambient_temp = ambient_temp;
 
-    SERIAL_ECHOLNPGM("Heating to 200C");
+    SERIAL_ECHOLNPGM(STR_MPC_HEATING_PAST_200);
+    LCD_MESSAGE(MSG_HEATING);
+    hotend.target = 200.0f;   // So M105 looks nice
     hotend.soft_pwm_amount = MPC_MAX >> 1;
     const millis_t heat_start_time = next_test_ms = ms;
     celsius_float_t temp_samples[16];
@@ -915,13 +955,13 @@ volatile bool Temperature::raw_temps_ready = false;
     uint16_t sample_distance = 1;
     float t1_time = 0;
 
-    while (wait_for_heatup) {
-      housekeeping(ms, current_temp, next_report_ms);
+    for (;;) { // Can be interrupted with M108
+      if (!housekeeping(ms, current_temp, next_report_ms)) return;
 
       if (ELAPSED(ms, next_test_ms)) {
-        // record samples between 100C and 200C
+        // Record samples between 100C and 200C
         if (current_temp >= 100.0f) {
-          // if there are too many samples, space them more widely
+          // If there are too many samples, space them more widely
           if (sample_count == COUNT(temp_samples)) {
             for (uint8_t i = 0; i < COUNT(temp_samples) / 2; i++)
               temp_samples[i] = temp_samples[i*2];
@@ -944,9 +984,9 @@ volatile bool Temperature::raw_temps_ready = false;
     sample_count = (sample_count + 1) / 2 * 2 - 1;
     const float t1 = temp_samples[0],
                 t2 = temp_samples[(sample_count - 1) >> 1],
-                t3 = temp_samples[sample_count - 1],
-                asymp_temp = (t2 * t2 - t1 * t3) / (2 * t2 - t1 - t3),
-                block_responsiveness = -log((t2 - asymp_temp) / (t1 - asymp_temp)) / (sample_distance * (sample_count >> 1));
+                t3 = temp_samples[sample_count - 1];
+    float asymp_temp = (t2 * t2 - t1 * t3) / (2 * t2 - t1 - t3),
+          block_responsiveness = -log((t2 - asymp_temp) / (t1 - asymp_temp)) / (sample_distance * (sample_count >> 1));
 
     constants.ambient_xfer_coeff_fan0 = constants.heater_power * MPC_MAX / 255 / (asymp_temp - ambient_temp);
     constants.fan255_adjustment = 0.0f;
@@ -957,7 +997,8 @@ volatile bool Temperature::raw_temps_ready = false;
     hotend.modeled_sensor_temp = current_temp;
 
     // Allow the system to stabilize under MPC, then get a better measure of ambient loss with and without fan
-    SERIAL_ECHOLNPGM("Measuring ambient heatloss at target ", hotend.modeled_block_temp);
+    SERIAL_ECHOLNPGM(STR_MPC_MEASURING_AMBIENT, hotend.modeled_block_temp);
+    LCD_MESSAGE(MSG_MPC_MEASURING_AMBIENT);
     hotend.target = hotend.modeled_block_temp;
     next_test_ms = ms + MPC_dT * 1000;
     constexpr millis_t settle_time = 20000UL, test_duration = 20000UL;
@@ -970,18 +1011,16 @@ volatile bool Temperature::raw_temps_ready = false;
     #endif
     float last_temp = current_temp;
 
-    while (wait_for_heatup) {
-      housekeeping(ms, current_temp, next_report_ms);
+    for (;;) { // Can be interrupted with M108
+      if (!housekeeping(ms, current_temp, next_report_ms)) return;
 
       if (ELAPSED(ms, next_test_ms)) {
-        // use MPC to control the temperature, let it settle for 30s and then track power output for 10s
         hotend.soft_pwm_amount = (int)get_pid_output_hotend(active_extruder) >> 1;
 
         if (ELAPSED(ms, settle_end_ms) && !ELAPSED(ms, test_end_ms) && TERN1(HAS_FAN, !fan0_done))
           total_energy_fan0 += constants.heater_power * hotend.soft_pwm_amount / 127 * MPC_dT + (last_temp - current_temp) * constants.block_heat_capacity;
         #if HAS_FAN
           else if (ELAPSED(ms, test_end_ms) && !fan0_done) {
-            SERIAL_ECHOLNPGM("Measuring ambient heatloss with full fan");
             set_fan_speed(ANY(MPC_FAN_0_ALL_HOTENDS, MPC_FAN_0_ACTIVE_HOTEND) ? 0 : active_extruder, 255);
             planner.sync_fan_speeds(fan_speed);
             settle_end_ms = ms + settle_time;
@@ -997,8 +1036,8 @@ volatile bool Temperature::raw_temps_ready = false;
         next_test_ms += MPC_dT * 1000;
       }
 
-      if (!WITHIN(current_temp, hotend.target - 15.0f, hotend.target + 15.0f)) {
-        SERIAL_ECHOLNPGM("Temperature error while measuring ambient loss");
+      if (!WITHIN(current_temp, t3 - 15.0f, hotend.target + 15.0f)) {
+        SERIAL_ECHOLNPGM(STR_MPC_TEMPERATURE_ERROR);
         break;
       }
     }
@@ -1012,16 +1051,14 @@ volatile bool Temperature::raw_temps_ready = false;
       constants.fan255_adjustment = ambient_xfer_coeff_fan255 - constants.ambient_xfer_coeff_fan0;
     #endif
 
-    hotend.target = 0.0f;
-    hotend.soft_pwm_amount = 0;
-    TERN_(HAS_FAN, set_fan_speed(ANY(MPC_FAN_0_ALL_HOTENDS, MPC_FAN_0_ACTIVE_HOTEND) ? 0 : active_extruder, 0));
-    TERN_(HAS_FAN, planner.sync_fan_speeds(fan_speed));
+    // Calculate a new and better asymptotic temperature and re-evaluate the other constants
+    asymp_temp = ambient_temp + constants.heater_power / constants.ambient_xfer_coeff_fan0;
+    block_responsiveness = -log((t2 - asymp_temp) / (t1 - asymp_temp)) / (sample_distance * (sample_count >> 1));
+    constants.block_heat_capacity = constants.ambient_xfer_coeff_fan0 / block_responsiveness;
+    constants.sensor_responsiveness = block_responsiveness / (1.0f - (ambient_temp - asymp_temp) * exp(-block_responsiveness * t1_time) / (t1 - asymp_temp));
 
-    if (!wait_for_heatup) SERIAL_ECHOLNPGM("Test was interrupted");
-
-    wait_for_heatup = false;
-
-    SERIAL_ECHOLNPGM("Done");
+    SERIAL_ECHOPGM(STR_MPC_AUTOTUNE);
+    SERIAL_ECHOLNPGM(STR_MPC_AUTOTUNE_FINISHED);
 
     /* <-- add a slash to enable
       SERIAL_ECHOLNPGM("t1_time ", t1_time);
@@ -1378,7 +1415,7 @@ void Temperature::min_temp_error(const heater_id_t heater_id) {
 
       // At startup, initialize modeled temperatures
       if (isnan(hotend.modeled_block_temp)) {
-        hotend.modeled_ambient_temp = min(30.0f, hotend.celsius);   // cap initial value at reasonable max room temperature of 30C
+        hotend.modeled_ambient_temp = min(30.0f, hotend.celsius);   // Cap initial value at reasonable max room temperature of 30C
         hotend.modeled_block_temp = hotend.modeled_sensor_temp = hotend.celsius;
       }
 
@@ -1399,16 +1436,16 @@ void Temperature::min_temp_error(const heater_id_t heater_id) {
         const int32_t e_position = stepper.position(E_AXIS);
         const float e_speed = (e_position - mpc_e_position) * planner.mm_per_step[E_AXIS] / MPC_dT;
 
-        // the position can appear to make big jumps when, e.g. homing
+        // The position can appear to make big jumps when, e.g. homing
         if (fabs(e_speed) > planner.settings.max_feedrate_mm_s[E_AXIS])
           mpc_e_position = e_position;
-        else if (e_speed > 0.0f) {  // ignore retract/recover moves
+        else if (e_speed > 0.0f) {  // Ignore retract/recover moves
           ambient_xfer_coeff += e_speed * FILAMENT_HEAT_CAPACITY_PERMM;
           mpc_e_position = e_position;
         }
       }
 
-      // update the modeled temperatures
+      // Update the modeled temperatures
       float blocktempdelta = hotend.soft_pwm_amount * constants.heater_power * (MPC_dT / 127) / constants.block_heat_capacity;
       blocktempdelta += (hotend.modeled_ambient_temp - hotend.modeled_block_temp) * ambient_xfer_coeff * MPC_dT / constants.block_heat_capacity;
       hotend.modeled_block_temp += blocktempdelta;
@@ -1422,18 +1459,18 @@ void Temperature::min_temp_error(const heater_id_t heater_id) {
       hotend.modeled_block_temp += delta_to_apply;
       hotend.modeled_sensor_temp += delta_to_apply;
 
-      // only correct ambient when close to steady state (output power is not clipped or asymptotic temperature is reached)
+      // Only correct ambient when close to steady state (output power is not clipped or asymptotic temperature is reached)
       if (WITHIN(hotend.soft_pwm_amount, 1, 126) || fabs(blocktempdelta + delta_to_apply) < (MPC_STEADYSTATE * MPC_dT))
         hotend.modeled_ambient_temp += delta_to_apply > 0.f ? max(delta_to_apply, MPC_MIN_AMBIENT_CHANGE * MPC_dT) : min(delta_to_apply, -MPC_MIN_AMBIENT_CHANGE * MPC_dT);
 
       float power = 0.0;
       if (hotend.target != 0 && TERN1(HEATER_IDLE_HANDLER, !heater_idle[ee].timed_out)) {
-        // plan power level to get to target temperature in 2 seconds
+        // Plan power level to get to target temperature in 2 seconds
         power = (hotend.target - hotend.modeled_block_temp) * constants.block_heat_capacity / 2.0f;
         power -= (hotend.modeled_ambient_temp - hotend.modeled_block_temp) * ambient_xfer_coeff;
       }
 
-      float pid_output = power * 254.0f / constants.heater_power + 1.0f;        // ensure correct quantization into a range of 0 to 127
+      float pid_output = power * 254.0f / constants.heater_power + 1.0f;        // Ensure correct quantization into a range of 0 to 127
       pid_output = constrain(pid_output, 0, MPC_MAX);
 
       /* <-- add a slash to enable
@@ -2079,7 +2116,7 @@ void Temperature::manage_heater() {
                         - (t.beta_recip * t.res_25_log) - (t.sh_c_coeff * cu(t.res_25_log));
     }
 
-    // maximum adc value .. take into account the over sampling
+    // Maximum ADC value .. take into account the over sampling
     constexpr raw_adc_t adc_max = MAX_RAW_THERMISTOR_VALUE;
     const raw_adc_t adc_raw = constrain(raw, 1, adc_max - 1); // constrain to prevent divide-by-zero
 
@@ -3077,7 +3114,7 @@ void Temperature::disable_all_heaters() {
         spiInit(MAX_TC_SPEED_BITS);
       #endif
 
-      MAXTC_CS_WRITE(LOW);  // enable MAXTC
+      MAXTC_CS_WRITE(LOW);  // Enable MAXTC
       DELAY_NS(100);        // Ensure 100ns delay
 
       // Read a big-endian temperature value without using a library
@@ -3086,7 +3123,7 @@ void Temperature::disable_all_heaters() {
         if (i > 0) max_tc_temp <<= 8; // shift left if not the last byte
       }
 
-      MAXTC_CS_WRITE(HIGH);  // disable MAXTC
+      MAXTC_CS_WRITE(HIGH);  // Disable MAXTC
     #else
       #if HAS_MAX6675_LIBRARY
         MAX6675 &max6675ref = THERMO_SEL(max6675_0, max6675_1);
@@ -3297,7 +3334,7 @@ void Temperature::isr() {
   static ADCSensorState adc_sensor_state = StartupDelay;
   static uint8_t pwm_count = _BV(SOFT_PWM_SCALE);
 
-  // avoid multiple loads of pwm_count
+  // Avoid multiple loads of pwm_count
   uint8_t pwm_count_tmp = pwm_count;
 
   #if HAS_ADC_BUTTONS
@@ -3575,8 +3612,8 @@ void Temperature::isr() {
     // 5:                /  4 = 244.1406 Hz
     pwm_count = pwm_count_tmp + _BV(SOFT_PWM_SCALE);
 
-    // increment slow_pwm_count only every 64th pwm_count,
-    // i.e. yielding a PWM frequency of 16/128 Hz (8s).
+    // Increment slow_pwm_count only every 64th pwm_count,
+    // i.e., yielding a PWM frequency of 16/128 Hz (8s).
     if (((pwm_count >> SOFT_PWM_SCALE) & 0x3F) == 0) {
       slow_pwm_count++;
       slow_pwm_count &= 0x7F;
@@ -4029,7 +4066,7 @@ void Temperature::isr() {
 
         // Prevent a wait-forever situation if R is misused i.e. M109 R0
         if (wants_to_cool) {
-          // break after MIN_COOLING_SLOPE_TIME seconds
+          // Break after MIN_COOLING_SLOPE_TIME seconds
           // if the temperature did not drop at least MIN_COOLING_SLOPE_DEG
           if (!next_cool_check_ms || ELAPSED(now, next_cool_check_ms)) {
             if (old_temp - temp < float(MIN_COOLING_SLOPE_DEG)) break;
@@ -4051,7 +4088,7 @@ void Temperature::isr() {
         wait_for_heatup = false;
         #if HAS_DWIN_E3V2_BASIC
           HMI_flag.heat_flag = 0;
-          duration_t elapsed = print_job_timer.duration();  // print timer
+          duration_t elapsed = print_job_timer.duration();  // Print timer
           dwin_heat_time = elapsed.value;
         #else
           ui.reset_status();
diff --git a/buildroot/tests/BIGTREE_GTR_V1_0 b/buildroot/tests/BIGTREE_GTR_V1_0
index 919822733b0..4d3ac49c988 100755
--- a/buildroot/tests/BIGTREE_GTR_V1_0
+++ b/buildroot/tests/BIGTREE_GTR_V1_0
@@ -32,9 +32,15 @@ restore_configs
 opt_set MOTHERBOARD BOARD_BTT_GTR_V1_0 SERIAL_PORT -1 \
         EXTRUDERS 3 TEMP_SENSOR_1 1 TEMP_SENSOR_2 1 \
         SERVO_DELAY '{ 300, 300, 300 }' \
-        SWITCHING_TOOLHEAD_X_POS '{ 215, 0 ,0 }'
-opt_enable SWITCHING_TOOLHEAD TOOL_SENSOR
-exec_test $1 $2 "BigTreeTech GTR | Switching Toolhead | Tool Sensors" "$3"
+        SWITCHING_TOOLHEAD_X_POS '{ 215, 0 ,0 }' \
+        MPC_HEATER_POWER '{ 40.0f, 40.0f, 40.0f }' \
+        MPC_BLOCK_HEAT_CAPACITY '{ 16.7f, 16.7f, 16.7f }' \
+        MPC_SENSOR_RESPONSIVENESS '{ 0.22f, 0.22f, 0.22f }' \
+        MPC_AMBIENT_XFER_COEFF '{ 0.068f, 0.068f, 0.068f }' \
+        MPC_AMBIENT_XFER_COEFF_FAN255 '{ 0.097f, 0.097f, 0.097f }'
+opt_enable SWITCHING_TOOLHEAD TOOL_SENSOR MPCTEMP
+opt_disable PIDTEMP
+exec_test $1 $2 "BigTreeTech GTR | MPC | Switching Toolhead | Tool Sensors" "$3"
 
 # clean up
 restore_configs
diff --git a/buildroot/tests/STM32F103RC_btt b/buildroot/tests/STM32F103RC_btt
index e76060aee82..16419cbfa23 100755
--- a/buildroot/tests/STM32F103RC_btt
+++ b/buildroot/tests/STM32F103RC_btt
@@ -13,7 +13,6 @@ 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
-
 exec_test $1 $2 "BigTreeTech SKR Mini E3 1.0 - Basic Config with TMC2209 HW Serial" "$3"
 
 # clean up
diff --git a/buildroot/tests/STM32F103RC_btt_maple b/buildroot/tests/STM32F103RC_btt_maple
index e74e5902132..90e33ab5292 100755
--- a/buildroot/tests/STM32F103RC_btt_maple
+++ b/buildroot/tests/STM32F103RC_btt_maple
@@ -13,7 +13,6 @@ 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
-
 exec_test $1 $2 "BigTreeTech SKR Mini E3 1.0 - Basic Config with TMC2209 HW Serial" "$3"
 
 # clean up
diff --git a/buildroot/tests/mega2560 b/buildroot/tests/mega2560
index 5ae9a2dbcf0..a9ac15ecda9 100755
--- a/buildroot/tests/mega2560
+++ b/buildroot/tests/mega2560
@@ -181,7 +181,6 @@ opt_set MOTHERBOARD BOARD_RAMPS_14_EFB EXTRUDERS 0 LCD_LANGUAGE en TEMP_SENSOR_C
         AXIS_RELATIVE_MODES '{ false, false, false }'
 opt_enable REPRAP_DISCOUNT_FULL_GRAPHIC_SMART_CONTROLLER SDSUPPORT EEPROM_SETTINGS EEPROM_BOOT_SILENT EEPROM_AUTO_INIT \
            LASER_FEATURE AIR_EVACUATION AIR_EVACUATION_PIN AIR_ASSIST AIR_ASSIST_PIN LASER_COOLANT_FLOW_METER MEATPACK_ON_SERIAL_PORT_1
-
 exec_test $1 $2 "MEGA2560 RAMPS | Laser Feature | Air Evacuation | Air Assist | Cooler | Flowmeter | 12864 LCD | meatpack | SERIAL_PORT_2 " "$3"
 
 #
@@ -196,7 +195,6 @@ opt_set MOTHERBOARD BOARD_RAMPS_14_EFB EXTRUDERS 0 LCD_LANGUAGE en TEMP_SENSOR_C
         AXIS_RELATIVE_MODES '{ false, false, false }'
 opt_enable REPRAP_DISCOUNT_SMART_CONTROLLER SDSUPPORT EEPROM_SETTINGS EEPROM_BOOT_SILENT EEPROM_AUTO_INIT \
            LASER_FEATURE AIR_EVACUATION AIR_EVACUATION_PIN AIR_ASSIST AIR_ASSIST_PIN LASER_COOLANT_FLOW_METER I2C_AMMETER
-
 exec_test $1 $2 "MEGA2560 RAMPS | Laser Feature | Air Evacuation | Air Assist | Cooler | Flowmeter | 44780 LCD " "$3"
 
 #
@@ -207,7 +205,8 @@ opt_set MOTHERBOARD BOARD_RAMPS_14_EFB EXTRUDERS 1 \
         TEMP_SENSOR_0 -2 TEMP_SENSOR_REDUNDANT -2 \
         TEMP_SENSOR_REDUNDANT_SOURCE E1 TEMP_SENSOR_REDUNDANT_TARGET E0 \
         TEMP_0_CS_PIN 11 TEMP_1_CS_PIN 12
-
+opt_enable MPCTEMP
+opt_disable PIDTEMP
 exec_test $1 $2 "MEGA2560 RAMPS | Redundant temperature sensor | 2x MAX6675" "$3"
 
 #