From 6b549e19719e855555c1a9db5e3c0453516afb28 Mon Sep 17 00:00:00 2001
From: Giuliano Zaro <3684609+GMagician@users.noreply.github.com>
Date: Sat, 22 Aug 2020 04:20:30 +0200
Subject: [PATCH] Optional homing in LCD Repeatability Test (#19104)

---
 Marlin/src/gcode/calibrate/M48.cpp    | 169 +++++++++++++-------------
 Marlin/src/lcd/language/language_en.h |   1 +
 Marlin/src/lcd/language/language_it.h |   1 +
 Marlin/src/lcd/menu/menu_motion.cpp   |   2 +-
 4 files changed, 90 insertions(+), 83 deletions(-)

diff --git a/Marlin/src/gcode/calibrate/M48.cpp b/Marlin/src/gcode/calibrate/M48.cpp
index 47c72eece7..fc9d22957b 100644
--- a/Marlin/src/gcode/calibrate/M48.cpp
+++ b/Marlin/src/gcode/calibrate/M48.cpp
@@ -27,13 +27,10 @@
 #include "../gcode.h"
 #include "../../module/motion.h"
 #include "../../module/probe.h"
+#include "../../lcd/ultralcd.h"
 
 #include "../../feature/bedlevel/bedlevel.h"
 
-#if HAS_SPI_LCD
-  #include "../../lcd/ultralcd.h"
-#endif
-
 #if HAS_LEVELING
   #include "../../module/planner.h"
 #endif
@@ -77,61 +74,85 @@ void GcodeSuite::M48() {
 
   const ProbePtRaise raise_after = parser.boolval('E') ? PROBE_PT_STOW : PROBE_PT_RAISE;
 
-  xy_float_t next_pos = current_position;
-
-  const xy_pos_t probe_pos = {
-    parser.linearval('X', next_pos.x + probe.offset_xy.x),  // If no X use the probe's current X position
-    parser.linearval('Y', next_pos.y + probe.offset_xy.y)   // If no Y, ditto
+  // Test at the current position by default, overridden by X and Y
+  const xy_pos_t test_position = {
+    parser.linearval('X', current_position.x + probe.offset_xy.x),  // If no X use the probe's current X position
+    parser.linearval('Y', current_position.y + probe.offset_xy.y)   // If no Y, ditto
   };
 
-  if (!probe.can_reach(probe_pos)) {
+  if (!probe.can_reach(test_position)) {
+    ui.set_status_P(GET_TEXT(MSG_M48_OUT_OF_BOUNDS), 99);
     SERIAL_ECHOLNPGM("? (X,Y) out of bounds.");
     return;
   }
 
+  // Get the number of leg moves per test-point
   bool seen_L = parser.seen('L');
   uint8_t n_legs = seen_L ? parser.value_byte() : 0;
   if (n_legs > 15) {
-    SERIAL_ECHOLNPGM("?Number of legs in movement not plausible (0-15).");
+    SERIAL_ECHOLNPGM("?Legs of movement implausible (0-15).");
     return;
   }
   if (n_legs == 1) n_legs = 2;
 
+  // Schizoid motion as an optional stress-test
   const bool schizoid_flag = parser.boolval('S');
   if (schizoid_flag && !seen_L) n_legs = 7;
 
-  /**
-   * Now get everything to the specified probe point So we can safely do a
-   * probe to get us close to the bed.  If the Z-Axis is far from the bed,
-   * we don't want to use that as a starting point for each probe.
-   */
   if (verbose_level > 2)
     SERIAL_ECHOLNPGM("Positioning the probe...");
 
-  // Disable bed level correction in M48 because we want the raw data when we probe
+  // Always disable Bed Level correction before probing...
 
   #if HAS_LEVELING
     const bool was_enabled = planner.leveling_active;
     set_bed_leveling_enabled(false);
   #endif
 
+  // Work with reasonable feedrates
   remember_feedrate_scaling_off();
 
-  float mean = 0.0, sigma = 0.0, min = 99999.9, max = -99999.9, sample_set[n_samples];
+  // Working variables
+  float mean = 0.0,     // The average of all points so far, used to calculate deviation
+        sigma = 0.0,    // Standard deviation of all points so far
+        min = 99999.9,  // Smallest value sampled so far
+        max = -99999.9, // Largest value sampled so far
+        sample_set[n_samples];  // Storage for sampled values
+
+  auto dev_report = [](const bool verbose, const float &mean, const float &sigma, const float &min, const float &max, const bool final=false) {
+    if (verbose) {
+      SERIAL_ECHOPAIR_F("Mean: ", mean, 6);
+      if (!final) SERIAL_ECHOPAIR_F(" Sigma: ", sigma, 6);
+      SERIAL_ECHOPAIR_F(" Min: ", min, 3);
+      SERIAL_ECHOPAIR_F(" Max: ", max, 3);
+      SERIAL_ECHOPAIR_F(" Range: ", max-min, 3);
+      if (final) SERIAL_EOL();
+    }
+    if (final) {
+      SERIAL_ECHOLNPAIR_F("Standard Deviation: ", sigma, 6);
+      SERIAL_EOL();
+    }
+  };
 
   // Move to the first point, deploy, and probe
-  const float t = probe.probe_at_point(probe_pos, raise_after, verbose_level);
+  const float t = probe.probe_at_point(test_position, raise_after, verbose_level);
   bool probing_good = !isnan(t);
 
   if (probing_good) {
     randomSeed(millis());
 
+    float sample_sum = 0.0;
+
     LOOP_L_N(n, n_samples) {
       #if HAS_SPI_LCD
         // Display M48 progress in the status bar
         ui.status_printf_P(0, PSTR(S_FMT ": %d/%d"), GET_TEXT(MSG_M48_POINT), int(n + 1), int(n_samples));
       #endif
+
+      // When there are "legs" of movement move around the point before probing
       if (n_legs) {
+
+        // Pick a random direction, starting angle, and radius
         const int dir = (random(0, 10) > 5.0) ? -1 : 1;  // clockwise or counter clockwise
         float angle = random(0, 360);
         const float radius = random(
@@ -142,48 +163,51 @@ void GcodeSuite::M48() {
             int(5), int(0.125 * _MIN(X_BED_SIZE, Y_BED_SIZE))
           #endif
         );
-
         if (verbose_level > 3) {
           SERIAL_ECHOPAIR("Start radius:", radius, " angle:", angle, " dir:");
           if (dir > 0) SERIAL_CHAR('C');
           SERIAL_ECHOLNPGM("CW");
         }
 
+        // Move from leg to leg in rapid succession
         LOOP_L_N(l, n_legs - 1) {
-          float delta_angle;
 
+          // Move some distance around the perimeter
+          float delta_angle;
           if (schizoid_flag) {
-            // The points of a 5 point star are 72 degrees apart.  We need to
-            // skip a point and go to the next one on the star.
+            // The points of a 5 point star are 72 degrees apart.
+            // Skip a point and go to the next one on the star.
             delta_angle = dir * 2.0 * 72.0;
           }
           else {
-            // If we do this line, we are just trying to move further
-            // around the circle.
-            delta_angle = dir * (float) random(25, 45);
+            // Just move further along the perimeter.
+            delta_angle = dir * (float)random(25, 45);
           }
-
           angle += delta_angle;
-          while (angle > 360.0) angle -= 360.0; // We probably do not need to keep the angle between 0 and 2*PI, but the
-                                                // Arduino documentation says the trig functions should not be given values
-          while (angle < 0.0) angle += 360.0;   // outside of this range.   It looks like they behave correctly with
-                                                // numbers outside of the range, but just to be safe we clamp them.
 
-          const xy_pos_t noz_pos = probe_pos - probe.offset_xy;
-          next_pos.set(noz_pos.x + cos(RADIANS(angle)) * radius,
-                       noz_pos.y + sin(RADIANS(angle)) * radius);
+          // Trig functions work without clamping, but just to be safe...
+          while (angle > 360.0) angle -= 360.0;
+          while (angle < 0.0) angle += 360.0;
 
-          #if DISABLED(DELTA)
-            LIMIT(next_pos.x, X_MIN_POS, X_MAX_POS);
-            LIMIT(next_pos.y, Y_MIN_POS, Y_MAX_POS);
-          #else
-            // If we have gone out too far, we can do a simple fix and scale the numbers
-            // back in closer to the origin.
+          // Choose the next position as an offset to chosen test position
+          const xy_pos_t noz_pos = test_position - probe.offset_xy;
+          xy_pos_t next_pos = {
+            noz_pos.x + cos(RADIANS(angle)) * radius,
+            noz_pos.y + sin(RADIANS(angle)) * radius
+          };
+
+          #if ENABLED(DELTA)
+            // If the probe can't reach the point on a round bed...
+            // Simply scale the numbers to bring them closer to origin.
             while (!probe.can_reach(next_pos)) {
               next_pos *= 0.8f;
               if (verbose_level > 3)
                 SERIAL_ECHOLNPAIR_P(PSTR("Moving inward: X"), next_pos.x, SP_Y_STR, next_pos.y);
             }
+          #else
+            // For a rectangular bed just keep the probe in bounds
+            LIMIT(next_pos.x, X_MIN_POS, X_MAX_POS);
+            LIMIT(next_pos.y, Y_MIN_POS, Y_MAX_POS);
           #endif
 
           if (verbose_level > 3)
@@ -194,45 +218,35 @@ void GcodeSuite::M48() {
       } // n_legs
 
       // Probe a single point
-      sample_set[n] = probe.probe_at_point(probe_pos, raise_after, 0);
+      const float pz = probe.probe_at_point(test_position, raise_after, 0);
 
       // Break the loop if the probe fails
-      probing_good = !isnan(sample_set[n]);
+      probing_good = !isnan(pz);
       if (!probing_good) break;
 
-      /**
-       * Get the current mean for the data points we have so far
-       */
-      float sum = 0.0;
-      LOOP_LE_N(j, n) sum += sample_set[j];
-      mean = sum / (n + 1);
+      // Store the new sample
+      sample_set[n] = pz;
 
-      NOMORE(min, sample_set[n]);
-      NOLESS(max, sample_set[n]);
+      // Keep track of the largest and smallest samples
+      NOMORE(min, pz);
+      NOLESS(max, pz);
 
-      /**
-       * Now, use that mean to calculate the standard deviation for the
-       * data points we have so far
-       */
-      sum = 0.0;
-      LOOP_LE_N(j, n)
-        sum += sq(sample_set[j] - mean);
+      // Get the mean value of all samples thus far
+      sample_sum += pz;
+      mean = sample_sum / (n + 1);
 
-      sigma = SQRT(sum / (n + 1));
-      if (verbose_level > 0) {
-        if (verbose_level > 1) {
-          SERIAL_ECHO(n + 1);
-          SERIAL_ECHOPAIR(" of ", int(n_samples));
-          SERIAL_ECHOPAIR_F(": z: ", sample_set[n], 3);
-          if (verbose_level > 2) {
-            SERIAL_ECHOPAIR_F(" mean: ", mean, 4);
-            SERIAL_ECHOPAIR_F(" sigma: ", sigma, 6);
-            SERIAL_ECHOPAIR_F(" min: ", min, 3);
-            SERIAL_ECHOPAIR_F(" max: ", max, 3);
-            SERIAL_ECHOPAIR_F(" range: ", max-min, 3);
-          }
-          SERIAL_EOL();
-        }
+      // Calculate the standard deviation so far.
+      // The value after the last sample will be the final output.
+      float dev_sum = 0.0;
+      LOOP_LE_N(j, n) dev_sum += sq(sample_set[j] - mean);
+      sigma = SQRT(dev_sum / (n + 1));
+
+      if (verbose_level > 1) {
+        SERIAL_ECHO(n + 1);
+        SERIAL_ECHOPAIR(" of ", int(n_samples));
+        SERIAL_ECHOPAIR_F(": z: ", pz, 3);
+        dev_report(verbose_level > 2, mean, sigma, min, max);
+        SERIAL_EOL();
       }
 
     } // n_samples loop
@@ -242,16 +256,7 @@ void GcodeSuite::M48() {
 
   if (probing_good) {
     SERIAL_ECHOLNPGM("Finished!");
-
-    if (verbose_level > 0) {
-      SERIAL_ECHOPAIR_F("Mean: ", mean, 6);
-      SERIAL_ECHOPAIR_F(" Min: ", min, 3);
-      SERIAL_ECHOPAIR_F(" Max: ", max, 3);
-      SERIAL_ECHOLNPAIR_F(" Range: ", max-min, 3);
-    }
-
-    SERIAL_ECHOLNPAIR_F("Standard Deviation: ", sigma, 6);
-    SERIAL_EOL();
+    dev_report(verbose_level > 0, mean, sigma, min, max, true);
 
     #if HAS_SPI_LCD
       // Display M48 results in the status bar
diff --git a/Marlin/src/lcd/language/language_en.h b/Marlin/src/lcd/language/language_en.h
index 3791a4ad18..5aa0d76a84 100644
--- a/Marlin/src/lcd/language/language_en.h
+++ b/Marlin/src/lcd/language/language_en.h
@@ -124,6 +124,7 @@ namespace Language_en {
   PROGMEM Language_Str MSG_USER_MENU                       = _UxGT("Custom Commands");
   PROGMEM Language_Str MSG_M48_TEST                        = _UxGT("M48 Probe Test");
   PROGMEM Language_Str MSG_M48_POINT                       = _UxGT("M48 Point");
+  PROGMEM Language_Str MSG_M48_OUT_OF_BOUNDS               = _UxGT("Probe out of bounds");
   PROGMEM Language_Str MSG_M48_DEVIATION                   = _UxGT("Deviation");
   PROGMEM Language_Str MSG_IDEX_MENU                       = _UxGT("IDEX Mode");
   PROGMEM Language_Str MSG_OFFSETS_MENU                    = _UxGT("Tool Offsets");
diff --git a/Marlin/src/lcd/language/language_it.h b/Marlin/src/lcd/language/language_it.h
index 1a5bdb22e9..43765d7c3a 100644
--- a/Marlin/src/lcd/language/language_it.h
+++ b/Marlin/src/lcd/language/language_it.h
@@ -122,6 +122,7 @@ namespace Language_it {
   PROGMEM Language_Str MSG_LCD_TILTING_MESH                = _UxGT("Punto inclinaz.");
   PROGMEM Language_Str MSG_M48_TEST                        = _UxGT("Test sonda M48");
   PROGMEM Language_Str MSG_M48_POINT                       = _UxGT("Punto M48");
+  PROGMEM Language_Str MSG_M48_OUT_OF_BOUNDS               = _UxGT("Sonda oltre i limiti");
   PROGMEM Language_Str MSG_M48_DEVIATION                   = _UxGT("Deviazione");
   PROGMEM Language_Str MSG_IDEX_MENU                       = _UxGT("Modo IDEX");
   PROGMEM Language_Str MSG_OFFSETS_MENU                    = _UxGT("Strumenti Offsets");
diff --git a/Marlin/src/lcd/menu/menu_motion.cpp b/Marlin/src/lcd/menu/menu_motion.cpp
index 914b229008..027be4029d 100644
--- a/Marlin/src/lcd/menu/menu_motion.cpp
+++ b/Marlin/src/lcd/menu/menu_motion.cpp
@@ -386,7 +386,7 @@ void menu_motion() {
   #endif
 
   #if ENABLED(Z_MIN_PROBE_REPEATABILITY_TEST)
-    GCODES_ITEM(MSG_M48_TEST, PSTR("G28\nM48 P10"));
+    GCODES_ITEM(MSG_M48_TEST, PSTR("G28 O\nM48 P10"));
   #endif
 
   //