From 9a74bcd4cf13efa2d7cfb365182d7e097eaa1f1d Mon Sep 17 00:00:00 2001
From: John Robertson <john@cirtech.co.uk>
Date: Tue, 31 May 2022 23:09:44 +0100
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Laser=20Safety=20Timeout=20(#24189)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Marlin/Configuration_adv.h               | 10 ++++++
 Marlin/src/MarlinCore.cpp                | 43 ++++++++++++------------
 Marlin/src/feature/spindle_laser.cpp     |  4 +--
 Marlin/src/feature/spindle_laser.h       |  3 +-
 Marlin/src/gcode/control/M17_M18_M84.cpp | 11 +++++-
 Marlin/src/gcode/control/M3-M5.cpp       |  4 +++
 Marlin/src/gcode/control/M85.cpp         |  9 ++++-
 Marlin/src/gcode/gcode.cpp               |  7 ++--
 Marlin/src/gcode/gcode.h                 | 16 ++++++---
 Marlin/src/inc/Conditionals_adv.h        |  4 +++
 Marlin/src/inc/SanityCheck.h             |  6 ++++
 Marlin/src/module/temperature.cpp        | 13 +++++++
 buildroot/tests/BIGTREE_SKR_PRO          |  2 +-
 buildroot/tests/mega2560                 |  8 ++---
 14 files changed, 102 insertions(+), 38 deletions(-)

diff --git a/Marlin/Configuration_adv.h b/Marlin/Configuration_adv.h
index ab0b7cdf20e..1202f1f5179 100644
--- a/Marlin/Configuration_adv.h
+++ b/Marlin/Configuration_adv.h
@@ -3544,6 +3544,16 @@
     #define LASER_TEST_PULSE_MIN           1   // Used with Laser Control Menu
     #define LASER_TEST_PULSE_MAX         999   // Caution: Menu may not show more than 3 characters
 
+   /**
+    * Laser Safety Timeout
+    *
+    * The laser should be turned off when there is no movement for a period of time.
+    * Consider material flammability, cut rate, and G-code order when setting this
+    * value. Too low and it could turn off during a very slow move; too high and
+    * the material could ignite.
+    */
+    #define LASER_SAFETY_TIMEOUT_MS     1000   // (ms)
+
     /**
      * Enable inline laser power to be handled in the planner / stepper routines.
      * Inline power is specified by the I (inline) flag in an M3 command (e.g., M3 S20 I)
diff --git a/Marlin/src/MarlinCore.cpp b/Marlin/src/MarlinCore.cpp
index e25c3a49119..76bc988483c 100644
--- a/Marlin/src/MarlinCore.cpp
+++ b/Marlin/src/MarlinCore.cpp
@@ -423,34 +423,35 @@ inline void manage_inactivity(const bool no_stepper_sleep=false) {
     kill();
   }
 
+  const bool has_blocks = planner.has_blocks_queued();  // Any moves in the planner?
+  if (has_blocks) gcode.reset_stepper_timeout(ms);      // Reset timeout for M18/M84, M85 max 'kill', and laser.
+
   // M18 / M84 : Handle steppers inactive time timeout
-  if (gcode.stepper_inactive_time) {
+  #if HAS_DISABLE_INACTIVE_AXIS
+    if (gcode.stepper_inactive_time) {
 
-    static bool already_shutdown_steppers; // = false
+      static bool already_shutdown_steppers; // = false
 
-    // Any moves in the planner? Resets both the M18/M84
-    // activity timeout and the M85 max 'kill' timeout
-    if (planner.has_blocks_queued())
-      gcode.reset_stepper_timeout(ms);
-    else if (!do_reset_timeout && gcode.stepper_inactive_timeout()) {
-      if (!already_shutdown_steppers) {
-        already_shutdown_steppers = true;  // L6470 SPI will consume 99% of free time without this
+      if (!has_blocks && !do_reset_timeout && gcode.stepper_inactive_timeout()) {
+        if (!already_shutdown_steppers) {
+          already_shutdown_steppers = true;  // L6470 SPI will consume 99% of free time without this
 
-        // Individual axes will be disabled if configured
-        TERN_(DISABLE_INACTIVE_X, stepper.disable_axis(X_AXIS));
-        TERN_(DISABLE_INACTIVE_Y, stepper.disable_axis(Y_AXIS));
-        TERN_(DISABLE_INACTIVE_Z, stepper.disable_axis(Z_AXIS));
-        TERN_(DISABLE_INACTIVE_I, stepper.disable_axis(I_AXIS));
-        TERN_(DISABLE_INACTIVE_J, stepper.disable_axis(J_AXIS));
-        TERN_(DISABLE_INACTIVE_K, stepper.disable_axis(K_AXIS));
-        TERN_(DISABLE_INACTIVE_E, stepper.disable_e_steppers());
+          // Individual axes will be disabled if configured
+          TERN_(DISABLE_INACTIVE_X, stepper.disable_axis(X_AXIS));
+          TERN_(DISABLE_INACTIVE_Y, stepper.disable_axis(Y_AXIS));
+          TERN_(DISABLE_INACTIVE_Z, stepper.disable_axis(Z_AXIS));
+          TERN_(DISABLE_INACTIVE_I, stepper.disable_axis(I_AXIS));
+          TERN_(DISABLE_INACTIVE_J, stepper.disable_axis(J_AXIS));
+          TERN_(DISABLE_INACTIVE_K, stepper.disable_axis(K_AXIS));
+          TERN_(DISABLE_INACTIVE_E, stepper.disable_e_steppers());
 
-        TERN_(AUTO_BED_LEVELING_UBL, bedlevel.steppers_were_disabled());
+          TERN_(AUTO_BED_LEVELING_UBL, bedlevel.steppers_were_disabled());
+        }
       }
+      else
+        already_shutdown_steppers = false;
     }
-    else
-      already_shutdown_steppers = false;
-  }
+  #endif
 
   #if ENABLED(PHOTO_GCODE) && PIN_EXISTS(CHDK)
     // Check if CHDK should be set to LOW (after M240 set it HIGH)
diff --git a/Marlin/src/feature/spindle_laser.cpp b/Marlin/src/feature/spindle_laser.cpp
index ef3503c40d8..8f70816c994 100644
--- a/Marlin/src/feature/spindle_laser.cpp
+++ b/Marlin/src/feature/spindle_laser.cpp
@@ -39,7 +39,8 @@
 #endif
 
 SpindleLaser cutter;
-uint8_t SpindleLaser::power;
+uint8_t SpindleLaser::power,
+        SpindleLaser::last_power_applied; // = 0                      // Basic power state tracking
 #if ENABLED(LASER_FEATURE)
   cutter_test_pulse_t SpindleLaser::testPulse = 50;                   // Test fire Pulse time ms value.
 #endif
@@ -113,7 +114,6 @@ void SpindleLaser::init() {
  * @param opwr Power value. Range 0 to MAX. When 0 disable spindle/laser.
  */
 void SpindleLaser::apply_power(const uint8_t opwr) {
-  static uint8_t last_power_applied = 0;
   if (opwr == last_power_applied) return;
   last_power_applied = opwr;
   power = opwr;
diff --git a/Marlin/src/feature/spindle_laser.h b/Marlin/src/feature/spindle_laser.h
index 1f9bec7bf0f..a9dc7794c4e 100644
--- a/Marlin/src/feature/spindle_laser.h
+++ b/Marlin/src/feature/spindle_laser.h
@@ -91,7 +91,8 @@ public:
   #endif
 
   static bool isReady;                    // Ready to apply power setting from the UI to OCR
-  static uint8_t power;
+  static uint8_t power,
+                 last_power_applied;      // Basic power state tracking
 
   #if ENABLED(MARLIN_DEV_MODE)
     static cutter_frequency_t frequency;  // Set PWM frequency; range: 2K-50K
diff --git a/Marlin/src/gcode/control/M17_M18_M84.cpp b/Marlin/src/gcode/control/M17_M18_M84.cpp
index a4738b6359b..ac3a602687a 100644
--- a/Marlin/src/gcode/control/M17_M18_M84.cpp
+++ b/Marlin/src/gcode/control/M17_M18_M84.cpp
@@ -213,7 +213,16 @@ void try_to_disable(const stepper_flags_t to_disable) {
 void GcodeSuite::M18_M84() {
   if (parser.seenval('S')) {
     reset_stepper_timeout();
-    stepper_inactive_time = parser.value_millis_from_seconds();
+    #if HAS_DISABLE_INACTIVE_AXIS
+      const millis_t ms = parser.value_millis_from_seconds();
+      #if LASER_SAFETY_TIMEOUT_MS > 0
+        if (ms && ms <= LASER_SAFETY_TIMEOUT_MS) {
+          SERIAL_ECHO_MSG("M18 timeout must be > ", MS_TO_SEC(LASER_SAFETY_TIMEOUT_MS + 999), " s for laser safety.");
+          return;
+        }
+      #endif
+      stepper_inactive_time = ms;
+    #endif
   }
   else {
     if (parser.seen_axis()) {
diff --git a/Marlin/src/gcode/control/M3-M5.cpp b/Marlin/src/gcode/control/M3-M5.cpp
index 2cf22e81ce0..a619b1b7620 100644
--- a/Marlin/src/gcode/control/M3-M5.cpp
+++ b/Marlin/src/gcode/control/M3-M5.cpp
@@ -66,6 +66,10 @@
  *  PWM duty cycle goes from 0 (off) to 255 (always on).
  */
 void GcodeSuite::M3_M4(const bool is_M4) {
+  #if LASER_SAFETY_TIMEOUT_MS > 0
+    reset_stepper_timeout(); // Reset timeout to allow subsequent G-code to power the laser (imm.)
+  #endif
+
   #if EITHER(SPINDLE_LASER_USE_PWM, SPINDLE_SERVO)
     auto get_s_power = [] {
       if (parser.seenval('S')) {
diff --git a/Marlin/src/gcode/control/M85.cpp b/Marlin/src/gcode/control/M85.cpp
index 9c8c02c59aa..ee868349ed7 100644
--- a/Marlin/src/gcode/control/M85.cpp
+++ b/Marlin/src/gcode/control/M85.cpp
@@ -29,7 +29,14 @@ void GcodeSuite::M85() {
 
   if (parser.seen('S')) {
     reset_stepper_timeout();
-    max_inactive_time = parser.value_millis_from_seconds();
+    const millis_t ms = parser.value_millis_from_seconds();
+    #if LASER_SAFETY_TIMEOUT_MS > 0
+      if (ms && ms <= LASER_SAFETY_TIMEOUT_MS) {
+        SERIAL_ECHO_MSG("M85 timeout must be > ", MS_TO_SEC(LASER_SAFETY_TIMEOUT_MS + 999), " s for laser safety.");
+        return;
+      }
+    #endif
+    max_inactive_time = ms;
   }
 
 }
diff --git a/Marlin/src/gcode/gcode.cpp b/Marlin/src/gcode/gcode.cpp
index 4f23db2e37c..70e2f36f2f7 100644
--- a/Marlin/src/gcode/gcode.cpp
+++ b/Marlin/src/gcode/gcode.cpp
@@ -73,8 +73,11 @@ GcodeSuite gcode;
 
 // Inactivity shutdown
 millis_t GcodeSuite::previous_move_ms = 0,
-         GcodeSuite::max_inactive_time = 0,
-         GcodeSuite::stepper_inactive_time = SEC_TO_MS(DEFAULT_STEPPER_DEACTIVE_TIME);
+         GcodeSuite::max_inactive_time = 0;
+
+#if HAS_DISABLE_INACTIVE_AXIS
+  millis_t GcodeSuite::stepper_inactive_time = SEC_TO_MS(DEFAULT_STEPPER_DEACTIVE_TIME);
+#endif
 
 // Relative motion mode for each logical axis
 static constexpr xyze_bool_t ar_init = AXIS_RELATIVE_MODES;
diff --git a/Marlin/src/gcode/gcode.h b/Marlin/src/gcode/gcode.h
index fe03879a0c3..772accba5dc 100644
--- a/Marlin/src/gcode/gcode.h
+++ b/Marlin/src/gcode/gcode.h
@@ -395,14 +395,20 @@ public:
     static bool select_coordinate_system(const int8_t _new);
   #endif
 
-  static millis_t previous_move_ms, max_inactive_time, stepper_inactive_time;
-  FORCE_INLINE static void reset_stepper_timeout(const millis_t ms=millis()) { previous_move_ms = ms; }
+  static millis_t previous_move_ms, max_inactive_time;
   FORCE_INLINE static bool stepper_max_timed_out(const millis_t ms=millis()) {
     return max_inactive_time && ELAPSED(ms, previous_move_ms + max_inactive_time);
   }
-  FORCE_INLINE static bool stepper_inactive_timeout(const millis_t ms=millis()) {
-    return ELAPSED(ms, previous_move_ms + stepper_inactive_time);
-  }
+  FORCE_INLINE static void reset_stepper_timeout(const millis_t ms=millis()) { previous_move_ms = ms; }
+
+  #if HAS_DISABLE_INACTIVE_AXIS
+    static millis_t stepper_inactive_time;
+    FORCE_INLINE static bool stepper_inactive_timeout(const millis_t ms=millis()) {
+      return ELAPSED(ms, previous_move_ms + stepper_inactive_time);
+    }
+  #else
+    static bool stepper_inactive_timeout(const millis_t) { return false; }
+  #endif
 
   static void report_echo_start(const bool forReplay);
   static void report_heading(const bool forReplay, FSTR_P const fstr, const bool eol=true);
diff --git a/Marlin/src/inc/Conditionals_adv.h b/Marlin/src/inc/Conditionals_adv.h
index 7a0c5dd934e..309648a69d8 100644
--- a/Marlin/src/inc/Conditionals_adv.h
+++ b/Marlin/src/inc/Conditionals_adv.h
@@ -1042,3 +1042,7 @@
   #undef CONFIGURATION_EMBEDDING
   #define CANNOT_EMBED_CONFIGURATION defined(__AVR__)
 #endif
+
+#if ANY(DISABLE_INACTIVE_X, DISABLE_INACTIVE_Y, DISABLE_INACTIVE_Z, DISABLE_INACTIVE_I, DISABLE_INACTIVE_J, DISABLE_INACTIVE_K, DISABLE_INACTIVE_U, DISABLE_INACTIVE_V, DISABLE_INACTIVE_W, DISABLE_INACTIVE_E)
+  #define HAS_DISABLE_INACTIVE_AXIS 1
+#endif
diff --git a/Marlin/src/inc/SanityCheck.h b/Marlin/src/inc/SanityCheck.h
index b4259bf53ad..dadbf27273d 100644
--- a/Marlin/src/inc/SanityCheck.h
+++ b/Marlin/src/inc/SanityCheck.h
@@ -3680,6 +3680,7 @@ static_assert(_PLUS_TEST(4), "HOMING_FEEDRATE_MM_M values must be positive.");
       #error "Enabled an inline laser feature without inline laser power being enabled."
     #endif
   #endif
+
   #define _PIN_CONFLICT(P) (PIN_EXISTS(P) && P##_PIN == SPINDLE_LASER_PWM_PIN)
   #if BOTH(SPINDLE_FEATURE, LASER_FEATURE)
     #error "Enable only one of SPINDLE_FEATURE or LASER_FEATURE."
@@ -3747,6 +3748,11 @@ static_assert(_PLUS_TEST(4), "HOMING_FEEDRATE_MM_M values must be positive.");
     #endif
   #endif
   #undef _PIN_CONFLICT
+
+  #ifdef LASER_SAFETY_TIMEOUT_MS
+    static_assert(LASER_SAFETY_TIMEOUT_MS < (DEFAULT_STEPPER_DEACTIVE_TIME) * 1000UL, "LASER_SAFETY_TIMEOUT_MS must be less than DEFAULT_STEPPER_DEACTIVE_TIME (" STRINGIFY(DEFAULT_STEPPER_DEACTIVE_TIME) " seconds)");
+  #endif
+
 #endif
 
 #if ENABLED(COOLANT_MIST) && !PIN_EXISTS(COOLANT_MIST)
diff --git a/Marlin/src/module/temperature.cpp b/Marlin/src/module/temperature.cpp
index 2a2b47d40b6..ce84b592336 100644
--- a/Marlin/src/module/temperature.cpp
+++ b/Marlin/src/module/temperature.cpp
@@ -71,6 +71,10 @@
   #include "../libs/nozzle.h"
 #endif
 
+#if LASER_SAFETY_TIMEOUT_MS > 0
+  #include "../feature/spindle_laser.h"
+#endif
+
 // MAX TC related macros
 #define TEMP_SENSOR_IS_MAX(n, M) (ENABLED(TEMP_SENSOR_##n##_IS_MAX##M) || (ENABLED(TEMP_SENSOR_REDUNDANT_IS_MAX##M) && REDUNDANT_TEMP_MATCH(SOURCE, E##n)))
 #define TEMP_SENSOR_IS_ANY_MAX_TC(n) (ENABLED(TEMP_SENSOR_##n##_IS_MAX_TC) || (ENABLED(TEMP_SENSOR_REDUNDANT_IS_MAX_TC) && REDUNDANT_TEMP_MATCH(SOURCE, E##n)))
@@ -3325,6 +3329,7 @@ public:
 
 /**
  * Handle various ~1kHz tasks associated with temperature
+ *  - Check laser safety timeout
  *  - Heater PWM (~1kHz with scaler)
  *  - LCD Button polling (~500Hz)
  *  - Start / Read one ADC sensor
@@ -3334,6 +3339,14 @@ public:
  */
 void Temperature::isr() {
 
+  // Shut down the laser if steppers are inactive for > LASER_SAFETY_TIMEOUT_MS ms
+  #if LASER_SAFETY_TIMEOUT_MS > 0
+    if (cutter.last_power_applied && ELAPSED(millis(), gcode.previous_move_ms + (LASER_SAFETY_TIMEOUT_MS))) {
+      cutter.power = 0;       // Prevent planner idle from re-enabling power
+      cutter.apply_power(0);
+    }
+  #endif
+
   static int8_t temp_count = -1;
   static ADCSensorState adc_sensor_state = StartupDelay;
   static uint8_t pwm_count = _BV(SOFT_PWM_SCALE);
diff --git a/buildroot/tests/BIGTREE_SKR_PRO b/buildroot/tests/BIGTREE_SKR_PRO
index 2503b28544a..2f1075c7ef2 100755
--- a/buildroot/tests/BIGTREE_SKR_PRO
+++ b/buildroot/tests/BIGTREE_SKR_PRO
@@ -26,7 +26,7 @@ opt_set MOTHERBOARD BOARD_BTT_SKR_PRO_V1_1 SERIAL_PORT -1 \
         CUTTER_POWER_UNIT PERCENT \
         SPINDLE_LASER_PWM_PIN HEATER_1_PIN SPINDLE_LASER_ENA_PIN HEATER_2_PIN \
         TEMP_SENSOR_COOLER 1000 TEMP_COOLER_PIN PD13
-opt_enable LASER_FEATURE REPRAP_DISCOUNT_SMART_CONTROLLER
+opt_enable LASER_FEATURE LASER_SAFETY_TIMEOUT_MS REPRAP_DISCOUNT_SMART_CONTROLLER
 exec_test $1 $2 "BigTreeTech SKR Pro | Laser (Percent) | Cooling | LCD" "$3"
 
 # clean up
diff --git a/buildroot/tests/mega2560 b/buildroot/tests/mega2560
index 3677abf60ef..fc24cf21250 100755
--- a/buildroot/tests/mega2560
+++ b/buildroot/tests/mega2560
@@ -179,8 +179,8 @@ opt_set MOTHERBOARD BOARD_RAMPS_14_EFB EXTRUDERS 0 LCD_LANGUAGE en TEMP_SENSOR_C
         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 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
+opt_enable REPRAP_DISCOUNT_FULL_GRAPHIC_SMART_CONTROLLER SDSUPPORT EEPROM_SETTINGS EEPROM_BOOT_SILENT EEPROM_AUTO_INIT MEATPACK_ON_SERIAL_PORT_1 \
+           LASER_FEATURE LASER_SAFETY_TIMEOUT_MS LASER_COOLANT_FLOW_METER AIR_EVACUATION AIR_EVACUATION_PIN AIR_ASSIST AIR_ASSIST_PIN
 exec_test $1 $2 "MEGA2560 RAMPS | Laser Feature | Air Evacuation | Air Assist | Cooler | Flowmeter | 12864 LCD | meatpack | SERIAL_PORT_2 " "$3"
 
 #
@@ -193,8 +193,8 @@ opt_set MOTHERBOARD BOARD_RAMPS_14_EFB EXTRUDERS 0 LCD_LANGUAGE en TEMP_SENSOR_C
         DEFAULT_MAX_ACCELERATION '{ 3000, 3000, 100 }' \
         MANUAL_FEEDRATE '{ 50*60, 50*60, 4*60 }' \
         AXIS_RELATIVE_MODES '{ false, false, false }'
-opt_enable REPRAP_DISCOUNT_SMART_CONTROLLER SDSUPPORT EEPROM_SETTINGS EEPROM_BOOT_SILENT EEPROM_AUTO_INIT PRINTCOUNTER \
-           LASER_FEATURE AIR_EVACUATION AIR_EVACUATION_PIN AIR_ASSIST AIR_ASSIST_PIN LASER_COOLANT_FLOW_METER I2C_AMMETER
+opt_enable REPRAP_DISCOUNT_SMART_CONTROLLER SDSUPPORT EEPROM_SETTINGS EEPROM_BOOT_SILENT EEPROM_AUTO_INIT PRINTCOUNTER I2C_AMMETER \
+           LASER_FEATURE LASER_SAFETY_TIMEOUT_MS LASER_COOLANT_FLOW_METER AIR_EVACUATION AIR_EVACUATION_PIN AIR_ASSIST AIR_ASSIST_PIN
 exec_test $1 $2 "MEGA2560 RAMPS | Laser Feature | Air Evacuation | Air Assist | Cooler | Flowmeter | 44780 LCD " "$3"
 
 #