Browse Source

✨ Prusa MMU3 (#26635)

Co-authored-by: Scott Lahteine <thinkyhead@users.noreply.github.com>
Erkan Ozgur Yilmaz 6 months ago
parent
commit
a9c529f004

+ 3 - 2
Marlin/Configuration.h

@@ -385,14 +385,15 @@
  *   PRUSA_MMU1           : Průša MMU1 (The "multiplexer" version)
  *   PRUSA_MMU2           : Průša MMU2
  *   PRUSA_MMU2S          : Průša MMU2S (Requires MK3S extruder with motion sensor, EXTRUDERS = 5)
+ *   PRUSA_MMU3           : Průša MMU3  (Requires MK3S extruder with motion sensor and MMU firmware version 3.x.x, EXTRUDERS = 5)
  *   EXTENDABLE_EMU_MMU2  : MMU with configurable number of filaments (ERCF, SMuFF or similar with Průša MMU2 compatible firmware)
  *   EXTENDABLE_EMU_MMU2S : MMUS with configurable number of filaments (ERCF, SMuFF or similar with Průša MMU2 compatible firmware)
  *
  * Requires NOZZLE_PARK_FEATURE to park print head in case MMU unit fails.
  * See additional options in Configuration_adv.h.
- * :["PRUSA_MMU1", "PRUSA_MMU2", "PRUSA_MMU2S", "EXTENDABLE_EMU_MMU2", "EXTENDABLE_EMU_MMU2S"]
+ * :["PRUSA_MMU1", "PRUSA_MMU2", "PRUSA_MMU2S", "PRUSA_MMU3", "EXTENDABLE_EMU_MMU2", "EXTENDABLE_EMU_MMU2S"]
  */
-//#define MMU_MODEL PRUSA_MMU2
+//#define MMU_MODEL PRUSA_MMU3
 
 // @section psu control
 

+ 150 - 28
Marlin/Configuration_adv.h

@@ -4389,44 +4389,89 @@
   //#define E_MUX0_PIN 40  // Always Required
   //#define E_MUX1_PIN 42  // Needed for 3 to 8 inputs
   //#define E_MUX2_PIN 44  // Needed for 5 to 8 inputs
-#elif HAS_PRUSA_MMU2
-  // Serial port used for communication with MMU2.
+#elif HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3
+  // Common settings for MMU2/MMU2S/MMU3
+  // Serial port used for communication with MMU2/MMU2S/MMU3.
   #define MMU2_SERIAL_PORT 2
+  #define MMU_BAUD 115200
 
   // Use hardware reset for MMU if a pin is defined for it
   //#define MMU2_RST_PIN 23
 
-  // Enable if the MMU2 has 12V stepper motors (MMU2 Firmware 1.0.2 and up)
-  //#define MMU2_MODE_12V
+  #if HAS_PRUSA_MMU2
+    // Enable if the MMU2 has 12V stepper motors (MMU2 Firmware 1.0.2 and up)
+    //#define MMU2_MODE_12V
 
-  // G-code to execute when MMU2 F.I.N.D.A. probe detects filament runout
-  #define MMU2_FILAMENT_RUNOUT_SCRIPT "M600"
+    // G-code to execute when MMU2 F.I.N.D.A. probe detects filament runout
+    #define MMU2_FILAMENT_RUNOUT_SCRIPT "M600"
+  #endif
 
-  // Add an LCD menu for MMU2
-  //#define MMU2_MENUS
+  // Add an LCD menu for MMU2/MMU2S/MMU3
+  //#define MMU_MENUS
 
   // Settings for filament load / unload from the LCD menu.
   // This is for Průša MK3-style extruders. Customize for your hardware.
   #define MMU2_FILAMENTCHANGE_EJECT_FEED 80.0
+
+  /**
+   * ------------
+   * MMU2 / MMU2S
+   * ------------
+   * MMU2 sequences use mm/min. Not compatible with MMU3 (see below).
+   * #define MMU2_LOAD_TO_NOZZLE_SEQUENCE \
+   *   {  4.4,  871 }, \
+   *   { 10.0, 1393 }, \
+   *   {  4.4,  871 }, \
+   *   { 10.0,  198 }
+   */
+
+  /* #define MMU2_RAMMING_SEQUENCE \
+   *   {   1.0, 1000 }, \
+   *   {   1.0, 1500 }, \
+   *   {   2.0, 2000 }, \
+   *   {   1.5, 3000 }, \
+   *   {   2.5, 4000 }, \
+   *   { -15.0, 5000 }, \
+   *   { -14.0, 1200 }, \
+   *   {  -6.0,  600 }, \
+   *   {  10.0,  700 }, \
+   *   { -10.0,  400 }, \
+   *   { -50.0, 2000 }
+   */
+
+  /**
+   * ----
+   * MMU3
+   * ----
+   * These values are compatible with MMU3 as they are defined in mm/s
+   */
+
+  #define MMU2_EXTRUDER_PTFE_LENGTH       42.3 // (mm)
+  #define MMU2_EXTRUDER_HEATBREAK_LENGTH  17.7 // (mm)
+
   #define MMU2_LOAD_TO_NOZZLE_SEQUENCE \
-    {  7.2, 1145 }, \
-    { 14.4,  871 }, \
-    { 36.0, 1393 }, \
-    { 14.4,  871 }, \
-    { 50.0,  198 }
+    { MMU2_EXTRUDER_PTFE_LENGTH,      MMM_TO_MMS(810) }, /* (13.5 mm/s) Fast load ahead of heatbreak */ \
+    { MMU2_EXTRUDER_HEATBREAK_LENGTH, MMM_TO_MMS(198) }  // ( 3.3 mm/s) Slow load after heatbreak
 
   #define MMU2_RAMMING_SEQUENCE \
-    {   1.0, 1000 }, \
-    {   1.0, 1500 }, \
-    {   2.0, 2000 }, \
-    {   1.5, 3000 }, \
-    {   2.5, 4000 }, \
-    { -15.0, 5000 }, \
-    { -14.0, 1200 }, \
-    {  -6.0,  600 }, \
-    {  10.0,  700 }, \
-    { -10.0,  400 }, \
-    { -50.0, 2000 }
+    { 0.2816,  MMM_TO_MMS(1339.0) }, \
+    { 0.3051,  MMM_TO_MMS(1451.0) }, \
+    { 0.3453,  MMM_TO_MMS(1642.0) }, \
+    { 0.3990,  MMM_TO_MMS(1897.0) }, \
+    { 0.4761,  MMM_TO_MMS(2264.0) }, \
+    { 0.5767,  MMM_TO_MMS(2742.0) }, \
+    { 0.5691,  MMM_TO_MMS(3220.0) }, \
+    { 0.1081,  MMM_TO_MMS(3220.0) }, \
+    { 0.7644,  MMM_TO_MMS(3635.0) }, \
+    { 0.8248,  MMM_TO_MMS(3921.0) }, \
+    { 0.8483,  MMM_TO_MMS(4033.0) }, \
+    { -15.0,   MMM_TO_MMS(6000.0) }, \
+    { -24.5,   MMM_TO_MMS(1200.0) }, \
+    {  -7.0,   MMM_TO_MMS( 600.0) }, \
+    {  -3.5,   MMM_TO_MMS( 360.0) }, \
+    {  20.0,   MMM_TO_MMS( 454.0) }, \
+    { -20.0,   MMM_TO_MMS( 303.0) }, \
+    { -35.0,   MMM_TO_MMS(2000.0) }
 
   /**
    * Using a sensor like the MMU2S
@@ -4436,11 +4481,26 @@
   #if HAS_PRUSA_MMU2S
     #define MMU2_C0_RETRY   5             // Number of retries (total time = timeout*retries)
 
+    /**
+     * This is called after the filament runout sensor is triggered to check if
+     * the filament has been loaded properly by moving the filament back and
+     * forth to see if the filament runout sensor is going to get triggered
+     * again, which should not occur if the filament is properly loaded.
+     *
+     * Thus, the MMU2_CAN_LOAD_SEQUENCE should contain some forward and
+     * backward moves. The forward moves should be greater than the backward
+     * moves.
+     *
+     * This is useless if your filament runout sensor is way behind the gears.
+     * In that case use {0, MMU2_CAN_LOAD_FEEDRATE}
+     *
+     * Adjust MMU2_CAN_LOAD_SEQUENCE according to your setup.
+     */
     #define MMU2_CAN_LOAD_FEEDRATE 800    // (mm/min)
     #define MMU2_CAN_LOAD_SEQUENCE \
-      {  0.1, MMU2_CAN_LOAD_FEEDRATE }, \
-      {  60.0, MMU2_CAN_LOAD_FEEDRATE }, \
-      { -52.0, MMU2_CAN_LOAD_FEEDRATE }
+      {   5.0, MMU2_CAN_LOAD_FEEDRATE }, \
+      {  15.0, MMU2_CAN_LOAD_FEEDRATE }, \
+      { -10.0, MMU2_CAN_LOAD_FEEDRATE }
 
     #define MMU2_CAN_LOAD_RETRACT   6.0   // (mm) Keep under the distance between Load Sequence values
     #define MMU2_CAN_LOAD_DEVIATION 0.8   // (mm) Acceptable deviation
@@ -4451,6 +4511,68 @@
 
     // Continue unloading if sensor detects filament after the initial unload move
     //#define MMU_IR_UNLOAD_MOVE
+
+  #elif HAS_PRUSA_MMU3
+
+    // MMU3 settings
+
+    #define MMU2_MAX_RETRIES 3  // Number of retries (total time = timeout*retries)
+
+    // Nominal distance from the extruder gear to the nozzle tip is 87mm
+    // However, some slipping may occur and we need separate distances for
+    // LoadToNozzle and ToolChange.
+    // - +5mm seemed good for LoadToNozzle,
+    // - but too much (made blobs) for a ToolChange
+    #define MMU2_LOAD_TO_NOZZLE_LENGTH 87.0 + 5.0
+
+    // As discussed with our PrusaSlicer profile specialist
+    // - ToolChange shall not try to push filament into the very tip of the nozzle
+    // to have some space for additional G-code to tune the extruded filament length
+    // in the profile
+    // Beware - this value is used to initialize the MMU logic layer - it will be sent to the MMU upon line up (written into its 8bit register 0x0b)
+    // However - in the G-code we can get a request to set the extra load distance at runtime to something else (M708 A0xb Xsomething).
+    // The printer intercepts such a call and sets its extra load distance to match the new value as well.
+    #define MMU2_FILAMENT_SENSOR_POSITION    0   // (mm)
+    #define MMU2_LOAD_DISTANCE_PAST_GEARS    5   // (mm)
+    #define MMU2_TOOL_CHANGE_LOAD_LENGTH MMU2_FILAMENT_SENSOR_POSITION + MMU2_LOAD_DISTANCE_PAST_GEARS // (mm)
+
+    #define MMU2_LOAD_TO_NOZZLE_FEED_RATE        20.0 // (mm/s)
+    #define MMU2_UNLOAD_TO_FINDA_FEED_RATE      120.0 // (mm/s)
+
+    #define MMU2_VERIFY_LOAD_TO_NOZZLE_FEED_RATE 50.0 // (mm/s)
+    #define MMU2_VERIFY_LOAD_TO_NOZZLE_TWEAK     -5.0 // (mm) Amount to adjust the length for verifying load-to-nozzle
+
+    // The first thing the MMU does is initialize its axis.
+    // Meanwhile the E-motor will unload 20mm of filament in about 1 second.
+    #define MMU2_RETRY_UNLOAD_TO_FINDA_LENGTH    80.0 // (mm)
+    #define MMU2_RETRY_UNLOAD_TO_FINDA_FEED_RATE 80.0 // (mm/s)
+
+    // After loading a new filament, the printer will extrude this length of filament
+    // then retract to the original position. This is used to check if the filament sensor
+    // reading flickers or filament is jammed.
+    #define MMU2_CHECK_FILAMENT_PRESENCE_EXTRUSION_LENGTH (MMU2_EXTRUDER_PTFE_LENGTH + MMU2_EXTRUDER_HEATBREAK_LENGTH + MMU2_VERIFY_LOAD_TO_NOZZLE_TWEAK + MMU2_FILAMENT_SENSOR_POSITION) // (mm)
+
+    #define MMU_HAS_CUTTER            // Enable cutter related functionalities
+    //#define MMU_FORCE_STEALTH_MODE  // Force stealth mode and disable menu item
+
+    /**
+     * SpoolJoin Consumes All Filament -- EXPERIMENTAL
+     *
+     * SpoolJoin normally triggers when FINDA sensor untriggers while printing.
+     * This is the default behaviour and it doesn't consume all the filament
+     * before triggering a filament change. This leaves some filament in the
+     * current slot and before switching to the next slot it is unloaded.
+     *
+     * Enabling this option will trigger the filament change when both FINDA
+     * and Filament Runout Sensor triggers during the print and it allows the
+     * filament in the current slot to be completely consumed before doing the
+     * filament change. But this can cause problems as a little bit of filament
+     * will be left between the extruder gears (thinking that the filament
+     * sensor is triggered through the gears) and the end of the PTFE tube and
+     * can cause filament load issues.
+     */
+    //#define MMU_SPOOL_JOIN_CONSUMES_ALL_FILAMENT
+
   #else
 
     /**
@@ -4473,7 +4595,7 @@
 
   //#define MMU2_DEBUG  // Write debug info to serial output
 
-#endif // HAS_PRUSA_MMU2
+#endif // HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3
 
 /**
  * Advanced Print Counter settings

+ 19 - 8
Marlin/src/MarlinCore.cpp

@@ -229,12 +229,14 @@
   #include "feature/controllerfan.h"
 #endif
 
-#if HAS_PRUSA_MMU1
-  #include "feature/mmu/mmu.h"
-#endif
-
-#if HAS_PRUSA_MMU2
+#if HAS_PRUSA_MMU3
+  #include "feature/mmu3/mmu2.h"
+  #include "feature/mmu3/mmu2_reporting.h"
+  #include "feature/mmu3/SpoolJoin.h"
+#elif HAS_PRUSA_MMU2
   #include "feature/mmu/mmu2.h"
+#elif HAS_PRUSA_MMU1
+  #include "feature/mmu/mmu.h"
 #endif
 
 #if ENABLED(PASSWORD_FEATURE)
@@ -351,6 +353,7 @@ void startOrResumeJob() {
     TERN_(CANCEL_OBJECTS, cancelable.reset());
     TERN_(LCD_SHOW_E_TOTAL, e_move_accumulator = 0);
     TERN_(SET_REMAINING_TIME, ui.reset_remaining_time());
+    TERN_(HAS_PRUSA_MMU3, MMU3::operation_statistics.reset_per_print_stats());
   }
   print_job_timer.start();
 }
@@ -785,7 +788,7 @@ void idle(const bool no_stepper_sleep/*=false*/) {
 
   // Handle filament runout sensors
   #if HAS_FILAMENT_SENSOR
-    if (TERN1(HAS_PRUSA_MMU2, !mmu2.enabled()))
+    if (TERN1(HAS_PRUSA_MMU2, !mmu2.enabled()) && TERN1(HAS_PRUSA_MMU3, !mmu3.enabled()))
       runout.run();
   #endif
 
@@ -850,7 +853,11 @@ void idle(const bool no_stepper_sleep/*=false*/) {
   #endif
 
   // Update the Průša MMU2
-  TERN_(HAS_PRUSA_MMU2, mmu2.mmu_loop());
+  #if HAS_PRUSA_MMU3
+    mmu3.mmu_loop();
+  #elif HAS_PRUSA_MMU2
+    mmu2.mmu_loop();
+  #endif
 
   // Handle Joystick jogging
   TERN_(POLL_JOG, joystick.inject_jog_moves());
@@ -1586,7 +1593,11 @@ void setup() {
     SETUP_RUN(stepper_driver_backward_report());
   #endif
 
-  #if HAS_PRUSA_MMU2
+  #if HAS_PRUSA_MMU3
+    if (mmu3.mmu_hw_enabled) SETUP_RUN(mmu3.start());
+    SETUP_RUN(mmu3.status());
+    SETUP_RUN(spooljoin.initStatus());
+  #elif HAS_PRUSA_MMU2
     SETUP_RUN(mmu2.init());
   #endif
 

+ 6 - 6
Marlin/src/feature/mmu/mmu2.cpp

@@ -526,7 +526,7 @@ inline void beep_bad_cmd() { BUZZ(400, 40); }
 
       switch (*special) {
         case '?': {
-          #if ENABLED(MMU2_MENUS)
+          #if ENABLED(MMU_MENUS)
             const uint8_t index = mmu2_choose_filament();
             while (!thermalManager.wait_for_hotend(active_extruder, false)) safe_delay(100);
             load_to_nozzle(index);
@@ -536,7 +536,7 @@ inline void beep_bad_cmd() { BUZZ(400, 40); }
         } break;
 
         case 'x': {
-          #if ENABLED(MMU2_MENUS)
+          #if ENABLED(MMU_MENUS)
             planner.synchronize();
             const uint8_t index = mmu2_choose_filament();
             stepper.disable_extruder();
@@ -614,7 +614,7 @@ inline void beep_bad_cmd() { BUZZ(400, 40); }
     switch (*special) {
       case '?': {
         DEBUG_ECHOLNPGM("case ?\n");
-        #if ENABLED(MMU2_MENUS)
+        #if ENABLED(MMU_MENUS)
           uint8_t index = mmu2_choose_filament();
           while (!thermalManager.wait_for_hotend(active_extruder, false)) safe_delay(100);
           load_to_nozzle(index);
@@ -625,7 +625,7 @@ inline void beep_bad_cmd() { BUZZ(400, 40); }
 
       case 'x': {
         DEBUG_ECHOLNPGM("case x\n");
-        #if ENABLED(MMU2_MENUS)
+        #if ENABLED(MMU_MENUS)
           planner.synchronize();
           uint8_t index = mmu2_choose_filament();
           stepper.disable_extruder();
@@ -729,7 +729,7 @@ inline void beep_bad_cmd() { BUZZ(400, 40); }
     switch (*special) {
       case '?': {
         DEBUG_ECHOLNPGM("case ?\n");
-        #if ENABLED(MMU2_MENUS)
+        #if ENABLED(MMU_MENUS)
           uint8_t index = mmu2_choose_filament();
           while (!thermalManager.wait_for_hotend(active_extruder, false)) safe_delay(100);
           load_to_nozzle(index);
@@ -740,7 +740,7 @@ inline void beep_bad_cmd() { BUZZ(400, 40); }
 
       case 'x': {
         DEBUG_ECHOLNPGM("case x\n");
-        #if ENABLED(MMU2_MENUS)
+        #if ENABLED(MMU_MENUS)
           planner.synchronize();
           uint8_t index = mmu2_choose_filament();
           stepper.disable_extruder();

+ 73 - 0
Marlin/src/feature/mmu3/SpoolJoin.cpp

@@ -0,0 +1,73 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 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/>.
+ *
+ */
+
+/**
+ * SpoolJoin.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "SpoolJoin.h"
+#include "../../module/settings.h"
+#include "../../core/language.h"
+
+SpoolJoin spooljoin;
+
+bool SpoolJoin::enabled;            // Initialized by settings.load
+int SpoolJoin::epprom_addr;         // Initialized by settings.load
+uint8_t SpoolJoin::currentMMUSlot;
+
+SpoolJoin::SpoolJoin() { setSlot(0); }
+
+void SpoolJoin::initStatus() {
+  // Useful information to see during bootup
+  SERIAL_ECHOLN(F("SpoolJoin is "), enabled ? F("On") : F("Off"));
+}
+
+void SpoolJoin::toggle() {
+  // Toggle enabled value.
+  enabled = !enabled;
+
+  // Following Prusa's implementation let's save the value to the EEPROM
+  // TODO: Move to settings.cpp
+  #if ENABLED(EEPROM_SETTINGS)
+    persistentStore.access_start();
+    persistentStore.write_data(epprom_addr, enabled);
+    persistentStore.access_finish();
+    settings.save();
+  #endif
+}
+
+bool SpoolJoin::isEnabled() { return enabled; }
+
+void SpoolJoin::setSlot(const uint8_t slot) { currentMMUSlot = slot; }
+
+uint8_t SpoolJoin::nextSlot() {
+  SERIAL_ECHOPGM("SpoolJoin: ", currentMMUSlot);
+  if (++currentMMUSlot >= 4) currentMMUSlot = 0;
+  SERIAL_ECHOLNPGM(" -> ", currentMMUSlot);
+  return currentMMUSlot;
+}
+
+#endif // HAS_PRUSA_MMU3

+ 72 - 0
Marlin/src/feature/mmu3/SpoolJoin.h

@@ -0,0 +1,72 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 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/>.
+ *
+ */
+#pragma once
+
+/**
+ * SpoolJoin.h
+ */
+
+#include "../../MarlinCore.h"
+
+#include <stdint.h>
+
+// See documentation here: https://help.prusa3d.com/article/spooljoin-mmu2s_134252
+
+class SpoolJoin {
+public:
+  SpoolJoin();
+
+  enum class EEPROM : uint8_t {
+    Unknown,      //!< SpoolJoin is unknown while printer is booting up
+    Enabled,      //!< SpoolJoin is enabled in EEPROM
+    Disabled,     //!< SpoolJoin is disabled in EEPROM
+    Empty = 0xFF  //!< EEPROM has not been set before and all bits are 1 (0xFF) - either a new printer or user erased the memory
+  };
+
+  // @brief Contrary to Prusa's implementation we store the enabled status in a variable
+  static int epprom_addr;
+  static bool enabled;
+
+  // @brief Called when EEPROM is ready to be read
+  static void initStatus();
+
+  // @brief Toggle SpoolJoin
+  static void toggle();
+
+  // @brief Check if SpoolJoin is enabled
+  // @return true if enabled, false if disabled
+  static bool isEnabled();
+
+  // @brief Update the saved MMU slot number so SpoolJoin can determine the next slot to use
+  // @param slot number of the slot to set
+  static void setSlot(const uint8_t slot);
+
+  // @brief Fetch the next slot number (0 to 4).
+  // When filament slot 4 is depleted, the next slot should be 0.
+  // @return the next slot (0 to 4)
+  static uint8_t nextSlot();
+
+private:
+  static uint8_t currentMMUSlot;   //!< Currently used slot (0 to 4)
+};
+
+extern SpoolJoin spooljoin;

+ 1185 - 0
Marlin/src/feature/mmu3/mmu2.cpp

@@ -0,0 +1,1185 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 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/>.
+ *
+ */
+
+/**
+ * mmu2.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "mmu2.h"
+#include "mmu2_error_converter.h"
+#include "mmu2_fsensor.h"
+#include "mmu2_log.h"
+#include "mmu2_marlin.h"
+#include "mmu2_marlin_macros.h"
+#include "mmu2_power.h"
+#include "mmu2_progress_converter.h"
+#include "mmu2_reporting.h"
+
+#include "strlen_cx.h"
+#include "SpoolJoin.h"
+
+#include "../../inc/MarlinConfig.h"
+
+#include "../../lcd/marlinui.h"
+#include "../../module/planner.h"
+#include "../../module/motion.h"
+#include "../../gcode/parser.h"
+#include "../../gcode/queue.h"
+#include "../runout.h"
+#if HAS_LEVELING
+  #include "../bedlevel/bedlevel.h"
+#endif
+#include "../pause.h"
+#include "../../libs/stopwatch.h"
+
+// As of FW 3.12 we only support building the FW with only one extruder, all the multi-extruder infrastructure will be removed.
+// Saves at least 800B of code size
+//#ifdef __AVR__
+//static_assert(EXTRUDERS == 1);
+//#endif
+
+#define MMU2_NO_TOOL 99
+
+MMU3::MMU3 mmu3;
+
+namespace MMU3 {
+
+  template <typename F>
+  void waitForHotendTargetTemp(uint16_t delay, F f) {
+    while (((thermal_degTargetHotend() - thermal_degHotend()) > 5)) {
+      f();
+      safe_delay_keep_alive(delay);
+    }
+  }
+
+  void WaitForHotendTargetTempBeep() {
+    waitForHotendTargetTemp(3000, []{});
+    //MakeSound(Prompt);
+  }
+
+  uint8_t MMU3::cutter_mode;    // Initialized by settings.load
+  int MMU3::cutter_mode_addr;   // Initialized by settings.load
+  uint8_t MMU3::stealth_mode;   // Initialized by settings.load
+  int MMU3::stealth_mode_addr;  // Initialized by settings.load
+  // TODO: Currently, by logic, the value stored in the EEPROM for is ignored and
+  //       mmu_hw_enabled is always overwritten by the MMU State. Thus restarting
+  //       printer will always set the MMU as senabled.
+  bool MMU3::mmu_hw_enabled;    // Initialized by settings.load
+  int MMU3::mmu_hw_enabled_addr; // Initialized by settings.load
+
+  MMU3::MMU3()
+    : logic(MMU2_TOOL_CHANGE_LOAD_LENGTH, MMU2_LOAD_TO_NOZZLE_FEED_RATE)
+    , extruder(MMU2_NO_TOOL)
+    , tool_change_extruder(MMU2_NO_TOOL)
+    , resume_position()
+    , resume_hotend_temp(0)
+    , logicStepLastStatus(StepStatus::Finished)
+    , _state(xState::Stopped)
+    , mmu_print_saved(SavedState::None)
+    , loadFilamentStarted(false)
+    , unloadFilamentStarted(false)
+    , toolchange_counter(0)
+    , _tmcFailures(0) { }
+
+  void MMU3::status() {
+    // Useful information to see during bootup and change state
+    SERIAL_ECHOLN(F("MMU is "), mmu_hw_enabled ? GET_TEXT_F(MSG_ON) : GET_TEXT_F(MSG_OFF));
+  }
+
+  void MMU3::start() {
+    mmu_hw_enabled = true;
+
+    #if ENABLED(EEPROM_SETTINGS)
+      // Save mmu_hw_enabled to EEPROM
+      // TODO: Move to settings.cpp (for now)
+      persistentStore.access_start();
+      persistentStore.write_data(mmu_hw_enabled_addr, mmu_hw_enabled);
+      persistentStore.access_finish();
+      settings.save();
+    #endif
+
+    MMU2_SERIAL.begin(MMU_BAUD);
+
+    powerOn();
+    MMU2_SERIAL.flush(); // Make sure the UART buffer is clear before starting communication
+
+    setCurrentTool(MMU2_NO_TOOL);
+    _state = xState::Connecting;
+
+    // Start communication
+    logic.start();
+    logic.ResetRetryAttempts();
+    logic.ResetCommunicationTimeoutAttempts();
+  }
+
+  void MMU3::stop() {
+    stopKeepPowered();
+    powerOff();
+  }
+
+  void MMU3::stopKeepPowered() {
+    mmu_hw_enabled = false;
+
+    #if ENABLED(EEPROM_SETTINGS)
+      // Save mmu_hw_enabled to EEPROM
+      persistentStore.access_start();
+      persistentStore.write_data(mmu_hw_enabled_addr, mmu_hw_enabled);
+      persistentStore.access_finish();
+      settings.save();
+    #endif
+
+    _state = xState::Stopped;
+    logic.stop();
+    MMU2_SERIAL.end();
+  }
+
+  void MMU3::tune() {
+    switch (lastErrorCode) {
+      case ErrorCode::HOMING_SELECTOR_FAILED:
+      case ErrorCode::HOMING_IDLER_FAILED: {
+        // Prompt a menu for different values
+        tuneIdlerStallguardThreshold();
+        break;
+      }
+      default: break;
+    }
+  }
+
+  void MMU3::reset(ResetForm level) {
+    switch (level) {
+      case Software:    resetX0(); break;
+      case ResetPin:    triggerResetPin(); break;
+      case CutThePower: powerCycle(); break;
+      case EraseEEPROM: resetX42(); break;
+      default: break;
+    }
+  }
+
+  void MMU3::resetX0() { logic.ResetMMU(); } // Send soft reset
+  void MMU3::resetX42() { logic.ResetMMU(42); }
+
+  void MMU3::triggerResetPin() { power_reset(); }
+
+  void MMU3::powerCycle() {
+    // cut the power to the MMU and after a while restore it
+    // Sadly, MK3/S/+ cannot do this
+    stop();
+    safe_delay_keep_alive(1000);
+    start();
+  }
+
+  void MMU3::powerOff() { power_off(); }
+  void MMU3::powerOn()  { power_on(); }
+
+  bool MMU3::readRegister(uint8_t address) {
+    if (!waitForMMUReady()) return false;
+
+    do {
+      logic.readRegister(address); // we may signal the accepted/rejected status of the response as return value of this function
+    } while (!manage_response(false, false));
+
+    // Update cached value
+    lastReadRegisterValue = logic.rsp.paramValue;
+    return true;
+  }
+
+  bool __attribute__((noinline)) MMU3::writeRegister(uint8_t address, uint16_t data) {
+    if (!waitForMMUReady()) return false;
+
+    // special cases - intercept requests of registers which influence the printer's behaviour too + perform the change even on the printer's side
+    switch (address) {
+      case (uint8_t)Register::Extra_Load_Distance:  logic.PlanExtraLoadDistance(data); break;
+      case (uint8_t)Register::Pulley_Slow_Feedrate: logic.PlanPulleySlowFeedRate(data); break;
+      default: break; // Don't intercept any other register writes
+    }
+
+    do {
+      logic.writeRegister(address, data); // we may signal the accepted/rejected status of the response as return value of this function
+    } while (!manage_response(false, false));
+
+    return true;
+  }
+
+  void MMU3::mmu_loop() {
+    // We only leave this method if the current command was successfully
+    // completed - that's the Marlin's way of blocking operation
+    // Atomic compare_exchange would have been the most appropriate solution
+    // here, but this gets called only in Marlin's task, so thread safety
+    // should be kept
+    static bool avoidRecursion = false;
+    if (avoidRecursion) return;
+    avoidRecursion = true;
+
+    mmu_loop_inner(true);
+
+    avoidRecursion = false;
+  }
+
+  void __attribute__((noinline)) MMU3::mmu_loop_inner(bool reportErrors) {
+    logicStepLastStatus = logicStep(reportErrors); // it looks like the mmu_loop doesn't need to be a blocking call
+    CheckErrorScreenUserInput();
+  }
+
+  /**
+   * Check if there are extruder moves planned ahead.
+   *
+   * TODO: This should go to the planner, but for now keep it here!
+   */
+  bool MMU3::e_active() {
+    unsigned char e_active = 0;
+    block_t *block;
+    if (planner.block_buffer_tail != planner.block_buffer_head) {
+      uint8_t block_index = planner.block_buffer_tail;
+      while (block_index != planner.block_buffer_head) {
+        block = &planner.block_buffer[block_index];
+        if (block->steps[E_AXIS] != 0) e_active++;
+        block_index = (block_index + 1) & (BLOCK_BUFFER_SIZE - 1);
+      }
+    }
+    return (e_active > 0);
+  }
+
+  /**
+   * Trigger an M600 or the SpoolJoin feature if the FINDA cannot detect any
+   * filament during the print.
+   *
+   * In case of SpoolJoin feature is triggered, Marlin's implementation is a
+   * little different than Prusa's, as we are completely consuming the filament
+   * before switching to the next slot. There will be a little bit of filament
+   * left when the new filament is extruded SpoolJoin is not intended to be used with
+   * multi color/material prints so this should be fine.
+   */
+  void MMU3::checkFINDARunout() {
+    if (!findaDetectsFilament()
+        //&& printJobOngoing()
+        && parser.codenum != 600
+        && TERN1(HAS_LEVELING, planner.leveling_active)
+        && xy_are_trusted()
+        && e_active()
+        #if ENABLED(MMU_SPOOL_JOIN_CONSUMES_ALL_FILAMENT)
+          && runout.enabled // to prevent M600 to be triggered during M600 AUTO
+          && !FILAMENT_PRESENT() // so the filament is totally consumed
+        #endif
+    ) {
+      SERIAL_ECHOLN_P("FINDA filament runout!");
+      if (spooljoin.isEnabled() && get_current_tool() != (uint8_t)FILAMENT_UNKNOWN) { // Can't auto if F=?
+        #if ENABLED(MMU_SPOOL_JOIN_CONSUMES_ALL_FILAMENT)
+          // set the current tool to FILAMENT_UNKNOWN so that we don't try to unload it
+          extruder = MMU2_NO_TOOL;
+          // disable the filament runout sensor (this is going to be re-enabled after the filament is loaded)
+          runout.reset();
+          runout.filament_ran_out = false; // trying to disable the purge more / continue message
+          runout.enabled = false;
+        #endif
+        queue.enqueue_now(F("M600A")); // Save print and run M600 command
+      }
+      else {
+        marlin_stop_and_save_print_to_ram();
+        resume_print();
+        queue.enqueue_now(F("M600")); // Save print and run M600 command
+      }
+    }
+  }
+
+  struct ReportingRAII {
+    CommandInProgress cip;
+    explicit inline __attribute__((always_inline)) ReportingRAII(CommandInProgress cip)
+      : cip(cip) {
+      BeginReport(cip, ProgressCode::EngagingIdler);
+    }
+    inline __attribute__((always_inline)) ~ReportingRAII() {
+      EndReport(cip, ProgressCode::OK);
+    }
+  };
+
+  bool MMU3::waitForMMUReady() {
+    switch (state()) {
+      case xState::Stopped: return false;
+      case xState::Connecting:
+      // Should we wait until the MMU reconnects?
+      // Fire up a fsm_dlg and show "MMU not responding"?
+      default: return true;
+    }
+  }
+
+  bool MMU3::retryIfPossible(const ErrorCode ec) {
+    if (logic.RetryAttempts()) {
+      SetButtonResponse(ButtonOperations::Retry);
+      // check, that Retry is actually allowed on that operation
+      if (ButtonAvailable(ec) != Buttons::NoButton) {
+        logic.SetInAutoRetry(true);
+        SERIAL_ECHOLN_P("RetryButtonPressed");
+        // We don't decrement until the button is acknowledged by the MMU.
+        // --retryAttempts; // "used" one retry attempt
+        return true;
+      }
+    }
+    logic.SetInAutoRetry(false);
+    return false;
+  }
+
+  bool MMU3::verifyFilamentEnteredPTFE() {
+    planner_synchronize();
+
+    if (WhereIsFilament() != FilamentState::AT_FSENSOR)
+      return false;
+
+    // MMU has finished its load, push the filament further by some defined constant length
+    // If the filament sensor reads 0 at any moment, then report FAILURE
+    const float tryload_length = MMU2_CHECK_FILAMENT_PRESENCE_EXTRUSION_LENGTH - logic.ExtraLoadDistance();
+    TryLoadUnloadReporter tlur(tryload_length);
+
+    /**
+     * The position is a triangle wave.
+     * Current position is not zero, it is an offset
+     *
+     * Keep in mind that the relationship between machine position
+     * and pixel index is not linear. The area around the amplitude
+     * needs to be taken care of carefully. The current implementation
+     * handles each move separately so there is no need to watch for the change
+     * in the slope's sign or check the last machine position.
+     *              y(x)
+     *              ▲
+     *              │     ^◄────────── tryload_length + current_position
+     *   machine    │    / \
+     *   position   │   /   \◄────────── stepper_position_mm + current_position
+     *    (mm)      │  /     \
+     *              │ /       \
+     *              │/         \◄───────current_position
+     *              └──────────────► x
+     *              0           19
+     *                 pixel #
+     */
+
+    bool filament_inserted = true; // Expect success
+    // Pixel index will go from 0 to 10, then back from 10 to 0.
+    // A change in this value indicates a new pixel should be drawn on the display.
+    for (uint8_t move = 0; move < 2; move++) {
+      extruder_move(move == 0 ? tryload_length : -tryload_length, MMU2_VERIFY_LOAD_TO_NOZZLE_FEED_RATE);
+      while (planner_any_moves()) {
+        filament_inserted = filament_inserted && (WhereIsFilament() == FilamentState::AT_FSENSOR);
+        tlur.Progress(filament_inserted);
+        safe_delay_keep_alive(0);
+      }
+    }
+    Disable_E0();
+    if (!filament_inserted) IncrementLoadFails();
+    tlur.DumpToSerial();
+    return filament_inserted;
+  }
+
+  bool MMU3::toolChangeCommonOnce(uint8_t slot) {
+    static_assert(MMU2_MAX_RETRIES > 1); // Need >1 retries to do the cut in the last attempt
+    uint8_t retries = 0;
+    for (;;) {
+      for (;;) {
+        Disable_E0(); // It may seem counterintuitive to disable the E-motor, but it gets enabled in the planner whenever the E-motor is to move
+        tool_change_extruder = slot;
+        logic.ToolChange(slot); // Let the MMU pull the filament out and push a new one in
+
+        if (manage_response(true, true)) break;
+
+        // Otherwise: failed to perform the command - unload first and then let it run again
+        IncrementMMUFails();
+
+        // Just in case we stood in an error screen for too long and the hotend got cold
+        resumeHotendTemp();
+        // If the extruder has been parked, it will get unparked once the ToolChange command finishes OK
+        // - so no resumeUnpark() at this spot
+
+        unloadInner();
+        // If we run out of retries, we must do something ... maybe raise an error screen and allow the user to do something.
+        // But honestly - if the MMU restarts during every toolchange something else is seriously broken
+        // and stopping a print is probably our best option.
+      }
+      if (verifyFilamentEnteredPTFE()) return true; // success
+
+      // Prepare a retry attempt
+      unloadInner();
+      if (retries == (MMU2_MAX_RETRIES) - 1 && cutter_enabled()) {
+        cutFilamentInner(slot); // try cutting filament tip at the last attempt
+        retries = 0; // reset retries every MMU2_MAX_RETRIES
+      }
+
+      ++retries;
+    }
+    return false; // Couldn't accomplish the task
+  }
+
+  void MMU3::toolChangeCommon(uint8_t slot) {
+    while (!toolChangeCommonOnce(slot)) { // While not successfully fed into extruder's PTFE tube...
+      // Failed autoretry, report an error by forcing a "printer" error into the MMU infrastructure - it is a hack to leverage existing code
+      // @@TODO theoretically logic layer may not need to be spoiled with the printer error - maybe just the manage_response needs it...
+      logic.SetPrinterError(ErrorCode::LOAD_TO_EXTRUDER_FAILED);
+      // We only have to wait for the user to fix the issue and press "Retry".
+      // Please see checkUserInput() for details how we "leave" manage_response.
+      // If manage_response returns false at this spot (MMU operation interrupted aka MMU reset)
+      // we can safely continue because the MMU is not doing an operation now.
+      static_cast<void>(manage_response(true, true)); // yes, I'd like to silence [[nodiscard]] warning at this spot by casting to void
+    }
+
+    setCurrentTool(slot); // filament change is finished
+    spooljoin.setSlot(slot);
+
+    ++toolchange_counter;
+
+    // Also increment the total number of tool changes
+    operation_statistics.increment_tool_change_counter();
+  }
+
+  bool MMU3::tool_change(uint8_t slot) {
+    if (!waitForMMUReady()) return false;
+
+    if (slot != extruder) {
+      if (
+        //findaDetectsFilament()
+        //!IS_SD_PRINTING() && !usb_timer.running()
+        !marlin_printingIsActive()
+      ) {
+        // If Tcodes are used manually through the serial
+        // we need to unload manually as well -- but only if FINDA detects filament
+        unload();
+      }
+
+      ReportingRAII rep(CommandInProgress::ToolChange);
+      FSensorBlockRunout blockRunout;
+      planner_synchronize();
+      toolChangeCommon(slot);
+    }
+    return true;
+  }
+
+  /**
+   * Handle special T?/Tx/Tc commands
+   *
+   * - T? Gcode to extrude shouldn't have to follow, load to extruder wheels is done automatically
+   * - Tx Same as T?, except nozzle doesn't have to be preheated. Tc must be placed after extruder nozzle is preheated to finish filament load.
+   * - Tc Load to nozzle after filament was prepared by Tx and extruder nozzle is already heated.
+   */
+  bool MMU3::tool_change(char code, uint8_t slot) {
+    if (!waitForMMUReady()) return false;
+
+    FSensorBlockRunout blockRunout;
+
+    switch (code) {
+      case '?': {
+        waitForHotendTargetTemp(100, []{});
+        load_to_nozzle(slot);
+      }
+      break;
+
+      case 'x': {
+        thermal_setExtrudeMintemp(0); // Allow cold extrusion since Tx only loads to the gears not nozzle
+        tool_change(slot);
+        thermal_setExtrudeMintemp(EXTRUDE_MINTEMP);
+      }
+      break;
+
+      case 'c': {
+        waitForHotendTargetTemp(100, []{});
+        execute_load_to_nozzle_sequence();
+      }
+      break;
+    }
+
+    return true;
+  }
+
+  void MMU3::get_statistics() {
+    logic.Statistics();
+  }
+
+  uint8_t __attribute__((noinline)) MMU3::get_current_tool() const {
+    return extruder == MMU2_NO_TOOL ? (uint8_t)FILAMENT_UNKNOWN : extruder;
+  }
+
+  uint8_t MMU3::get_tool_change_tool() const {
+    return tool_change_extruder == MMU2_NO_TOOL ? (uint8_t)FILAMENT_UNKNOWN : tool_change_extruder;
+  }
+
+  void MMU3::setCurrentTool(uint8_t ex) {
+    extruder = ex;
+    MMU2_ECHO_MSGRPGM(PSTR("MMU2tool="));
+    SERIAL_ECHOLN((int)ex);
+  }
+
+  bool MMU3::set_filament_type(uint8_t /*slot*/, uint8_t /*type*/) {
+    if (!waitForMMUReady()) return false;
+
+    // @@TODO - this is not supported in the new MMU yet
+    //    slot = slot; // @@TODO
+    //    type = type; // @@TODO
+    // cmd_arg = filamentType;
+    // command(MMU_CMD_F0 + index);
+
+    if (!manage_response(false, false)) {
+      // @@TODO failed to perform the command - retry
+      // Comment: how is it possible for a filament type set to fail? manage_response(true, true)
+    }
+
+    return true;
+  }
+
+  void MMU3::unloadInner() {
+    FSensorBlockRunout blockRunout;
+    filament_ramming();
+
+    // we assume the printer managed to relieve filament tip from the gears,
+    // so repeating that part in case of an MMU restart is not necessary
+    for (;;) {
+      Disable_E0();
+      logic.UnloadFilament();
+      if (manage_response(false, true)) break;
+      IncrementMMUFails();
+    }
+    //MakeSound(Confirm);
+
+    // no active tool
+    setCurrentTool(MMU2_NO_TOOL);
+    tool_change_extruder = MMU2_NO_TOOL;
+  }
+
+  bool MMU3::unload() {
+    if (!waitForMMUReady()) return false;
+
+    WaitForHotendTargetTempBeep();
+
+    // Scope for ReportingRAII
+    {
+      ReportingRAII rep(CommandInProgress::UnloadFilament);
+      unloadInner();
+    }
+
+    ScreenUpdateEnable();
+    return true;
+  }
+
+  void MMU3::cutFilamentInner(uint8_t slot) {
+    for (;;) {
+      Disable_E0();
+      logic.CutFilament(slot);
+      if (manage_response(false, true)) break;
+      IncrementMMUFails();
+    }
+  }
+
+  bool MMU3::cut_filament(uint8_t slot, bool enableFullScreenMsg /*= true*/) {
+    if (!waitForMMUReady()) return false;
+
+    if (enableFullScreenMsg) fullScreenMsgCut(slot);
+
+    // Scope for ReportingRAII
+    {
+      if (findaDetectsFilament()) unload();
+
+      ReportingRAII rep(CommandInProgress::CutFilament);
+      cutFilamentInner(slot);
+      setCurrentTool(MMU2_NO_TOOL);
+      tool_change_extruder = MMU2_NO_TOOL;
+      //MakeSound(SoundType::Confirm);
+    }
+    ScreenUpdateEnable();
+    return true;
+  }
+
+  bool MMU3::loading_test(uint8_t slot) {
+    fullScreenMsgTest(slot);
+    tool_change(slot);
+    planner_synchronize();
+    unload();
+    ScreenUpdateEnable();
+    return true;
+  }
+
+  bool MMU3::load_to_feeder(uint8_t slot) {
+    if (!waitForMMUReady()) return false;
+
+    fullScreenMsgLoad(slot);
+
+    // Scope for ReportingRAII
+    {
+      ReportingRAII rep(CommandInProgress::LoadFilament);
+      for (;;) {
+        Disable_E0();
+        logic.LoadFilament(slot);
+        if (manage_response(false, false)) break;
+        IncrementMMUFails();
+      }
+      //MakeSound(SoundType::Confirm);
+    }
+    ScreenUpdateEnable();
+    return true;
+  }
+
+  bool MMU3::load_to_nozzle(uint8_t slot) {
+    if (!waitForMMUReady()) return false;
+
+    WaitForHotendTargetTempBeep();
+
+    fullScreenMsgLoad(slot);
+
+    // Scope for ReportingRAII
+    {
+      // Used for MMU-menu operation "Load to Nozzle"
+      ReportingRAII rep(CommandInProgress::ToolChange);
+      FSensorBlockRunout blockRunout;
+
+      // Filament already loaded? Free it and shape its tip properly.
+      if (extruder != MMU2_NO_TOOL) filament_ramming();
+
+      toolChangeCommon(slot);
+
+      // Finish loading to the nozzle with finely tuned steps.
+      execute_load_to_nozzle_sequence();
+      //MakeSound(Confirm);
+    }
+    ScreenUpdateEnable();
+    return true;
+  }
+
+  bool MMU3::eject_filament(uint8_t slot, bool enableFullScreenMsg /* = true */) {
+    if (!waitForMMUReady()) return false;
+
+    if (enableFullScreenMsg) fullScreenMsgEject(slot);
+
+    // Scope for ReportingRAII
+    {
+      if (findaDetectsFilament())
+        unload();
+
+      ReportingRAII rep(CommandInProgress::EjectFilament);
+      for (;;) {
+        Disable_E0();
+        logic.EjectFilament(slot);
+        if (manage_response(false, true))
+          break;
+        IncrementMMUFails();
+      }
+      setCurrentTool(MMU2_NO_TOOL);
+      tool_change_extruder = MMU2_NO_TOOL;
+      //MakeSound(Confirm);
+    }
+    ScreenUpdateEnable();
+    return true;
+  }
+
+  void MMU3::button(uint8_t index) {
+    LogEchoEvent(F("button"));
+    logic.button(index);
+  }
+
+  void MMU3::home(uint8_t mode) {
+    logic.home(mode);
+  }
+
+  void MMU3::saveHotendTemp(bool turn_off_nozzle) {
+    if (mmu_print_saved & SavedState::Cooldown) return;
+
+    if (turn_off_nozzle && !(mmu_print_saved & SavedState::CooldownPending)) {
+      Disable_E0();
+      resume_hotend_temp = thermal_degTargetHotend();
+      mmu_print_saved |= SavedState::CooldownPending;
+      LogEchoEvent(F("Heater cooldown pending"));
+    }
+  }
+
+  void MMU3::saveAndPark(bool move_axes) {
+    if (mmu_print_saved == SavedState::None) { // First occurrence. Save current position, park print head, disable nozzle heater.
+      LogEchoEvent(F("Saving and parking"));
+      Disable_E0();
+      planner_synchronize();
+
+      // In case a power panic happens while waiting for the user
+      // take a partial back up of print state into RAM (current position, etc.)
+      marlin_refresh_print_state_in_ram();
+
+      if (move_axes) {
+        mmu_print_saved |= SavedState::ParkExtruder;
+        resume_position = planner_current_position(); // save current pos
+
+        // Do not lift Z, as it will double lift if there is another error
+        // right after the current one is solved.
+
+        // Move XY aside
+        if (xy_are_trusted()) nozzle_park();
+      }
+    }
+  }
+
+  void MMU3::resumeHotendTemp() {
+    if ((mmu_print_saved & SavedState::CooldownPending)) {
+      // Clear the "pending" flag if we haven't cooled yet.
+      mmu_print_saved &= ~(SavedState::CooldownPending);
+      LogEchoEvent(F("Cooldown flag cleared"));
+    }
+    if ((mmu_print_saved & SavedState::Cooldown) && resume_hotend_temp) {
+      LogEchoEvent(F("Resuming Temp"));
+      // @@TODO MMU2_ECHO_MSGRPGM(PSTR("Restoring hotend temperature "));
+      SERIAL_ECHOLN(resume_hotend_temp);
+      mmu_print_saved &= ~(SavedState::Cooldown);
+      thermal_setTargetHotend(resume_hotend_temp);
+      fullScreenMsgRestoringTemperature();
+      // @todo better report the event and let the GUI do its work somewhere else
+      ReportErrorHookSensorLineRender();
+      waitForHotendTargetTemp(100, [] {
+        marlin_manage_inactivity(true);
+        mmu3.mmu_loop_inner(false);
+        ReportErrorHookDynamicRender();
+      });
+      ScreenUpdateEnable(); // temporary hack to stop this locking the printer...
+      LogEchoEvent(F("Hotend temperature reached"));
+      ScreenClear();
+    }
+  }
+
+  void MMU3::resumeUnpark() {
+    if (mmu_print_saved & SavedState::ParkExtruder) {
+      LogEchoEvent(F("Resuming XYZ"));
+
+      // Move XY to starting position, then Z
+      motion_do_blocking_move_to_xy(resume_position.x, resume_position.x, feedRate_t(NOZZLE_PARK_XY_FEEDRATE));
+
+      // Move Z_AXIS to saved position
+      motion_do_blocking_move_to_z(resume_position.z, feedRate_t(NOZZLE_PARK_Z_FEEDRATE));
+
+      // From this point forward, power panic should not use
+      // the partial backup in RAM since the extruder is no
+      // longer in parking position
+      marlin_clear_print_state_in_ram();
+
+      mmu_print_saved &= ~(SavedState::ParkExtruder);
+    }
+  }
+
+  void MMU3::checkUserInput() {
+    auto btn = ButtonPressed(lastErrorCode);
+
+    // Was a button pressed on the MMU itself instead of the LCD?
+    if (btn == Buttons::NoButton && lastButton != Buttons::NoButton) {
+      btn = lastButton;
+      lastButton = Buttons::NoButton; // Clear it.
+    }
+
+    if (mmuLastErrorSource() == MMU3::ErrorSourcePrinter && btn != Buttons::NoButton) {
+      // When the printer has raised an error screen, and a button was selected
+      // the error screen should always be dismissed.
+      clearPrinterError();
+      // A horrible hack - clear the explicit printer error allowing manage_response to recover on MMU's Finished state
+      // Moreover - if the MMU is currently doing something (like the LoadFilament - see comment above)
+      // we'll actually wait for it automagically in manage_response and after it finishes correctly,
+      // we'll issue another command (like toolchange)
+    }
+
+    switch (btn) {
+      case Buttons::Left:
+      case Buttons::Middle:
+      case Buttons::Right:
+        SERIAL_ECHOPGM("checkUserInput-btnLMR ");
+        SERIAL_ECHOLN((int)buttons_to_uint8t(btn));
+        resumeHotendTemp(); // Recover the hotend temp before we attempt to do anything else...
+
+        if (mmuLastErrorSource() == MMU3::ErrorSourceMMU)
+          // Do not send a button to the MMU unless the MMU is in error state
+          button(buttons_to_uint8t(btn));
+
+        // A quick hack: for specific error codes move the E-motor every time.
+        // Not sure if we can rely on the fsensor.
+        // Just plan the move, let the MMU take over when it is ready
+        switch (lastErrorCode) {
+          case ErrorCode::FSENSOR_DIDNT_SWITCH_OFF:
+          case ErrorCode::FSENSOR_TOO_EARLY: helpUnloadToFinda(); break;
+          default: break;
+        }
+        break;
+      case Buttons::TuneMMU:
+        tune();
+        break;
+      case Buttons::Load:
+      case Buttons::Eject:
+        // High level operation
+        setPrinterButtonOperation(btn);
+        break;
+      case Buttons::ResetMMU:
+        reset(ResetPin); // Cannot do power cycle on the MK3
+        // ... but mmu2_power.cpp knows this and triggers a soft-reset instead.
+        break;
+      case Buttons::DisableMMU:
+        stop();
+        //DisableMMUInSettings(); // stop() already does this...
+        status();
+        break;
+      case Buttons::StopPrint:
+        // @@TODO Unsure if we should handle this high level operation at this spot
+        break;
+      default: break;
+    }
+  }
+
+  /**
+   * Originally, this was used to wait for response and deal with timeout if necessary.
+   * The new protocol implementation enables much nicer and intense reporting, so this method will boil down
+   * just to verify the result of an issued command (which was basically the original idea)
+   *
+   * It is closely related to mmu_loop() (which corresponds to our ProtocolLogic::Step()), which does NOT perform any blocking wait for a command to finish.
+   * But - in case of an error, the command is not yet finished, but we must react accordingly - move the printhead elsewhere, stop heating, eat a cat or so.
+   * That's what's being done here...
+   */
+  bool MMU3::manage_response(const bool move_axes, const bool turn_off_nozzle) {
+    mmu_print_saved = SavedState::None;
+
+    MARLIN_KEEPALIVE_STATE_IN_PROCESS;
+
+    Stopwatch nozzle_timer;
+
+    for (;;) {
+      // in our new implementation, we know the exact state of the MMU at any moment, we do not have to wait for a timeout
+      // So in this case we should decide if the operation is:
+      // - still running -> wait normally in idle()
+      // - failed -> then do the safety moves on the printer like before
+      // - finished ok -> proceed with reading other commands
+      safe_delay_keep_alive(0); // calls logicStep() and remembers its return status
+
+      if (mmu_print_saved & SavedState::CooldownPending) {
+        if (!nozzle_timer.isRunning()) {
+          nozzle_timer.start();
+          LogEchoEvent(F("Cooling Timeout started"));
+        }
+        else if (nozzle_timer.duration() > (PAUSE_PARK_NOZZLE_TIMEOUT * 1000ul)) { // mins->msec.
+          mmu_print_saved &= ~(SavedState::CooldownPending);
+          mmu_print_saved |= SavedState::Cooldown;
+          thermal_setTargetHotend(0);
+          LogEchoEvent(F("Heater cooldown"));
+        }
+      }
+      else if (nozzle_timer.isRunning()) {
+        nozzle_timer.stop();
+        LogEchoEvent(F("Cooling timer stopped"));
+      }
+
+      switch (logicStepLastStatus) {
+        case Finished:
+          // command/operation completed, let Marlin continue its work
+          // the E may have some more moves to finish - wait for them
+          resumeHotendTemp();
+          resumeUnpark();         // We can now travel back to the tower or wherever we were when we saved.
+          if (!TuneMenuEntered()) {
+            // If the error screen is sleeping (running 'Tune' menu)
+            // then don't reset retry attempts because we this will trigger
+            // an automatic retry attempt when 'Tune' button is selected. We want the
+            // error screen to appear once more so the user can hit 'Retry' button manually.
+            logic.ResetRetryAttempts(); // Reset the retry counter.
+          }
+          planner_synchronize();
+          return true;
+        case Interrupted:
+          // now what :D ... big bad ... ramming, unload, retry the whole command originally issued
+          return false;
+        case VersionMismatch: // this basically means the MMU will be disabled until reconnected
+          checkUserInput();
+          return true;
+        case PrinterError:
+          saveAndPark(move_axes);
+          saveHotendTemp(turn_off_nozzle);
+          checkUserInput();
+          // if button pressed "Done", return true, otherwise stay within manage_response
+          // Please see checkUserInput() for details how we "leave" manage_response
+          break;
+        case CommandError:
+        case CommunicationTimeout:
+        case ProtocolError:
+        case ButtonPushed:
+          if (!logic.InAutoRetry()) {
+            // Don't proceed to the park/save if we are doing an autoretry.
+            saveAndPark(move_axes);
+            saveHotendTemp(turn_off_nozzle);
+            checkUserInput();
+          }
+          break;
+        case CommunicationRecovered: // @@TODO communication recovered and maybe an error recovered as well
+          // Maybe the logic layer can detect the change of state a respond with one "Recovered" to be handled here
+          resumeHotendTemp();
+          resumeUnpark();
+          break;
+        case Processing: // Wait for the MMU to respond
+        default: break;
+      }
+    }
+  }
+
+  StepStatus MMU3::logicStep(bool reportErrors) {
+    // Process any buttons before proceeding with another MMU Query
+    checkUserInput();
+
+    const StepStatus ss = logic.Step();
+    switch (ss) {
+
+      case Finished:
+        // At this point it is safe to trigger a runout and not interrupt the MMU protocol
+        checkFINDARunout();
+        break;
+
+      case Processing:
+        onMMUProgressMsg(logic.Progress());
+        break;
+
+      case ButtonPushed:
+        lastButton = logic.button();
+        LogEchoEvent(F("MMU button pushed"));
+        checkUserInput(); // Process the button immediately
+        break;
+
+      case Interrupted:
+        // can be silently handed over to a higher layer, no processing necessary at this spot
+        break;
+
+      default:
+        if (reportErrors) {
+          switch (ss) {
+
+            case CommandError:
+              reportError(logic.Error(), ErrorSourceMMU);
+              break;
+
+            case CommunicationTimeout:
+              _state = xState::Connecting;
+              reportError(ErrorCode::MMU_NOT_RESPONDING, ErrorSourcePrinter);
+              break;
+
+            case ProtocolError:
+              _state = xState::Connecting;
+              reportError(ErrorCode::PROTOCOL_ERROR, ErrorSourcePrinter);
+              break;
+
+            case VersionMismatch:
+              stopKeepPowered();
+              reportError(ErrorCode::VERSION_MISMATCH, ErrorSourcePrinter);
+              break;
+
+            case PrinterError:
+              reportError(logic.PrinterError(), ErrorSourcePrinter);
+              break;
+
+            default:
+              break;
+          }
+        }
+    }
+
+    if (logic.Running()) _state = xState::Active;
+
+    return ss;
+  }
+
+  void MMU3::filament_ramming() {
+    execute_extruder_sequence(ramming_sequence, sizeof(ramming_sequence) / sizeof(E_Step));
+  }
+
+  void MMU3::execute_extruder_sequence(const E_Step *sequence, uint8_t steps) {
+    planner_synchronize();
+
+    const E_Step *step = sequence;
+    for (uint8_t i = steps; i > 0; --i) {
+      extruder_move(pgm_read_float(&(step->extrude)), pgm_read_float(&(step->feedRate)));
+      step++;
+    }
+    planner_synchronize(); // it looks like it's better to sync the moves at the end - smoother move (if the sequence is not too long).
+
+    Disable_E0();
+  }
+
+  void MMU3::execute_load_to_nozzle_sequence() {
+    planner_synchronize();
+    // Compensate for configurable Extra Loading Distance
+    planner_set_current_position_E(planner_get_current_position_E() - (logic.ExtraLoadDistance() - MMU2_FILAMENT_SENSOR_POSITION));
+    execute_extruder_sequence(load_to_nozzle_sequence, sizeof(load_to_nozzle_sequence) / sizeof(load_to_nozzle_sequence[0]));
+  }
+
+  void MMU3::reportError(ErrorCode ec, ErrorSource res) {
+    // Due to a potential lossy error reporting layers linked to this hook
+    // we'd better report everything to make sure especially the error states
+    // do not get lost.
+    // - The good news here is the fact, that the MMU reports the errors repeatedly until resolved.
+    // - The bad news is, that MMU not responding may repeatedly occur on printers not having the MMU at all.
+    //
+    // Not sure how to properly handle this situation, options:
+    // - skip reporting "MMU not responding" (at least for now)
+    // - report only changes of states (we can miss an error message)
+    // - maybe some combination of MMUAvailable + UseMMU flags and decide based on their state
+    // Right now the filtering of MMU_NOT_RESPONDING is done in ReportErrorHook() as it is not a problem if mmu2.cpp
+
+    // Depending on the Progress code, we may want to do some action when an error occurs
+    switch (logic.Progress()) {
+      case ProgressCode::UnloadingToFinda:
+        unloadFilamentStarted = false;
+        planner_abort_queued_moves(); // Abort excess E-moves to be safe
+        break;
+      case ProgressCode::FeedingToFSensor:
+        // FSENSOR error during load. Make sure E-motor stops moving.
+        loadFilamentStarted = false;
+        planner_abort_queued_moves(); // Abort excess E-moves to be safe
+        break;
+      default: break;
+    }
+
+    if (ec != lastErrorCode) { // deduplicate: only report changes in error codes into the log
+      lastErrorCode = ec;
+      lastErrorSource = res;
+      LogErrorEvent(PrusaErrorTitle(PrusaErrorCodeIndex(ec)));
+
+      if (ec != ErrorCode::OK && ec != ErrorCode::FILAMENT_EJECTED && ec != ErrorCode::FILAMENT_CHANGE) {
+        IncrementMMUFails();
+
+        // Check if it is a "power" failure. TMC-related errors are considered power failures.
+        static constexpr uint16_t tmcMask =
+          (  (uint16_t)ErrorCode::TMC_IOIN_MISMATCH
+           | (uint16_t)ErrorCode::TMC_RESET
+           | (uint16_t)ErrorCode::TMC_UNDERVOLTAGE_ON_CHARGE_PUMP
+           | (uint16_t)ErrorCode::TMC_SHORT_TO_GROUND
+           | (uint16_t)ErrorCode::TMC_OVER_TEMPERATURE_WARN
+           | (uint16_t)ErrorCode::TMC_OVER_TEMPERATURE_ERROR
+           | (uint16_t)ErrorCode::MMU_SOLDERING_NEEDS_ATTENTION ) & 0x7FFFU; // skip the top bit
+
+        static_assert(tmcMask == 0x7E00); // Just make sure we fail compilation if any of the TMC error codes change
+
+        if ((uint16_t)ec & tmcMask) { // @@TODO can be optimized to uint8_t operation
+          // TMC-related errors are from 0x8200 higher
+          incrementTMCFailures();
+        }
+      }
+    }
+
+    if (!retryIfPossible(ec))
+      // If retry attempts are all used up
+      // or if 'Retry' operation is not available
+      // raise the MMU error screen and wait for user input
+      ReportErrorHook((CommandInProgress)logic.CommandInProgress(), ec, uint8_t(lastErrorSource));
+  }
+
+  void MMU3::reportProgress(ProgressCode pc) {
+    ReportProgressHook((CommandInProgress)logic.CommandInProgress(), pc);
+    LogEchoEvent(ProgressCodeToText(pc));
+  }
+
+  void MMU3::onMMUProgressMsg(ProgressCode pc) {
+    if (pc != lastProgressCode)
+      onMMUProgressMsgChanged(pc);
+    else
+      onMMUProgressMsgSame(pc);
+  }
+
+  void MMU3::onMMUProgressMsgChanged(ProgressCode pc) {
+    reportProgress(pc);
+    lastProgressCode = pc;
+    switch (pc) {
+      case ProgressCode::UnloadingToFinda:
+        if (  (CommandInProgress)logic.CommandInProgress() == CommandInProgress::UnloadFilament
+          || ((CommandInProgress)logic.CommandInProgress() == CommandInProgress::ToolChange)
+        ) {
+          // If MK3S sent U0 command, ramming sequence takes care of releasing the filament.
+          // If Toolchange is done while printing, PrusaSlicer takes care of releasing the filament
+          // If printing is not in progress, ToolChange will issue a U0 command.
+          break;
+        }
+        else {
+          // We're likely recovering from an MMU error
+          planner_synchronize();
+          unloadFilamentStarted = true;
+          helpUnloadToFinda();
+        }
+        break;
+      case ProgressCode::FeedingToFSensor:
+        // prepare for the movement of the E-motor
+        planner_synchronize();
+        loadFilamentStarted = true;
+        break;
+      default: break; // do nothing yet
+    }
+  }
+
+  void __attribute__((noinline)) MMU3::helpUnloadToFinda() {
+    extruder_move(-MMU2_RETRY_UNLOAD_TO_FINDA_LENGTH, MMU2_RETRY_UNLOAD_TO_FINDA_FEED_RATE);
+  }
+
+  void MMU3::onMMUProgressMsgSame(ProgressCode pc) {
+    const uint8_t pulley_slow_feedrate = logic.PulleySlowFeedRate();
+    const float extrude_distance = _MIN(_MAX(EXTRUDE_MAXLENGTH - 1, 1), pulley_slow_feedrate);
+
+    switch (pc) {
+      case ProgressCode::UnloadingToFinda:
+        if (unloadFilamentStarted && !planner_any_moves()) { // Only plan a move if there is no move ongoing
+          switch (WhereIsFilament()) {
+            case FilamentState::AT_FSENSOR:
+            case FilamentState::IN_NOZZLE:
+            case FilamentState::UNAVAILABLE: // actually Unavailable makes sense as well to start the E-move to release the filament from the gears
+              helpUnloadToFinda();
+              break;
+            default:
+              unloadFilamentStarted = false;
+          }
+        }
+        break;
+
+      case ProgressCode::FeedingToFSensor:
+        if (loadFilamentStarted) {
+          switch (WhereIsFilament()) {
+            case FilamentState::AT_FSENSOR:
+              // fsensor triggered, finish FeedingToExtruder state
+              loadFilamentStarted = false;
+
+              // Abort any excess E-move from the planner queue
+              planner_abort_queued_moves();
+
+              // After the MMU knows the FSENSOR is triggered it will:
+              // 1. Push the filament by additional 30mm (see fsensorToNozzle)
+              // 2. Disengage the idler and push another 2mm.
+              extruder_move(logic.ExtraLoadDistance() + 2, logic.PulleySlowFeedRate());
+              break;
+            case FilamentState::NOT_PRESENT:
+              // fsensor not triggered, continue moving extruder
+              //
+              // Instead of doing a very long extrude as in PrusaFirmware,
+              // Marlin's own MMU2s code has a better approach to this by spinning
+              // the extruder indefinitelly...
+              //
+              // this ensures that while the MMU is pushing the filament,
+              // the extruder will keep rotating, preventing the filament to hit
+              // the extruder gears...
+              while (planner.movesplanned() < 3) {
+                extruder_move(extrude_distance, pulley_slow_feedrate, false);
+              }
+              break;
+            default: break; // Abort here?
+          }
+        }
+        break;
+
+      default: break; // do nothing yet
+    }
+  }
+
+} // MMU3
+
+#endif // HAS_PRUSA_MMU3

+ 419 - 0
Marlin/src/feature/mmu3/mmu2.h

@@ -0,0 +1,419 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 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/>.
+ *
+ */
+#pragma once
+
+/**
+ * mmu2.h
+ */
+
+#include "mmu2_state.h"
+#include "mmu2_marlin.h"
+
+#include "mmu2_protocol_logic.h"
+
+#include "../../MarlinCore.h"
+
+  #ifdef __AVR__
+    typedef float feedRate_t;
+  #else
+    //#include <atomic>
+  #endif
+
+  struct E_Step {
+    float extrude;  //!< extrude distance in mm
+    float feedRate; //!< feed rate in mm/s
+  };
+
+  static constexpr E_Step ramming_sequence[] PROGMEM = { MMU2_RAMMING_SEQUENCE };
+  static constexpr E_Step load_to_nozzle_sequence[] PROGMEM = { MMU2_LOAD_TO_NOZZLE_SEQUENCE };
+
+  namespace MMU3 {
+
+  // general MMU setup for MK3
+  enum : uint8_t {
+    FILAMENT_UNKNOWN = 0xFFU
+  };
+
+  struct Version {
+    uint8_t major, minor, build;
+  };
+
+  // Top-level interface between Logic and Marlin.
+  // Intentionally named MMU3 to be (almost) a drop-in replacement for the previous implementation.
+  // Most of the public methods share the original naming convention as well.
+  class MMU3 {
+  public:
+    MMU3();
+
+    // Powers ON the MMU, then initializes the UART and protocol logic
+    void start();
+
+    // Stops the protocol logic, closes the UART, powers OFF the MMU
+    void stop();
+
+    // Serial output of MMU state
+    void status();
+
+    xState state() const { return _state; }
+
+    bool enabled() const { mmu_hw_enabled = state() == xState::Active; return mmu_hw_enabled; }
+
+    // Different levels of resetting the MMU
+    enum ResetForm : uint8_t {
+      Software = 0,   //!< sends a X0 command into the MMU, the MMU will watchdog-reset itself
+      ResetPin = 1,   //!< trigger the reset pin of the MMU
+      CutThePower = 2, //!< power off and power on (that includes +5V and +24V power lines)
+      EraseEEPROM = 42, //!< erase MMU EEPROM and then perform a software reset
+    };
+
+    // Saved print state on error.
+    enum SavedState : uint8_t {
+      None = 0,       // No state saved.
+      ParkExtruder = 1, // The extruder was parked.
+      Cooldown = 2,   // The extruder was allowed to cool.
+      CooldownPending = 4,
+    };
+
+    // Source of operation error
+    enum ErrorSource : uint8_t {
+      ErrorSourcePrinter = 0,
+      ErrorSourceMMU = 1,
+      ErrorSourceNone = 0xFF,
+    };
+
+    // Tune value in MMU registers as a way to recover from errors
+    // e.g. Idler Stallguard threshold
+    void tune();
+
+    // Perform a reset of the MMU
+    // @param level physical form of the reset
+    void reset(ResetForm level);
+
+    // Power off the MMU (cut the power)
+    void powerOff();
+
+    // Power on the MMU
+    void powerOn();
+
+    // Read from a MMU register (See gcode M707)
+    // @param address Address of register in hexidecimal
+    // @return true upon success
+    bool readRegister(uint8_t address);
+
+    // Write from a MMU register (See gcode M708)
+    // @param address Address of register in hexidecimal
+    // @param data Data to write to register
+    // @return true upon success
+    bool writeRegister(uint8_t address, uint16_t data);
+
+    // The main loop of MMU processing.
+    // Doesn't loop (block) inside, performs just one step of logic state machines.
+    // Also, internally it prevents recursive entries.
+    void mmu_loop();
+
+    // The main MMU command - select a different slot
+    // @param slot of the slot to be selected
+    // @return false if the operation cannot be performed (Stopped)
+    bool tool_change(uint8_t slot);
+
+    // Handling of special Tx, Tc, T? commands
+    bool tool_change(char code, uint8_t slot);
+
+    // Unload of filament in collaboration with the MMU.
+    // That includes rotating the printer's extruder in order to release filament.
+    // @return false if the operation cannot be performed (Stopped or cold extruder)
+    bool unload();
+
+    // Load (insert) filament just into the MMU (not into printer's nozzle)
+    // @return false if the operation cannot be performed (Stopped)
+    bool load_to_feeder(uint8_t slot);
+
+    // Load (push) filament from the MMU into the printer's nozzle
+    // @return false if the operation cannot be performed (Stopped or cold extruder)
+    bool load_to_nozzle(uint8_t slot);
+
+    // Move MMU's selector aside and push the selected filament forward.
+    // Usable for improving filament's tip or pulling the remaining piece of filament out completely.
+    bool eject_filament(uint8_t slot, bool enableFullScreenMsg=true);
+
+    // Issue a Cut command into the MMU
+    // Requires unloaded filament from the printer (obviously)
+    // @return false if the operation cannot be performed (Stopped)
+    bool cut_filament(uint8_t slot, bool enableFullScreenMsg=true);
+
+    // Issue a planned request for statistics data from MMU
+    void get_statistics();
+
+    // Issue a Try-Load command
+    // It behaves very similarly like a ToolChange, but it doesn't load the filament
+    // all the way down to the nozzle. The sole purpose of this operation
+    // is to check, that the filament will be ready for printing.
+    // @param slot index of slot to be tested
+    // @return true
+    bool loading_test(uint8_t slot);
+
+    // @return the active filament slot index (0-4) or 0xff in case of no active tool
+    uint8_t get_current_tool() const;
+
+    // @return The filament slot index (0 to 4) that will be loaded next, 0xff in case of no active tool change
+    uint8_t get_tool_change_tool() const;
+
+    bool set_filament_type(uint8_t slot, uint8_t type);
+
+    // Issue a "button" click into the MMU - to be used from Error screens of the MMU
+    // to select one of the 3 possible options to resolve the issue
+    void button(uint8_t index);
+
+    // Issue an explicit "homing" command into the MMU
+    void home(uint8_t mode);
+
+    // @return current state of FINDA (true=filament present, false=filament not present)
+    bool findaDetectsFilament() const { return logic.findaPressed(); }
+
+    uint16_t totalFailStatistics() const { return logic.FailStatistics(); }
+
+    // @return Current error code
+    ErrorCode mmuCurrentErrorCode() const { return logic.Error(); }
+
+    // @return Command in progress
+    uint8_t getCommandInProgress() const { return logic.CommandInProgress(); }
+
+    // @return Last error source
+    ErrorSource mmuLastErrorSource() const { return lastErrorSource; }
+
+    // @return Last error code
+    ErrorCode getLastErrorCode() const { return lastErrorCode; }
+
+    // @return the version of the connected MMU FW.
+    // In the future we'll return the trully detected FW version
+    Version getMMUFWVersion() const {
+      if (state() == xState::Active) {
+        return { logic.mmuFwVersionMajor(), logic.mmuFwVersionMinor(), logic.mmuFwVersionRevision() };
+      }
+      else {
+        return { 0, 0, 0 };
+      }
+    }
+
+    // Method to read-only mmu_print_saved
+    bool MMU_PRINT_SAVED() const { return mmu_print_saved != SavedState::None; }
+
+    // Automagically "press" a Retry button if we have any retry attempts left
+    // @param ec ErrorCode enum value
+    // @return true if auto-retry is ongoing, false when retry is unavailable or retry attempts are all used up
+    bool retryIfPossible(const ErrorCode ec);
+
+    // @return count for toolchange in current print
+    uint16_t toolChangeCounter() const { return toolchange_counter; }
+
+    // Set toolchange counter to zero
+    void resetToolChangeCounter() { toolchange_counter = 0; }
+
+    uint16_t tmcFailures() const { return _tmcFailures; }
+    void incrementTMCFailures() { ++_tmcFailures; }
+    void resetTMCFailures() { _tmcFailures = 0; }
+
+    // Retrieve cached value parsed from readRegister()
+    // or using M707
+    uint16_t getLastReadRegisterValue() const {
+      return lastReadRegisterValue;
+    }
+    void invokeErrorScreen(const ErrorCode ec) {
+      if (logic.CommandInProgress()) return;        // MMU must not be busy
+      if (lastErrorCode == ec) return;              // The error code is not a duplicate
+      if (mmuCurrentErrorCode() == ErrorCode::OK) { // The protocol must not be in error state
+        reportError(ec, ErrorSource::ErrorSourcePrinter);
+      }
+    }
+
+    void clearPrinterError() {
+      logic.clearPrinterError();
+      lastErrorCode = ErrorCode::OK;
+      lastErrorSource = ErrorSource::ErrorSourceNone;
+    }
+
+    // @brief Queue a button operation which the printer can act upon
+    // @param btn Button operation
+    void setPrinterButtonOperation(Buttons btn) {
+      printerButtonOperation = btn;
+    }
+
+    // @brief Get the printer button operation
+    // @return currently set printer button operation, it can be NoButton if nothing is queued
+    Buttons getPrinterButtonOperation() {
+      return printerButtonOperation;
+    }
+
+    void clearPrinterButtonOperation() {
+      printerButtonOperation = Buttons::NoButton;
+    }
+
+    static uint8_t cutter_mode;   // mode 0:disabled | 1:enabled | 2:always (EXPERIMENTAL)
+    static int cutter_mode_addr;  // EEPROM addr for cutter enabled setting
+    static uint8_t stealth_mode;  // stealth mode
+    static int stealth_mode_addr; // EEPROM addr for stealth_mode setting
+    static bool mmu_hw_enabled;   // MMU hardware can be Enabled/Disabled
+                                  // with the M709 S0 or M709 S1 commands
+                                  // and the last state is stored in the
+                                  // EEPROM
+
+    static int mmu_hw_enabled_addr; // EEPROM addr for mmu_hw_enabled
+
+    bool e_active();
+
+    #ifndef UNITTEST
+      private:
+    #endif
+
+    // Perform software self-reset of the MMU (sends an X0 command)
+    void resetX0();
+
+    // Perform software self-reset of the MMU + erase its EEPROM (sends X2a command)
+    void resetX42();
+
+    // Trigger reset pin of the MMU
+    void triggerResetPin();
+
+    // Perform power cycle of the MMU (cold boot)
+    // Please note this is a blocking operation (sleeps for some time inside while doing the power cycle)
+    void powerCycle();
+
+    // Stop the communication, but keep the MMU powered on (for scenarios with incorrect FW version)
+    void stopKeepPowered();
+
+    // Along with the mmu_loop method, this loops until a response from the MMU is received and acts upon.
+    // In case of an error, it parks the print head and turns off nozzle heating
+    // @return false if the command could not have been completed (MMU interrupted)
+    [[nodiscard]] bool manage_response(const bool move_axes, const bool turn_off_nozzle);
+
+    // The inner private implementation of mmu_loop()
+    // which is NOT (!!!) recursion-guarded. Use caution - but we do need it during waiting for hotend resume to keep comms alive!
+    // @param reportErrors true if Errors should raise MMU Error screen, false otherwise
+    void mmu_loop_inner(bool reportErrors);
+
+    // Performs one step of the protocol logic state machine
+    // and reports progress and errors if needed to attached ExtUIs.
+    // Updates the global state of MMU (Active/Connecting/Stopped) at runtime, see @ref State
+    // @param reportErrors true if Errors should raise MMU Error screen, false otherwise
+    StepStatus logicStep(bool reportErrors);
+
+    void filament_ramming();
+    void execute_extruder_sequence(const E_Step *sequence, uint8_t steps);
+    void execute_load_to_nozzle_sequence();
+
+    // Reports an error into attached ExtUIs
+    // @param ec error code, see ErrorCode
+    // @param res reporter error source, is either Printer (0) or MMU (1)
+    void reportError(ErrorCode ec, ErrorSource res);
+
+    // Reports progress of operations into attached ExtUIs
+    // @param pc progress code, see ProgressCode
+    void reportProgress(ProgressCode pc);
+
+    // Responds to a change of MMU's progress
+    // - plans additional steps, e.g. starts the E-motor after fsensor trigger
+    // The function is quite complex, because it needs to handle asynchronnous
+    // progress and error reports coming from the MMU without an explicit command
+    // - typically after MMU's start or after some HW issue on the MMU.
+    // It must ensure, that calls to @ref reportProgress and/or @ref reportError are
+    // only executed after @ref BeginReport has been called first.
+    void onMMUProgressMsg(ProgressCode pc);
+    // Progress code changed - act accordingly
+    void onMMUProgressMsgChanged(ProgressCode pc);
+    // Repeated calls when progress code remains the same
+    void onMMUProgressMsgSame(ProgressCode pc);
+
+    // @brief Save hotend temperature and set flag to cooldown hotend after 60 minutes
+    // @param turn_off_nozzle if true, the hotend temperature will be set to 0degC after 60 minutes
+    void saveHotendTemp(bool turn_off_nozzle);
+
+    // Save print and park the print head
+    void saveAndPark(bool move_axes);
+
+    // Resume hotend temperature, if it was cooled. Safe to call if we aren't saved.
+    void resumeHotendTemp();
+
+    // Resume position, if the extruder was parked. Safe to all if state was not saved.
+    void resumeUnpark();
+
+    // Check for any button/user input coming from the printer's UI
+    void checkUserInput();
+
+    // @brief Check whether to trigger a FINDA runout. If triggered this function will call M600 AUTO
+    // if SpoolJoin is enabled, otherwise M600 is called without AUTO which will prompt the user
+    // for the next filament slot to use
+    void checkFINDARunout();
+
+    // Entry check of all external commands.
+    // It can wait until the MMU becomes ready.
+    // Optionally, it can also emit/display an error screen and the user can decide what to do next.
+    // @return false if the MMU is not ready to perform the command (for whatever reason)
+    bool waitForMMUReady();
+
+    // After MMU completes a tool-change command
+    // the printer will push the filament by a constant distance. If the Fsensor untriggers
+    // at any moment the test fails. Else the test passes, and the E-motor retracts the
+    // filament back to its original position.
+    // @return false if test fails, true otherwise
+    bool verifyFilamentEnteredPTFE();
+
+    // Common processing of pushing filament into the extruder - shared by tool_change, load_to_nozzle and probably others
+    void toolChangeCommon(uint8_t slot);
+    bool toolChangeCommonOnce(uint8_t slot);
+
+    void helpUnloadToFinda();
+    void unloadInner();
+    void cutFilamentInner(uint8_t slot);
+
+    void setCurrentTool(uint8_t ex);
+
+    ProtocolLogic logic;        //!< implementation of the protocol logic layer
+    uint8_t extruder;           //!< currently active slot in the MMU ... somewhat... not sure where to get it from yet
+    uint8_t tool_change_extruder; //!< only used for UI purposes
+
+    xyz_pos_t resume_position;
+    int16_t resume_hotend_temp;
+
+    ProgressCode lastProgressCode = ProgressCode::OK;
+    ErrorCode lastErrorCode = ErrorCode::MMU_NOT_RESPONDING;
+    ErrorSource lastErrorSource = ErrorSource::ErrorSourceNone;
+    Buttons lastButton = Buttons::NoButton;
+    uint16_t lastReadRegisterValue = 0;
+    Buttons printerButtonOperation = Buttons::NoButton;
+
+    StepStatus logicStepLastStatus;
+
+    enum xState _state;
+
+    uint8_t mmu_print_saved;
+    bool loadFilamentStarted;
+    bool unloadFilamentStarted;
+
+    uint16_t toolchange_counter;
+    uint16_t _tmcFailures;
+  };
+
+  } // MMU3
+
+// following Marlin's way of doing stuff - one and only instance of MMU implementation in the code base
+// + avoiding buggy singletons on the AVR platform
+extern MMU3::MMU3 mmu3;

+ 53 - 0
Marlin/src/feature/mmu3/mmu2_crc.cpp

@@ -0,0 +1,53 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 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/>.
+ *
+ */
+
+/**
+ * mmu2_crc.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "mmu2_crc.h"
+
+#ifdef __AVR__
+  #include <util/crc16.h>
+#endif
+
+namespace modules {
+
+namespace crc {
+
+uint8_t CRC8::CCITT_update(uint8_t crc, uint8_t b) {
+  #ifdef __AVR__
+    return _crc8_ccitt_update(crc, b);
+  #else
+    return CCITT_updateCX(crc, b);
+  #endif
+}
+
+} // namespace crc
+
+} // namespace modules
+
+#endif // HAS_PRUSA_MMU3

+ 73 - 0
Marlin/src/feature/mmu3/mmu2_crc.h

@@ -0,0 +1,73 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 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/>.
+ *
+ */
+#pragma once
+
+/**
+ * mmu2_crc.h
+ */
+
+#include <stdint.h>
+
+namespace modules {
+
+// prevent silly indenting of the whole file
+
+// Contains all the necessary functions for computation of CRC
+namespace crc {
+
+class CRC8 {
+public:
+  // Compute/update CRC8 CCIIT from 8bits.
+  // Details: https://www.nongnu.org/avr-libc/user-manual/group__util__crc.html
+  static uint8_t CCITT_update(uint8_t crc, uint8_t b);
+
+  static constexpr uint8_t CCITT_updateCX(uint8_t crc, uint8_t b) {
+    uint8_t data = crc ^ b;
+    for (uint8_t i = 0; i < 8; i++) {
+      if ((data & 0x80U) != 0) {
+        data <<= 1U;
+        data ^= 0x07U;
+      }
+      else {
+        data <<= 1U;
+      }
+    }
+    return data;
+  }
+
+  // Compute/update CRC8 CCIIT from 16bits (convenience wrapper)
+  static constexpr uint8_t CCITT_updateW(uint8_t crc, uint16_t w) {
+    union U {
+      uint8_t b[2];
+      uint16_t w;
+      explicit constexpr inline U(uint16_t w)
+      : w(w) {}
+    }
+    u(w);
+    return CCITT_updateCX(CCITT_updateCX(crc, u.b[0]), u.b[1]);
+  }
+};
+
+} // namespace crc
+
+
+} // namespace modules

Some files were not shown because too many files changed in this diff