From 83eec683c969d793b473015476d03f868b470c04 Mon Sep 17 00:00:00 2001
From: Erkan Colak <erkanc@gmx.de>
Date: Wed, 18 Mar 2020 19:41:12 +0100
Subject: [PATCH] New Controller Fan options and M710 gcode (#17149)

---
 Marlin/Configuration_adv.h                    | 19 +++--
 Marlin/src/MarlinCore.cpp                     | 10 +--
 Marlin/src/feature/controllerfan.cpp          | 85 ++++++++++++-------
 Marlin/src/feature/controllerfan.h            | 54 +++++++++++-
 Marlin/src/feature/power.cpp                  |  4 +-
 .../src/gcode/feature/controllerfan/M710.cpp  | 83 ++++++++++++++++++
 Marlin/src/gcode/gcode.cpp                    |  4 +
 Marlin/src/gcode/gcode.h                      |  4 +
 Marlin/src/inc/SanityCheck.h                  |  2 +
 Marlin/src/lcd/language/language_de.h         |  5 ++
 Marlin/src/lcd/language/language_en.h         |  5 ++
 Marlin/src/lcd/menu/menu_configuration.cpp    | 25 ++++++
 Marlin/src/module/configuration_store.cpp     | 47 ++++++++++
 buildroot/share/tests/mega2560-tests          | 12 +--
 14 files changed, 307 insertions(+), 52 deletions(-)
 create mode 100644 Marlin/src/gcode/feature/controllerfan/M710.cpp

diff --git a/Marlin/Configuration_adv.h b/Marlin/Configuration_adv.h
index 6149864ab..370fbaf6e 100644
--- a/Marlin/Configuration_adv.h
+++ b/Marlin/Configuration_adv.h
@@ -338,15 +338,22 @@
  * Controller Fan
  * To cool down the stepper drivers and MOSFETs.
  *
- * The fan will turn on automatically whenever any stepper is enabled
- * and turn off after a set period after all steppers are turned off.
+ * The fan turns on automatically whenever any driver is enabled and turns
+ * off (or reduces to idle speed) shortly after drivers are turned off.
+ *
  */
 //#define USE_CONTROLLER_FAN
 #if ENABLED(USE_CONTROLLER_FAN)
-  //#define CONTROLLER_FAN_PIN -1           // Set a custom pin for the controller fan
-  #define CONTROLLERFAN_SECS 60             // Duration in seconds for the fan to run after all motors are disabled
-  #define CONTROLLERFAN_SPEED 255           // 255 == full speed
-  //#define CONTROLLERFAN_SPEED_Z_ONLY 127  // Reduce noise on machines that keep Z enabled
+  //#define CONTROLLER_FAN_PIN -1        // Set a custom pin for the controller fan
+  //#define CONTROLLER_FAN_USE_Z_ONLY    // With this option only the Z axis is considered
+  #define CONTROLLERFAN_SPEED_MIN      0 // (0-255) Minimum speed. (If set below this value the fan is turned off.)
+  #define CONTROLLERFAN_SPEED_ACTIVE 255 // (0-255) Active speed, used when any motor is enabled
+  #define CONTROLLERFAN_SPEED_IDLE     0 // (0-255) Idle speed, used when motors are disabled
+  #define CONTROLLERFAN_IDLE_TIME     60 // (seconds) Extra time to keep the fan running after disabling motors
+  //#define CONTROLLER_FAN_EDITABLE      // Enable M710 configurable settings
+  #if ENABLED(CONTROLLER_FAN_EDITABLE)
+    #define CONTROLLER_FAN_MENU          // Enable the Controller Fan submenu
+  #endif
 #endif
 
 // When first starting the main fan, run it at full speed for the
diff --git a/Marlin/src/MarlinCore.cpp b/Marlin/src/MarlinCore.cpp
index 5e2fe0207..0ea1a22fd 100644
--- a/Marlin/src/MarlinCore.cpp
+++ b/Marlin/src/MarlinCore.cpp
@@ -567,7 +567,7 @@ inline void manage_inactivity(const bool ignore_stepper_queue=false) {
   #endif
 
   #if ENABLED(USE_CONTROLLER_FAN)
-    controllerfan_update(); // Check if fan should be turned on to cool stepper drivers down
+    controllerFan.update(); // Check if fan should be turned on to cool stepper drivers down
   #endif
 
   #if ENABLED(AUTO_POWER_CONTROL)
@@ -984,6 +984,10 @@ void setup() {
     SETUP_RUN(leds.setup());
   #endif
 
+  #if ENABLED(USE_CONTROLLER_FAN)     // Set up fan controller to initialize also the default configurations.
+    SETUP_RUN(controllerFan.setup());
+  #endif
+
   SETUP_RUN(ui.init());
   SETUP_RUN(ui.reset_status());       // Load welcome message early. (Retained if no errors exist.)
 
@@ -1047,10 +1051,6 @@ void setup() {
     SETUP_RUN(endstops.enable_z_probe(false));
   #endif
 
-  #if ENABLED(USE_CONTROLLER_FAN)
-    SET_OUTPUT(CONTROLLER_FAN_PIN);
-  #endif
-
   #if HAS_STEPPER_RESET
     SETUP_RUN(enableStepperDrivers());
   #endif
diff --git a/Marlin/src/feature/controllerfan.cpp b/Marlin/src/feature/controllerfan.cpp
index b9d8c3946..ed7fca1fe 100644
--- a/Marlin/src/feature/controllerfan.cpp
+++ b/Marlin/src/feature/controllerfan.cpp
@@ -24,60 +24,79 @@
 
 #if ENABLED(USE_CONTROLLER_FAN)
 
+#include "controllerfan.h"
 #include "../module/stepper/indirection.h"
 #include "../module/temperature.h"
 
-uint8_t controllerfan_speed;
+ControllerFan controllerFan;
 
-void controllerfan_update() {
-  static millis_t lastMotorOn = 0, // Last time a motor was turned on
+uint8_t ControllerFan::speed;
+
+#if ENABLED(CONTROLLER_FAN_EDITABLE)
+  controllerFan_settings_t ControllerFan::settings; // {0}
+#endif
+
+void ControllerFan::setup() {
+  SET_OUTPUT(CONTROLLER_FAN_PIN);
+  init();
+}
+
+void ControllerFan::set_fan_speed(const uint8_t s) {
+  speed = s < (CONTROLLERFAN_SPEED_MIN) ? 0 : s; // Fan OFF below minimum
+}
+
+void ControllerFan::update() {
+  static millis_t lastMotorOn = 0,    // Last time a motor was turned on
                   nextMotorCheck = 0; // Last time the state was checked
   const millis_t ms = millis();
   if (ELAPSED(ms, nextMotorCheck)) {
     nextMotorCheck = ms + 2500UL; // Not a time critical function, so only check every 2.5s
 
-    const bool xory = X_ENABLE_READ() == bool(X_ENABLE_ON) || Y_ENABLE_READ() == bool(Y_ENABLE_ON);
+    #define MOTOR_IS_ON(A,B) (A##_ENABLE_READ() == bool(B##_ENABLE_ON))
+    #define _OR_ENABLED_E(N) || MOTOR_IS_ON(E##N,E)
+
+    const bool
+    xy_motor_on = MOTOR_IS_ON(X,X) || MOTOR_IS_ON(Y,Y)
+                  #if HAS_X2_ENABLE
+                    || MOTOR_IS_ON(X2,X)
+                  #endif
+                  #if HAS_Y2_ENABLE
+                    || MOTOR_IS_ON(Y2,Y)
+                  #endif
+                  ,
+    z_motor_on  = MOTOR_IS_ON(Z,Z)
+                  #if HAS_Z2_ENABLE
+                    || MOTOR_IS_ON(Z2,Z)
+                  #endif
+                  #if HAS_Z3_ENABLE
+                    || MOTOR_IS_ON(Z3,Z)
+                  #endif
+                  #if HAS_Z4_ENABLE
+                    || MOTOR_IS_ON(Z4,Z)
+                  #endif
+                  ;
 
     // If any of the drivers or the bed are enabled...
-    if (xory || Z_ENABLE_READ() == bool(Z_ENABLE_ON)
+    if (xy_motor_on || z_motor_on
       #if HAS_HEATED_BED
         || thermalManager.temp_bed.soft_pwm_amount > 0
       #endif
-      #if HAS_X2_ENABLE
-        || X2_ENABLE_READ() == bool(X_ENABLE_ON)
-      #endif
-      #if HAS_Y2_ENABLE
-        || Y2_ENABLE_READ() == bool(Y_ENABLE_ON)
-      #endif
-      #if HAS_Z2_ENABLE
-        || Z2_ENABLE_READ() == bool(Z_ENABLE_ON)
-      #endif
-      #if HAS_Z3_ENABLE
-        || Z3_ENABLE_READ() == bool(Z_ENABLE_ON)
-      #endif
-      #if HAS_Z4_ENABLE
-        || Z4_ENABLE_READ() == bool(Z_ENABLE_ON)
-      #endif
       #if E_STEPPERS
-        #define _OR_ENABLED_E(N) || E##N##_ENABLE_READ() == bool(E_ENABLE_ON)
         REPEAT(E_STEPPERS, _OR_ENABLED_E)
       #endif
-    ) {
-      lastMotorOn = ms; //... set time to NOW so the fan will turn on
-    }
+    ) lastMotorOn = ms; //... set time to NOW so the fan will turn on
 
-    // Fan off if no steppers have been enabled for CONTROLLERFAN_SECS seconds
-    controllerfan_speed = (!lastMotorOn || ELAPSED(ms, lastMotorOn + (CONTROLLERFAN_SECS) * 1000UL)) ? 0 : (
-      #ifdef CONTROLLERFAN_SPEED_Z_ONLY
-        xory ? CONTROLLERFAN_SPEED : CONTROLLERFAN_SPEED_Z_ONLY
-      #else
-        CONTROLLERFAN_SPEED
-      #endif
+    // Fan Settings. Set fan > 0:
+    //  - If AutoMode is on and steppers have been enabled for CONTROLLERFAN_IDLE_TIME seconds.
+    //  - If System is on idle and idle fan speed settings is activated.
+    set_fan_speed(
+      settings.auto_mode && lastMotorOn && PENDING(ms, lastMotorOn + settings.duration * 1000UL)
+      ? settings.active_speed : settings.idle_speed
     );
 
     // Allow digital or PWM fan output (see M42 handling)
-    WRITE(CONTROLLER_FAN_PIN, controllerfan_speed);
-    analogWrite(pin_t(CONTROLLER_FAN_PIN), controllerfan_speed);
+    WRITE(CONTROLLER_FAN_PIN, speed);
+    analogWrite(pin_t(CONTROLLER_FAN_PIN), speed);
   }
 }
 
diff --git a/Marlin/src/feature/controllerfan.h b/Marlin/src/feature/controllerfan.h
index f2facc288..bc48a6e26 100644
--- a/Marlin/src/feature/controllerfan.h
+++ b/Marlin/src/feature/controllerfan.h
@@ -21,4 +21,56 @@
  */
 #pragma once
 
-void controllerfan_update();
+#include "../inc/MarlinConfigPre.h"
+
+typedef struct {
+  uint8_t   active_speed,    // 0-255 (fullspeed); Speed with enabled stepper motors
+            idle_speed;      // 0-255 (fullspeed); Speed after idle period with all motors are disabled
+  uint16_t  duration;        // Duration in seconds for the fan to run after all motors are disabled
+  bool      auto_mode;       // Default true
+} controllerFan_settings_t;
+
+#ifndef CONTROLLERFAN_SPEED_ACTIVE
+  #define CONTROLLERFAN_SPEED_ACTIVE 255
+#endif
+#ifndef CONTROLLERFAN_SPEED_IDLE
+  #define CONTROLLERFAN_SPEED_IDLE     0
+#endif
+#ifndef CONTROLLERFAN_IDLE_TIME
+  #define CONTROLLERFAN_IDLE_TIME     60
+#endif
+
+static constexpr controllerFan_settings_t controllerFan_defaults = {
+  CONTROLLERFAN_SPEED_ACTIVE,
+  CONTROLLERFAN_SPEED_IDLE,
+  CONTROLLERFAN_IDLE_TIME,
+  true
+};
+
+#if ENABLED(USE_CONTROLLER_FAN)
+
+class ControllerFan {
+  private:
+    static uint8_t speed;
+    static void set_fan_speed(const uint8_t s);
+
+  public:
+    #if ENABLED(CONTROLLER_FAN_EDITABLE)
+      static controllerFan_settings_t settings;
+    #else
+      static const controllerFan_settings_t &settings = controllerFan_defaults;
+    #endif
+    static inline bool state() { return speed > 0; }
+    static inline void init() { reset(); }
+    static inline void reset() {
+      #if ENABLED(CONTROLLER_FAN_EDITABLE)
+        settings = controllerFan_defaults;
+      #endif
+    }
+    static void setup();
+    static void update();
+};
+
+extern ControllerFan controllerFan;
+
+#endif
diff --git a/Marlin/src/feature/power.cpp b/Marlin/src/feature/power.cpp
index cf18e2130..1fa751811 100644
--- a/Marlin/src/feature/power.cpp
+++ b/Marlin/src/feature/power.cpp
@@ -46,8 +46,8 @@ bool Power::is_power_needed() {
     HOTEND_LOOP() if (thermalManager.autofan_speed[e]) return true;
   #endif
 
-  #if ENABLED(AUTO_POWER_CONTROLLERFAN, USE_CONTROLLER_FAN) && HAS_CONTROLLER_FAN
-    if (controllerfan_speed) return true;
+  #if BOTH(USE_CONTROLLER_FAN, AUTO_POWER_CONTROLLERFAN)
+    if (controllerFan.state()) return true;
   #endif
 
   #if ENABLED(AUTO_POWER_CHAMBER_FAN)
diff --git a/Marlin/src/gcode/feature/controllerfan/M710.cpp b/Marlin/src/gcode/feature/controllerfan/M710.cpp
new file mode 100644
index 000000000..8e5845fa8
--- /dev/null
+++ b/Marlin/src/gcode/feature/controllerfan/M710.cpp
@@ -0,0 +1,83 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "../../../inc/MarlinConfigPre.h"
+
+#if ENABLED(CONTROLLER_FAN_EDITABLE)
+
+#include "../../gcode.h"
+#include "../../../feature/controllerfan.h"
+
+void M710_report(const bool forReplay) {
+  if (!forReplay) { SERIAL_ECHOLNPGM("; Controller Fan"); SERIAL_ECHO_START(); }
+  SERIAL_ECHOLNPAIR("M710 "
+    "S", int(controllerFan.settings.active_speed),
+    "I", int(controllerFan.settings.idle_speed),
+    "A", int(controllerFan.settings.auto_mode),
+    "D", controllerFan.settings.duration,
+    " ; (", (int(controllerFan.settings.active_speed) * 100) / 255, "%"
+    " ", (int(controllerFan.settings.idle_speed) * 100) / 255, "%)"
+  );
+}
+
+/**
+ * M710: Set controller fan settings
+ *
+ *  R         : Reset to defaults
+ *  S[0-255]  : Fan speed when motors are active
+ *  I[0-255]  : Fan speed when motors are idle
+ *  A[0|1]    : Turn auto mode on or off
+ *  D         : Set auto mode idle duration
+ *
+ * Examples:
+ *   M710                   ; Report current Settings
+ *   M710 R                 ; Reset SIAD to defaults
+ *   M710 I64               ; Set controller fan Idle Speed to 25%
+ *   M710 S255              ; Set controller fan Active Speed to 100%
+ *   M710 S0                ; Set controller fan Active Speed to OFF
+ *   M710 I255 A0           ; Set controller fan Idle Speed to 100% with Auto Mode OFF
+ *   M710 I127 A1 S255 D160 ; Set controller fan idle speed 50%, AutoMode On, Fan speed 100%, duration to 160 Secs
+ */
+void GcodeSuite::M710() {
+
+  const bool seenR = parser.seen('R');
+  if (seenR) controllerFan.reset();
+
+  const bool seenS = parser.seenval('S');
+  if (seenS) controllerFan.settings.active_speed = parser.value_byte();
+
+  const bool seenI = parser.seenval('I');
+  if (seenI) controllerFan.settings.idle_speed = parser.value_byte();
+
+  const bool seenA = parser.seenval('A');
+  if (seenA) controllerFan.settings.auto_mode = parser.value_bool();
+
+  const bool seenD = parser.seenval('D');
+  if (seenD) controllerFan.settings.duration = parser.value_ushort();
+
+  if (seenR || seenS || seenI || seenA || seenD)
+    controllerFan.update();
+  else
+    M710_report(false);
+}
+
+#endif // CONTROLLER_FAN_EDITABLE
diff --git a/Marlin/src/gcode/gcode.cpp b/Marlin/src/gcode/gcode.cpp
index 266361a09..1ce1e3d5c 100644
--- a/Marlin/src/gcode/gcode.cpp
+++ b/Marlin/src/gcode/gcode.cpp
@@ -752,6 +752,10 @@ void GcodeSuite::process_parsed_command(const bool no_ok/*=false*/) {
         case 702: M702(); break;                                  // M702: Unload Filament
       #endif
 
+      #if ENABLED(CONTROLLER_FAN_EDITABLE)
+        case 710: M710(); break;                                  // M710: Set Controller Fan settings
+      #endif
+
       #if ENABLED(GCODE_MACROS)
         case 810: case 811: case 812: case 813: case 814:
         case 815: case 816: case 817: case 818: case 819:
diff --git a/Marlin/src/gcode/gcode.h b/Marlin/src/gcode/gcode.h
index 27a038dde..c1024ac44 100644
--- a/Marlin/src/gcode/gcode.h
+++ b/Marlin/src/gcode/gcode.h
@@ -972,6 +972,10 @@ private:
     static void M7219();
   #endif
 
+  #if ENABLED(CONTROLLER_FAN_EDITABLE)
+    static void M710();
+  #endif
+
   static void T(const uint8_t tool_index);
 
 };
diff --git a/Marlin/src/inc/SanityCheck.h b/Marlin/src/inc/SanityCheck.h
index 574d6a336..89b53362d 100644
--- a/Marlin/src/inc/SanityCheck.h
+++ b/Marlin/src/inc/SanityCheck.h
@@ -270,6 +270,8 @@
   #error "Replace SLED_PIN with SOL1_PIN (applies to both Z_PROBE_SLED and SOLENOID_PROBE)."
 #elif defined(CONTROLLERFAN_PIN)
   #error "CONTROLLERFAN_PIN is now CONTROLLER_FAN_PIN, enabled with USE_CONTROLLER_FAN. Please update your Configuration_adv.h."
+#elif defined(CONTROLLERFAN_SPEED)
+  #error "CONTROLLERFAN_SPEED is now CONTROLLERFAN_SPEED_ACTIVE. Please update your Configuration_adv.h."
 #elif defined(MIN_RETRACT)
   #error "MIN_RETRACT is now MIN_AUTORETRACT and MAX_AUTORETRACT. Please update your Configuration_adv.h."
 #elif defined(ADVANCE)
diff --git a/Marlin/src/lcd/language/language_de.h b/Marlin/src/lcd/language/language_de.h
index 5ac8ee04d..cd201ded3 100644
--- a/Marlin/src/lcd/language/language_de.h
+++ b/Marlin/src/lcd/language/language_de.h
@@ -236,6 +236,11 @@ namespace Language_de {
   PROGMEM Language_Str MSG_FAN_SPEED_N                     = _UxGT("Lüfter ~");
   PROGMEM Language_Str MSG_EXTRA_FAN_SPEED                 = _UxGT("Geschw. Extralüfter");
   PROGMEM Language_Str MSG_EXTRA_FAN_SPEED_N               = _UxGT("Geschw. Extralüfter ~");
+  PROGMEM Language_Str MSG_CONTROLLER_FAN                  = _UxGT("Lüfter Kontroller");
+  PROGMEM Language_Str MSG_CONTROLLER_FAN_IDLE_SPEED       = _UxGT("Lüfter Leerlauf");
+  PROGMEM Language_Str MSG_CONTROLLER_FAN_AUTO_ON          = _UxGT("Motorlast Modus");
+  PROGMEM Language_Str MSG_CONTROLLER_FAN_SPEED            = _UxGT("Lüfter Motorlast");
+  PROGMEM Language_Str MSG_CONTROLLER_FAN_DURATION         = _UxGT("Ausschalt Delay");
   PROGMEM Language_Str MSG_FLOW                            = _UxGT("Flussrate");
   PROGMEM Language_Str MSG_FLOW_N                          = _UxGT("Flussrate ~");
   PROGMEM Language_Str MSG_CONTROL                         = _UxGT("Einstellungen");
diff --git a/Marlin/src/lcd/language/language_en.h b/Marlin/src/lcd/language/language_en.h
index b5f508973..75c13de9c 100644
--- a/Marlin/src/lcd/language/language_en.h
+++ b/Marlin/src/lcd/language/language_en.h
@@ -247,6 +247,11 @@ namespace Language_en {
   PROGMEM Language_Str MSG_STORED_FAN_N                    = _UxGT("Stored Fan ~");
   PROGMEM Language_Str MSG_EXTRA_FAN_SPEED                 = _UxGT("Extra Fan Speed");
   PROGMEM Language_Str MSG_EXTRA_FAN_SPEED_N               = _UxGT("Extra Fan Speed ~");
+  PROGMEM Language_Str MSG_CONTROLLER_FAN                  = _UxGT("Controller Fan");
+  PROGMEM Language_Str MSG_CONTROLLER_FAN_IDLE_SPEED       = _UxGT("Idle Speed");
+  PROGMEM Language_Str MSG_CONTROLLER_FAN_AUTO_ON          = _UxGT("Auto Mode");
+  PROGMEM Language_Str MSG_CONTROLLER_FAN_SPEED            = _UxGT("Active Speed");
+  PROGMEM Language_Str MSG_CONTROLLER_FAN_DURATION         = _UxGT("Idle Period");
   PROGMEM Language_Str MSG_FLOW                            = _UxGT("Flow");
   PROGMEM Language_Str MSG_FLOW_N                          = _UxGT("Flow ~");
   PROGMEM Language_Str MSG_CONTROL                         = _UxGT("Control");
diff --git a/Marlin/src/lcd/menu/menu_configuration.cpp b/Marlin/src/lcd/menu/menu_configuration.cpp
index c9b9d26d6..59c80b1af 100644
--- a/Marlin/src/lcd/menu/menu_configuration.cpp
+++ b/Marlin/src/lcd/menu/menu_configuration.cpp
@@ -227,6 +227,24 @@ void menu_advanced_settings();
   }
 #endif
 
+#if ENABLED(CONTROLLER_FAN_MENU)
+
+  #include "../../feature/controllerfan.h"
+
+  void menu_controller_fan() {
+    START_MENU();
+    BACK_ITEM(MSG_CONFIGURATION);
+    EDIT_ITEM_FAST(percent, MSG_CONTROLLER_FAN_IDLE_SPEED, &controllerFan.settings.idle_speed, _MAX(1, CONTROLLERFAN_SPEED_MIN) - 1, 255, controllerFan.update);
+    EDIT_ITEM(bool, MSG_CONTROLLER_FAN_AUTO_ON, &controllerFan.settings.auto_mode, controllerFan.update);
+    if (controllerFan.settings.auto_mode) {
+      EDIT_ITEM_FAST(percent, MSG_CONTROLLER_FAN_SPEED, &controllerFan.settings.active_speed, _MAX(1, CONTROLLERFAN_SPEED_MIN) - 1, 255, controllerFan.update);
+      EDIT_ITEM(uint16_4, MSG_CONTROLLER_FAN_DURATION, &controllerFan.settings.duration, 0, 4800, controllerFan.update);
+    }
+    END_MENU();
+  }
+
+#endif
+
 #if ENABLED(CASE_LIGHT_MENU)
 
   #include "../../feature/caselight.h"
@@ -320,6 +338,13 @@ void menu_configuration() {
     EDIT_ITEM(LCD_Z_OFFSET_TYPE, MSG_ZPROBE_ZOFFSET, &probe.offset.z, Z_PROBE_OFFSET_RANGE_MIN, Z_PROBE_OFFSET_RANGE_MAX);
   #endif
 
+  //
+  // Set Fan Controller speed
+  //
+  #if ENABLED(CONTROLLER_FAN_MENU)
+    SUBMENU(MSG_CONTROLLER_FAN, menu_controller_fan);
+  #endif
+
   const bool busy = printer_busy();
   if (!busy) {
     #if EITHER(DELTA_CALIBRATION_MENU, DELTA_AUTO_CALIBRATION)
diff --git a/Marlin/src/module/configuration_store.cpp b/Marlin/src/module/configuration_store.cpp
index 3a83dd7c1..4b7946c6a 100644
--- a/Marlin/src/module/configuration_store.cpp
+++ b/Marlin/src/module/configuration_store.cpp
@@ -122,6 +122,11 @@
   #include "../feature/probe_temp_comp.h"
 #endif
 
+#include "../feature/controllerfan.h"
+#if ENABLED(CONTROLLER_FAN_EDITABLE)
+  void M710_report(const bool forReplay);
+#endif
+
 #pragma pack(push, 1) // No padding between variables
 
 typedef struct { uint16_t X, Y, Z, X2, Y2, Z2, Z3, Z4, E0, E1, E2, E3, E4, E5; } tmc_stepper_current_t;
@@ -292,6 +297,11 @@ typedef struct SettingsDataStruct {
   //
   int16_t lcd_contrast;                                 // M250 C
 
+  //
+  // Controller fan settings
+  //
+  controllerFan_settings_t controllerFan_settings;      // M710
+
   //
   // POWER_LOSS_RECOVERY
   //
@@ -880,6 +890,19 @@ void MarlinSettings::postprocess() {
       EEPROM_WRITE(lcd_contrast);
     }
 
+    //
+    // Controller Fan
+    //
+    {
+      _FIELD_TEST(controllerFan_settings);
+      #if ENABLED(USE_CONTROLLER_FAN)
+        const controllerFan_settings_t &cfs = controllerFan.settings;
+      #else
+        controllerFan_settings_t cfs = controllerFan_defaults;
+      #endif
+      EEPROM_WRITE(cfs);
+    }
+
     //
     // Power-Loss Recovery
     //
@@ -1719,6 +1742,19 @@ void MarlinSettings::postprocess() {
         #endif
       }
 
+      //
+      // Controller Fan
+      //
+      {
+        _FIELD_TEST(controllerFan_settings);
+        #if ENABLED(CONTROLLER_FAN_EDITABLE)
+          const controllerFan_settings_t &cfs = controllerFan.settings;
+        #else
+          controllerFan_settings_t cfs = { 0 };
+        #endif
+        EEPROM_READ(cfs);
+      }
+
       //
       // Power-Loss Recovery
       //
@@ -2590,6 +2626,13 @@ void MarlinSettings::reset() {
     ui.set_contrast(DEFAULT_LCD_CONTRAST);
   #endif
 
+  //
+  // Controller Fan
+  //
+  #if ENABLED(USE_CONTROLLER_FAN)
+    controllerFan.reset();
+  #endif
+
   //
   // Power-Loss Recovery
   //
@@ -3154,6 +3197,10 @@ void MarlinSettings::reset() {
       SERIAL_ECHOLNPAIR("  M250 C", ui.contrast);
     #endif
 
+    #if ENABLED(CONTROLLER_FAN_EDITABLE)
+      M710_report(forReplay);
+    #endif
+
     #if ENABLED(POWER_LOSS_RECOVERY)
       CONFIG_ECHO_HEADING("Power-Loss Recovery:");
       CONFIG_ECHO_START();
diff --git a/buildroot/share/tests/mega2560-tests b/buildroot/share/tests/mega2560-tests
index f120cdd6e..0bedfd5a7 100755
--- a/buildroot/share/tests/mega2560-tests
+++ b/buildroot/share/tests/mega2560-tests
@@ -115,21 +115,23 @@ exec_test $1 $2 "RAMPS | ZONESTAR_LCD | MMU2 | Servo Probe | ABL 3-Pt | Debug Le
 # Test MINIRAMBO with PWM_MOTOR_CURRENT and many features
 #
 restore_configs
-opt_set MOTHERBOARD BOARD_MINIRAMBO
+opt_set MOTHERBOARD BOARD_MEGACONTROLLER
 opt_set LCD_LANGUAGE de
 opt_enable EEPROM_SETTINGS EEPROM_CHITCHAT \
-           ULTIMAKERCONTROLLER SDSUPPORT PCA9632 LCD_INFO_MENU \
+           MINIPANEL SDSUPPORT PCA9632 LCD_INFO_MENU \
            AUTO_BED_LEVELING_BILINEAR PROBE_MANUALLY LCD_BED_LEVELING G26_MESH_VALIDATION MESH_EDIT_MENU \
-            LIN_ADVANCE EXTRA_LIN_ADVANCE_K \
+           LIN_ADVANCE EXTRA_LIN_ADVANCE_K \
            INCH_MODE_SUPPORT TEMPERATURE_UNITS_SUPPORT EXPERIMENTAL_I2CBUS M100_FREE_MEMORY_WATCHER \
            NOZZLE_PARK_FEATURE NOZZLE_CLEAN_FEATURE \
            ADVANCED_PAUSE_FEATURE PARK_HEAD_ON_PAUSE ADVANCED_PAUSE_CONTINUOUS_PURGE FILAMENT_LOAD_UNLOAD_GCODES \
-           PRINTCOUNTER SERVICE_NAME_1 SERVICE_INTERVAL_1 M114_DETAIL
+           PRINTCOUNTER SERVICE_NAME_1 SERVICE_INTERVAL_1 M114_DETAIL \
+           USE_CONTROLLER_FAN CONTROLLER_FAN_EDITABLE
+opt_set CONTROLLERFAN_SPEED_IDLE 128
 opt_add M100_FREE_MEMORY_DUMPER
 opt_add M100_FREE_MEMORY_CORRUPTOR
 opt_set PWM_MOTOR_CURRENT "{ 1300, 1300, 1250 }"
 opt_set I2C_SLAVE_ADDRESS 63
-exec_test $1 $2 "MINIRAMBO | Ultimaker LCD | M100 | PWM_MOTOR_CURRENT | PRINTCOUNTER | Advanced Pause ..."
+exec_test $1 $2 "MEGACONTROLLER | Ultimaker LCD | M100 | PWM_MOTOR_CURRENT | PRINTCOUNTER | Advanced Pause ..."
 
 #
 # Mixing Extruder with 5 steppers, Cyrillic