From 0b841941276b246c06b52f65e5e45199d4792785 Mon Sep 17 00:00:00 2001
From: tombrazier <68918209+tombrazier@users.noreply.github.com>
Date: Mon, 1 Nov 2021 23:03:50 +0000
Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=B8=20More=20flexible=20Probe=20Temper?=
 =?UTF-8?q?ature=20Compensation=20(#23033)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Marlin/Configuration_adv.h                   | 100 +++---
 Marlin/src/feature/probe_temp_comp.cpp       |  86 ++---
 Marlin/src/feature/probe_temp_comp.h         | 116 ++----
 Marlin/src/gcode/bedlevel/abl/G29.cpp        |  10 +-
 Marlin/src/gcode/calibrate/G76_M192_M871.cpp | 358 -------------------
 Marlin/src/gcode/calibrate/G76_M871.cpp      | 337 +++++++++++++++++
 Marlin/src/gcode/gcode.cpp                   |   9 +-
 Marlin/src/gcode/gcode.h                     |  57 ++-
 Marlin/src/gcode/temp/M192.cpp               |  56 +++
 Marlin/src/inc/Conditionals_adv.h            |  14 +
 Marlin/src/inc/SanityCheck.h                 |  78 ++--
 Marlin/src/module/settings.cpp               |  54 ++-
 buildroot/tests/rambo                        |   8 +-
 ini/features.ini                             |   9 +-
 platformio.ini                               |   3 +-
 15 files changed, 672 insertions(+), 623 deletions(-)
 delete mode 100644 Marlin/src/gcode/calibrate/G76_M192_M871.cpp
 create mode 100644 Marlin/src/gcode/calibrate/G76_M871.cpp
 create mode 100644 Marlin/src/gcode/temp/M192.cpp

diff --git a/Marlin/Configuration_adv.h b/Marlin/Configuration_adv.h
index 22474b0183..35602373d2 100644
--- a/Marlin/Configuration_adv.h
+++ b/Marlin/Configuration_adv.h
@@ -1988,65 +1988,69 @@
 
 /**
  * Thermal Probe Compensation
- * Probe measurements are adjusted to compensate for temperature distortion.
- * Use G76 to calibrate this feature. Use M871 to set values manually.
- * For a more detailed explanation of the process see G76_M871.cpp.
+ *
+ * Adjust probe measurements to compensate for distortion associated with the temperature
+ * of the probe, bed, and/or hotend.
+ * Use G76 to automatically calibrate this feature for probe and bed temperatures.
+ * (Extruder temperature/offset values must be calibrated manually.)
+ * Use M871 to set temperature/offset values manually.
+ * For more details see https://marlinfw.org/docs/features/probe_temp_compensation.html
  */
-#if HAS_BED_PROBE && TEMP_SENSOR_PROBE && TEMP_SENSOR_BED
-  // Enable thermal first layer compensation using bed and probe temperatures
-  #define PROBE_TEMP_COMPENSATION
+//#define PTC_PROBE    // Compensate based on probe temperature
+//#define PTC_BED      // Compensate based on bed temperature
+//#define PTC_HOTEND   // Compensate based on hotend temperature
 
-  // Add additional compensation depending on hotend temperature
-  // Note: this values cannot be calibrated and have to be set manually
-  #if ENABLED(PROBE_TEMP_COMPENSATION)
+#if ANY(PTC_PROBE, PTC_BED, PTC_HOTEND)
+  /**
+   * If the probe is outside the defined range, use linear extrapolation with the closest
+   * point and the point with index PTC_LINEAR_EXTRAPOLATION. e.g., If set to 4 it will use the
+   * linear extrapolation between data[0] and data[4] for values below PTC_PROBE_START.
+   */
+  //#define PTC_LINEAR_EXTRAPOLATION 4
+
+  #if ENABLED(PTC_PROBE)
+    // Probe temperature calibration generates a table of values starting at PTC_PROBE_START
+    // (e.g., 30), in steps of PTC_PROBE_RES (e.g., 5) with PTC_PROBE_COUNT (e.g., 10) samples.
+    #define PTC_PROBE_START   30    // (°C)
+    #define PTC_PROBE_RES      5    // (°C)
+    #define PTC_PROBE_COUNT   10
+    #define PTC_PROBE_ZOFFS   { 0 } // (µm) Z adjustments per sample
+  #endif
+
+  #if ENABLED(PTC_BED)
+    // Bed temperature calibration builds a similar table.
+    #define PTC_BED_START     60    // (°C)
+    #define PTC_BED_RES        5    // (°C)
+    #define PTC_BED_COUNT     10
+    #define PTC_BED_ZOFFS     { 0 } // (µm) Z adjustments per sample
+  #endif
+
+  #if ENABLED(PTC_HOTEND)
+    // Note: There is no automatic calibration for the hotend. Use M871.
+    #define PTC_HOTEND_START 180    // (°C)
+    #define PTC_HOTEND_RES     5    // (°C)
+    #define PTC_HOTEND_COUNT  20
+    #define PTC_HOTEND_ZOFFS  { 0 } // (µm) Z adjustments per sample
+  #endif
+
+  // G76 options
+  #if BOTH(PTC_PROBE, PTC_BED)
     // Park position to wait for probe cooldown
     #define PTC_PARK_POS   { 0, 0, 100 }
 
     // Probe position to probe and wait for probe to reach target temperature
+    //#define PTC_PROBE_POS  { 12.0f, 7.3f } // Example: MK52 magnetic heatbed
     #define PTC_PROBE_POS  { 90, 100 }
 
-    // Enable additional compensation using hotend temperature
-    // Note: this values cannot be calibrated automatically but have to be set manually via M871.
-    //#define USE_TEMP_EXT_COMPENSATION
-
-    // Probe temperature calibration generates a table of values starting at PTC_SAMPLE_START
-    // (e.g., 30), in steps of PTC_SAMPLE_RES (e.g., 5) with PTC_SAMPLE_COUNT (e.g., 10) samples.
-
-    //#define PTC_SAMPLE_START  30  // (°C)
-    //#define PTC_SAMPLE_RES     5  // (°C)
-    //#define PTC_SAMPLE_COUNT  10
-
-    // Bed temperature calibration builds a similar table.
-
-    //#define BTC_SAMPLE_START  60  // (°C)
-    //#define BTC_SAMPLE_RES     5  // (°C)
-    //#define BTC_SAMPLE_COUNT  10
-
-    #if ENABLED(USE_TEMP_EXT_COMPENSATION)
-      //#define ETC_SAMPLE_START 180  // (°C)
-      //#define ETC_SAMPLE_RES     5  // (°C)
-      //#define ETC_SAMPLE_COUNT  20
-    #endif
-
-    // The temperature the probe should be at while taking measurements during bed temperature
-    // calibration.
-    //#define BTC_PROBE_TEMP    30  // (°C)
+    // The temperature the probe should be at while taking measurements during
+    // bed temperature calibration.
+    #define PTC_PROBE_TEMP    30  // (°C)
 
     // Height above Z=0.0 to raise the nozzle. Lowering this can help the probe to heat faster.
-    // Note: the Z=0.0 offset is determined by the probe offset which can be set using M851.
-    //#define PTC_PROBE_HEATING_OFFSET 0.5
-
-    // Height to raise the Z-probe between heating and taking the next measurement. Some probes
-    // may fail to untrigger if they have been triggered for a long time, which can be solved by
-    // increasing the height the probe is raised to.
-    //#define PTC_PROBE_RAISE 15
-
-    // If the probe is outside of the defined range, use linear extrapolation using the closest
-    // point and the PTC_LINEAR_EXTRAPOLATION'th next point. E.g. if set to 4 it will use data[0]
-    // and data[4] to perform linear extrapolation for values below PTC_SAMPLE_START.
-    //#define PTC_LINEAR_EXTRAPOLATION 4
+    // Note: The Z=0.0 offset is determined by the probe Z offset (e.g., as set with M851 Z).
+    #define PTC_PROBE_HEATING_OFFSET 0.5
   #endif
-#endif
+#endif // PTC_PROBE || PTC_BED || PTC_HOTEND
 
 // @section extras
 
diff --git a/Marlin/src/feature/probe_temp_comp.cpp b/Marlin/src/feature/probe_temp_comp.cpp
index 5f3bc985e6..9a975d6763 100644
--- a/Marlin/src/feature/probe_temp_comp.cpp
+++ b/Marlin/src/feature/probe_temp_comp.cpp
@@ -22,39 +22,53 @@
 
 #include "../inc/MarlinConfigPre.h"
 
-#if ENABLED(PROBE_TEMP_COMPENSATION)
+#if HAS_PTC
 
 //#define DEBUG_PTC   // Print extra debug output with 'M871'
 
 #include "probe_temp_comp.h"
 #include <math.h>
 
-ProbeTempComp temp_comp;
+ProbeTempComp ptc;
 
-int16_t ProbeTempComp::z_offsets_probe[cali_info_init[TSI_PROBE].measurements],  // = {0}
-        ProbeTempComp::z_offsets_bed[cali_info_init[TSI_BED].measurements];      // = {0}
+#if ENABLED(PTC_PROBE)
+  constexpr int16_t z_offsets_probe_default[PTC_PROBE_COUNT] = PTC_PROBE_ZOFFS;
+  int16_t ProbeTempComp::z_offsets_probe[PTC_PROBE_COUNT] = PTC_PROBE_ZOFFS;
+#endif
 
-#if ENABLED(USE_TEMP_EXT_COMPENSATION)
-  int16_t ProbeTempComp::z_offsets_ext[cali_info_init[TSI_EXT].measurements];    // = {0}
+#if ENABLED(PTC_BED)
+  constexpr int16_t z_offsets_bed_default[PTC_BED_COUNT] = PTC_BED_ZOFFS;
+  int16_t ProbeTempComp::z_offsets_bed[PTC_BED_COUNT] = PTC_BED_ZOFFS;
+#endif
+
+#if ENABLED(PTC_HOTEND)
+  constexpr int16_t z_offsets_hotend_default[PTC_HOTEND_COUNT] = PTC_HOTEND_ZOFFS;
+  int16_t ProbeTempComp::z_offsets_hotend[PTC_HOTEND_COUNT] = PTC_HOTEND_ZOFFS;
 #endif
 
 int16_t *ProbeTempComp::sensor_z_offsets[TSI_COUNT] = {
-  ProbeTempComp::z_offsets_probe, ProbeTempComp::z_offsets_bed
-  OPTARG(USE_TEMP_EXT_COMPENSATION, ProbeTempComp::z_offsets_ext)
+  #if ENABLED(PTC_PROBE)
+    ProbeTempComp::z_offsets_probe,
+  #endif
+  #if ENABLED(PTC_BED)
+    ProbeTempComp::z_offsets_bed,
+  #endif
+  #if ENABLED(PTC_HOTEND)
+    ProbeTempComp::z_offsets_hotend,
+  #endif
 };
 
-const temp_calib_t ProbeTempComp::cali_info[TSI_COUNT] = {
-  cali_info_init[TSI_PROBE], cali_info_init[TSI_BED]
-  OPTARG(USE_TEMP_EXT_COMPENSATION, cali_info_init[TSI_EXT])
-};
-
-constexpr xyz_pos_t ProbeTempComp::park_point;
-constexpr xy_pos_t ProbeTempComp::measure_point;
-constexpr celsius_t ProbeTempComp::probe_calib_bed_temp;
+constexpr temp_calib_t ProbeTempComp::cali_info[TSI_COUNT];
 
 uint8_t ProbeTempComp::calib_idx; // = 0
 float ProbeTempComp::init_measurement; // = 0.0
 
+void ProbeTempComp::reset() {
+  TERN_(PTC_PROBE, LOOP_L_N(i, PTC_PROBE_COUNT) z_offsets_probe[i] = z_offsets_probe_default[i]);
+  TERN_(PTC_BED, LOOP_L_N(i, PTC_BED_COUNT) z_offsets_bed[i] = z_offsets_bed_default[i]);
+  TERN_(PTC_HOTEND, LOOP_L_N(i, PTC_HOTEND_COUNT) z_offsets_hotend[i] = z_offsets_hotend_default[i]);
+}
+
 void ProbeTempComp::clear_offsets(const TempSensorID tsi) {
   LOOP_L_N(i, cali_info[tsi].measurements)
     sensor_z_offsets[tsi][i] = 0;
@@ -71,10 +85,9 @@ void ProbeTempComp::print_offsets() {
   LOOP_L_N(s, TSI_COUNT) {
     celsius_t temp = cali_info[s].start_temp;
     for (int16_t i = -1; i < cali_info[s].measurements; ++i) {
-      SERIAL_ECHOF(s == TSI_BED ? F("Bed") :
-        #if ENABLED(USE_TEMP_EXT_COMPENSATION)
-          s == TSI_EXT ? F("Extruder") :
-        #endif
+      SERIAL_ECHOF(
+        TERN_(PTC_BED, s == TSI_BED ? F("Bed") :)
+        TERN_(PTC_HOTEND, s == TSI_EXT ? F("Extruder") :)
         F("Probe")
       );
       SERIAL_ECHOLNPGM(
@@ -100,21 +113,13 @@ void ProbeTempComp::prepare_new_calibration(const_float_t init_meas_z) {
 }
 
 void ProbeTempComp::push_back_new_measurement(const TempSensorID tsi, const_float_t meas_z) {
-  switch (tsi) {
-    case TSI_PROBE:
-    case TSI_BED:
-    //case TSI_EXT:
-      if (calib_idx >= cali_info[tsi].measurements) return;
-      sensor_z_offsets[tsi][calib_idx++] = static_cast<int16_t>(meas_z * 1000.0f - init_measurement * 1000.0f);
-    default: break;
-  }
+  if (calib_idx >= cali_info[tsi].measurements) return;
+  sensor_z_offsets[tsi][calib_idx++] = static_cast<int16_t>((meas_z - init_measurement) * 1000.0f);
 }
 
 bool ProbeTempComp::finish_calibration(const TempSensorID tsi) {
-  if (tsi != TSI_PROBE && tsi != TSI_BED) return false;
-
-  if (calib_idx < 3) {
-    SERIAL_ECHOLNPGM("!Insufficient measurements (min. 3).");
+  if (!calib_idx) {
+    SERIAL_ECHOLNPGM("!No measurements.");
     clear_offsets(tsi);
     return false;
   }
@@ -130,16 +135,15 @@ bool ProbeTempComp::finish_calibration(const TempSensorID tsi) {
     SERIAL_ECHOLNPGM("Got ", calib_idx, " measurements. ");
     if (linear_regression(tsi, k, d)) {
       SERIAL_ECHOPGM("Applying linear extrapolation");
-      calib_idx--;
       for (; calib_idx < measurements; ++calib_idx) {
-        const celsius_float_t temp = start_temp + float(calib_idx) * res_temp;
+        const celsius_float_t temp = start_temp + float(calib_idx + 1) * res_temp;
         data[calib_idx] = static_cast<int16_t>(k * temp + d);
       }
     }
     else {
       // Simply use the last measured value for higher temperatures
       SERIAL_ECHOPGM("Failed to extrapolate");
-      const int16_t last_val = data[calib_idx];
+      const int16_t last_val = data[calib_idx-1];
       for (; calib_idx < measurements; ++calib_idx)
         data[calib_idx] = last_val;
     }
@@ -157,7 +161,7 @@ bool ProbeTempComp::finish_calibration(const TempSensorID tsi) {
     // Restrict the max. offset difference between two probings
     if (calib_idx > 0 && ABS(data[calib_idx - 1] - data[calib_idx]) > 800) {
       SERIAL_ECHOLNPGM("!Invalid Z-offset between two probings detected (0-0.8).");
-      clear_offsets(TSI_PROBE);
+      clear_offsets(tsi);
       return false;
     }
   }
@@ -168,8 +172,8 @@ bool ProbeTempComp::finish_calibration(const TempSensorID tsi) {
 void ProbeTempComp::compensate_measurement(const TempSensorID tsi, const celsius_t temp, float &meas_z) {
   const uint8_t measurements = cali_info[tsi].measurements;
   const celsius_t start_temp = cali_info[tsi].start_temp,
-                    end_temp = cali_info[tsi].end_temp,
-                    res_temp = cali_info[tsi].temp_resolution;
+                  res_temp = cali_info[tsi].temp_resolution,
+                  end_temp = start_temp + measurements * res_temp;
   const int16_t * const data = sensor_z_offsets[tsi];
 
   // Given a data index, return { celsius, zoffset } in the form { x, y }
@@ -208,9 +212,7 @@ void ProbeTempComp::compensate_measurement(const TempSensorID tsi, const celsius
 }
 
 bool ProbeTempComp::linear_regression(const TempSensorID tsi, float &k, float &d) {
-  if (tsi != TSI_PROBE && tsi != TSI_BED) return false;
-
-  if (!WITHIN(calib_idx, 2, cali_info[tsi].measurements)) return false;
+  if (!WITHIN(calib_idx, 1, cali_info[tsi].measurements)) return false;
 
   const celsius_t start_temp = cali_info[tsi].start_temp,
                     res_temp = cali_info[tsi].temp_resolution;
@@ -243,4 +245,4 @@ bool ProbeTempComp::linear_regression(const TempSensorID tsi, float &k, float &d
   return true;
 }
 
-#endif // PROBE_TEMP_COMPENSATION
+#endif // HAS_PTC
diff --git a/Marlin/src/feature/probe_temp_comp.h b/Marlin/src/feature/probe_temp_comp.h
index e5d459b8e8..4579f2187c 100644
--- a/Marlin/src/feature/probe_temp_comp.h
+++ b/Marlin/src/feature/probe_temp_comp.h
@@ -24,9 +24,13 @@
 #include "../inc/MarlinConfig.h"
 
 enum TempSensorID : uint8_t {
-  TSI_PROBE,
-  TSI_BED,
-  #if ENABLED(USE_TEMP_EXT_COMPENSATION)
+  #if ENABLED(PTC_PROBE)
+    TSI_PROBE,
+  #endif
+  #if ENABLED(PTC_BED)
+    TSI_BED,
+  #endif
+  #if ENABLED(PTC_HOTEND)
     TSI_EXT,
   #endif
   TSI_COUNT
@@ -35,8 +39,7 @@ enum TempSensorID : uint8_t {
 typedef struct {
   uint8_t measurements;       // Max. number of measurements to be stored (35 - 80°C)
   celsius_t temp_resolution,  // Resolution in °C between measurements
-            start_temp,       // Base measurement; z-offset == 0
-            end_temp;
+            start_temp;       // Base measurement; z-offset == 0
 } temp_calib_t;
 
 /**
@@ -45,93 +48,40 @@ typedef struct {
  * measurement errors/shifts due to changed temperature.
  */
 
-// Probe temperature calibration constants
-#ifndef PTC_SAMPLE_COUNT
-  #define PTC_SAMPLE_COUNT 10
-#endif
-#ifndef PTC_SAMPLE_RES
-  #define PTC_SAMPLE_RES 5
-#endif
-#ifndef PTC_SAMPLE_START
-  #define PTC_SAMPLE_START 30
-#endif
-#define PTC_SAMPLE_END (PTC_SAMPLE_START + (PTC_SAMPLE_COUNT) * PTC_SAMPLE_RES)
-
-// Bed temperature calibration constants
-#ifndef BTC_PROBE_TEMP
-  #define BTC_PROBE_TEMP 30
-#endif
-#ifndef BTC_SAMPLE_COUNT
-  #define BTC_SAMPLE_COUNT 10
-#endif
-#ifndef BTC_SAMPLE_RES
-  #define BTC_SAMPLE_RES 5
-#endif
-#ifndef BTC_SAMPLE_START
-  #define BTC_SAMPLE_START 60
-#endif
-#define BTC_SAMPLE_END (BTC_SAMPLE_START + (BTC_SAMPLE_COUNT) * BTC_SAMPLE_RES)
-
-// Extruder temperature calibration constants
-#if ENABLED(USE_TEMP_EXT_COMPENSATION)
-  #ifndef ETC_SAMPLE_COUNT
-    #define ETC_SAMPLE_COUNT 20
-  #endif
-  #ifndef ETC_SAMPLE_RES
-    #define ETC_SAMPLE_RES 5
-  #endif
-  #ifndef ETC_SAMPLE_START
-    #define ETC_SAMPLE_START 180
-  #endif
-  #define ETC_SAMPLE_END (ETC_SAMPLE_START + (ETC_SAMPLE_COUNT) * ETC_SAMPLE_RES)
-#endif
-
-#ifndef PTC_PROBE_HEATING_OFFSET
-  #define PTC_PROBE_HEATING_OFFSET 0.5f
-#endif
-
-#ifndef PTC_PROBE_RAISE
-  #define PTC_PROBE_RAISE 10
-#endif
-
-static constexpr temp_calib_t cali_info_init[TSI_COUNT] = {
-  { PTC_SAMPLE_COUNT, PTC_SAMPLE_RES, PTC_SAMPLE_START, PTC_SAMPLE_END },   // Probe
-  { BTC_SAMPLE_COUNT, BTC_SAMPLE_RES, BTC_SAMPLE_START, BTC_SAMPLE_END },   // Bed
-  #if ENABLED(USE_TEMP_EXT_COMPENSATION)
-    { ETC_SAMPLE_COUNT, ETC_SAMPLE_RES, ETC_SAMPLE_START, ETC_SAMPLE_END }, // Extruder
-  #endif
-};
-
 class ProbeTempComp {
   public:
 
-    static const temp_calib_t cali_info[TSI_COUNT];
+    static constexpr temp_calib_t cali_info[TSI_COUNT] = {
+      #if ENABLED(PTC_PROBE)
+        { PTC_PROBE_COUNT, PTC_PROBE_RES, PTC_PROBE_START },   // Probe
+      #endif
+      #if ENABLED(PTC_BED)
+        { PTC_BED_COUNT, PTC_BED_RES, PTC_BED_START },   // Bed
+      #endif
+      #if ENABLED(PTC_HOTEND)
+        { PTC_HOTEND_COUNT, PTC_HOTEND_RES, PTC_HOTEND_START }, // Extruder
+      #endif
+    };
 
-    // Where to park nozzle to wait for probe cooldown
-    static constexpr xyz_pos_t park_point = PTC_PARK_POS;
-
-    // XY coordinates of nozzle for probing the bed
-    static constexpr xy_pos_t measure_point    = PTC_PROBE_POS;     // Coordinates to probe
-                            //measure_point    = { 12.0f, 7.3f };   // Coordinates for the MK52 magnetic heatbed
-
-    static constexpr celsius_t probe_calib_bed_temp = BED_MAX_TARGET,  // Bed temperature while calibrating probe
-                               bed_calib_probe_temp = BTC_PROBE_TEMP;  // Probe temperature while calibrating bed
-
-    static int16_t *sensor_z_offsets[TSI_COUNT],
-                   z_offsets_probe[cali_info_init[TSI_PROBE].measurements], // (µm)
-                   z_offsets_bed[cali_info_init[TSI_BED].measurements];     // (µm)
-
-    #if ENABLED(USE_TEMP_EXT_COMPENSATION)
-      static int16_t z_offsets_ext[cali_info_init[TSI_EXT].measurements];   // (µm)
+    static int16_t *sensor_z_offsets[TSI_COUNT];
+    #if ENABLED(PTC_PROBE)
+      static int16_t z_offsets_probe[PTC_PROBE_COUNT]; // (µm)
+    #endif
+    #if ENABLED(PTC_BED)
+      static int16_t z_offsets_bed[PTC_BED_COUNT];   // (µm)
+    #endif
+    #if ENABLED(PTC_HOTEND)
+      static int16_t z_offsets_hotend[PTC_HOTEND_COUNT];   // (µm)
     #endif
 
     static inline void reset_index() { calib_idx = 0; };
     static inline uint8_t get_index() { return calib_idx; }
+    static void reset();
     static void clear_offsets(const TempSensorID tsi);
     static inline void clear_all_offsets() {
-      clear_offsets(TSI_BED);
-      clear_offsets(TSI_PROBE);
-      TERN_(USE_TEMP_EXT_COMPENSATION, clear_offsets(TSI_EXT));
+      TERN_(PTC_PROBE, clear_offsets(TSI_PROBE));
+      TERN_(PTC_BED, clear_offsets(TSI_BED));
+      TERN_(PTC_HOTEND, clear_offsets(TSI_EXT));
     }
     static bool set_offset(const TempSensorID tsi, const uint8_t idx, const int16_t offset);
     static void print_offsets();
@@ -156,4 +106,4 @@ class ProbeTempComp {
     static bool linear_regression(const TempSensorID tsi, float &k, float &d);
 };
 
-extern ProbeTempComp temp_comp;
+extern ProbeTempComp ptc;
diff --git a/Marlin/src/gcode/bedlevel/abl/G29.cpp b/Marlin/src/gcode/bedlevel/abl/G29.cpp
index 5d94797f16..14da38c8fe 100644
--- a/Marlin/src/gcode/bedlevel/abl/G29.cpp
+++ b/Marlin/src/gcode/bedlevel/abl/G29.cpp
@@ -36,7 +36,7 @@
 #include "../../../module/probe.h"
 #include "../../queue.h"
 
-#if ENABLED(PROBE_TEMP_COMPENSATION)
+#if HAS_PTC
   #include "../../../feature/probe_temp_comp.h"
   #include "../../../module/temperature.h"
 #endif
@@ -645,11 +645,9 @@ G29_TYPE GcodeSuite::G29() {
             break; // Breaks out of both loops
           }
 
-          #if ENABLED(PROBE_TEMP_COMPENSATION)
-            temp_comp.compensate_measurement(TSI_BED, thermalManager.degBed(), abl.measured_z);
-            temp_comp.compensate_measurement(TSI_PROBE, thermalManager.degProbe(), abl.measured_z);
-            TERN_(USE_TEMP_EXT_COMPENSATION, temp_comp.compensate_measurement(TSI_EXT, thermalManager.degHotend(0), abl.measured_z));
-          #endif
+          TERN_(PTC_BED,    ptc.compensate_measurement(TSI_BED,   thermalManager.degBed(),     abl.measured_z));
+          TERN_(PTC_PROBE,  ptc.compensate_measurement(TSI_PROBE, thermalManager.degProbe(),   abl.measured_z));
+          TERN_(PTC_HOTEND, ptc.compensate_measurement(TSI_EXT,   thermalManager.degHotend(0), abl.measured_z));
 
           #if ENABLED(AUTO_BED_LEVELING_LINEAR)
 
diff --git a/Marlin/src/gcode/calibrate/G76_M192_M871.cpp b/Marlin/src/gcode/calibrate/G76_M192_M871.cpp
deleted file mode 100644
index 0fc41ed929..0000000000
--- a/Marlin/src/gcode/calibrate/G76_M192_M871.cpp
+++ /dev/null
@@ -1,358 +0,0 @@
-/**
- * Marlin 3D Printer Firmware
- * Copyright (c) 2020 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/>.
- *
- */
-
-/**
- * G76_M871.cpp - Temperature calibration/compensation for z-probing
- */
-
-#include "../../inc/MarlinConfig.h"
-
-#if ENABLED(PROBE_TEMP_COMPENSATION)
-
-#include "../gcode.h"
-#include "../../module/motion.h"
-#include "../../module/planner.h"
-#include "../../module/probe.h"
-#include "../../feature/bedlevel/bedlevel.h"
-#include "../../module/temperature.h"
-#include "../../module/probe.h"
-#include "../../feature/probe_temp_comp.h"
-#include "../../lcd/marlinui.h"
-
-/**
- * G76: calibrate probe and/or bed temperature offsets
- *  Notes:
- *  - When calibrating probe, bed temperature is held constant.
- *    Compensation values are deltas to first probe measurement at probe temp. = 30°C.
- *  - When calibrating bed, probe temperature is held constant.
- *    Compensation values are deltas to first probe measurement at bed temp. = 60°C.
- *  - The hotend will not be heated at any time.
- *  - On my Průša MK3S clone I put a piece of paper between the probe and the hotend
- *    so the hotend fan would not cool my probe constantly. Alternatively you could just
- *    make sure the fan is not running while running the calibration process.
- *
- *  Probe calibration:
- *  - Moves probe to cooldown point.
- *  - Heats up bed to 100°C.
- *  - Moves probe to probing point (1mm above heatbed).
- *  - Waits until probe reaches target temperature (30°C).
- *  - Does a z-probing (=base value) and increases target temperature by 5°C.
- *  - Waits until probe reaches increased target temperature.
- *  - Does a z-probing (delta to base value will be a compensation value) and increases target temperature by 5°C.
- *  - Repeats last two steps until max. temperature reached or timeout (i.e. probe does not heat up any further).
- *  - Compensation values of higher temperatures will be extrapolated (using linear regression first).
- *    While this is not exact by any means it is still better than simply using the last compensation value.
- *
- *  Bed calibration:
- *  - Moves probe to cooldown point.
- *  - Heats up bed to 60°C.
- *  - Moves probe to probing point (1mm above heatbed).
- *  - Waits until probe reaches target temperature (30°C).
- *  - Does a z-probing (=base value) and increases bed temperature by 5°C.
- *  - Moves probe to cooldown point.
- *  - Waits until probe is below 30°C and bed has reached target temperature.
- *  - Moves probe to probing point and waits until it reaches target temperature (30°C).
- *  - Does a z-probing (delta to base value will be a compensation value) and increases bed temperature by 5°C.
- *  - Repeats last four points until max. bed temperature reached (110°C) or timeout.
- *  - Compensation values of higher temperatures will be extrapolated (using linear regression first).
- *    While this is not exact by any means it is still better than simply using the last compensation value.
- *
- *  G76 [B | P]
- *  - no flag - Both calibration procedures will be run.
- *  - `B` - Run bed temperature calibration.
- *  - `P` - Run probe temperature calibration.
- */
-
-static void say_waiting_for()               { SERIAL_ECHOPGM("Waiting for "); }
-static void say_waiting_for_probe_heating() { say_waiting_for(); SERIAL_ECHOLNPGM("probe heating."); }
-static void say_successfully_calibrated()   { SERIAL_ECHOPGM("Successfully calibrated"); }
-static void say_failed_to_calibrate()       { SERIAL_ECHOPGM("!Failed to calibrate"); }
-
-void GcodeSuite::G76() {
-  // Check if heated bed is available and z-homing is done with probe
-  #if TEMP_SENSOR_BED == 0 || !(HOMING_Z_WITH_PROBE)
-    return;
-  #endif
-
-  auto report_temps = [](millis_t &ntr, millis_t timeout=0) {
-    idle_no_sleep();
-    const millis_t ms = millis();
-    if (ELAPSED(ms, ntr)) {
-      ntr = ms + 1000;
-      thermalManager.print_heater_states(active_extruder);
-    }
-    return (timeout && ELAPSED(ms, timeout));
-  };
-
-  auto wait_for_temps = [&](const celsius_t tb, const celsius_t tp, millis_t &ntr, const millis_t timeout=0) {
-    say_waiting_for(); SERIAL_ECHOLNPGM("bed and probe temperature.");
-    while (thermalManager.wholeDegBed() != tb || thermalManager.wholeDegProbe() > tp)
-      if (report_temps(ntr, timeout)) return true;
-    return false;
-  };
-
-  auto g76_probe = [](const TempSensorID sid, celsius_t &targ, const xy_pos_t &nozpos) {
-    do_z_clearance(5.0); // Raise nozzle before probing
-    const float measured_z = probe.probe_at_point(nozpos, PROBE_PT_STOW, 0, false);  // verbose=0, probe_relative=false
-    if (isnan(measured_z))
-      SERIAL_ECHOLNPGM("!Received NAN. Aborting.");
-    else {
-      SERIAL_ECHOLNPAIR_F("Measured: ", measured_z);
-      if (targ == cali_info_init[sid].start_temp)
-        temp_comp.prepare_new_calibration(measured_z);
-      else
-        temp_comp.push_back_new_measurement(sid, measured_z);
-      targ += cali_info_init[sid].temp_resolution;
-    }
-    return measured_z;
-  };
-
-  #if ENABLED(BLTOUCH)
-    // Make sure any BLTouch error condition is cleared
-    bltouch_command(BLTOUCH_RESET, BLTOUCH_RESET_DELAY);
-    set_bltouch_deployed(false);
-  #endif
-
-  bool do_bed_cal = parser.boolval('B'), do_probe_cal = parser.boolval('P');
-  if (!do_bed_cal && !do_probe_cal) do_bed_cal = do_probe_cal = true;
-
-  // Synchronize with planner
-  planner.synchronize();
-
-  const xyz_pos_t parkpos = temp_comp.park_point,
-            probe_pos_xyz = xyz_pos_t(temp_comp.measure_point) + xyz_pos_t({ 0.0f, 0.0f, PTC_PROBE_HEATING_OFFSET }),
-              noz_pos_xyz = probe_pos_xyz - probe.offset_xy;  // Nozzle position based on probe position
-
-  if (do_bed_cal || do_probe_cal) {
-    // Ensure park position is reachable
-    bool reachable = position_is_reachable(parkpos) || WITHIN(parkpos.z, Z_MIN_POS - fslop, Z_MAX_POS + fslop);
-    if (!reachable)
-      SERIAL_ECHOLNPGM("!Park");
-    else {
-      // Ensure probe position is reachable
-      reachable = probe.can_reach(probe_pos_xyz);
-      if (!reachable) SERIAL_ECHOLNPGM("!Probe");
-    }
-
-    if (!reachable) {
-      SERIAL_ECHOLNPGM(" position unreachable - aborting.");
-      return;
-    }
-
-    process_subcommands_now(FPSTR(G28_STR));
-  }
-
-  remember_feedrate_scaling_off();
-
-  /******************************************
-   * Calibrate bed temperature offsets
-   ******************************************/
-
-  // Report temperatures every second and handle heating timeouts
-  millis_t next_temp_report = millis() + 1000;
-
-  auto report_targets = [&](const celsius_t tb, const celsius_t tp) {
-    SERIAL_ECHOLNPGM("Target Bed:", tb, " Probe:", tp);
-  };
-
-  if (do_bed_cal) {
-
-    celsius_t target_bed = cali_info_init[TSI_BED].start_temp,
-            target_probe = temp_comp.bed_calib_probe_temp;
-
-    say_waiting_for(); SERIAL_ECHOLNPGM(" cooling.");
-    while (thermalManager.wholeDegBed() > target_bed || thermalManager.wholeDegProbe() > target_probe)
-      report_temps(next_temp_report);
-
-    // Disable leveling so it won't mess with us
-    TERN_(HAS_LEVELING, set_bed_leveling_enabled(false));
-
-    for (;;) {
-      thermalManager.setTargetBed(target_bed);
-
-      report_targets(target_bed, target_probe);
-
-      // Park nozzle
-      do_blocking_move_to(parkpos);
-
-      // Wait for heatbed to reach target temp and probe to cool below target temp
-      if (wait_for_temps(target_bed, target_probe, next_temp_report, millis() + MIN_TO_MS(15))) {
-        SERIAL_ECHOLNPGM("!Bed heating timeout.");
-        break;
-      }
-
-      // Move the nozzle to the probing point and wait for the probe to reach target temp
-      do_blocking_move_to(noz_pos_xyz);
-      say_waiting_for_probe_heating();
-      SERIAL_EOL();
-      while (thermalManager.wholeDegProbe() < target_probe)
-        report_temps(next_temp_report);
-
-      const float measured_z = g76_probe(TSI_BED, target_bed, noz_pos_xyz);
-      if (isnan(measured_z) || target_bed > (BED_MAX_TARGET)) break;
-    }
-
-    SERIAL_ECHOLNPGM("Retrieved measurements: ", temp_comp.get_index());
-    if (temp_comp.finish_calibration(TSI_BED)) {
-      say_successfully_calibrated();
-      SERIAL_ECHOLNPGM(" bed.");
-    }
-    else {
-      say_failed_to_calibrate();
-      SERIAL_ECHOLNPGM(" bed. Values reset.");
-    }
-
-    // Cleanup
-    thermalManager.setTargetBed(0);
-    TERN_(HAS_LEVELING, set_bed_leveling_enabled(true));
-  } // do_bed_cal
-
-  /********************************************
-   * Calibrate probe temperature offsets
-   ********************************************/
-
-  if (do_probe_cal) {
-
-    // Park nozzle
-    do_blocking_move_to(parkpos);
-
-    // Initialize temperatures
-    const celsius_t target_bed = temp_comp.probe_calib_bed_temp;
-    thermalManager.setTargetBed(target_bed);
-
-    celsius_t target_probe = cali_info_init[TSI_PROBE].start_temp;
-
-    report_targets(target_bed, target_probe);
-
-    // Wait for heatbed to reach target temp and probe to cool below target temp
-    wait_for_temps(target_bed, target_probe, next_temp_report);
-
-    // Disable leveling so it won't mess with us
-    TERN_(HAS_LEVELING, set_bed_leveling_enabled(false));
-
-    bool timeout = false;
-    for (;;) {
-      // Move probe to probing point and wait for it to reach target temperature
-      do_blocking_move_to(noz_pos_xyz);
-
-      say_waiting_for_probe_heating();
-      SERIAL_ECHOLNPGM(" Bed:", target_bed, " Probe:", target_probe);
-      const millis_t probe_timeout_ms = millis() + SEC_TO_MS(900UL);
-      while (thermalManager.degProbe() < target_probe) {
-        if (report_temps(next_temp_report, probe_timeout_ms)) {
-          SERIAL_ECHOLNPGM("!Probe heating timed out.");
-          timeout = true;
-          break;
-        }
-      }
-      if (timeout) break;
-
-      const float measured_z = g76_probe(TSI_PROBE, target_probe, noz_pos_xyz);
-      if (isnan(measured_z) || target_probe > cali_info_init[TSI_PROBE].end_temp) break;
-    }
-
-    SERIAL_ECHOLNPGM("Retrieved measurements: ", temp_comp.get_index());
-    if (temp_comp.finish_calibration(TSI_PROBE))
-      say_successfully_calibrated();
-    else
-      say_failed_to_calibrate();
-    SERIAL_ECHOLNPGM(" probe.");
-
-    // Cleanup
-    thermalManager.setTargetBed(0);
-    TERN_(HAS_LEVELING, set_bed_leveling_enabled(true));
-
-    SERIAL_ECHOLNPGM("Final compensation values:");
-    temp_comp.print_offsets();
-  } // do_probe_cal
-
-  restore_feedrate_and_scaling();
-}
-
-/**
- * M871: Report / reset temperature compensation offsets.
- *       Note: This does not affect values in EEPROM until M500.
- *
- *   M871 [ R | B | P | E ]
- *
- *    No Parameters - Print current offset values.
- *
- * Select only one of these flags:
- *    R - Reset all offsets to zero (i.e., disable compensation).
- *    B - Manually set offset for bed
- *    P - Manually set offset for probe
- *    E - Manually set offset for extruder
- *
- * With B, P, or E:
- *    I[index] - Index in the array
- *    V[value] - Adjustment in µm
- */
-void GcodeSuite::M871() {
-
-  if (parser.seen('R')) {
-    // Reset z-probe offsets to factory defaults
-    temp_comp.clear_all_offsets();
-    SERIAL_ECHOLNPGM("Offsets reset to default.");
-  }
-  else if (parser.seen("BPE")) {
-    if (!parser.seenval('V')) return;
-    const int16_t offset_val = parser.value_int();
-    if (!parser.seenval('I')) return;
-    const int16_t idx = parser.value_int();
-    const TempSensorID mod = (parser.seen('B') ? TSI_BED :
-                                #if ENABLED(USE_TEMP_EXT_COMPENSATION)
-                                  parser.seen('E') ? TSI_EXT :
-                                #endif
-                                TSI_PROBE
-                              );
-    if (idx > 0 && temp_comp.set_offset(mod, idx - 1, offset_val))
-      SERIAL_ECHOLNPGM("Set value: ", offset_val);
-    else
-      SERIAL_ECHOLNPGM("!Invalid index. Failed to set value (note: value at index 0 is constant).");
-
-  }
-  else // Print current Z-probe adjustments. Note: Values in EEPROM might differ.
-    temp_comp.print_offsets();
-}
-
-/**
- * M192: Wait for probe temperature sensor to reach a target
- *
- * Select only one of these flags:
- *    R - Wait for heating or cooling
- *    S - Wait only for heating
- */
-void GcodeSuite::M192() {
-  if (DEBUGGING(DRYRUN)) return;
-
-  const bool no_wait_for_cooling = parser.seenval('S');
-  if (!no_wait_for_cooling && ! parser.seenval('R')) {
-    SERIAL_ERROR_MSG("No target temperature set.");
-    return;
-  }
-
-  const celsius_t target_temp = parser.value_celsius();
-  ui.set_status(thermalManager.isProbeBelowTemp(target_temp) ? GET_TEXT_F(MSG_PROBE_HEATING) : GET_TEXT_F(MSG_PROBE_COOLING));
-  thermalManager.wait_for_probe(target_temp, no_wait_for_cooling);
-}
-
-#endif // PROBE_TEMP_COMPENSATION
diff --git a/Marlin/src/gcode/calibrate/G76_M871.cpp b/Marlin/src/gcode/calibrate/G76_M871.cpp
new file mode 100644
index 0000000000..21bb2c7590
--- /dev/null
+++ b/Marlin/src/gcode/calibrate/G76_M871.cpp
@@ -0,0 +1,337 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2020 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/>.
+ *
+ */
+
+/**
+ * G76_M871.cpp - Temperature calibration/compensation for z-probing
+ */
+
+#include "../../inc/MarlinConfig.h"
+
+#if HAS_PTC
+
+#include "../gcode.h"
+#include "../../module/motion.h"
+#include "../../module/planner.h"
+#include "../../module/probe.h"
+#include "../../feature/bedlevel/bedlevel.h"
+#include "../../module/temperature.h"
+#include "../../module/probe.h"
+#include "../../feature/probe_temp_comp.h"
+#include "../../lcd/marlinui.h"
+
+/**
+ * G76: calibrate probe and/or bed temperature offsets
+ *  Notes:
+ *  - When calibrating probe, bed temperature is held constant.
+ *    Compensation values are deltas to first probe measurement at probe temp. = 30°C.
+ *  - When calibrating bed, probe temperature is held constant.
+ *    Compensation values are deltas to first probe measurement at bed temp. = 60°C.
+ *  - The hotend will not be heated at any time.
+ *  - On my Průša MK3S clone I put a piece of paper between the probe and the hotend
+ *    so the hotend fan would not cool my probe constantly. Alternatively you could just
+ *    make sure the fan is not running while running the calibration process.
+ *
+ *  Probe calibration:
+ *  - Moves probe to cooldown point.
+ *  - Heats up bed to 100°C.
+ *  - Moves probe to probing point (1mm above heatbed).
+ *  - Waits until probe reaches target temperature (30°C).
+ *  - Does a z-probing (=base value) and increases target temperature by 5°C.
+ *  - Waits until probe reaches increased target temperature.
+ *  - Does a z-probing (delta to base value will be a compensation value) and increases target temperature by 5°C.
+ *  - Repeats last two steps until max. temperature reached or timeout (i.e. probe does not heat up any further).
+ *  - Compensation values of higher temperatures will be extrapolated (using linear regression first).
+ *    While this is not exact by any means it is still better than simply using the last compensation value.
+ *
+ *  Bed calibration:
+ *  - Moves probe to cooldown point.
+ *  - Heats up bed to 60°C.
+ *  - Moves probe to probing point (1mm above heatbed).
+ *  - Waits until probe reaches target temperature (30°C).
+ *  - Does a z-probing (=base value) and increases bed temperature by 5°C.
+ *  - Moves probe to cooldown point.
+ *  - Waits until probe is below 30°C and bed has reached target temperature.
+ *  - Moves probe to probing point and waits until it reaches target temperature (30°C).
+ *  - Does a z-probing (delta to base value will be a compensation value) and increases bed temperature by 5°C.
+ *  - Repeats last four points until max. bed temperature reached (110°C) or timeout.
+ *  - Compensation values of higher temperatures will be extrapolated (using linear regression first).
+ *    While this is not exact by any means it is still better than simply using the last compensation value.
+ *
+ *  G76 [B | P]
+ *  - no flag - Both calibration procedures will be run.
+ *  - `B` - Run bed temperature calibration.
+ *  - `P` - Run probe temperature calibration.
+ */
+
+static void say_waiting_for()               { SERIAL_ECHOPGM("Waiting for "); }
+static void say_waiting_for_probe_heating() { say_waiting_for(); SERIAL_ECHOLNPGM("probe heating."); }
+static void say_successfully_calibrated()   { SERIAL_ECHOPGM("Successfully calibrated"); }
+static void say_failed_to_calibrate()       { SERIAL_ECHOPGM("!Failed to calibrate"); }
+
+#if BOTH(PTC_PROBE, PTC_BED)
+
+  void GcodeSuite::G76() {
+    auto report_temps = [](millis_t &ntr, millis_t timeout=0) {
+      idle_no_sleep();
+      const millis_t ms = millis();
+      if (ELAPSED(ms, ntr)) {
+        ntr = ms + 1000;
+        thermalManager.print_heater_states(active_extruder);
+      }
+      return (timeout && ELAPSED(ms, timeout));
+    };
+
+    auto wait_for_temps = [&](const celsius_t tb, const celsius_t tp, millis_t &ntr, const millis_t timeout=0) {
+      say_waiting_for(); SERIAL_ECHOLNPGM("bed and probe temperature.");
+      while (thermalManager.wholeDegBed() != tb || thermalManager.wholeDegProbe() > tp)
+        if (report_temps(ntr, timeout)) return true;
+      return false;
+    };
+
+    auto g76_probe = [](const TempSensorID sid, celsius_t &targ, const xy_pos_t &nozpos) {
+      do_z_clearance(5.0); // Raise nozzle before probing
+      const float measured_z = probe.probe_at_point(nozpos, PROBE_PT_STOW, 0, false);  // verbose=0, probe_relative=false
+      if (isnan(measured_z))
+        SERIAL_ECHOLNPGM("!Received NAN. Aborting.");
+      else {
+        SERIAL_ECHOLNPAIR_F("Measured: ", measured_z);
+        if (targ == ProbeTempComp::cali_info[sid].start_temp)
+          ptc.prepare_new_calibration(measured_z);
+        else
+          ptc.push_back_new_measurement(sid, measured_z);
+        targ += ProbeTempComp::cali_info[sid].temp_resolution;
+      }
+      return measured_z;
+    };
+
+    #if ENABLED(BLTOUCH)
+      // Make sure any BLTouch error condition is cleared
+      bltouch_command(BLTOUCH_RESET, BLTOUCH_RESET_DELAY);
+      set_bltouch_deployed(false);
+    #endif
+
+    bool do_bed_cal = parser.boolval('B'), do_probe_cal = parser.boolval('P');
+    if (!do_bed_cal && !do_probe_cal) do_bed_cal = do_probe_cal = true;
+
+    // Synchronize with planner
+    planner.synchronize();
+
+    #ifndef PTC_PROBE_HEATING_OFFSET
+      #define PTC_PROBE_HEATING_OFFSET 0
+    #endif
+    const xyz_pos_t parkpos = PTC_PARK_POS,
+              probe_pos_xyz = xyz_pos_t(PTC_PROBE_POS) + xyz_pos_t({ 0.0f, 0.0f, PTC_PROBE_HEATING_OFFSET }),
+                noz_pos_xyz = probe_pos_xyz - probe.offset_xy;  // Nozzle position based on probe position
+
+    if (do_bed_cal || do_probe_cal) {
+      // Ensure park position is reachable
+      bool reachable = position_is_reachable(parkpos) || WITHIN(parkpos.z, Z_MIN_POS - fslop, Z_MAX_POS + fslop);
+      if (!reachable)
+        SERIAL_ECHOLNPGM("!Park");
+      else {
+        // Ensure probe position is reachable
+        reachable = probe.can_reach(probe_pos_xyz);
+        if (!reachable) SERIAL_ECHOLNPGM("!Probe");
+      }
+
+      if (!reachable) {
+        SERIAL_ECHOLNPGM(" position unreachable - aborting.");
+        return;
+      }
+
+      process_subcommands_now(FPSTR(G28_STR));
+    }
+
+    remember_feedrate_scaling_off();
+
+    /******************************************
+     * Calibrate bed temperature offsets
+     ******************************************/
+
+    // Report temperatures every second and handle heating timeouts
+    millis_t next_temp_report = millis() + 1000;
+
+    auto report_targets = [&](const celsius_t tb, const celsius_t tp) {
+      SERIAL_ECHOLNPGM("Target Bed:", tb, " Probe:", tp);
+    };
+
+    if (do_bed_cal) {
+
+      celsius_t target_bed = PTC_BED_START,
+                target_probe = PTC_PROBE_TEMP;
+
+      say_waiting_for(); SERIAL_ECHOLNPGM(" cooling.");
+      while (thermalManager.wholeDegBed() > target_bed || thermalManager.wholeDegProbe() > target_probe)
+        report_temps(next_temp_report);
+
+      // Disable leveling so it won't mess with us
+      TERN_(HAS_LEVELING, set_bed_leveling_enabled(false));
+
+      for (uint8_t idx = 0; idx <= PTC_BED_COUNT; idx++) {
+        thermalManager.setTargetBed(target_bed);
+
+        report_targets(target_bed, target_probe);
+
+        // Park nozzle
+        do_blocking_move_to(parkpos);
+
+        // Wait for heatbed to reach target temp and probe to cool below target temp
+        if (wait_for_temps(target_bed, target_probe, next_temp_report, millis() + MIN_TO_MS(15))) {
+          SERIAL_ECHOLNPGM("!Bed heating timeout.");
+          break;
+        }
+
+        // Move the nozzle to the probing point and wait for the probe to reach target temp
+        do_blocking_move_to(noz_pos_xyz);
+        say_waiting_for_probe_heating();
+        SERIAL_EOL();
+        while (thermalManager.wholeDegProbe() < target_probe)
+          report_temps(next_temp_report);
+
+        const float measured_z = g76_probe(TSI_BED, target_bed, noz_pos_xyz);
+        if (isnan(measured_z) || target_bed > (BED_MAX_TARGET)) break;
+      }
+
+      SERIAL_ECHOLNPGM("Retrieved measurements: ", ptc.get_index());
+      if (ptc.finish_calibration(TSI_BED)) {
+        say_successfully_calibrated();
+        SERIAL_ECHOLNPGM(" bed.");
+      }
+      else {
+        say_failed_to_calibrate();
+        SERIAL_ECHOLNPGM(" bed. Values reset.");
+      }
+
+      // Cleanup
+      thermalManager.setTargetBed(0);
+      TERN_(HAS_LEVELING, set_bed_leveling_enabled(true));
+    } // do_bed_cal
+
+    /********************************************
+     * Calibrate probe temperature offsets
+     ********************************************/
+
+    if (do_probe_cal) {
+
+      // Park nozzle
+      do_blocking_move_to(parkpos);
+
+      // Initialize temperatures
+      const celsius_t target_bed = BED_MAX_TARGET;
+      thermalManager.setTargetBed(target_bed);
+
+      celsius_t target_probe = PTC_PROBE_START;
+
+      report_targets(target_bed, target_probe);
+
+      // Wait for heatbed to reach target temp and probe to cool below target temp
+      wait_for_temps(target_bed, target_probe, next_temp_report);
+
+      // Disable leveling so it won't mess with us
+      TERN_(HAS_LEVELING, set_bed_leveling_enabled(false));
+
+      bool timeout = false;
+      for (uint8_t idx = 0; idx <= PTC_PROBE_COUNT; idx++) {
+        // Move probe to probing point and wait for it to reach target temperature
+        do_blocking_move_to(noz_pos_xyz);
+
+        say_waiting_for_probe_heating();
+        SERIAL_ECHOLNPGM(" Bed:", target_bed, " Probe:", target_probe);
+        const millis_t probe_timeout_ms = millis() + SEC_TO_MS(900UL);
+        while (thermalManager.degProbe() < target_probe) {
+          if (report_temps(next_temp_report, probe_timeout_ms)) {
+            SERIAL_ECHOLNPGM("!Probe heating timed out.");
+            timeout = true;
+            break;
+          }
+        }
+        if (timeout) break;
+
+        const float measured_z = g76_probe(TSI_PROBE, target_probe, noz_pos_xyz);
+        if (isnan(measured_z)) break;
+      }
+
+      SERIAL_ECHOLNPGM("Retrieved measurements: ", ptc.get_index());
+      if (ptc.finish_calibration(TSI_PROBE))
+        say_successfully_calibrated();
+      else
+        say_failed_to_calibrate();
+      SERIAL_ECHOLNPGM(" probe.");
+
+      // Cleanup
+      thermalManager.setTargetBed(0);
+      TERN_(HAS_LEVELING, set_bed_leveling_enabled(true));
+
+      SERIAL_ECHOLNPGM("Final compensation values:");
+      ptc.print_offsets();
+    } // do_probe_cal
+
+    restore_feedrate_and_scaling();
+  }
+
+#endif // PTC_PROBE && PTC_BED
+
+/**
+ * M871: Report / reset temperature compensation offsets.
+ *       Note: This does not affect values in EEPROM until M500.
+ *
+ *   M871 [ R | B | P | E ]
+ *
+ *    No Parameters - Print current offset values.
+ *
+ * Select only one of these flags:
+ *    R - Reset all offsets to zero (i.e., disable compensation).
+ *    B - Manually set offset for bed
+ *    P - Manually set offset for probe
+ *    E - Manually set offset for extruder
+ *
+ * With B, P, or E:
+ *    I[index] - Index in the array
+ *    V[value] - Adjustment in µm
+ */
+void GcodeSuite::M871() {
+
+  if (parser.seen('R')) {
+    // Reset z-probe offsets to factory defaults
+    ptc.clear_all_offsets();
+    SERIAL_ECHOLNPGM("Offsets reset to default.");
+  }
+  else if (parser.seen("BPE")) {
+    if (!parser.seenval('V')) return;
+    const int16_t offset_val = parser.value_int();
+    if (!parser.seenval('I')) return;
+    const int16_t idx = parser.value_int();
+    const TempSensorID mod = TERN_(PTC_BED,    parser.seen_test('B') ? TSI_BED   :)
+                             TERN_(PTC_HOTEND, parser.seen_test('E') ? TSI_EXT   :)
+                             TERN_(PTC_PROBE,  parser.seen_test('P') ? TSI_PROBE :) TSI_COUNT;
+    if (mod == TSI_COUNT)
+      SERIAL_ECHOLNPGM("!Invalid sensor.");
+    else if (idx > 0 && ptc.set_offset(mod, idx - 1, offset_val))
+      SERIAL_ECHOLNPGM("Set value: ", offset_val);
+    else
+      SERIAL_ECHOLNPGM("!Invalid index. Failed to set value (note: value at index 0 is constant).");
+  }
+  else // Print current Z-probe adjustments. Note: Values in EEPROM might differ.
+    ptc.print_offsets();
+}
+
+#endif // HAS_PTC
diff --git a/Marlin/src/gcode/gcode.cpp b/Marlin/src/gcode/gcode.cpp
index 25c06ce269..c72ac64e98 100644
--- a/Marlin/src/gcode/gcode.cpp
+++ b/Marlin/src/gcode/gcode.cpp
@@ -424,7 +424,7 @@ void GcodeSuite::process_parsed_command(const bool no_ok/*=false*/) {
         case 61: G61(); break;                                    // G61:  Apply/restore saved coordinates.
       #endif
 
-      #if ENABLED(PROBE_TEMP_COMPENSATION)
+      #if BOTH(PTC_PROBE, PTC_BED)
         case 76: G76(); break;                                    // G76: Calibrate first layer compensation values
       #endif
 
@@ -587,6 +587,10 @@ void GcodeSuite::process_parsed_command(const bool no_ok/*=false*/) {
         case 191: M191(); break;                                  // M191: Wait for chamber temperature to reach target
       #endif
 
+      #if HAS_TEMP_PROBE
+        case 192: M192(); break;                                  // M192: Wait for probe temp
+      #endif
+
       #if HAS_COOLER
         case 143: M143(); break;                                  // M143: Set cooler temperature
         case 193: M193(); break;                                  // M193: Wait for cooler temperature to reach target
@@ -921,8 +925,7 @@ void GcodeSuite::process_parsed_command(const bool no_ok/*=false*/) {
         case 852: M852(); break;                                  // M852: Set Skew factors
       #endif
 
-      #if ENABLED(PROBE_TEMP_COMPENSATION)
-        case 192: M192(); break;                                  // M192: Wait for probe temp
+      #if HAS_PTC
         case 871: M871(); break;                                  // M871: Print/reset/clear first layer temperature offset values
       #endif
 
diff --git a/Marlin/src/gcode/gcode.h b/Marlin/src/gcode/gcode.h
index 8de15fc9c7..09dd53d6f6 100644
--- a/Marlin/src/gcode/gcode.h
+++ b/Marlin/src/gcode/gcode.h
@@ -66,7 +66,7 @@
  * G42  - Coordinated move to a mesh point (Requires MESH_BED_LEVELING, AUTO_BED_LEVELING_BLINEAR, or AUTO_BED_LEVELING_UBL)
  * G60  - Save current position. (Requires SAVED_POSITIONS)
  * G61  - Apply/restore saved coordinates. (Requires SAVED_POSITIONS)
- * G76  - Calibrate first layer temperature offsets. (Requires PROBE_TEMP_COMPENSATION)
+ * G76  - Calibrate first layer temperature offsets. (Requires PTC_PROBE and PTC_BED)
  * G80  - Cancel current motion mode (Requires GCODE_MOTION_MODES)
  * G90  - Use Absolute Coordinates
  * G91  - Use Relative Coordinates
@@ -88,6 +88,8 @@
  * M16  - Expected printer check. (Requires EXPECTED_PRINTER_CHECK)
  * M17  - Enable/Power all stepper motors
  * M18  - Disable all stepper motors; same as M84
+ *
+ *** Print from Media (SDSUPPORT) ***
  * M20  - List SD card. (Requires SDSUPPORT)
  * M21  - Init SD card. (Requires SDSUPPORT)
  * M22  - Release SD card. (Requires SDSUPPORT)
@@ -100,30 +102,36 @@
  *        OR, with 'C' get the current filename.
  * M28  - Start SD write: "M28 /path/file.gco". (Requires SDSUPPORT)
  * M29  - Stop SD write. (Requires SDSUPPORT)
- * M30  - Delete file from SD: "M30 /path/file.gco"
+ * M30  - Delete file from SD: "M30 /path/file.gco" (Requires SDSUPPORT)
  * M31  - Report time since last M109 or SD card start to serial.
  * M32  - Select file and start SD print: "M32 [S<bytepos>] !/path/file.gco#". (Requires SDSUPPORT)
  *        Use P to run other files as sub-programs: "M32 P !filename#"
  *        The '#' is necessary when calling from within sd files, as it stops buffer prereading
  * M33  - Get the longname version of a path. (Requires LONG_FILENAME_HOST_SUPPORT)
  * M34  - Set SD Card sorting options. (Requires SDCARD_SORT_ALPHA)
+ *
  * M42  - Change pin status via gcode: M42 P<pin> S<value>. LED pin assumed if P is omitted. (Requires DIRECT_PIN_CONTROL)
- * M43  - Display pin status, watch pins for changes, watch endstops & toggle LED, Z servo probe test, toggle pins
+ * M43  - Display pin status, watch pins for changes, watch endstops & toggle LED, Z servo probe test, toggle pins (Requires PINS_DEBUGGING)
  * M48  - Measure Z Probe repeatability: M48 P<points> X<pos> Y<pos> V<level> E<engage> L<legs> S<chizoid>. (Requires Z_MIN_PROBE_REPEATABILITY_TEST)
+ *
  * M73  - Set the progress percentage. (Requires LCD_SET_PROGRESS_MANUALLY)
  * M75  - Start the print job timer.
  * M76  - Pause the print job timer.
  * M77  - Stop the print job timer.
  * M78  - Show statistical information about the print jobs. (Requires PRINTCOUNTER)
+ *
  * M80  - Turn on Power Supply. (Requires PSU_CONTROL)
  * M81  - Turn off Power Supply. (Requires PSU_CONTROL)
+ *
  * M82  - Set E codes absolute (default).
  * M83  - Set E codes relative while in Absolute (G90) mode.
  * M84  - Disable steppers until next move, or use S<seconds> to specify an idle
  *        duration after which steppers should turn off. S0 disables the timeout.
  * M85  - Set inactivity shutdown timer with parameter S<seconds>. To disable set zero (default)
  * M92  - Set planner.settings.axis_steps_per_mm for one or more axes.
+ *
  * M100 - Watch Free Memory (for debugging) (Requires M100_FREE_MEMORY_WATCHER)
+ *
  * M104 - Set extruder target temp.
  * M105 - Report current temperatures.
  * M106 - Set print fan speed.
@@ -132,23 +140,29 @@
  * M109 - S<temp> Wait for extruder current temp to reach target temp. ** Wait only when heating! **
  *        R<temp> Wait for extruder current temp to reach target temp. ** Wait for heating or cooling. **
  *        If AUTOTEMP is enabled, S<mintemp> B<maxtemp> F<factor>. Exit autotemp by any M109 without F
+ *
  * M110 - Set the current line number. (Used by host printing)
  * M111 - Set debug flags: "M111 S<flagbits>". See flag bits defined in enum.h.
  * M112 - Full Shutdown.
+ *
  * M113 - Get or set the timeout interval for Host Keepalive "busy" messages. (Requires HOST_KEEPALIVE_FEATURE)
  * M114 - Report current position.
  * M115 - Report capabilities. (Extended capabilities requires EXTENDED_CAPABILITIES_REPORT)
  * M117 - Display a message on the controller screen. (Requires an LCD)
  * M118 - Display a message in the host console.
+ *
  * M119 - Report endstops status.
  * M120 - Enable endstops detection.
  * M121 - Disable endstops detection.
+ *
  * M122 - Debug stepper (Requires at least one _DRIVER_TYPE defined as TMC2130/2160/5130/5160/2208/2209/2660 or L6470)
  * M125 - Save current position and move to filament change position. (Requires PARK_HEAD_ON_PAUSE)
+ *
  * M126 - Solenoid Air Valve Open. (Requires BARICUDA)
  * M127 - Solenoid Air Valve Closed. (Requires BARICUDA)
  * M128 - EtoP Open. (Requires BARICUDA)
  * M129 - EtoP Closed. (Requires BARICUDA)
+ *
  * M140 - Set bed target temp. S<temp>
  * M141 - Set heated chamber target temp. S<temp> (Requires a chamber heater)
  * M143 - Set cooler target temp. S<temp> (Requires a laser cooling device)
@@ -161,9 +175,9 @@
  * M164 - Commit the mix and save to a virtual tool (current, or as specified by 'S'). (Requires MIXING_EXTRUDER)
  * M165 - Set the mix for the mixing extruder (and current virtual tool) with parameters ABCDHI. (Requires MIXING_EXTRUDER and DIRECT_MIXING_IN_G1)
  * M166 - Set the Gradient Mix for the mixing extruder. (Requires GRADIENT_MIX)
- * M190 - S<temp> Wait for bed current temp to reach target temp. ** Wait only when heating! **
- *        R<temp> Wait for bed current temp to reach target temp. ** Wait for heating or cooling. **
- * M193 - R<temp> Wait for cooler temp to reach target temp. ** Wait for cooling. **
+ * M190 - Set bed target temperature and wait. R<temp> Set target temperature and wait. S<temp> Set, but only wait when heating. (Requires TEMP_SENSOR_BED)
+ * M192 - Wait for probe to reach target temperature. (Requires TEMP_SENSOR_PROBE)
+ * M193 - R<temp> Wait for cooler to reach target temp. ** Wait for cooling. **
  * M200 - Set filament diameter, D<diameter>, setting E axis units to cubic. (Use S0 to revert to linear units.)
  * M201 - Set max acceleration in units/s^2 for print moves: "M201 X<accel> Y<accel> Z<accel> E<accel>"
  * M202 - Set max acceleration in units/s^2 for travel moves: "M202 X<accel> Y<accel> Z<accel> E<accel>" ** UNUSED IN MARLIN! **
@@ -183,7 +197,7 @@
  * M218 - Set/get a tool offset: "M218 T<index> X<offset> Y<offset>". (Requires 2 or more extruders)
  * M220 - Set Feedrate Percentage: "M220 S<percent>" (i.e., "FR" on the LCD)
  *        Use "M220 B" to back up the Feedrate Percentage and "M220 R" to restore it. (Requires an MMU_MODEL version 2 or 2S)
- * M221 - Set Flow Percentage: "M221 S<percent>"
+ * M221 - Set Flow Percentage: "M221 S<percent>" (Requires an extruder)
  * M226 - Wait until a pin is in a given state: "M226 P<pin> S<state>" (Requires DIRECT_PIN_CONTROL)
  * M240 - Trigger a camera to take a photograph. (Requires PHOTO_GCODE)
  * M250 - Set LCD contrast: "M250 C<contrast>" (0-63). (Requires LCD support)
@@ -230,9 +244,9 @@
  * M502 - Revert to the default "factory settings". ** Does not write them to EEPROM! **
  * M503 - Print the current settings (in memory): "M503 S<verbose>". S0 specifies compact output.
  * M504 - Validate EEPROM contents. (Requires EEPROM_SETTINGS)
- * M510 - Lock Printer
- * M511 - Unlock Printer
- * M512 - Set/Change/Remove Password
+ * M510 - Lock Printer (Requires PASSWORD_FEATURE)
+ * M511 - Unlock Printer (Requires PASSWORD_UNLOCK_GCODE)
+ * M512 - Set/Change/Remove Password (Requires PASSWORD_CHANGE_GCODE)
  * M524 - Abort the current SD print job started with M24. (Requires SDSUPPORT)
  * M540 - Enable/disable SD card abort on endstop hit: "M540 S<state>". (Requires SD_ABORT_ON_ENDSTOP_HIT)
  * M552 - Get or set IP address. Enable/disable network interface. (Requires enabled Ethernet port)
@@ -252,7 +266,9 @@
  * M808 - Set or Goto a Repeat Marker (Requires GCODE_REPEAT_MARKERS)
  * M810-M819 - Define/execute a G-code macro (Requires GCODE_MACROS)
  * M851 - Set Z probe's XYZ offsets in current units. (Negative values: X=left, Y=front, Z=below)
- * M852 - Set skew factors: "M852 [I<xy>] [J<xz>] [K<yz>]". (Requires SKEW_CORRECTION_GCODE, and SKEW_CORRECTION_FOR_Z for IJ)
+ * M852 - Set skew factors: "M852 [I<xy>] [J<xz>] [K<yz>]". (Requires SKEW_CORRECTION_GCODE, plus SKEW_CORRECTION_FOR_Z for IJ)
+ *
+ *** I2C_POSITION_ENCODERS ***
  * M860 - Report the position of position encoder modules.
  * M861 - Report the status of position encoder modules.
  * M862 - Perform an axis continuity test for position encoder modules.
@@ -263,8 +279,8 @@
  * M867 - Enable/disable or toggle error correction for position encoder modules.
  * M868 - Report or set position encoder module error correction threshold.
  * M869 - Report position encoder module error.
- * M871 - Print/reset/clear first layer temperature offset values. (Requires PROBE_TEMP_COMPENSATION)
- * M192 - Wait for probe temp (Requires PROBE_TEMP_COMPENSATION)
+ *
+ * M871 - Print/reset/clear first layer temperature offset values. (Requires PTC_PROBE, PTC_BED, or PTC_HOTEND)
  * M876 - Handle Prompt Response. (Requires HOST_PROMPT_SUPPORT and not EMERGENCY_PARSER)
  * M900 - Get or Set Linear Advance K-factor. (Requires LIN_ADVANCE)
  * M906 - Set or get motor current in milliamps using axis codes X, Y, Z, E. Report values if no axis codes given. (Requires at least one _DRIVER_TYPE defined as TMC2130/2160/5130/5160/2208/2209/2660 or L6470)
@@ -282,13 +298,14 @@
  * M951 - Set Magnetic Parking Extruder parameters. (Requires MAGNETIC_PARKING_EXTRUDER)
  * M7219 - Control Max7219 Matrix LEDs. (Requires MAX7219_GCODE)
  *
+ *** SCARA ***
  * M360 - SCARA calibration: Move to cal-position ThetaA (0 deg calibration)
  * M361 - SCARA calibration: Move to cal-position ThetaB (90 deg calibration - steps per degree)
  * M362 - SCARA calibration: Move to cal-position PsiA (0 deg calibration)
  * M363 - SCARA calibration: Move to cal-position PsiB (90 deg calibration - steps per degree)
  * M364 - SCARA calibration: Move to cal-position PSIC (90 deg to Theta calibration position)
  *
- * ************ Custom codes - This can change to suit future G-code regulations
+ *** Custom codes (can be changed to suit future G-code standards) ***
  * G425 - Calibrate using a conductive object. (Requires CALIBRATION_GCODE)
  * M928 - Start SD logging: "M928 filename.gco". Stop with M29. (Requires SDSUPPORT)
  * M993 - Backup SPI Flash to SD
@@ -296,10 +313,11 @@
  * M995 - Touch screen calibration for TFT display
  * M997 - Perform in-application firmware update
  * M999 - Restart after being stopped by error
+ *
  * D... - Custom Development G-code. Add hooks to 'gcode_D.cpp' for developers to test features. (Requires MARLIN_DEV_MODE)
  *        D576 - Set buffer monitoring options. (Requires BUFFER_MONITORING)
  *
- * "T" Codes
+ *** "T" Codes ***
  *
  * T0-T3 - Select an extruder (tool) by index: "T<n> F<units/min>"
  */
@@ -551,7 +569,7 @@ private:
     static void G59();
   #endif
 
-  #if ENABLED(PROBE_TEMP_COMPENSATION)
+  #if BOTH(PTC_PROBE, PTC_BED)
     static void G76();
   #endif
 
@@ -744,6 +762,10 @@ private:
     static void M191();
   #endif
 
+  #if HAS_TEMP_PROBE
+    static void M192();
+  #endif
+
   #if HAS_COOLER
     static void M143();
     static void M193();
@@ -1087,8 +1109,7 @@ private:
     FORCE_INLINE static void M869() { I2CPEM.M869(); }
   #endif
 
-  #if ENABLED(PROBE_TEMP_COMPENSATION)
-    static void M192();
+  #if HAS_PTC
     static void M871();
   #endif
 
diff --git a/Marlin/src/gcode/temp/M192.cpp b/Marlin/src/gcode/temp/M192.cpp
new file mode 100644
index 0000000000..a96e2d34a4
--- /dev/null
+++ b/Marlin/src/gcode/temp/M192.cpp
@@ -0,0 +1,56 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2021 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/>.
+ *
+ */
+
+/**
+ * M192.cpp - Wait for probe to reach temperature
+ */
+
+#include "../../inc/MarlinConfig.h"
+
+#if HAS_TEMP_PROBE
+
+#include "../gcode.h"
+#include "../../module/temperature.h"
+#include "../../lcd/marlinui.h"
+
+/**
+ * M192: Wait for probe temperature sensor to reach a target
+ *
+ * Select only one of these flags:
+ *    R - Wait for heating or cooling
+ *    S - Wait only for heating
+ */
+void GcodeSuite::M192() {
+  if (DEBUGGING(DRYRUN)) return;
+
+  const bool no_wait_for_cooling = parser.seenval('S');
+  if (!no_wait_for_cooling && !parser.seenval('R')) {
+    SERIAL_ERROR_MSG("No target temperature set.");
+    return;
+  }
+
+  const celsius_t target_temp = parser.value_celsius();
+  ui.set_status(thermalManager.isProbeBelowTemp(target_temp) ? GET_TEXT_F(MSG_PROBE_HEATING) : GET_TEXT_F(MSG_PROBE_COOLING));
+  thermalManager.wait_for_probe(target_temp, no_wait_for_cooling);
+}
+
+#endif // HAS_TEMP_PROBE
diff --git a/Marlin/src/inc/Conditionals_adv.h b/Marlin/src/inc/Conditionals_adv.h
index efb9db420d..49067a5606 100644
--- a/Marlin/src/inc/Conditionals_adv.h
+++ b/Marlin/src/inc/Conditionals_adv.h
@@ -550,6 +550,20 @@
   #endif
 #endif
 
+// Probe Temperature Compensation
+#if !TEMP_SENSOR_PROBE
+  #undef PTC_PROBE
+#endif
+#if !TEMP_SENSOR_BED
+  #undef PTC_BED
+#endif
+#if !HAS_EXTRUDERS
+  #undef PTC_HOTEND
+#endif
+#if ANY(PTC_PROBE, PTC_BED, PTC_HOTEND)
+  #define HAS_PTC 1
+#endif
+
 // Let SD_FINISHED_RELEASECOMMAND stand in for SD_FINISHED_STEPPERRELEASE
 #if ENABLED(SD_FINISHED_STEPPERRELEASE)
   #ifndef SD_FINISHED_RELEASECOMMAND
diff --git a/Marlin/src/inc/SanityCheck.h b/Marlin/src/inc/SanityCheck.h
index a015873fc0..edd12a2885 100644
--- a/Marlin/src/inc/SanityCheck.h
+++ b/Marlin/src/inc/SanityCheck.h
@@ -597,6 +597,10 @@
   #error "SPINDLE_LASER_PWM (true) is now set with SPINDLE_LASER_USE_PWM (enabled)."
 #elif ANY(IS_RAMPS_EEB, IS_RAMPS_EEF, IS_RAMPS_EFB, IS_RAMPS_EFF, IS_RAMPS_SF)
   #error "The IS_RAMPS_* conditionals (for heater/fan/bed pins) are now called FET_ORDER_*."
+#elif defined(PROBE_TEMP_COMPENSATION)
+  #error "PROBE_TEMP_COMPENSATION is now set using the PTC_PROBE, PTC_BED, PTC_HOTEND options."
+#elif defined(BTC_PROBE_TEMP)
+  #error "BTC_PROBE_TEMP is now PTC_PROBE_TEMP."
 #endif
 
 #if MB(DUE3DOM_MINI) && PIN_EXISTS(TEMP_2) && DISABLED(TEMP_SENSOR_BOARD)
@@ -611,60 +615,60 @@ static_assert(COUNT(arm) == LOGICAL_AXES, "AXIS_RELATIVE_MODES must contain " _L
 /**
  * Probe temp compensation requirements
  */
-
-#if ENABLED(PROBE_TEMP_COMPENSATION)
-  #if defined(PTC_PARK_POS_X) || defined(PTC_PARK_POS_Y) || defined(PTC_PARK_POS_Z)
-    #error "PTC_PARK_POS_[XYZ] is now PTC_PARK_POS (array)."
-  #elif !defined(PTC_PARK_POS)
-    #error "PROBE_TEMP_COMPENSATION requires PTC_PARK_POS."
-  #elif defined(PTC_PROBE_POS_X) || defined(PTC_PROBE_POS_Y)
-    #error "PTC_PROBE_POS_[XY] is now PTC_PROBE_POS (array)."
-  #elif !defined(PTC_PROBE_POS)
-    #error "PROBE_TEMP_COMPENSATION requires PTC_PROBE_POS."
+#if HAS_PTC
+  #if TEMP_SENSOR_PROBE && TEMP_SENSOR_BED
+    #if defined(PTC_PARK_POS_X) || defined(PTC_PARK_POS_Y) || defined(PTC_PARK_POS_Z)
+      #error "PTC_PARK_POS_[XYZ] is now PTC_PARK_POS (array)."
+    #elif !defined(PTC_PARK_POS)
+      #error "PTC_PARK_POS is required for Probe Temperature Compensation."
+    #elif defined(PTC_PROBE_POS_X) || defined(PTC_PROBE_POS_Y)
+      #error "PTC_PROBE_POS_[XY] is now PTC_PROBE_POS (array)."
+    #elif !defined(PTC_PROBE_POS)
+      #error "PTC_PROBE_POS is required for Probe Temperature Compensation."
+    #endif
   #endif
 
-  #ifdef PTC_SAMPLE_START
-    constexpr auto _ptc_sample_start = PTC_SAMPLE_START;
+  #ifdef PTC_PROBE_START
+    constexpr auto _ptc_sample_start = PTC_PROBE_START;
     constexpr decltype(_ptc_sample_start) _test_ptc_sample_start = 12.3f;
-    static_assert(_test_ptc_sample_start != 12.3f, "PTC_SAMPLE_START must be a whole number.");
+    static_assert(_test_ptc_sample_start != 12.3f, "PTC_PROBE_START must be a whole number.");
   #endif
-  #ifdef PTC_SAMPLE_RES
-    constexpr auto _ptc_sample_res = PTC_SAMPLE_RES;
+  #ifdef PTC_PROBE_RES
+    constexpr auto _ptc_sample_res = PTC_PROBE_RES;
     constexpr decltype(_ptc_sample_res) _test_ptc_sample_res = 12.3f;
-    static_assert(_test_ptc_sample_res != 12.3f, "PTC_SAMPLE_RES must be a whole number.");
+    static_assert(_test_ptc_sample_res != 12.3f, "PTC_PROBE_RES must be a whole number.");
   #endif
-  #ifdef BTC_SAMPLE_START
-    constexpr auto _btc_sample_start = BTC_SAMPLE_START;
+  #ifdef PTC_BED_START
+    constexpr auto _btc_sample_start = PTC_BED_START;
     constexpr decltype(_btc_sample_start) _test_btc_sample_start = 12.3f;
-    static_assert(_test_btc_sample_start != 12.3f, "BTC_SAMPLE_START must be a whole number.");
+    static_assert(_test_btc_sample_start != 12.3f, "PTC_BED_START must be a whole number.");
   #endif
-  #ifdef BTC_SAMPLE_RES
-    constexpr auto _btc_sample_res = BTC_SAMPLE_RES;
+  #ifdef PTC_BED_RES
+    constexpr auto _btc_sample_res = PTC_BED_RES;
     constexpr decltype(_btc_sample_res) _test_btc_sample_res = 12.3f;
-    static_assert(_test_btc_sample_res != 12.3f, "BTC_SAMPLE_RES must be a whole number.");
+    static_assert(_test_btc_sample_res != 12.3f, "PTC_BED_RES must be a whole number.");
   #endif
-  #ifdef BTC_PROBE_TEMP
-    constexpr auto _btc_probe_temp = BTC_PROBE_TEMP;
+  #ifdef PTC_PROBE_TEMP
+    constexpr auto _btc_probe_temp = PTC_PROBE_TEMP;
     constexpr decltype(_btc_probe_temp) _test_btc_probe_temp = 12.3f;
-    static_assert(_test_btc_probe_temp != 12.3f, "BTC_PROBE_TEMP must be a whole number.");
+    static_assert(_test_btc_probe_temp != 12.3f, "PTC_PROBE_TEMP must be a whole number.");
   #endif
-  #if ENABLED(USE_TEMP_EXT_COMPENSATION)
-    #ifdef ETC_SAMPLE_START
-      constexpr auto _etc_sample_start = ETC_SAMPLE_START;
+  #if ENABLED(PTC_HOTEND)
+    #if EXTRUDERS != 1
+      #error "PTC_HOTEND only works with a single extruder."
+    #endif
+    #ifdef PTC_HOTEND_START
+      constexpr auto _etc_sample_start = PTC_HOTEND_START;
       constexpr decltype(_etc_sample_start) _test_etc_sample_start = 12.3f;
-      static_assert(_test_etc_sample_start != 12.3f, "ETC_SAMPLE_START must be a whole number.");
+      static_assert(_test_etc_sample_start != 12.3f, "PTC_HOTEND_START must be a whole number.");
     #endif
-    #ifdef ETC_SAMPLE_RES
-      constexpr auto _etc_sample_res = ETC_SAMPLE_RES;
+    #ifdef PTC_HOTEND_RES
+      constexpr auto _etc_sample_res = PTC_HOTEND_RES;
       constexpr decltype(_etc_sample_res) _test_etc_sample_res = 12.3f;
-      static_assert(_test_etc_sample_res != 12.3f, "ETC_SAMPLE_RES must be a whole number.");
+      static_assert(_test_etc_sample_res != 12.3f, "PTC_HOTEND_RES must be a whole number.");
     #endif
   #endif
-
-  #if ENABLED(USE_TEMP_EXT_COMPENSATION) && EXTRUDERS != 1
-    #error "USE_TEMP_EXT_COMPENSATION only works with a single extruder."
-  #endif
-#endif
+#endif // HAS_PTC
 
 /**
  * Marlin release, version and default string
diff --git a/Marlin/src/module/settings.cpp b/Marlin/src/module/settings.cpp
index c82f5aad0d..01a5c47fd5 100644
--- a/Marlin/src/module/settings.cpp
+++ b/Marlin/src/module/settings.cpp
@@ -128,7 +128,7 @@
   #include "../feature/tmc_util.h"
 #endif
 
-#if ENABLED(PROBE_TEMP_COMPENSATION)
+#if HAS_PTC
   #include "../feature/probe_temp_comp.h"
 #endif
 
@@ -264,13 +264,16 @@ typedef struct SettingsDataStruct {
   //
   // Temperature first layer compensation values
   //
-  #if ENABLED(PROBE_TEMP_COMPENSATION)
-    int16_t z_offsets_probe[COUNT(temp_comp.z_offsets_probe)], // M871 P I V
-            z_offsets_bed[COUNT(temp_comp.z_offsets_bed)]      // M871 B I V
-            #if ENABLED(USE_TEMP_EXT_COMPENSATION)
-              , z_offsets_ext[COUNT(temp_comp.z_offsets_ext)]  // M871 E I V
-            #endif
-            ;
+  #if HAS_PTC
+    #if ENABLED(PTC_PROBE)
+      int16_t z_offsets_probe[COUNT(ptc.z_offsets_probe)]; // M871 P I V
+    #endif
+    #if ENABLED(PTC_BED)
+      int16_t z_offsets_bed[COUNT(ptc.z_offsets_bed)];     // M871 B I V
+    #endif
+    #if ENABLED(PTC_HOTEND)
+      int16_t z_offsets_hotend[COUNT(ptc.z_offsets_hotend)];     // M871 E I V
+    #endif
   #endif
 
   //
@@ -844,11 +847,15 @@ void MarlinSettings::postprocess() {
     //
     // Thermal first layer compensation values
     //
-    #if ENABLED(PROBE_TEMP_COMPENSATION)
-      EEPROM_WRITE(temp_comp.z_offsets_probe);
-      EEPROM_WRITE(temp_comp.z_offsets_bed);
-      #if ENABLED(USE_TEMP_EXT_COMPENSATION)
-        EEPROM_WRITE(temp_comp.z_offsets_ext);
+    #if HAS_PTC
+      #if ENABLED(PTC_PROBE)
+        EEPROM_WRITE(ptc.z_offsets_probe);
+      #endif
+      #if ENABLED(PTC_BED)
+        EEPROM_WRITE(ptc.z_offsets_bed);
+      #endif
+      #if ENABLED(PTC_HOTEND)
+        EEPROM_WRITE(ptc.z_offsets_hotend);
       #endif
     #else
       // No placeholder data for this feature
@@ -1710,13 +1717,17 @@ void MarlinSettings::postprocess() {
       //
       // Thermal first layer compensation values
       //
-      #if ENABLED(PROBE_TEMP_COMPENSATION)
-        EEPROM_READ(temp_comp.z_offsets_probe);
-        EEPROM_READ(temp_comp.z_offsets_bed);
-        #if ENABLED(USE_TEMP_EXT_COMPENSATION)
-          EEPROM_READ(temp_comp.z_offsets_ext);
+      #if HAS_PTC
+        #if ENABLED(PTC_PROBE)
+          EEPROM_READ(ptc.z_offsets_probe);
         #endif
-        temp_comp.reset_index();
+        # if ENABLED(PTC_BED)
+          EEPROM_READ(ptc.z_offsets_bed);
+        #endif
+        #if ENABLED(PTC_HOTEND)
+          EEPROM_READ(ptc.z_offsets_hotend);
+        #endif
+        ptc.reset_index();
       #else
         // No placeholder data for this feature
       #endif
@@ -2728,6 +2739,11 @@ void MarlinSettings::reset() {
   //
   TERN_(EDITABLE_SERVO_ANGLES, COPY(servo_angles, base_servo_angles)); // When not editable only one copy of servo angles exists
 
+  //
+  // Probe Temperature Compensation
+  //
+  TERN_(HAS_PTC, ptc.reset());
+
   //
   // BLTOUCH
   //
diff --git a/buildroot/tests/rambo b/buildroot/tests/rambo
index a563bd4ed3..b7136e445e 100755
--- a/buildroot/tests/rambo
+++ b/buildroot/tests/rambo
@@ -18,7 +18,7 @@ opt_set MOTHERBOARD BOARD_RAMBO \
         FANMUX0_PIN 53
 opt_disable Z_MIN_PROBE_USES_Z_MIN_ENDSTOP_PIN USE_WATCHDOG
 opt_enable USE_ZMAX_PLUG REPRAP_DISCOUNT_SMART_CONTROLLER LCD_PROGRESS_BAR LCD_PROGRESS_BAR_TEST \
-           FIX_MOUNTED_PROBE CODEPENDENT_XY_HOMING PIDTEMPBED PROBE_TEMP_COMPENSATION \
+           FIX_MOUNTED_PROBE CODEPENDENT_XY_HOMING PIDTEMPBED PTC_PROBE PTC_BED \
            PREHEAT_BEFORE_PROBING PROBING_HEATERS_OFF PROBING_FANS_OFF PROBING_STEPPERS_OFF WAIT_FOR_BED_HEATER \
            EEPROM_SETTINGS SDSUPPORT SD_REPRINT_LAST_SELECTED_FILE BINARY_FILE_TRANSFER \
            BLINKM PCA9533 PCA9632 RGB_LED RGB_LED_R_PIN RGB_LED_G_PIN RGB_LED_B_PIN LED_CONTROL_MENU \
@@ -61,16 +61,16 @@ opt_disable MIN_SOFTWARE_ENDSTOP_Z MAX_SOFTWARE_ENDSTOPS
 exec_test $1 $2 "Rambo CNC Configuration" "$3"
 
 #
-# Rambo heated bed only
+# Rambo heated bed and probe temp sensor
 #
 restore_configs
-opt_set MOTHERBOARD BOARD_RAMBO EXTRUDERS 0 TEMP_SENSOR_BED 1 \
+opt_set MOTHERBOARD BOARD_RAMBO EXTRUDERS 0 TEMP_SENSOR_BED 1 TEMP_SENSOR_PROBE 1 TEMP_PROBE_PIN 12 \
         DEFAULT_AXIS_STEPS_PER_UNIT '{ 80, 80, 4000 }' \
         DEFAULT_MAX_FEEDRATE '{ 300, 300, 5 }' \
         DEFAULT_MAX_ACCELERATION '{ 3000, 3000, 100 }' \
         MANUAL_FEEDRATE '{ 50*60, 50*60, 4*60 }' \
         AXIS_RELATIVE_MODES '{ false, false, false }'
-opt_enable REPRAP_DISCOUNT_FULL_GRAPHIC_SMART_CONTROLLER
+opt_enable REPRAP_DISCOUNT_FULL_GRAPHIC_SMART_CONTROLLER FIX_MOUNTED_PROBE Z_SAFE_HOMING
 exec_test $1 $2 "Rambo heated bed only" "$3"
 
 #
diff --git a/ini/features.ini b/ini/features.ini
index f54b645f85..4c14651298 100644
--- a/ini/features.ini
+++ b/ini/features.ini
@@ -136,20 +136,20 @@ ADVANCED_PAUSE_FEATURE                 = src_filter=+<src/feature/pause.cpp> +<s
 PSU_CONTROL                            = src_filter=+<src/feature/power.cpp>
 HAS_POWER_MONITOR                      = src_filter=+<src/feature/power_monitor.cpp> +<src/gcode/feature/power_monitor>
 POWER_LOSS_RECOVERY                    = src_filter=+<src/feature/powerloss.cpp> +<src/gcode/feature/powerloss>
-PROBE_TEMP_COMPENSATION                = src_filter=+<src/feature/probe_temp_comp.cpp> +<src/gcode/calibrate/G76_M192_M871.cpp>
+HAS_PTC                                = src_filter=+<src/feature/probe_temp_comp.cpp> +<src/gcode/calibrate/G76_M871.cpp>
 HAS_FILAMENT_SENSOR                    = src_filter=+<src/feature/runout.cpp> +<src/gcode/feature/runout>
 (EXT|MANUAL)_SOLENOID.*                = src_filter=+<src/feature/solenoid.cpp> +<src/gcode/control/M380_M381.cpp>
 MK2_MULTIPLEXER                        = src_filter=+<src/feature/snmm.cpp>
 HAS_CUTTER                             = src_filter=+<src/feature/spindle_laser.cpp> +<src/gcode/control/M3-M5.cpp>
 HAS_DRIVER_SAFE_POWER_PROTECT          = src_filter=+<src/feature/stepper_driver_safety.cpp>
 EXPERIMENTAL_I2CBUS                    = src_filter=+<src/feature/twibus.cpp> +<src/gcode/feature/i2c>
-MECHANICAL_GANTRY_CAL.+                = src_filter=+<src/gcode/calibrate/G34.cpp>
-Z_MULTI_ENDSTOPS                       = src_filter=+<src/gcode/calibrate/G34_M422.cpp>
-Z_STEPPER_AUTO_ALIGN                   = src_filter=+<src/feature/z_stepper_align.cpp> +<src/gcode/calibrate/G34_M422.cpp>
 G26_MESH_VALIDATION                    = src_filter=+<src/gcode/bedlevel/G26.cpp>
 ASSISTED_TRAMMING                      = src_filter=+<src/feature/tramming.cpp> +<src/gcode/bedlevel/G35.cpp>
 HAS_MESH                               = src_filter=+<src/gcode/bedlevel/G42.cpp>
 HAS_LEVELING                           = src_filter=+<src/gcode/bedlevel/M420.cpp> +<src/feature/bedlevel/bedlevel.cpp>
+MECHANICAL_GANTRY_CAL.+                = src_filter=+<src/gcode/calibrate/G34.cpp>
+Z_MULTI_ENDSTOPS|Z_STEPPER_AUTO_ALIGN  = src_filter=+<src/gcode/calibrate/G34_M422.cpp>
+Z_STEPPER_AUTO_ALIGN                   = src_filter=+<src/feature/z_stepper_align.cpp>
 DELTA_AUTO_CALIBRATION                 = src_filter=+<src/gcode/calibrate/G33.cpp>
 CALIBRATION_GCODE                      = src_filter=+<src/gcode/calibrate/G425.cpp>
 Z_MIN_PROBE_REPEATABILITY_TEST         = src_filter=+<src/gcode/calibrate/M48.cpp>
@@ -209,6 +209,7 @@ SDSUPPORT                              = src_filter=+<src/sd/cardreader.cpp> +<s
 HAS_MEDIA_SUBCALLS                     = src_filter=+<src/gcode/sd/M32.cpp>
 GCODE_REPEAT_MARKERS                   = src_filter=+<src/feature/repeat.cpp> +<src/gcode/sd/M808.cpp>
 HAS_EXTRUDERS                          = src_filter=+<src/gcode/units/M82_M83.cpp> +<src/gcode/temp/M104_M109.cpp> +<src/gcode/config/M221.cpp>
+HAS_TEMP_PROBE                         = src_filter=+<src/gcode/temp/M192.cpp>
 HAS_COOLER                             = src_filter=+<src/gcode/temp/M143_M193.cpp>
 HAS_COOLER|LASER_COOLANT_FLOW_METER    = src_filter=+<src/feature/cooler.cpp>
 AUTO_REPORT_TEMPERATURES               = src_filter=+<src/gcode/temp/M155.cpp>
diff --git a/platformio.ini b/platformio.ini
index 106e454d10..a364e8920f 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -152,7 +152,7 @@ default_src_filter = +<src/*> -<src/config> -<src/HAL> +<src/HAL/shared>
   -<src/gcode/calibrate/G33.cpp>
   -<src/gcode/calibrate/G34.cpp>
   -<src/gcode/calibrate/G34_M422.cpp>
-  -<src/gcode/calibrate/G76_M192_M871.cpp>
+  -<src/gcode/calibrate/G76_M871.cpp>
   -<src/gcode/calibrate/G425.cpp>
   -<src/gcode/calibrate/M12.cpp>
   -<src/gcode/calibrate/M48.cpp>
@@ -229,6 +229,7 @@ default_src_filter = +<src/*> -<src/config> -<src/HAL> +<src/HAL/shared>
   -<src/gcode/sd/M808.cpp>
   -<src/gcode/temp/M104_M109.cpp>
   -<src/gcode/temp/M155.cpp>
+  -<src/gcode/temp/M192.cpp>
   -<src/gcode/units/G20_G21.cpp>
   -<src/gcode/units/M82_M83.cpp>
   -<src/gcode/units/M149.cpp>