Browse Source

MMU2 interface overhaul

First port of the new MMU2-printer interface into 8bit FW.
D.R.racer 2 years ago
parent
commit
2e293e90a0

+ 1 - 1
Firmware/Marlin.h

@@ -461,7 +461,7 @@ void gcode_M114();
 #if (defined(FANCHECK) && (((defined(TACH_0) && (TACH_0 >-1)) || (defined(TACH_1) && (TACH_1 > -1)))))
 void gcode_M123();
 #endif //FANCHECK and TACH_0 and TACH_1
-void gcode_M701();
+void gcode_M701(uint8_t mmuSlotIndex);
 
 #define UVLO !(PINE & (1<<4))
 

+ 92 - 242
Firmware/Marlin_main.cpp

@@ -86,6 +86,7 @@
 #include <avr/wdt.h>
 #include <avr/pgmspace.h>
 
+#include "Tcodes.h"
 #include "Dcodes.h"
 #include "AutoDeplete.h"
 
@@ -125,7 +126,7 @@
 #include <SPI.h>
 #endif
 
-#include "mmu.h"
+#include "mmu2.h"
 
 #define VERSION_STRING  "1.0.2"
 
@@ -1047,7 +1048,7 @@ void setup()
 {
 	timer2_init(); // enables functional millis
 
-	mmu_init();
+	MMU2::mmu2.Start();
 
 	ultralcd_init();
 
@@ -1623,7 +1624,7 @@ void setup()
 #endif //UVLO_SUPPORT
 
   fCheckModeInit();
-  fSetMmuMode(mmu_enabled);
+  fSetMmuMode(MMU2::mmu2.Enabled());
   KEEPALIVE_STATE(NOT_BUSY);
 #ifdef WATCHDOG
   wdt_enable(WDTO_4S);
@@ -1856,7 +1857,7 @@ void loop()
 		}
 	}
 #endif //TMC2130
-	mmu_loop();
+	MMU2::mmu2.mmu_loop();
 }
 
 #define DEFINE_PGM_READ_ANY(type, reader)       \
@@ -3469,15 +3470,14 @@ static T gcode_M600_filament_change_z_shift()
 #else
 	return T(0);
 #endif
-}	
+}
 
-static void gcode_M600(bool automatic, float x_position, float y_position, float z_shift, float e_shift, float /*e_shift_late*/)
-{
+static void gcode_M600(bool automatic, float x_position, float y_position, float z_shift, float e_shift, float /*e_shift_late*/) {
     st_synchronize();
     float lastpos[4];
 
         prusa_statistics(22);
-
+    
     //First backup current position and settings
     int feedmultiplyBckp = feedmultiply;
     float HotendTempBckp = degTargetHotend(active_extruder);
@@ -3488,33 +3488,35 @@ static void gcode_M600(bool automatic, float x_position, float y_position, float
     lastpos[Z_AXIS] = current_position[Z_AXIS];
     lastpos[E_AXIS] = current_position[E_AXIS];
 
-    //Retract E
+    // Retract E
     current_position[E_AXIS] += e_shift;
     plan_buffer_line_curposXYZE(FILAMENTCHANGE_RFEED);
     st_synchronize();
 
-    //Lift Z
+    // Lift Z
     current_position[Z_AXIS] += z_shift;
     clamp_to_software_endstops(current_position);
     plan_buffer_line_curposXYZE(FILAMENTCHANGE_ZFEED);
     st_synchronize();
 
-    //Move XY to side
+    // Move XY to side
     current_position[X_AXIS] = x_position;
     current_position[Y_AXIS] = y_position;
     plan_buffer_line_curposXYZE(FILAMENTCHANGE_XYFEED);
     st_synchronize();
 
-    //Beep, manage nozzle heater and wait for user to start unload filament
-    if(!mmu_enabled) M600_wait_for_user(HotendTempBckp);
+    // Beep, manage nozzle heater and wait for user to start unload filament
+    if (!MMU2::mmu2.Enabled())
+        M600_wait_for_user(HotendTempBckp);
 
     lcd_change_fil_state = 0;
 
     // Unload filament
-    if (mmu_enabled) extr_unload();	//unload just current filament for multimaterial printers (used also in M702)
-    else unload_filament(true); //unload filament for single material (used also in M702)
-    //finish moves
-    st_synchronize();
+    if (MMU2::mmu2.Enabled())
+        MMU2::mmu2.unload(); // unload just current filament for multimaterial printers (used also in M702)
+    else
+        unload_filament(true); // unload filament for single material (used also in M702)
+    st_synchronize();          // finish moves
 
 #ifdef FILAMENT_SENSOR
     fsensor.setRunoutEnabled(false); //suppress filament runouts while loading filament.
@@ -3524,14 +3526,11 @@ static void gcode_M600(bool automatic, float x_position, float y_position, float
 #endif //(FILAMENT_SENSOR_TYPE == FSENSOR_PAT9125)
 #endif
 
-    if (!mmu_enabled)
-    {
+    if (!MMU2::mmu2.Enabled()) {
         KEEPALIVE_STATE(PAUSED_FOR_USER);
-        lcd_change_fil_state = lcd_show_fullscreen_message_yes_no_and_wait_P(
-                _i("Was filament unload successful?"), ////MSG_UNLOAD_SUCCESSFUL c=20 r=2
-                false, true);
-        if (lcd_change_fil_state == 0)
-        {
+        lcd_change_fil_state =
+            lcd_show_fullscreen_message_yes_no_and_wait_P(_i("Was filament unload successful?"), false, true); ////MSG_UNLOAD_SUCCESSFUL c=20 r=2
+        if (lcd_change_fil_state == 0) {
 			lcd_clear();
 			lcd_puts_at_P(0, 2, _T(MSG_PLEASE_WAIT));
 			current_position[X_AXIS] -= 100;
@@ -3541,55 +3540,53 @@ static void gcode_M600(bool automatic, float x_position, float y_position, float
         }
     }
 
-    if (mmu_enabled)
-    {
+    if (MMU2::mmu2.Enabled()) {
         if (!automatic) {
-            if (saved_printing) mmu_eject_filament(mmu_extruder, false); //if M600 was invoked by filament senzor (FINDA) eject filament so user can easily remove it
-            mmu_M600_wait_and_beep();
+            if (saved_printing)
+                MMU2::mmu2.eject_filament(MMU2::mmu2.get_current_tool(),
+                                          false); // if M600 was invoked by filament senzor (FINDA) eject filament so user can easily remove it
+//@@TODO            mmu_M600_wait_and_beep();
             if (saved_printing) {
 
                 lcd_clear();
                 lcd_puts_at_P(0, 2, _T(MSG_PLEASE_WAIT));
 
-                mmu_command(MmuCmd::R0);
-                manage_response(false, false);
+//@@TODO                mmu_command(MmuCmd::R0);
+//                manage_response(false, false);
             }
         }
-        mmu_M600_load_filament(automatic, HotendTempBckp);
-    }
-    else
+//@@TODO        mmu_M600_load_filament(automatic, HotendTempBckp);
+    } else
         M600_load_filament();
 
-    if (!automatic) M600_check_state(HotendTempBckp);
+    if (!automatic)
+        M600_check_state(HotendTempBckp);
 
-		lcd_update_enable(true);
+    lcd_update_enable(true);
 
-    //Not let's go back to print
+    // Not let's go back to print
     fanSpeed = fanSpeedBckp;
 
-    //Feed a little of filament to stabilize pressure
-    if (!automatic)
-    {
+    // Feed a little of filament to stabilize pressure
+    if (!automatic) {
         current_position[E_AXIS] += FILAMENTCHANGE_RECFEED;
         plan_buffer_line_curposXYZE(FILAMENTCHANGE_EXFEED);
     }
 
-    //Move XY back
-    plan_buffer_line(lastpos[X_AXIS], lastpos[Y_AXIS], current_position[Z_AXIS], current_position[E_AXIS],
-            FILAMENTCHANGE_XYFEED, active_extruder);
+    // Move XY back
+    plan_buffer_line(lastpos[X_AXIS], lastpos[Y_AXIS], current_position[Z_AXIS], current_position[E_AXIS], FILAMENTCHANGE_XYFEED, active_extruder);
     st_synchronize();
-    //Move Z back
-    plan_buffer_line(lastpos[X_AXIS], lastpos[Y_AXIS], lastpos[Z_AXIS], current_position[E_AXIS],
-            FILAMENTCHANGE_ZFEED, active_extruder);
+    // Move Z back
+    plan_buffer_line(lastpos[X_AXIS], lastpos[Y_AXIS], lastpos[Z_AXIS], current_position[E_AXIS], FILAMENTCHANGE_ZFEED, active_extruder);
     st_synchronize();
 
-    //Set E position to original
+    // Set E position to original
     plan_set_e_position(lastpos[E_AXIS]);
 
     memcpy(current_position, lastpos, sizeof(lastpos));
     set_destination_to_current();
 
-    //Recover feed rate
+    // Recover feed rate
     feedmultiply = feedmultiplyBckp;
     char cmd[9];
     sprintf_P(cmd, PSTR("M220 S%i"), feedmultiplyBckp);
@@ -3603,33 +3600,26 @@ static void gcode_M600(bool automatic, float x_position, float y_position, float
     custom_message_type = CustomMsg::Status;
 }
 
-void gcode_M701()
-{
-	printf_P(PSTR("gcode_M701 begin\n"));
+void gcode_M701(uint8_t mmuSlotIndex){
+    printf_P(PSTR("gcode_M701 begin\n"));
 
 #ifdef FILAMENT_SENSOR
-	fsensor.setRunoutEnabled(false); //suppress filament runouts while loading filament.
-	fsensor.setAutoLoadEnabled(false); //suppress filament autoloads while loading filament.
+    fsensor.setRunoutEnabled(false);   // suppress filament runouts while loading filament.
+    fsensor.setAutoLoadEnabled(false); // suppress filament autoloads while loading filament.
 #if (FILAMENT_SENSOR_TYPE == FSENSOR_PAT9125)
-	fsensor.setJamDetectionEnabled(false); //suppress filament jam detection while loading filament.
-#endif //(FILAMENT_SENSOR_TYPE == FSENSOR_PAT9125)
+    fsensor.setJamDetectionEnabled(false); // suppress filament jam detection while loading filament.
+#endif                                     //(FILAMENT_SENSOR_TYPE == FSENSOR_PAT9125)
 #endif
 
-	prusa_statistics(22);
-
-	if (mmu_enabled) 
-	{
-		extr_adj(tmp_extruder);//loads current extruder
-		mmu_extruder = tmp_extruder;
+		prusa_statistics(22);
 	}
-	else
-	{
-		enable_z();
-		custom_message_type = CustomMsg::FilamentLoading;
 
-#ifdef FSENSOR_QUALITY
-		fsensor_oq_meassure_start(40);
-#endif //FSENSOR_QUALITY
+    if (MMU2::mmu2.Enabled() && mmuSlotIndex < MMU_FILAMENT_COUNT) {
+        MMU2::mmu2.load_filament(mmuSlotIndex); // loads current extruder
+        // mmu_extruder = mmuSlotIndex; // @@TODO shall load filament set current tool to some specific index? We don't do that anymore.
+    } else {
+        enable_z();
+        custom_message_type = CustomMsg::FilamentLoading;
 
         const int feed_mm_before_raising = 30;
         static_assert(feed_mm_before_raising <= FILAMENTCHANGE_FIRSTFEED);
@@ -3639,41 +3629,30 @@ void gcode_M701()
 		plan_buffer_line_curposXYZE(FILAMENTCHANGE_EFEED_FIRST); //fast sequence
 		st_synchronize();
 
-        raise_z_above(MIN_Z_FOR_LOAD, false);
+		raise_z_above(MIN_Z_FOR_LOAD, false);
 		current_position[E_AXIS] += feed_mm_before_raising;
 		plan_buffer_line_curposXYZE(FILAMENTCHANGE_EFEED_FIRST); //fast sequence
-		
-		load_filament_final_feed(); //slow sequence
-		st_synchronize();
 
-		Sound_MakeCustom(50,500,false);
+        load_filament_final_feed(); // slow sequence
+        st_synchronize();
 
-		if (!farm_mode && loading_flag) {
-			lcd_load_filament_color_check();
-		}
-		lcd_update_enable(true);
-		lcd_update(2);
-		lcd_setstatuspgm(MSG_WELCOME);
-		disable_z();
-		loading_flag = false;
-		custom_message_type = CustomMsg::Status;
+        Sound_MakeCustom(50, 500, false);
+
+        if (!farm_mode && loading_flag) {
+            lcd_load_filament_color_check();
+        }
+        lcd_update_enable(true);
+        lcd_update(2);
+        lcd_setstatuspgm(MSG_WELCOME);
+        disable_z();
+        loading_flag = false;
+        custom_message_type = CustomMsg::Status;
+    }
 
-#ifdef FSENSOR_QUALITY
-        fsensor_oq_meassure_stop();
+    eFilamentAction = FilamentAction::None;
 
-        if (!fsensor_oq_result())
-        {
-            bool disable = lcd_show_fullscreen_message_yes_no_and_wait_P(_n("Fil. sensor response is poor, disable it?"), false, true);
-            lcd_update_enable(true);
-            lcd_update(2);
-            if (disable)
-                fsensor_disable();
-	}
-	
-	eFilamentAction = FilamentAction::None;
-	
 #ifdef FILAMENT_SENSOR
-	fsensor.settings_init(); //restore filament runout state.
+    fsensor.settings_init(); // restore filament runout state.
 #endif
 }
 /**
@@ -4263,7 +4242,7 @@ void process_commands()
         }
 		else if (code_seen_P(PSTR("MMURES"))) // PRUSA MMURES
 		{
-			mmu_reset();
+			MMU2::mmu2.Reset(MMU2::MMU2::Software);
 		}
 		else if (code_seen_P(PSTR("RESET"))) { // PRUSA RESET
 #ifdef WATCHDOG
@@ -7614,14 +7593,13 @@ Sigma_Exit:
 	{
 		// currently three different materials are needed (default, flex and PVA)
 		// add storing this information for different load/unload profiles etc. in the future
-		// firmware does not wait for "ok" from mmu
-		if (mmu_enabled)
+		if (MMU2::mmu2.Enabled())
 		{
 			uint8_t extruder = 255;
 			uint8_t filament = FILAMENT_UNDEFINED;
 			if(code_seen('E')) extruder = code_value_uint8();
 			if(code_seen('F')) filament = code_value_uint8();
-			mmu_set_filament_type(extruder, filament);
+			MMU2::mmu2.set_filament_type(extruder, filament);
 		}
 	}
 	break;
@@ -7859,7 +7837,7 @@ Sigma_Exit:
           #endif
         }
 
-		if (mmu_enabled && code_seen_P(PSTR("AUTO")))
+		if (MMU2::mmu2.Enabled() && code_seen_P(PSTR("AUTO")))
 			automatic = true;
 
 		gcode_M600(automatic, x_position, y_position, z_shift, e_shift_init, e_shift_late);
@@ -8509,9 +8487,10 @@ Sigma_Exit:
     */
 	case 701:
 	{
-		if (mmu_enabled && (code_seen('E') || code_seen('T')))
-			tmp_extruder = code_value_uint8();
-		gcode_M701();
+        uint8_t mmuSlotIndex = 0xffU;
+		if (MMU2::mmu2.Enabled() && code_seen('E'))
+			mmuSlotIndex = code_value_uint8();
+		gcode_M701(mmuSlotIndex);
 	}
 	break;
 
@@ -8528,10 +8507,10 @@ Sigma_Exit:
 	case 702:
 	{
 		if (code_seen('C')) {
-			if(mmu_enabled) extr_unload(); //! if "C" unload current filament; if mmu is not present no action is performed
+			if(MMU2::mmu2.Enabled()) MMU2::mmu2.unload(); //! if "C" unload current filament; if mmu is not present no action is performed
 		}
 		else {
-			if(mmu_enabled) extr_unload(); //! unload current filament
+			if(MMU2::mmu2.Enabled()) MMU2::mmu2.unload(); //! unload current filament
 			else unload_filament();
 		}
 	}
@@ -8558,139 +8537,8 @@ Sigma_Exit:
   @n 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.
   @n Tc Load to nozzle after filament was prepared by Tc and extruder nozzle is already heated.
   */
-  else if(code_seen('T'))
-  {
-      static const char duplicate_Tcode_ignored[] PROGMEM = "Duplicate T-code ignored.";
-      
-      int index;
-      bool load_to_nozzle = false;
-      for (index = 1; *(strchr_pointer + index) == ' ' || *(strchr_pointer + index) == '\t'; index++);
-
-      *(strchr_pointer + index) = tolower(*(strchr_pointer + index));
-
-      if ((*(strchr_pointer + index) < '0' || *(strchr_pointer + index) > '4') && *(strchr_pointer + index) != '?' && *(strchr_pointer + index) != 'x' && *(strchr_pointer + index) != 'c') {
-          SERIAL_ECHOLNPGM("Invalid T code.");
-      }
-	  else if (*(strchr_pointer + index) == 'x'){ //load to bondtech gears; if mmu is not present do nothing
-		if (mmu_enabled)
-		{
-			tmp_extruder = choose_menu_P(_T(MSG_SELECT_FILAMENT), _T(MSG_FILAMENT));
-			if ((tmp_extruder == mmu_extruder) && mmu_fil_loaded) //dont execute the same T-code twice in a row
-			{
-				puts_P(duplicate_Tcode_ignored);
-			}
-			else
-			{
-				st_synchronize();
-				mmu_command(MmuCmd::T0 + tmp_extruder);
-				manage_response(true, true, MMU_TCODE_MOVE);
-			}
-		}
-	  }
-	  else if (*(strchr_pointer + index) == 'c') { //load to from bondtech gears to nozzle (nozzle should be preheated)
-	  	if (mmu_enabled) 
-		{
-			st_synchronize();
-			mmu_continue_loading(usb_timer.running()  || (lcd_commands_type == LcdCommands::Layer1Cal));
-			mmu_extruder = tmp_extruder; //filament change is finished
-			mmu_load_to_nozzle();
-		}
-	  }
-      else {
-          if (*(strchr_pointer + index) == '?')
-          {
-              if(mmu_enabled)
-              {
-                  tmp_extruder = choose_menu_P(_T(MSG_SELECT_FILAMENT), _T(MSG_FILAMENT));
-                  load_to_nozzle = true;
-              } else
-              {
-                  tmp_extruder = choose_menu_P(_T(MSG_SELECT_EXTRUDER), _T(MSG_EXTRUDER));
-              }
-          }
-          else {
-              tmp_extruder = code_value();
-              if (mmu_enabled && lcd_autoDepleteEnabled())
-              {
-                  tmp_extruder = ad_getAlternative(tmp_extruder);
-              }
-          }
-          st_synchronize();
-
-          if (mmu_enabled)
-          {
-              if ((tmp_extruder == mmu_extruder) && mmu_fil_loaded) //dont execute the same T-code twice in a row
-              {
-                  puts_P(duplicate_Tcode_ignored);
-              }
-			  else
-			  {
-#if defined(MMU_HAS_CUTTER) && defined(MMU_ALWAYS_CUT)
-			      if (EEPROM_MMU_CUTTER_ENABLED_always == eeprom_read_byte((uint8_t*)EEPROM_MMU_CUTTER_ENABLED))
-                  {
-                      mmu_command(MmuCmd::K0 + tmp_extruder);
-                      manage_response(true, true, MMU_UNLOAD_MOVE);
-                  }
-#endif //defined(MMU_HAS_CUTTER) && defined(MMU_ALWAYS_CUT)
-				  mmu_command(MmuCmd::T0 + tmp_extruder);
-				  manage_response(true, true, MMU_TCODE_MOVE);
-		          mmu_continue_loading(usb_timer.running()  || (lcd_commands_type == LcdCommands::Layer1Cal));
-
-				  mmu_extruder = tmp_extruder; //filament change is finished
-
-				  if (load_to_nozzle)// for single material usage with mmu
-				  {
-					  mmu_load_to_nozzle();
-				  }
-			  }
-          }
-          else
-          {
-              if (tmp_extruder >= EXTRUDERS) {
-                  SERIAL_ECHO_START;
-                  SERIAL_ECHO('T');
-                  SERIAL_PROTOCOLLN((int)tmp_extruder);
-                  SERIAL_ECHOLNRPGM(_n("Invalid extruder"));////MSG_INVALID_EXTRUDER
-              }
-              else {
-#if EXTRUDERS > 1
-                  bool make_move = false;
-#endif
-                  if (code_seen('F')) {
-#if EXTRUDERS > 1
-                      make_move = true;
-#endif
-                      next_feedrate = code_value();
-                      if (next_feedrate > 0.0) {
-                          feedrate = next_feedrate;
-                      }
-                  }
-#if EXTRUDERS > 1
-                  if (tmp_extruder != active_extruder) {
-                      // Save current position to return to after applying extruder offset
-                      set_destination_to_current();
-                      // Offset extruder (only by XY)
-                      int i;
-                      for (i = 0; i < 2; i++) {
-                          current_position[i] = current_position[i] -
-                                  extruder_offset[i][active_extruder] +
-                                  extruder_offset[i][tmp_extruder];
-                      }
-                      // Set the new active extruder and position
-                      active_extruder = tmp_extruder;
-                      plan_set_position_curposXYZE();
-                      // Move to the old position if 'F' was in the parameters
-                      if (make_move) {
-                          prepare_move();
-                      }
-                  }
-#endif
-                  SERIAL_ECHO_START;
-                  SERIAL_ECHORPGM(_n("Active Extruder: "));////MSG_ACTIVE_EXTRUDER
-                  SERIAL_PROTOCOLLN((int)active_extruder);
-              }
-          }
-      }
+  else if(code_seen('T')){
+        TCodes(strchr_pointer, code_value());
   } // end if(code_seen('T')) (end of T codes)
   /*!
   #### End of T-Codes
@@ -9491,7 +9339,7 @@ void manage_inactivity(bool ignore_stepper_queue/*=false*/) //default argument s
     }
   #endif
   check_axes_activity();
-  mmu_loop();
+  MMU2::mmu2.mmu_loop();
 
   // handle longpress
   if(lcd_longpress_trigger)
@@ -11422,10 +11270,12 @@ void M600_check_state(float nozzle_temp)
         {
         // Filament failed to load so load it again
         case 2:
-            if (mmu_enabled)
-                mmu_M600_load_filament(false, nozzle_temp); //nonautomatic load; change to "wrong filament loaded" option?
-            else
+            if (MMU2::mmu2.Enabled()){
+//@@TODO                mmu_M600_load_filament(false, nozzle_temp); //nonautomatic load; change to "wrong filament loaded" option?
+                
+            } else {
                 M600_load_filament_movements();
+            }
             break;
 
         // Filament loaded properly but color is not clear

+ 124 - 0
Firmware/Tcodes.cpp

@@ -0,0 +1,124 @@
+#include "Tcodes.h"
+#include "Marlin.h"
+#include "mmu2.h"
+#include "stepper.h"
+#include <avr/pgmspace.h>
+#include <ctype.h>
+#include <stdint.h>
+#include "language.h"
+#include "messages.h"
+#include "ultralcd.h"
+#include <stdio.h>
+
+static const char duplicate_Tcode_ignored[] PROGMEM = "Duplicate T-code ignored.";
+
+inline bool IsInvalidTCode(char *const s, uint8_t i) { 
+    return ((s[i] < '0' || s[i] > '4') && s[i] != '?' && s[i] != 'x' && s[i] != 'c'); 
+}
+
+inline void TCodeInvalid() { 
+    SERIAL_ECHOLNPGM("Invalid T code."); 
+}
+
+// load to bondtech gears; if mmu is not present do nothing
+void TCodeX() {
+    if (MMU2::mmu2.Enabled()) {
+        uint8_t selectedSlot = choose_menu_P(_T(MSG_CHOOSE_FILAMENT), _T(MSG_FILAMENT));
+        if ((selectedSlot == MMU2::mmu2.get_current_tool()) /*&& mmu_fil_loaded @@TODO */){ 
+            // dont execute the same T-code twice in a row
+            puts_P(duplicate_Tcode_ignored);
+        } else {
+            st_synchronize();
+            MMU2::mmu2.tool_change(selectedSlot);
+        }
+    }
+}
+
+// load to from bondtech gears to nozzle (nozzle should be preheated)
+void TCodeC() {
+    if (MMU2::mmu2.Enabled()) {
+        st_synchronize();
+// @@TODO       mmu_continue_loading(usb_timer.running() || (lcd_commands_type == LcdCommands::Layer1Cal));
+//        mmu_extruder = selectedSlot; // filament change is finished
+//        MMU2::mmu2.load_filament_to_nozzle();
+    }
+}
+
+struct SChooseFromMenu {
+    uint8_t slot:7;
+    uint8_t loadToNozzle:1;
+    inline constexpr SChooseFromMenu(uint8_t slot, bool loadToNozzle):slot(slot), loadToNozzle(loadToNozzle){}
+    inline constexpr SChooseFromMenu():slot(0), loadToNozzle(false) { }
+};
+
+SChooseFromMenu TCodeChooseFromMenu() {
+    if (MMU2::mmu2.Enabled()) {
+        return SChooseFromMenu( choose_menu_P(_T(MSG_CHOOSE_FILAMENT), _T(MSG_FILAMENT)), true );
+    } else {
+        return SChooseFromMenu( choose_menu_P(_T(MSG_CHOOSE_EXTRUDER), _T(MSG_EXTRUDER)), false );
+    }
+}
+
+void TCodes(char *const strchr_pointer, uint8_t codeValue) {
+    uint8_t index;
+    for (index = 1; strchr_pointer[index] == ' ' || strchr_pointer[index] == '\t'; index++)
+        ;
+
+    strchr_pointer[index] = tolower(strchr_pointer[index]);
+
+    if (IsInvalidTCode(strchr_pointer, index))
+        TCodeInvalid();
+    else if (strchr_pointer[index] == 'x')
+        TCodeX();
+    else if (strchr_pointer[index] == 'c')
+        TCodeC();
+    else {
+        SChooseFromMenu selectedSlot;
+        if (strchr_pointer[index] == '?')
+            selectedSlot = TCodeChooseFromMenu();
+        else {
+            selectedSlot.slot = codeValue;
+            if (MMU2::mmu2.Enabled() && lcd_autoDepleteEnabled()) {
+// @@TODO                selectedSlot.slot = ad_getAlternative(selectedSlot);
+            }
+        }
+        st_synchronize();
+
+        if (MMU2::mmu2.Enabled()) {
+            if ((selectedSlot.slot == MMU2::mmu2.get_current_tool()) /*&& mmu_fil_loaded @@TODO*/){ 
+                // don't execute the same T-code twice in a row
+                puts_P(duplicate_Tcode_ignored);
+            } else {
+#if defined(MMU_HAS_CUTTER) && defined(MMU_ALWAYS_CUT)
+                if (EEPROM_MMU_CUTTER_ENABLED_always == eeprom_read_byte((uint8_t *)EEPROM_MMU_CUTTER_ENABLED)) {
+                    mmu_command(MmuCmd::K0 + selectedSlot);
+                    manage_response(true, true, MMU_UNLOAD_MOVE);
+                }
+#endif // defined(MMU_HAS_CUTTER) && defined(MMU_ALWAYS_CUT)
+                MMU2::mmu2.tool_change(selectedSlot.slot);
+// @@TODO                mmu_continue_loading(usb_timer.running() || (lcd_commands_type == LcdCommands::Layer1Cal));
+
+                if (selectedSlot.loadToNozzle){ // for single material usage with mmu
+                    MMU2::mmu2.load_filament_to_nozzle(selectedSlot.slot);
+                }
+            }
+        } else {
+            if (selectedSlot.slot >= EXTRUDERS) {
+                SERIAL_ECHO_START;
+                SERIAL_ECHO('T');
+                SERIAL_ECHOLN(selectedSlot.slot + '0');
+                SERIAL_ECHOLNRPGM(_n("Invalid extruder")); ////MSG_INVALID_EXTRUDER
+            } else {
+// @@TODO               if (code_seen('F')) {
+//                    next_feedrate = code_value();
+//                    if (next_feedrate > 0.0) {
+//                        feedrate = next_feedrate;
+//                    }
+//                }
+                SERIAL_ECHO_START;
+                SERIAL_ECHORPGM(_n("Active Extruder: ")); ////MSG_ACTIVE_EXTRUDER
+                SERIAL_ECHOLN(active_extruder + '0'); // this is not changed in our FW at all, can be optimized away
+            }
+        }
+    }
+}

+ 5 - 0
Firmware/Tcodes.h

@@ -0,0 +1,5 @@
+/// @file
+#pragma once
+#include <stdint.h>
+
+void TCodes(char * const strchr_pointer, uint8_t codeValue);

+ 3 - 3
Firmware/first_lay_cal.cpp

@@ -8,7 +8,7 @@
 #include "language.h"
 #include "Marlin.h"
 #include "cmdqueue.h"
-#include "mmu.h"
+#include "mmu2.h"
 #include <avr/pgmspace.h>
 
 //! @brief Wait for preheat
@@ -34,7 +34,7 @@ void lay1cal_wait_preheat()
 //! @param filament filament to use (applies for MMU only)
 void lay1cal_load_filament(char *cmd_buffer, uint8_t filament)
 {
-    if (mmu_enabled)
+    if (MMU2::mmu2.Enabled())
     {
         enquecommand_P(PSTR("M83"));
         enquecommand_P(PSTR("G1 Y-3.0 F1000.0"));
@@ -73,7 +73,7 @@ void lay1cal_intro_line()
         cmd_intro_mmu_12,
     };
 
-    if (mmu_enabled)
+    if (MMU2::mmu2.Enabled())
     {
         for (uint8_t i = 0; i < (sizeof(intro_mmu_cmd)/sizeof(intro_mmu_cmd[0])); ++i)
         {

+ 0 - 120
Firmware/mmu.h

@@ -1,120 +0,0 @@
-//! @file
-
-#ifndef MMU_H
-#define MMU_H
-
-#include <inttypes.h>
-#include "Timer.h"
-
-
-extern bool mmu_enabled;
-extern bool mmu_fil_loaded;
-
-extern uint8_t mmu_extruder;
-
-extern uint8_t tmp_extruder;
-
-extern int8_t mmu_finda;
-extern LongTimer mmu_last_finda_response;
-extern bool ir_sensor_detected;
-
-extern int16_t mmu_version;
-extern int16_t mmu_buildnr;
-
-extern uint16_t mmu_power_failures;
-
-#define MMU_FILAMENT_UNKNOWN 255
-
-#define MMU_NO_MOVE 0
-#define MMU_UNLOAD_MOVE 1
-#define MMU_LOAD_MOVE 2
-#define MMU_TCODE_MOVE 3
-
-#define MMU_LOAD_FEEDRATE 19.02f //mm/s
-#define MMU_LOAD_TIME_MS 2000 //should be fine tuned to load time for shortest allowed PTFE tubing and maximum loading speed
-
-enum class MmuCmd : uint_least8_t
-{
-    None,
-    T0,
-    T1,
-    T2,
-    T3,
-    T4,
-    L0,
-    L1,
-    L2,
-    L3,
-    L4,
-    C0,
-    U0,
-    E0,
-    E1,
-    E2,
-    E3,
-    E4,
-    K0,
-    K1,
-    K2,
-    K3,
-    K4,
-    R0,
-    S3,
-    W0, //!< Wait and signal load error
-};
-
-inline MmuCmd operator+ (MmuCmd cmd, uint8_t filament)
-{
-    return static_cast<MmuCmd>(static_cast<uint8_t>(cmd) + filament );
-}
-
-inline uint8_t operator- (MmuCmd cmda, MmuCmd cmdb)
-{
-    return (static_cast<uint8_t>(cmda) - static_cast<uint8_t>(cmdb));
-}
-
-extern int mmu_puts_P(const char* str);
-
-extern int mmu_printf_P(const char* format, ...);
-
-extern int8_t mmu_rx_ok(void);
-
-extern bool check_for_ir_sensor();
-
-extern void mmu_init(void);
-
-extern void mmu_loop(void);
-
-
-extern void mmu_reset(void);
-
-extern int8_t mmu_set_filament_type(uint8_t extruder, uint8_t filament);
-
-extern void mmu_command(MmuCmd cmd);
-
-extern bool mmu_get_response(uint8_t move = 0);
-
-extern void manage_response(bool move_axes, bool turn_off_nozzle, uint8_t move = MMU_NO_MOVE);
-
-extern void mmu_load_to_nozzle();
-
-extern void mmu_M600_load_filament(bool automatic, float nozzle_temp);
-extern void mmu_M600_wait_and_beep();
-
-extern void extr_adj(uint8_t extruder);
-extern void extr_unload();
-extern void load_all();
-
-extern bool mmu_check_version();
-extern void mmu_show_warning();
-extern void lcd_mmu_load_to_nozzle(uint8_t filament_nr);
-extern void mmu_eject_filament(uint8_t filament, bool recover);
-#ifdef MMU_HAS_CUTTER
-extern void mmu_cut_filament(uint8_t filament_nr);
-#endif //MMU_HAS_CUTTER
-extern void mmu_continue_loading(bool blocking);
-extern void mmu_filament_ramming();
-extern void mmu_wait_for_heater_blocking();
-extern void mmu_load_step(bool synchronize = true);
-
-#endif //MMU_H

+ 711 - 0
Firmware/mmu2.cpp

@@ -0,0 +1,711 @@
+#include "mmu2.h"
+#include "mmu2_fsensor.h"
+#include "mmu2_log.h"
+#include "mmu2_power.h"
+#include "mmu2_reporting.h"
+
+#include "Marlin.h"
+#include "stepper.h"
+#include "mmu2_error_converter.h"
+#include "mmu2_progress_converter.h"
+
+// @@TODO remove this and enable it in the configuration files
+// Settings for filament load / unload from the LCD menu.
+// This is for Prusa MK3-style extruders. Customize for your hardware.
+#define MMU2_FILAMENTCHANGE_EJECT_FEED 80.0
+#define MMU2_LOAD_TO_NOZZLE_SEQUENCE \
+    { 7.2, 562 },                    \
+        { 14.4, 871 },               \
+        { 36.0, 1393 },              \
+        { 14.4, 871 },               \
+    { 50.0, 198 }
+
+// @@TODO
+#define FILAMENT_MMU2_RAMMING_SEQUENCE { 7.2, 562 } 
+
+//@@TODO extract into configuration if it makes sense
+
+// 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
+static constexpr float MMU2_LOAD_TO_NOZZLE_LENGTH = 87.0F + 5.0F;
+
+// 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
+static constexpr float MMU2_TOOL_CHANGE_LOAD_LENGTH = 30.0F;
+
+static constexpr float MMU2_LOAD_TO_NOZZLE_FEED_RATE = 20.0F;
+static constexpr uint8_t MMU2_NO_TOOL = 99;
+static constexpr uint32_t MMU_BAUD = 115200;
+
+typedef uint16_t feedRate_t;
+
+struct E_Step {
+    float extrude;       ///< extrude distance in mm
+    feedRate_t feedRate; ///< feed rate in mm/s
+};
+
+static constexpr E_Step ramming_sequence[] PROGMEM = FILAMENT_MMU2_RAMMING_SEQUENCE;
+static constexpr E_Step load_to_nozzle_sequence[] PROGMEM = { MMU2_LOAD_TO_NOZZLE_SEQUENCE };
+
+namespace MMU2 {
+
+void execute_extruder_sequence(const E_Step *sequence, int steps);
+
+MMU2 mmu2;
+
+MMU2::MMU2()
+    : logic(&mmu2Serial)
+    , extruder(MMU2_NO_TOOL)
+    , resume_position()
+    , resume_hotend_temp(0)
+    , logicStepLastStatus(StepStatus::Finished)
+    , state(xState::Stopped)
+    , mmu_print_saved(false)
+    , loadFilamentStarted(false)
+    , loadingToNozzle(false)
+{
+}
+
+void MMU2::Start() {
+    mmu2Serial.begin(MMU_BAUD);
+
+    PowerOn();
+    mmu2Serial.flush(); // make sure the UART buffer is clear before starting communication
+
+    extruder = MMU2_NO_TOOL;
+    state = xState::Connecting;
+
+    // start the communication
+    logic.Start();
+}
+
+void MMU2::Stop() {
+    StopKeepPowered();
+    PowerOff();
+}
+
+void MMU2::StopKeepPowered(){
+    state = xState::Stopped;
+    logic.Stop();
+    mmu2Serial.close();
+}
+
+void MMU2::Reset(ResetForm level){
+    switch (level) {
+    case Software: ResetX0(); break;
+    case ResetPin: TriggerResetPin(); break;
+    case CutThePower: PowerCycle(); break;
+    default: break;
+    }
+}
+
+void MMU2::ResetX0() {
+    logic.ResetMMU(); // Send soft reset
+}
+
+void MMU2::TriggerResetPin(){
+    reset();
+}
+
+void MMU2::PowerCycle(){
+    // cut the power to the MMU and after a while restore it
+    PowerOff();
+    _delay(1000); //@@TODO
+    PowerOn();
+}
+
+void MMU2::PowerOff(){
+    power_off();
+}
+
+void MMU2::PowerOn(){
+    power_on();
+}
+
+void MMU2::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;
+
+    logicStepLastStatus = LogicStep(); // it looks like the mmu_loop doesn't need to be a blocking call
+    
+    avoidRecursion = false;
+}
+
+struct ReportingRAII {
+    CommandInProgress cip;
+    inline ReportingRAII(CommandInProgress cip):cip(cip){
+        BeginReport(cip, (uint16_t)ProgressCode::EngagingIdler);
+    }
+    inline ~ReportingRAII(){
+        EndReport(cip, (uint16_t)ProgressCode::OK);
+    }
+};
+
+bool MMU2::WaitForMMUReady(){
+    switch(State()){
+    case xState::Stopped:
+        return false;
+    case xState::Connecting:
+        // shall we wait until the MMU reconnects?
+        // fire-up a fsm_dlg and show "MMU not responding"?
+    default:
+        return true;
+    }
+}
+
+bool MMU2::tool_change(uint8_t index) {
+    if( ! WaitForMMUReady())
+        return false;
+
+    if (index != extruder) {
+        ReportingRAII rep(CommandInProgress::ToolChange);
+        BlockRunoutRAII blockRunout;
+
+        st_synchronize();
+
+        logic.ToolChange(index); // let the MMU pull the filament out and push a new one in
+        manage_response(false, false); // true, true);
+        
+        // reset current position to whatever the planner thinks it is
+        // @@TODO is there some "standard" way of doing this?
+//@@TODO        current_position[E_AXIS] = Planner::get_machine_position_mm()[3];
+
+        extruder = index; //filament change is finished
+        SetActiveExtruder(0);
+
+        // @@TODO really report onto the serial? May be for the Octoprint? Not important now
+        //        SERIAL_ECHO_START();
+        //        SERIAL_ECHOLNPAIR(MSG_ACTIVE_EXTRUDER, int(extruder));
+    }
+    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 MMU2::tool_change(const char *special) {
+    if( ! WaitForMMUReady())
+        return false;
+
+#if 0 //@@TODO
+    BlockRunoutRAII blockRunout;
+
+    switch (*special) {
+    case '?': {
+        uint8_t index = 0; // mmu2_choose_filament(); //@@TODO GUI - user selects
+        while (!thermalManager.wait_for_hotend(active_extruder, false))
+            safe_delay(100);
+        load_filament_to_nozzle(index);
+    } break;
+
+    case 'x': {
+        planner.synchronize();
+        uint8_t index = 0; //mmu2_choose_filament(); //@@TODO GUI - user selects
+        disable_E0();
+        logic.ToolChange(index);
+        manage_response(false, false); // true, true);
+
+        enable_E0();
+        extruder = index;
+        SetActiveExtruder(0);
+    } break;
+
+    case 'c': {
+        while (!thermalManager.wait_for_hotend(active_extruder, false))
+            safe_delay(100);
+        execute_extruder_sequence((const E_Step *)load_to_nozzle_sequence, COUNT(load_to_nozzle_sequence));
+    } break;
+    }
+
+#endif
+    return true;
+}
+
+uint8_t MMU2::get_current_tool() {
+    return extruder == MMU2_NO_TOOL ? -1 : extruder;
+}
+
+bool MMU2::set_filament_type(uint8_t index, uint8_t type) {
+    if( ! WaitForMMUReady())
+        return false;
+    
+    // @@TODO - this is not supported in the new MMU yet
+    // cmd_arg = filamentType;
+    // command(MMU_CMD_F0 + index);
+
+    manage_response(false, false); // true, true);
+    
+    return true;
+}
+
+bool MMU2::unload() {
+    if( ! WaitForMMUReady())
+        return false;
+
+    // @@TODO
+//    if (thermalManager.tooColdToExtrude(active_extruder)) {
+//        BUZZ(200, 404);
+//        LCD_ALERTMESSAGEPGM(MSG_HOTEND_TOO_COLD);
+//        return false;
+//    }
+
+    {
+        ReportingRAII rep(CommandInProgress::UnloadFilament);
+        filament_ramming();
+
+        logic.UnloadFilament();
+        manage_response(false, false); // false, true);
+
+//        BUZZ(200, 404);
+
+        // no active tool
+        extruder = MMU2_NO_TOOL;
+    }
+    return true;
+}
+
+bool MMU2::cut_filament(uint8_t index){
+    if( ! WaitForMMUReady())
+        return false;
+
+    ReportingRAII rep(CommandInProgress::CutFilament);
+    logic.CutFilament(index);
+    manage_response(false, false); // false, true);
+    
+    return true;
+}
+
+bool MMU2::load_filament(uint8_t index) {
+    if( ! WaitForMMUReady())
+        return false;
+
+    ReportingRAII rep(CommandInProgress::LoadFilament);
+    logic.LoadFilament(index);
+    manage_response(false, false);
+//    BUZZ(200, 404);
+    
+    return true;
+}
+
+struct LoadingToNozzleRAII {
+    MMU2 &mmu2;
+    inline LoadingToNozzleRAII(MMU2 &mmu2):mmu2(mmu2){
+        mmu2.loadingToNozzle = true;
+    }
+    inline ~LoadingToNozzleRAII(){
+        mmu2.loadingToNozzle = false;
+    }
+};
+
+bool MMU2::load_filament_to_nozzle(uint8_t index) {
+    if( ! WaitForMMUReady())
+        return false;
+
+    LoadingToNozzleRAII ln(*this);
+    
+    // if (0){ // @@TODO DEBUG 
+    
+    // @@TODO how is this supposed to be done in 8bit FW?
+/*    if (thermalManager.tooColdToExtrude(active_extruder)) {
+        BUZZ(200, 404);
+        LCD_ALERTMESSAGEPGM(MSG_HOTEND_TOO_COLD);
+        return false;
+    } else*/ {
+        // used for MMU-menu operation "Load to Nozzle"
+        ReportingRAII rep(CommandInProgress::ToolChange);
+        BlockRunoutRAII blockRunout;
+
+        if( extruder != MMU2_NO_TOOL ){ // we already have some filament loaded - free it + shape its tip properly
+            filament_ramming();
+        }
+        
+        logic.ToolChange(index);
+        manage_response(false, false); // true, true);
+
+        // reset current position to whatever the planner thinks it is
+        // @@TODO is there some "standard" way of doing this?
+//@@TODO        current_position[E_AXIS] = Planner::get_machine_position_mm()[3];
+
+        extruder = index;
+        SetActiveExtruder(0);
+
+//        BUZZ(200, 404);
+        return true;
+    }
+}
+
+bool MMU2::eject_filament(uint8_t index, bool recover) {
+    if( ! WaitForMMUReady())
+        return false;
+
+    //@@TODO
+//    if (thermalManager.tooColdToExtrude(active_extruder)) {
+//        BUZZ(200, 404);
+//        LCD_ALERTMESSAGEPGM(MSG_HOTEND_TOO_COLD);
+//        return false;
+//    }
+
+    ReportingRAII rep(CommandInProgress::EjectFilament);
+    current_position[E_AXIS] -= MMU2_FILAMENTCHANGE_EJECT_FEED;
+//@@TODO    line_to_current_position(2500 / 60);
+    st_synchronize();
+    logic.EjectFilament(index);
+    manage_response(false, false);
+
+    if (recover) {
+        //        LCD_MESSAGEPGM(MSG_MMU2_EJECT_RECOVER);
+//        BUZZ(200, 404);
+        
+//@@TODO        wait_for_user = true;
+        
+        //#if ENABLED(HOST_PROMPT_SUPPORT)
+        //        host_prompt_do(PROMPT_USER_CONTINUE, PSTR("MMU2 Eject Recover"), PSTR("Continue"));
+        //#endif
+        //#if ENABLED(EXTENSIBLE_UI)
+        //        ExtUI::onUserConfirmRequired_P(PSTR("MMU2 Eject Recover"));
+        //#endif
+        
+//@@TODO        while (wait_for_user) idle(true);
+        
+//        BUZZ(200, 404);
+//        BUZZ(200, 404);
+
+        // logic.Command(); //@@TODO command(MMU_CMD_R0);
+        manage_response(false, false);
+    }
+
+    //@@TODO ui.reset_status();
+
+    // no active tool
+    extruder = MMU2_NO_TOOL;
+
+//    BUZZ(200, 404);
+
+//    disable_E0();
+
+    return true;
+}
+
+void MMU2::Button(uint8_t index){
+    logic.Button(index);
+}
+
+void MMU2::Home(uint8_t mode){
+    logic.Home(mode);
+}
+
+void MMU2::SaveAndPark(bool move_axes, bool turn_off_nozzle) {
+//@@TODO    static constexpr xyz_pos_t park_point = NOZZLE_PARK_POINT_M600;
+//    if (!mmu_print_saved) { // First occurrence. Save current position, park print head, disable nozzle heater.
+//        LogEchoEvent("Saving and parking");
+//        st_synchronize();
+
+//        mmu_print_saved = true;
+
+//        resume_hotend_temp = thermalManager.degTargetHotend(active_extruder);
+//        resume_position = current_position;
+
+//        if (move_axes && all_axes_homed())
+//            nozzle.park(2, park_point);
+
+//        if (turn_off_nozzle){
+//            LogEchoEvent("Heater off");
+//            thermalManager.setTargetHotend(0, active_extruder);
+//        }
+//    }
+//    // keep the motors powered forever (until some other strategy is chosen)
+//    gcode.reset_stepper_timeout();
+}
+
+void MMU2::ResumeAndUnPark(bool move_axes, bool turn_off_nozzle) {
+    if (mmu_print_saved) {
+        LogEchoEvent("Resuming print");
+
+        if (turn_off_nozzle && resume_hotend_temp) {
+            MMU2_ECHO_MSG("Restoring hotend temperature ");
+            SERIAL_ECHOLN(resume_hotend_temp);
+//@@TODO            thermalManager.setTargetHotend(resume_hotend_temp, active_extruder);
+
+//            while (!thermalManager.wait_for_hotend(active_extruder, false)){
+//                safe_delay(1000);
+//            }
+            LogEchoEvent("Hotend temperature reached");
+        }
+
+//@@TODO        if (move_axes && all_axes_homed()) {
+//            LogEchoEvent("Resuming XYZ");
+
+//            // Move XY to starting position, then Z
+//            do_blocking_move_to_xy(resume_position, feedRate_t(NOZZLE_PARK_XY_FEEDRATE));
+
+//            // Move Z_AXIS to saved position
+//            do_blocking_move_to_z(resume_position.z, feedRate_t(NOZZLE_PARK_Z_FEEDRATE));
+//        } else {
+//            LogEchoEvent("NOT resuming XYZ");
+//        }
+    }
+}
+
+void MMU2::CheckUserInput(){
+    auto btn = ButtonPressed((uint16_t)lastErrorCode);
+    switch (btn) {
+    case Left:
+    case Middle:
+    case Right:
+        Button(btn);
+        break;
+    case RestartMMU:
+        Reset(CutThePower);
+        break;
+    case StopPrint:
+        // @@TODO not sure if we shall 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...
+void MMU2::manage_response(const bool move_axes, const bool turn_off_nozzle) {
+    mmu_print_saved = false;
+
+    KEEPALIVE_STATE(PAUSED_FOR_USER);
+
+    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 shall 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
+        
+        // @@TODO this needs verification - we need something which matches Marlin2's idle()
+        manage_inactivity(true); // calls LogicStep() and remembers its return status
+
+        switch (logicStepLastStatus) {
+        case Finished: 
+            // command/operation completed, let Marlin continue its work
+            // the E may have some more moves to finish - wait for them
+            st_synchronize(); 
+            return;
+        case VersionMismatch: // this basically means the MMU will be disabled until reconnected
+            return;
+        case CommunicationTimeout:
+        case CommandError:
+        case ProtocolError:
+            SaveAndPark(move_axes, turn_off_nozzle); // and wait for the user to resolve the problem
+            CheckUserInput();
+            break;
+        case CommunicationRecovered: // @@TODO communication recovered and may be an error recovered as well
+            // may be the logic layer can detect the change of state a respond with one "Recovered" to be handled here
+            ResumeAndUnPark(move_axes, turn_off_nozzle);
+            break;
+        case Processing: // wait for the MMU to respond
+        default:
+            break;
+        }
+    }
+}
+
+StepStatus MMU2::LogicStep() {
+    StepStatus ss = logic.Step();
+    switch (ss) {
+    case Finished:
+    case Processing:
+        OnMMUProgressMsg(logic.Progress());
+        break;
+    case CommandError:
+        ReportError(logic.Error());
+        break;
+    case CommunicationTimeout:
+        state = xState::Connecting;
+        ReportError(ErrorCode::MMU_NOT_RESPONDING);
+        break;
+    case ProtocolError:
+        state = xState::Connecting;
+        ReportError(ErrorCode::PROTOCOL_ERROR);
+        break;
+    case VersionMismatch:
+        StopKeepPowered();
+        ReportError(ErrorCode::VERSION_MISMATCH);
+        break;
+    default:
+        break;
+    }
+    
+    if( logic.Running() ){
+        state = xState::Active;
+    }
+    return ss;
+}
+
+void MMU2::filament_ramming() {
+    execute_extruder_sequence((const E_Step *)ramming_sequence, sizeof(ramming_sequence) / sizeof(E_Step));
+}
+
+void MMU2::execute_extruder_sequence(const E_Step *sequence, int steps) {
+
+    st_synchronize();
+
+    const E_Step *step = sequence;
+
+    for (uint8_t i = 0; i < steps; i++) {
+        const float es = pgm_read_float(&(step->extrude));
+        const feedRate_t fr_mm_m = pgm_read_float(&(step->feedRate));
+
+        //    DEBUG_ECHO_START();
+        //    DEBUG_ECHOLNPAIR("E step ", es, "/", fr_mm_m);
+
+        current_position[E_AXIS] += es;
+//        line_to_current_position(MMM_TO_MMS(fr_mm_m));
+        st_synchronize();
+
+        step++;
+    }
+
+//    disable_E0();
+}
+
+void MMU2::SetActiveExtruder(uint8_t ex){ 
+    active_extruder = ex; 
+}
+
+constexpr int strlen_constexpr(const char* str){
+    return *str ? 1 + strlen_constexpr(str + 1) : 0;
+}
+
+void MMU2::ReportError(ErrorCode ec) {
+    // 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)
+    // - may be 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
+    ReportErrorHook((CommandInProgress)logic.CommandInProgress(), (uint16_t)ec);
+
+    if( ec != lastErrorCode ){ // deduplicate: only report changes in error codes into the log
+        lastErrorCode = ec;
+
+        // Log error format: MMU2:E=32766 TextDescription
+        char msg[64];
+        snprintf(msg, sizeof(msg), "MMU2:E=%hu", (uint16_t)ec);
+        // Append a human readable form of the error code(s)
+        TranslateErr((uint16_t)ec, msg, sizeof(msg));
+
+        // beware - the prefix in the message ("MMU2") will get stripped by the logging subsystem
+        // and a correct MMU2 component will be assigned accordingly - see appmain.cpp
+        // Therefore I'm not calling MMU2_ERROR_MSG or MMU2_ECHO_MSG here
+        SERIAL_ECHO_START;
+        SERIAL_ECHOLN(msg);
+    }
+    
+    static_assert(mmu2Magic[0] == 'M' 
+        && mmu2Magic[1] == 'M' 
+        && mmu2Magic[2] == 'U' 
+        && mmu2Magic[3] == '2' 
+        && mmu2Magic[4] == ':' 
+        && strlen_constexpr(mmu2Magic) == 5, 
+        "MMU2 logging prefix mismatch, must be updated at various spots"
+    );
+}
+
+void MMU2::ReportProgress(ProgressCode pc) {
+    ReportProgressHook((CommandInProgress)logic.CommandInProgress(), (uint16_t)pc);
+    
+    // Log progress - example: MMU2:P=123 EngageIdler
+    char msg[64];
+    snprintf(msg, sizeof(msg), "MMU2:P=%hu", (uint16_t)pc);
+    // Append a human readable form of the progress code
+    TranslateProgress((uint16_t)pc, msg, sizeof(msg));
+    
+    SERIAL_ECHO_START;
+    SERIAL_ECHOLN(msg);
+}
+
+void MMU2::OnMMUProgressMsg(ProgressCode pc){
+    if( pc != lastProgressCode){
+        ReportProgress(pc);
+        lastProgressCode = pc;
+
+        // Act accordingly - one-time handling
+        switch(pc){
+        case ProgressCode::FeedingToBondtech:
+            // prepare for the movement of the E-motor
+            st_synchronize();
+//@@TODO            sync_plan_position();
+//            enable_E0();
+            loadFilamentStarted = true;
+            break;
+        default:
+            // do nothing yet
+            break;
+        }
+    } else {
+        // Act accordingly - every status change (even the same state)
+        switch(pc){
+        case ProgressCode::FeedingToBondtech:
+            if( WhereIsFilament() == FilamentState::AT_FSENSOR && loadFilamentStarted){// fsensor triggered, move the extruder to help loading
+                // rotate the extruder motor - no planner sync, just add more moves - as long as they are roughly at the same speed as the MMU is pushing,
+                // it really doesn't matter
+                // char tmp[64]; // @@TODO this shouldn't be needed anymore, but kept here in case of something strange
+                //               // happens in Marlin again
+                // snprintf(tmp,sizeof (tmp), "E moveTo=%4.1f f=%4.0f s=%hu\n", current_position.e, feedrate_mm_s, feedrate_percentage);
+                // MMU2_ECHO_MSG(tmp);
+
+                // Ideally we'd use:
+                // line_to_current_position(MMU2_LOAD_TO_NOZZLE_FEED_RATE); 
+                // However, as it ignores MBL completely (which I don't care about in case of E-movement), 
+                // we need to take the raw Z coordinates and only add some movement to E
+                // otherwise we risk planning a very short Z move with an extremely long E-move, 
+                // which obviously ends up in a disaster (over/underflow of E/Z steps).
+                // The problem becomes obvious in Planner::_populate_block when computing da, db, dc like this:
+                //   const int32_t da = target.a - position.a, db = target.b - position.b, dc = target.c - position.c;
+                // And since current_position[3] != position_float[3], we get a tiny move in Z, which is something I really want to avoid here
+                // @@TODO is there a "standard" way of doing this?
+//@@TODO                xyze_pos_t tgt = Planner::get_machine_position_mm();
+                const float e = loadingToNozzle ? MMU2_LOAD_TO_NOZZLE_LENGTH : MMU2_TOOL_CHANGE_LOAD_LENGTH;
+//@@TODO                tgt[3] += e / planner.e_factor[active_extruder];
+//                plan_buffer_line(tgt, MMU2_LOAD_TO_NOZZLE_FEED_RATE, active_extruder); // @@TODO magic constant - must match the feedrate of the MMU
+                loadFilamentStarted = false;
+            }
+            break;
+        default:
+            // do nothing yet
+            break;
+        }
+    }
+}
+
+void MMU2::LogErrorEvent(const char *msg){
+    MMU2_ERROR_MSG(msg);
+    SERIAL_ECHOLN();
+}
+
+void MMU2::LogEchoEvent(const char *msg){
+    MMU2_ECHO_MSG(msg);
+    SERIAL_ECHOLN();
+}
+
+} // namespace MMU2

+ 204 - 0
Firmware/mmu2.h

@@ -0,0 +1,204 @@
+/// @file
+#pragma once
+#include "mmu2_protocol_logic.h"
+
+struct E_Step;
+
+namespace MMU2 {
+
+struct xyz_pos_t {
+    uint16_t xyz[3]; // @@TODO
+    xyz_pos_t()=default;
+};
+
+// general MMU setup for MK3
+enum : uint8_t {
+    FILAMENT_UNKNOWN = 0xffU
+};
+
+/// Top-level interface between Logic and Marlin.
+/// Intentionally named MMU2 to be (almost) a drop-in replacement for the previous implementation.
+/// Most of the public methods share the original naming convention as well.
+class MMU2 {
+public:
+    MMU2();
+    
+    /// 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();
+    
+    /// States of a printer with the MMU:
+    /// - Active
+    /// - Connecting
+    /// - Stopped
+    /// 
+    /// When the printer's FW starts, the MMU2 mode is either Stopped or NotResponding (based on user's preference).
+    /// When the MMU successfully establishes communication, the state changes to Active.
+    enum class xState : uint_fast8_t {
+        Active, ///< MMU has been detected, connected, communicates and is ready to be worked with.
+        Connecting, ///< MMU is connected but it doesn't communicate (yet). The user wants the MMU, but it is not ready to be worked with.
+        Stopped ///< The user doesn't want the printer to work with the MMU. The MMU itself is not powered and does not work at all.
+    };
+    
+    inline xState State() const { return state; }
+    
+    // @@TODO temporary wrappers to make old gcc survive the code
+    inline bool Enabled()const { return State() == xState::Active; }
+
+    /// 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)
+    };
+    
+    /// 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();
+
+
+    /// 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 index of the slot to be selected
+    /// @returns false if the operation cannot be performed (Stopped)
+    bool tool_change(uint8_t index);
+    
+    /// Handling of special Tx, Tc, T? commands
+    bool tool_change(const char *special);
+
+    /// Unload of filament in collaboration with the MMU.
+    /// That includes rotating the printer's extruder in order to release filament.
+    /// @returns 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)
+    /// @returns false if the operation cannot be performed (Stopped)
+    bool load_filament(uint8_t index);
+    
+    /// Load (push) filament from the MMU into the printer's nozzle
+    /// @returns false if the operation cannot be performed (Stopped or cold extruder)
+    bool load_filament_to_nozzle(uint8_t index);
+
+    /// 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 index, bool recover);
+
+    /// Issue a Cut command into the MMU
+    /// Requires unloaded filament from the printer (obviously)
+    /// @returns false if the operation cannot be performed (Stopped)
+    bool cut_filament(uint8_t index);
+    
+    /// @returns the active filament slot index (0-4) or 0xff in case of no active tool
+    uint8_t get_current_tool();
+    
+    bool set_filament_type(uint8_t index, 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);
+
+    /// @returns current state of FINDA (true=filament present, false=filament not present)
+    inline bool FindaDetectsFilament()const { return logic.FindaPressed(); }
+    
+private:
+    /// Perform software self-reset of the MMU (sends an X0 command)
+    void ResetX0();
+    
+    /// 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
+    void manage_response(const bool move_axes, const bool turn_off_nozzle);
+    
+    /// 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
+    StepStatus LogicStep();
+    
+    void filament_ramming();
+    void execute_extruder_sequence(const E_Step *sequence, int steps);
+    void SetActiveExtruder(uint8_t ex);
+
+    /// Reports an error into attached ExtUIs
+    /// @param ec error code, see ErrorCode
+    void ReportError(ErrorCode ec);
+
+    /// 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
+    void OnMMUProgressMsg(ProgressCode pc);
+    
+    /// Report the msg into the general logging subsystem (through Marlin's SERIAL_ECHO stuff)
+    void LogErrorEvent(const char *msg);
+    
+    /// Report the msg into the general logging subsystem (through Marlin's SERIAL_ECHO stuff)
+    void LogEchoEvent(const char *msg);
+
+    /// Save print and park the print head
+    void SaveAndPark(bool move_axes, bool turn_off_nozzle);
+
+    /// Resume print (unpark, turn on heating etc.)
+    void ResumeAndUnPark(bool move_axes, bool turn_off_nozzle);
+
+    /// Check for any button/user input coming from the printer's UI
+    void CheckUserInput();
+    
+    /// 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.
+    /// @returns false if the MMU is not ready to perform the command (for whatever reason)
+    bool WaitForMMUReady();
+    
+    ProtocolLogic logic; ///< implementation of the protocol logic layer
+    int extruder; ///< currently active slot in the MMU ... somewhat... not sure where to get it from yet
+
+    xyz_pos_t resume_position;
+    int16_t resume_hotend_temp;
+    
+    ProgressCode lastProgressCode = ProgressCode::OK;
+    ErrorCode lastErrorCode = ErrorCode::MMU_NOT_RESPONDING;
+
+    StepStatus logicStepLastStatus;
+    
+    enum xState state;
+
+    bool mmu_print_saved;
+    bool loadFilamentStarted;
+    
+    friend struct LoadingToNozzleRAII;
+    /// true in case we are doing the LoadToNozzle operation - that means the filament shall be loaded all the way down to the nozzle
+    /// unlike the mid-print ToolChange commands, which only load the first ~30mm and then the G-code takes over.
+    bool loadingToNozzle;
+};
+
+/// 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 MMU2 mmu2;
+
+} // namespace MMU2

+ 97 - 0
Firmware/mmu2/error_codes.h

@@ -0,0 +1,97 @@
+/// @file error_codes.h
+#pragma once
+#include <stdint.h>
+
+/// A complete set of error codes which may be a result of a high-level command/operation.
+/// This header file shall be included in the printer's firmware as well as a reference,
+/// therefore the error codes have been extracted to one place.
+///
+/// Please note the errors are intentionally coded as "negative" values (highest bit set),
+/// becase they are a complement to reporting the state of the high-level state machines -
+/// positive values are considered as normal progress, negative values are errors.
+///
+/// Please note, that multiple TMC errors can occur at once, thus they are defined as a bitmask of the higher byte.
+/// Also, as there are 3 TMC drivers on the board, each error is added a bit for the corresponding TMC -
+/// TMC_PULLEY_BIT, TMC_SELECTOR_BIT, TMC_IDLER_BIT,
+/// The resulting error is a bitwise OR over 3 TMC drivers and their status, which should cover most of the situations correctly.
+enum class ErrorCode : uint_fast16_t {
+    RUNNING = 0x0000, ///< the operation is still running - keep this value as ZERO as it is used for initialization of error codes as well
+    OK = 0x0001, ///< the operation finished OK
+
+    // TMC bit masks
+    TMC_PULLEY_BIT = 0x0040, ///< +64 TMC Pulley bit
+    TMC_SELECTOR_BIT = 0x0080, ///< +128 TMC Pulley bit
+    TMC_IDLER_BIT = 0x0100, ///< +256 TMC Pulley bit
+
+    /// Unload Filament related error codes
+    FINDA_DIDNT_SWITCH_ON = 0x8001, ///< E32769 FINDA didn't switch on while loading filament - either there is something blocking the metal ball or a cable is broken/disconnected
+    FINDA_DIDNT_SWITCH_OFF = 0x8002, ///< E32770 FINDA didn't switch off while unloading filament
+
+    FSENSOR_DIDNT_SWITCH_ON = 0x8003, ///< E32771 Filament sensor didn't switch on while performing LoadFilament
+    FSENSOR_DIDNT_SWITCH_OFF = 0x8004, ///< E32772 Filament sensor didn't switch off while performing UnloadFilament
+
+    FILAMENT_ALREADY_LOADED = 0x8005, ///< E32773 cannot perform operation LoadFilament or move the selector as the filament is already loaded
+
+    INVALID_TOOL = 0x8006, ///< E32774 tool/slot index out of range (typically issuing T5 into an MMU with just 5 slots - valid range 0-4)
+
+    HOMING_FAILED = 0x8007, ///< generic homing failed error - always reported with the corresponding axis bit set (Idler or Selector) as follows:
+    HOMING_SELECTOR_FAILED = HOMING_FAILED | TMC_SELECTOR_BIT, ///< E32903 the Selector was unable to home properly - that means something is blocking its movement
+    HOMING_IDLER_FAILED = HOMING_FAILED | TMC_IDLER_BIT, ///< E33031 the Idler was unable to home properly - that means something is blocking its movement
+    STALLED_PULLEY = HOMING_FAILED | TMC_PULLEY_BIT, ///< E32839 for the Pulley "homing" means just stallguard detected during Pulley's operation (Pulley doesn't home)
+
+    QUEUE_FULL = 0x802b, ///< E32811 internal logic error - attempt to move with a full queue
+
+    VERSION_MISMATCH = 0x802c, ///< E32812 internal error of the printer - incompatible version of the MMU FW
+    PROTOCOL_ERROR = 0x802d, ///< E32813 internal error of the printer - communication with the MMU got garbled - protocol decoder couldn't decode the incoming messages
+    MMU_NOT_RESPONDING = 0x802e, ///< E32814 internal error of the printer - communication with the MMU is not working
+    INTERNAL = 0x802f, ///< E32815 internal runtime error (software)
+
+    /// TMC driver init error - TMC dead or bad communication
+    /// - E33344 Pulley TMC driver
+    /// - E33404 Selector TMC driver
+    /// - E33536 Idler TMC driver
+    /// - E33728 All 3 TMC driver
+    TMC_IOIN_MISMATCH = 0x8200,
+
+    /// TMC driver reset - recoverable, we just need to rehome the axis
+    /// Idler: can be rehomed any time
+    /// Selector: if there is a filament, remove it and rehome, if there is no filament, just rehome
+    /// Pulley: do nothing - for the loading sequence - just restart and move slowly, for the unload sequence just restart
+    /// - E33856 Pulley TMC driver
+    /// - E33920 Selector TMC driver
+    /// - E34048 Idler TMC driver
+    /// - E34240 All 3 TMC driver
+    TMC_RESET = 0x8400,
+
+    /// not enough current for the TMC, NOT RECOVERABLE
+    /// - E34880 Pulley TMC driver
+    /// - E34944 Selector TMC driver
+    /// - E35072 Idler TMC driver
+    /// - E35264 All 3 TMC driver
+    TMC_UNDERVOLTAGE_ON_CHARGE_PUMP = 0x8800,
+
+    /// TMC driver serious error - short to ground on coil A or coil B - dangerous to recover
+    /// - E36928 Pulley TMC driver
+    /// - E36992 Selector TMC driver
+    /// - E37120 Idler TMC driver
+    /// - E37312 All 3 TMC driver
+    TMC_SHORT_TO_GROUND = 0x9000,
+
+    /// TMC driver over temperature warning - can be recovered by restarting the driver.
+    /// If this error happens, we should probably go into the error state as soon as the current command is finished.
+    /// The driver technically still works at this point.
+    /// - E41024 Pulley TMC driver
+    /// - E41088 Selector TMC driver
+    /// - E41216 Idler TMC driver
+    /// - E41408 All 3 TMC driver
+    TMC_OVER_TEMPERATURE_WARN = 0xA000,
+
+    /// TMC driver over temperature error - we really shouldn't ever reach this error.
+    /// It can still be recovered if the driver cools down below 120C.
+    /// The driver needs to be disabled and enabled again for operation to resume after this error is cleared.
+    /// - E49216 Pulley TMC driver
+    /// - E49280 Selector TMC driver
+    /// - E49408 Idler TMC driver
+    /// - E49600 All 3 TMC driver
+    TMC_OVER_TEMPERATURE_ERROR = 0xC000
+};

+ 42 - 0
Firmware/mmu2/progress_codes.h

@@ -0,0 +1,42 @@
+/// @file progress_codes.h
+#pragma once
+#include <stdint.h>
+
+/// A complete set of progress codes which may be reported while running a high-level command/operation
+/// This header file shall be included in the printer's firmware as well as a reference,
+/// therefore the progress codes have been extracted to one place
+enum class ProgressCode : uint_fast8_t {
+    OK = 0, ///< finished ok
+
+    EngagingIdler, // P1
+    DisengagingIdler, // P2
+    UnloadingToFinda, // P3
+    UnloadingToPulley, //P4
+    FeedingToFinda, // P5
+    FeedingToBondtech, // P6
+    FeedingToNozzle, // P7
+    AvoidingGrind, // P8
+    FinishingMoves, // P9
+
+    ERRDisengagingIdler, // P10
+    ERREngagingIdler, // P11
+    ERRWaitingForUser, // P12
+    ERRInternal, // P13
+    ERRHelpingFilament, // P14
+    ERRTMCFailed, // P15
+
+    UnloadingFilament, // P16
+    LoadingFilament, // P17
+    SelectingFilamentSlot, // P18
+    PreparingBlade, // P19
+    PushingFilament, // P20
+    PerformingCut, // P21
+    ReturningSelector, // P22
+    ParkingSelector, // P23
+    EjectingFilament, // P24
+    RetractingFromFinda, // P25
+
+    Homing,
+
+    Empty = 0xff // dummy empty state
+};

+ 6 - 0
Firmware/mmu2_error_converter.cpp

@@ -0,0 +1,6 @@
+#include "mmu2_error_converter.h"
+
+namespace MMU2 {
+// @@TODO
+void TranslateErr(uint16_t ec, char *dst, size_t dstSize) { }
+}

+ 7 - 0
Firmware/mmu2_error_converter.h

@@ -0,0 +1,7 @@
+#pragma once
+#include <stdint.h>
+#include <stddef.h>
+
+namespace MMU2 {
+void TranslateErr(uint16_t ec, char *dst, size_t dstSize);
+}

+ 14 - 0
Firmware/mmu2_fsensor.cpp

@@ -0,0 +1,14 @@
+#include "mmu2_fsensor.h"
+
+namespace MMU2 {
+
+FilamentState WhereIsFilament(){
+    // @@TODO
+    return FilamentState::IN_NOZZLE;
+}
+
+// on AVR this does nothing
+BlockRunoutRAII::BlockRunoutRAII() { }
+BlockRunoutRAII::~BlockRunoutRAII() { }
+
+} // namespace MMU2

+ 24 - 0
Firmware/mmu2_fsensor.h

@@ -0,0 +1,24 @@
+#pragma once
+#include <stdint.h>
+
+namespace MMU2 {
+
+/// Possible states of filament from the perspective of presence in various parts of the printer
+/// Beware, the numeric codes are important and sent into the MMU
+enum class FilamentState : uint_fast8_t {
+    NOT_PRESENT = 0, ///< filament sensor doesn't see the filament
+    AT_FSENSOR = 1, ///< filament detected by the filament sensor, but the nozzle has not detected the filament yet
+    IN_NOZZLE = 2 ///< filament detected by the filament sensor and also loaded in the nozzle
+};
+
+FilamentState WhereIsFilament();
+
+/// Can be used to block printer's filament sensor handling - to avoid errorneous injecting of M600
+/// while doing a toolchange with the MMU
+class BlockRunoutRAII {
+public:
+    BlockRunoutRAII();
+    ~BlockRunoutRAII();
+};
+
+} // namespace MMU2

+ 23 - 0
Firmware/mmu2_log.h

@@ -0,0 +1,23 @@
+#pragma once
+
+#ifndef UNITTEST
+#include "Marlin.h"
+
+// Beware - before changing this prefix, think twice
+// you'd need to change appmain.cpp app_marlin_serial_output_write_hook
+// and MMU2::ReportError + MMU2::ReportProgress
+static constexpr char mmu2Magic[] PROGMEM = "MMU2:";
+
+#define SERIAL_MMU2() { serialprintPGM(mmu2Magic); }
+
+#define MMU2_ECHO_MSG(S) do{ SERIAL_ECHO_START; SERIAL_MMU2(); SERIAL_ECHO(S); }while(0)
+#define MMU2_ERROR_MSG(S) do{ SERIAL_ERROR_START; SERIAL_MMU2(); SERIAL_ECHO(S); }while(0)
+
+#else // #ifndef UNITTEST
+
+#define MMU2_ECHO_MSG(S) /* */
+#define MMU2_ERROR_MSG(S) /* */
+#define SERIAL_ECHO(S) /* */
+#define SERIAL_ECHOLN(S) /* */
+
+#endif // #ifndef UNITTEST

+ 12 - 0
Firmware/mmu2_power.cpp

@@ -0,0 +1,12 @@
+#include "mmu2_power.h"
+
+namespace MMU2 {
+
+// sadly, on MK3 we cannot do this on HW
+void power_on() { }
+
+void power_off() { }
+
+void reset() { }
+
+} // namespace MMU2

+ 11 - 0
Firmware/mmu2_power.h

@@ -0,0 +1,11 @@
+#pragma once
+
+namespace MMU2 {
+
+void power_on();
+
+void power_off();
+
+void reset();
+
+} // namespace MMU2

+ 6 - 0
Firmware/mmu2_progress_converter.cpp

@@ -0,0 +1,6 @@
+#include "mmu2_progress_converter.h"
+
+namespace MMU2 {
+//@@TODO
+void TranslateProgress(uint16_t pc, char *dst, size_t dstSize) { }
+}

+ 7 - 0
Firmware/mmu2_progress_converter.h

@@ -0,0 +1,7 @@
+#pragma once
+#include <stdint.h>
+#include <stddef.h>
+
+namespace MMU2 {
+void TranslateProgress(uint16_t pc, char *dst, size_t dstSize);
+}

+ 247 - 0
Firmware/mmu2_protocol.cpp

@@ -0,0 +1,247 @@
+/// @file
+#include "mmu2_protocol.h"
+
+// protocol definition
+// command: Q0
+// meaning: query operation status
+// Query/command: query
+// Expected reply from the MMU:
+//  any of the running operation statuses: OID: [T|L|U|E|C|W|K][0-4]
+//  <OID> P[0-9]      : command being processed i.e. operation running, may contain a state number
+//  <OID> E[0-9][0-9] : error 1-9 while doing a tool change
+//  <OID> F           : operation finished - will be repeated to "Q" messages until a new command is issued
+
+namespace modules {
+namespace protocol {
+
+// decoding automaton
+// states:  input -> transition into state
+// Code      QTLMUXPSBEWK -> msgcode
+//           \n ->start
+//           * ->error
+// error     \n ->start
+//           * ->error
+// msgcode   0-9 ->msgvalue
+//           * ->error
+// msgvalue  0-9 ->msgvalue
+//           \n ->start successfully accepted command
+
+DecodeStatus Protocol::DecodeRequest(uint8_t c) {
+    switch (rqState) {
+    case RequestStates::Code:
+        switch (c) {
+        case 'Q':
+        case 'T':
+        case 'L':
+        case 'M':
+        case 'U':
+        case 'X':
+        case 'P':
+        case 'S':
+        case 'B':
+        case 'E':
+        case 'W':
+        case 'K':
+        case 'F':
+        case 'f':
+        case 'H':
+            requestMsg.code = (RequestMsgCodes)c;
+            requestMsg.value = 0;
+            rqState = RequestStates::Value;
+            return DecodeStatus::NeedMoreData;
+        default:
+            requestMsg.code = RequestMsgCodes::unknown;
+            rqState = RequestStates::Error;
+            return DecodeStatus::Error;
+        }
+    case RequestStates::Value:
+        if (IsDigit(c)) {
+            requestMsg.value *= 10;
+            requestMsg.value += c - '0';
+            return DecodeStatus::NeedMoreData;
+        } else if (IsNewLine(c)) {
+            rqState = RequestStates::Code;
+            return DecodeStatus::MessageCompleted;
+        } else {
+            requestMsg.code = RequestMsgCodes::unknown;
+            rqState = RequestStates::Error;
+            return DecodeStatus::Error;
+        }
+    default: //case error:
+        if (IsNewLine(c)) {
+            rqState = RequestStates::Code;
+            return DecodeStatus::MessageCompleted;
+        } else {
+            requestMsg.code = RequestMsgCodes::unknown;
+            rqState = RequestStates::Error;
+            return DecodeStatus::Error;
+        }
+    }
+}
+
+uint8_t Protocol::EncodeRequest(const RequestMsg &msg, uint8_t *txbuff) {
+    constexpr uint8_t reqSize = 3;
+    txbuff[0] = (uint8_t)msg.code;
+    txbuff[1] = msg.value + '0';
+    txbuff[2] = '\n';
+    return reqSize;
+    static_assert(reqSize <= MaxRequestSize(), "Request message length exceeded the maximum size, increase the magic constant in MaxRequestSize()");
+}
+
+DecodeStatus Protocol::DecodeResponse(uint8_t c) {
+    switch (rspState) {
+    case ResponseStates::RequestCode:
+        switch (c) {
+        case 'Q':
+        case 'T':
+        case 'L':
+        case 'M':
+        case 'U':
+        case 'X':
+        case 'P':
+        case 'S':
+        case 'B':
+        case 'E':
+        case 'W':
+        case 'K':
+        case 'F':
+        case 'f':
+        case 'H':
+            responseMsg.request.code = (RequestMsgCodes)c;
+            responseMsg.request.value = 0;
+            rspState = ResponseStates::RequestValue;
+            return DecodeStatus::NeedMoreData;
+        case 0x0a:
+        case 0x0d:
+            // skip leading whitespace if any (makes integration with other SW easier/tolerant)
+            return DecodeStatus::NeedMoreData;
+        default:
+            rspState = ResponseStates::Error;
+            return DecodeStatus::Error;
+        }
+    case ResponseStates::RequestValue:
+        if (IsDigit(c)) {
+            responseMsg.request.value *= 10;
+            responseMsg.request.value += c - '0';
+            return DecodeStatus::NeedMoreData;
+        } else if (c == ' ') {
+            rspState = ResponseStates::ParamCode;
+            return DecodeStatus::NeedMoreData;
+        } else {
+            rspState = ResponseStates::Error;
+            return DecodeStatus::Error;
+        }
+    case ResponseStates::ParamCode:
+        switch (c) {
+        case 'P':
+        case 'E':
+        case 'F':
+        case 'A':
+        case 'R':
+            rspState = ResponseStates::ParamValue;
+            responseMsg.paramCode = (ResponseMsgParamCodes)c;
+            responseMsg.paramValue = 0;
+            return DecodeStatus::NeedMoreData;
+        default:
+            responseMsg.paramCode = ResponseMsgParamCodes::unknown;
+            rspState = ResponseStates::Error;
+            return DecodeStatus::Error;
+        }
+    case ResponseStates::ParamValue:
+        if (IsDigit(c)) {
+            responseMsg.paramValue *= 10;
+            responseMsg.paramValue += c - '0';
+            return DecodeStatus::NeedMoreData;
+        } else if (IsNewLine(c)) {
+            rspState = ResponseStates::RequestCode;
+            return DecodeStatus::MessageCompleted;
+        } else {
+            responseMsg.paramCode = ResponseMsgParamCodes::unknown;
+            rspState = ResponseStates::Error;
+            return DecodeStatus::Error;
+        }
+    default: //case error:
+        if (IsNewLine(c)) {
+            rspState = ResponseStates::RequestCode;
+            return DecodeStatus::MessageCompleted;
+        } else {
+            responseMsg.paramCode = ResponseMsgParamCodes::unknown;
+            return DecodeStatus::Error;
+        }
+    }
+}
+
+uint8_t Protocol::EncodeResponseCmdAR(const RequestMsg &msg, ResponseMsgParamCodes ar, uint8_t *txbuff) {
+    txbuff[0] = (uint8_t)msg.code;
+    txbuff[1] = msg.value + '0';
+    txbuff[2] = ' ';
+    txbuff[3] = (uint8_t)ar;
+    txbuff[4] = '\n';
+    return 5;
+}
+
+uint8_t Protocol::EncodeResponseReadFINDA(const RequestMsg &msg, uint8_t findaValue, uint8_t *txbuff) {
+    txbuff[0] = (uint8_t)msg.code;
+    txbuff[1] = msg.value + '0';
+    txbuff[2] = ' ';
+    txbuff[3] = (uint8_t)ResponseMsgParamCodes::Accepted;
+    txbuff[4] = findaValue + '0';
+    txbuff[5] = '\n';
+    return 6;
+}
+
+uint8_t Protocol::EncodeResponseVersion(const RequestMsg &msg, uint8_t value, uint8_t *txbuff) {
+    txbuff[0] = (uint8_t)msg.code;
+    txbuff[1] = msg.value + '0';
+    txbuff[2] = ' ';
+    txbuff[3] = (uint8_t)ResponseMsgParamCodes::Accepted;
+    uint8_t *dst = txbuff + 4;
+    if (value < 10) {
+        *dst++ = value + '0';
+    } else if (value < 100) {
+        *dst++ = value / 10 + '0';
+        *dst++ = value % 10 + '0';
+    } else {
+        *dst++ = value / 100 + '0';
+        *dst++ = (value / 10) % 10 + '0';
+        *dst++ = value % 10 + '0';
+    }
+    *dst = '\n';
+    return dst - txbuff + 1;
+}
+
+uint8_t Protocol::EncodeResponseQueryOperation(const RequestMsg &msg, ResponseMsgParamCodes code, uint16_t value, uint8_t *txbuff) {
+    txbuff[0] = (uint8_t)msg.code;
+    txbuff[1] = msg.value + '0';
+    txbuff[2] = ' ';
+    txbuff[3] = (uint8_t)code;
+    uint8_t *dst = txbuff + 4;
+    if (code != ResponseMsgParamCodes::Finished) {
+        if (value < 10) {
+            *dst++ = value + '0';
+        } else if (value < 100) {
+            *dst++ = value / 10 + '0';
+            *dst++ = value % 10 + '0';
+        } else if (value < 1000) {
+            *dst++ = value / 100 + '0';
+            *dst++ = (value / 10) % 10 + '0';
+            *dst++ = value % 10 + '0';
+        } else if (value < 10000) {
+            *dst++ = value / 1000 + '0';
+            *dst++ = (value / 100) % 10 + '0';
+            *dst++ = (value / 10) % 10 + '0';
+            *dst++ = value % 10 + '0';
+        } else {
+            *dst++ = value / 10000 + '0';
+            *dst++ = (value / 1000) % 10 + '0';
+            *dst++ = (value / 100) % 10 + '0';
+            *dst++ = (value / 10) % 10 + '0';
+            *dst++ = value % 10 + '0';
+        }
+    }
+    *dst = '\n';
+    return dst - txbuff + 1;
+}
+
+} // namespace protocol
+} // namespace modules

+ 184 - 0
Firmware/mmu2_protocol.h

@@ -0,0 +1,184 @@
+/// @file protocol.h
+#pragma once
+#include <stdint.h>
+
+namespace modules {
+
+/// @brief The MMU communication protocol implementation and related stuff.
+///
+/// See description of the new protocol in the MMU 2021 doc
+/// @@TODO possibly add some checksum to verify the correctness of messages
+namespace protocol {
+
+/// Definition of request message codes
+enum class RequestMsgCodes : uint8_t {
+    unknown = 0,
+    Query = 'Q',
+    Tool = 'T',
+    Load = 'L',
+    Mode = 'M',
+    Unload = 'U',
+    Reset = 'X',
+    Finda = 'P',
+    Version = 'S',
+    Button = 'B',
+    Eject = 'E',
+    Wait = 'W',
+    Cut = 'K',
+    FilamentType = 'F',
+    FilamentSensor = 'f',
+    Home = 'H'
+};
+
+/// Definition of response message parameter codes
+enum class ResponseMsgParamCodes : uint8_t {
+    unknown = 0,
+    Processing = 'P',
+    Error = 'E',
+    Finished = 'F',
+    Accepted = 'A',
+    Rejected = 'R'
+};
+
+/// A request message - requests are being sent by the printer into the MMU.
+struct RequestMsg {
+    RequestMsgCodes code; ///< code of the request message
+    uint8_t value; ///< value of the request message
+
+    /// @param code of the request message
+    /// @param value of the request message
+    inline RequestMsg(RequestMsgCodes code, uint8_t value)
+        : code(code)
+        , value(value) {}
+};
+
+/// A response message - responses are being sent from the MMU into the printer as a response to a request message.
+struct ResponseMsg {
+    RequestMsg request; ///< response is always preceeded by the request message
+    ResponseMsgParamCodes paramCode; ///< code of the parameter
+    uint16_t paramValue; ///< value of the parameter
+
+    /// @param request the source request message this response is a reply to
+    /// @param paramCode code of the parameter
+    /// @param paramValue value of the parameter
+    inline ResponseMsg(RequestMsg request, ResponseMsgParamCodes paramCode, uint16_t paramValue)
+        : request(request)
+        , paramCode(paramCode)
+        , paramValue(paramValue) {}
+};
+
+/// Message decoding return values
+enum class DecodeStatus : uint_fast8_t {
+    MessageCompleted, ///< message completed and successfully lexed
+    NeedMoreData, ///< message incomplete yet, waiting for another byte to come
+    Error, ///< input character broke message decoding
+};
+
+/// Protocol class is responsible for creating/decoding messages in Rx/Tx buffer
+///
+/// Beware - in the decoding more, it is meant to be a statefull instance which works through public methods
+/// processing one input byte per call.
+class Protocol {
+public:
+    inline Protocol()
+        : rqState(RequestStates::Code)
+        , requestMsg(RequestMsgCodes::unknown, 0)
+        , rspState(ResponseStates::RequestCode)
+        , responseMsg(RequestMsg(RequestMsgCodes::unknown, 0), ResponseMsgParamCodes::unknown, 0) {
+    }
+
+    /// Takes the input byte c and steps one step through the state machine
+    /// @returns state of the message being decoded
+    DecodeStatus DecodeRequest(uint8_t c);
+
+    /// Decodes response message in rxbuff
+    /// @returns decoded response message structure
+    DecodeStatus DecodeResponse(uint8_t c);
+
+    /// Encodes request message msg into txbuff memory
+    /// It is expected the txbuff is large enough to fit the message
+    /// @returns number of bytes written into txbuff
+    static uint8_t EncodeRequest(const RequestMsg &msg, uint8_t *txbuff);
+
+    /// @returns the maximum byte length necessary to encode a request message
+    /// Beneficial in case of pre-allocating a buffer for enconding a RequestMsg.
+    static constexpr uint8_t MaxRequestSize() { return 3; }
+
+    /// Encode generic response Command Accepted or Rejected
+    /// @param msg source request message for this response
+    /// @param ar code of response parameter
+    /// @param txbuff where to format the message
+    /// @returns number of bytes written into txbuff
+    static uint8_t EncodeResponseCmdAR(const RequestMsg &msg, ResponseMsgParamCodes ar, uint8_t *txbuff);
+
+    /// Encode response to Read FINDA query
+    /// @param msg source request message for this response
+    /// @param findaValue 1/0 (on/off) status of FINDA
+    /// @param txbuff where to format the message
+    /// @returns number of bytes written into txbuff
+    static uint8_t EncodeResponseReadFINDA(const RequestMsg &msg, uint8_t findaValue, uint8_t *txbuff);
+
+    /// Encode response to Version query
+    /// @param msg source request message for this response
+    /// @param value version number (0-255)
+    /// @param txbuff where to format the message
+    /// @returns number of bytes written into txbuff
+    static uint8_t EncodeResponseVersion(const RequestMsg &msg, uint8_t value, uint8_t *txbuff);
+
+    /// Encode response to Query operation status
+    /// @param msg source request message for this response
+    /// @param code status of operation (Processing, Error, Finished)
+    /// @param value related to status of operation(e.g. error code or progress)
+    /// @param txbuff where to format the message
+    /// @returns number of bytes written into txbuff
+    static uint8_t EncodeResponseQueryOperation(const RequestMsg &msg, ResponseMsgParamCodes code, uint16_t value, uint8_t *txbuff);
+
+    /// @returns the most recently lexed request message
+    inline const RequestMsg GetRequestMsg() const { return requestMsg; }
+
+    /// @returns the most recently lexed response message
+    inline const ResponseMsg GetResponseMsg() const { return responseMsg; }
+
+    /// resets the internal request decoding state (typically after an error)
+    void ResetRequestDecoder() {
+        rqState = RequestStates::Code;
+    }
+
+    /// resets the internal response decoding state (typically after an error)
+    void ResetResponseDecoder() {
+        rspState = ResponseStates::RequestCode;
+    }
+
+private:
+    enum class RequestStates : uint8_t {
+        Code, ///< starting state - expects message code
+        Value, ///< expecting code value
+        Error ///< automaton in error state
+    };
+
+    RequestStates rqState;
+    RequestMsg requestMsg;
+
+    enum class ResponseStates : uint8_t {
+        RequestCode, ///< starting state - expects message code
+        RequestValue, ///< expecting code value
+        ParamCode, ///< expecting param code
+        ParamValue, ///< expecting param value
+        Error ///< automaton in error state
+    };
+
+    ResponseStates rspState;
+    ResponseMsg responseMsg;
+
+    static bool IsNewLine(uint8_t c) {
+        return c == '\n' || c == '\r';
+    }
+    static bool IsDigit(uint8_t c) {
+        return c >= '0' && c <= '9';
+    }
+};
+
+} // namespace protocol
+} // namespace modules
+
+namespace mp = modules::protocol;

+ 564 - 0
Firmware/mmu2_protocol_logic.cpp

@@ -0,0 +1,564 @@
+#include "mmu2_protocol_logic.h"
+#include "mmu2_log.h"
+#include "mmu2_fsensor.h"
+#include "system_timer.h"
+#include <string.h>
+
+namespace MMU2 {
+
+StepStatus ProtocolLogicPartBase::ProcessFINDAReqSent(StepStatus finishedRV, State nextState){
+    auto expmsg = logic->ExpectingMessage(linkLayerTimeout);
+    if (expmsg != MessageReady)
+        return expmsg;
+    logic->findaPressed = logic->rsp.paramValue;
+    state = nextState;
+    return finishedRV;
+}
+
+void ProtocolLogicPartBase::CheckAndReportAsyncEvents(){
+    // even when waiting for a query period, we need to report a change in filament sensor's state
+    // - it is vital for a precise synchronization of moves of the printer and the MMU
+    uint8_t fs = (uint8_t)WhereIsFilament();
+    if( fs != logic->lastFSensor ){
+        SendAndUpdateFilamentSensor();
+    }
+}
+
+void ProtocolLogicPartBase::SendQuery(){
+    logic->SendMsg(RequestMsg(RequestMsgCodes::Query, 0));
+    state = State::QuerySent;
+}
+
+void ProtocolLogicPartBase::SendFINDAQuery(){
+    logic->SendMsg(RequestMsg(RequestMsgCodes::Finda, 0 ) );
+    state = State::FINDAReqSent;
+}
+
+void ProtocolLogicPartBase::SendAndUpdateFilamentSensor(){
+    logic->SendMsg(RequestMsg(RequestMsgCodes::FilamentSensor, logic->lastFSensor = (uint8_t)WhereIsFilament() ) );
+    state = State::FilamentSensorStateSent;
+}
+
+void ProtocolLogicPartBase::SendButton(uint8_t btn){
+    logic->SendMsg(RequestMsg(RequestMsgCodes::Button, btn));
+    state = State::ButtonSent;
+}
+
+StepStatus ProtocolLogic::ProcessUARTByte(uint8_t c) {
+    switch (protocol.DecodeResponse(c)) {
+    case DecodeStatus::MessageCompleted:
+        // @@TODO reset direction of communication
+        return MessageReady;
+    case DecodeStatus::NeedMoreData:
+        return Processing;
+    case DecodeStatus::Error:
+    default:
+        return ProtocolError;
+    }
+}
+
+StepStatus ProtocolLogic::ExpectingMessage(uint32_t timeout) {
+    int bytesConsumed = 0;
+    int c = -1;
+    
+    // try to consume as many rx bytes as possible (until a message has been completed)
+    while((c = uart->read()) >= 0){
+        ++bytesConsumed;
+        RecordReceivedByte(c);
+        switch (protocol.DecodeResponse(c)) {
+        case DecodeStatus::MessageCompleted:
+            rsp = protocol.GetResponseMsg();
+            LogResponse();
+            // @@TODO reset direction of communication
+            RecordUARTActivity(); // something has happened on the UART, update the timeout record
+            return MessageReady;
+        case DecodeStatus::NeedMoreData:
+            break;
+        case DecodeStatus::Error:
+        default:
+            RecordUARTActivity(); // something has happened on the UART, update the timeout record
+            return ProtocolError;
+        }
+    }
+    if( bytesConsumed != 0 ){
+        RecordUARTActivity(); // something has happened on the UART, update the timeout record
+        return Processing; // consumed some bytes, but message still not ready
+    } else if (Elapsed(timeout)) {
+        return CommunicationTimeout;
+    }
+    return Processing;
+}
+
+void ProtocolLogic::SendMsg(RequestMsg rq) {
+    uint8_t txbuff[Protocol::MaxRequestSize()];
+    uint8_t len = Protocol::EncodeRequest(rq, txbuff);
+    uart->write(txbuff, len);
+    LogRequestMsg(txbuff, len);
+    RecordUARTActivity();
+}
+
+void StartSeq::Restart() {
+    state = State::S0Sent;
+    logic->SendMsg(RequestMsg(RequestMsgCodes::Version, 0));
+}
+
+StepStatus StartSeq::Step() {
+    auto expmsg = logic->ExpectingMessage(linkLayerTimeout);
+    if (expmsg != MessageReady)
+        return expmsg;
+
+    // solve initial handshake
+    switch (state) {
+    case State::S0Sent: // received response to S0 - major
+        if (logic->rsp.paramValue != 2) {
+            return VersionMismatch;
+        }
+        logic->dataTO.Reset(); // got meaningful response from the MMU, stop data layer timeout tracking
+        logic->SendMsg(RequestMsg(RequestMsgCodes::Version, 1));
+        state = State::S1Sent;
+        break;
+    case State::S1Sent: // received response to S1 - minor
+        if (logic->rsp.paramValue != 0) {
+            return VersionMismatch;
+        }
+        logic->SendMsg(RequestMsg(RequestMsgCodes::Version, 2));
+        state = State::S2Sent;
+        break;
+    case State::S2Sent: // received response to S2 - revision
+        if (logic->rsp.paramValue != 0) {
+            return VersionMismatch;
+        }
+        // Start General Interrogation after line up.
+        // For now we just send the state of the filament sensor, but we may request
+        // data point states from the MMU as well. TBD in the future, especially with another protocol
+        SendAndUpdateFilamentSensor();
+        break;
+    case State::FilamentSensorStateSent:
+        state = State::Ready;
+        return Finished;
+        break;
+    default:
+        return VersionMismatch;
+    }
+    return Processing;
+}
+
+void Command::Restart() {
+    state = State::CommandSent;
+    logic->SendMsg(logic->command.rq);
+}
+
+StepStatus Command::Step() {
+    switch (state) {
+    case State::CommandSent: {
+        auto expmsg = logic->ExpectingMessage(linkLayerTimeout);
+        if (expmsg != MessageReady)
+            return expmsg;
+
+        switch (logic->rsp.paramCode) { // the response should be either accepted or rejected
+        case ResponseMsgParamCodes::Accepted:
+            logic->progressCode = ProgressCode::OK;
+            logic->errorCode = ErrorCode::RUNNING;
+            state = State::Wait;
+            break;
+        case ResponseMsgParamCodes::Rejected:
+            // rejected - should normally not happen, but report the error up
+            logic->progressCode = ProgressCode::OK;
+            logic->errorCode = ErrorCode::PROTOCOL_ERROR;
+            return CommandRejected;
+        default:
+            return ProtocolError;
+        }
+        } break;
+    case State::Wait:
+        if (logic->Elapsed(heartBeatPeriod)) {
+            SendQuery();
+        } else { 
+            // even when waiting for a query period, we need to report a change in filament sensor's state
+            // - it is vital for a precise synchronization of moves of the printer and the MMU
+            CheckAndReportAsyncEvents();
+        }
+        break;
+    case State::QuerySent: {
+        auto expmsg = logic->ExpectingMessage(linkLayerTimeout);
+        if (expmsg != MessageReady)
+            return expmsg;
+        }
+        // [[fallthrough]];
+    case State::ContinueFromIdle:
+        switch (logic->rsp.paramCode) {
+        case ResponseMsgParamCodes::Processing:
+            logic->progressCode = static_cast<ProgressCode>(logic->rsp.paramValue);
+            logic->errorCode = ErrorCode::OK;
+            SendAndUpdateFilamentSensor(); // keep on reporting the state of fsensor regularly
+            break;
+        case ResponseMsgParamCodes::Error:
+            // in case of an error the progress code remains as it has been before
+            logic->errorCode = static_cast<ErrorCode>(logic->rsp.paramValue);
+            // keep on reporting the state of fsensor regularly even in command error state
+            // - the MMU checks FINDA and fsensor even while recovering from errors
+            SendAndUpdateFilamentSensor();
+            return CommandError;
+        case ResponseMsgParamCodes::Finished:
+            logic->progressCode = ProgressCode::OK;
+            state = State::Ready;
+            return Finished;
+        default:
+            return ProtocolError;
+        }
+        break;
+    case State::FilamentSensorStateSent:{
+        auto expmsg = logic->ExpectingMessage(linkLayerTimeout);
+        if (expmsg != MessageReady)
+            return expmsg;
+        SendFINDAQuery();
+        } break;
+    case State::FINDAReqSent:
+        return ProcessFINDAReqSent(Processing, State::Wait);
+    case State::ButtonSent:{
+        // button is never confirmed ... may be it should be
+        // auto expmsg = logic->ExpectingMessage(linkLayerTimeout);
+        // if (expmsg != MessageReady)
+        //     return expmsg;
+        SendQuery();
+        } break;
+    default:
+        return ProtocolError;
+    }
+    return Processing;
+}
+
+void Idle::Restart() {
+    state = State::Ready;
+}
+
+StepStatus Idle::Step() {
+    switch (state) {
+    case State::Ready: // check timeout
+        if (logic->Elapsed(heartBeatPeriod)) {
+            logic->SendMsg(RequestMsg(RequestMsgCodes::Query, 0));
+            state = State::QuerySent;
+            return Processing;
+        }
+        break;
+    case State::QuerySent: { // check UART
+        auto expmsg = logic->ExpectingMessage(linkLayerTimeout);
+        if (expmsg != MessageReady)
+            return expmsg;
+        // If we are accidentally in Idle and we receive something like "T0 P1" - that means the communication dropped out while a command was in progress.
+        // That causes no issues here, we just need to switch to Command processing and continue there from now on.
+        // The usual response in this case should be some command and "F" - finished - that confirms we are in an Idle state even on the MMU side.
+        switch( logic->rsp.request.code ){
+        case RequestMsgCodes::Cut:
+        case RequestMsgCodes::Eject:
+        case RequestMsgCodes::Load:
+        case RequestMsgCodes::Mode:
+        case RequestMsgCodes::Tool:
+        case RequestMsgCodes::Unload:
+            if( logic->rsp.paramCode != ResponseMsgParamCodes::Finished ){
+                logic->SwitchFromIdleToCommand();
+                return Processing;
+            }
+        default:
+            break;
+        }
+        SendFINDAQuery();
+        return Processing;
+    } break;
+    case State::FINDAReqSent:
+        return ProcessFINDAReqSent(Finished, State::Ready);
+    default:
+        return ProtocolError;
+    }
+
+    // The "return Finished" in this state machine requires a bit of explanation:
+    // The Idle state either did nothing (still waiting for the heartbeat timeout)
+    // or just successfully received the answer to Q0, whatever that was.
+    // In both cases, it is ready to hand over work to a command or something else,
+    // therefore we are returning Finished (also to exit mmu_loop() and unblock Marlin's loop!).
+    // If there is no work, we'll end up in the Idle state again
+    // and we'll send the heartbeat message after the specified timeout.
+    return Finished;
+}
+
+ProtocolLogic::ProtocolLogic(MMU2Serial *uart)
+    : stopped(this)
+    , startSeq(this)
+    , idle(this)
+    , command(this)
+    , currentState(&stopped)
+    , plannedRq(RequestMsgCodes::unknown, 0)
+    , lastUARTActivityMs(0)
+    , rsp(RequestMsg(RequestMsgCodes::unknown, 0), ResponseMsgParamCodes::unknown, 0)
+    , state(State::Stopped)
+    , lrb(0)
+    , uart(uart)
+    , lastFSensor((uint8_t)WhereIsFilament())
+{}
+
+void ProtocolLogic::Start() {
+    state = State::InitSequence;
+    currentState = &startSeq;
+    startSeq.Restart();
+}
+
+void ProtocolLogic::Stop() {
+    state = State::Stopped;
+    currentState = &stopped;
+}
+
+void ProtocolLogic::ToolChange(uint8_t slot) {
+    PlanGenericRequest(RequestMsg(RequestMsgCodes::Tool, slot));
+}
+
+void ProtocolLogic::UnloadFilament() {
+    PlanGenericRequest(RequestMsg(RequestMsgCodes::Unload, 0));
+}
+
+void ProtocolLogic::LoadFilament(uint8_t slot) {
+    PlanGenericRequest(RequestMsg(RequestMsgCodes::Load, slot));
+}
+
+void ProtocolLogic::EjectFilament(uint8_t slot) {
+    PlanGenericRequest(RequestMsg(RequestMsgCodes::Eject, slot));
+}
+
+void ProtocolLogic::CutFilament(uint8_t slot){
+    PlanGenericRequest(RequestMsg(RequestMsgCodes::Cut, slot));
+}
+
+void ProtocolLogic::ResetMMU() {
+    PlanGenericRequest(RequestMsg(RequestMsgCodes::Reset, 0));
+}
+
+void ProtocolLogic::Button(uint8_t index){
+    PlanGenericRequest(RequestMsg(RequestMsgCodes::Button, index));
+}
+
+void ProtocolLogic::Home(uint8_t mode){
+    PlanGenericRequest(RequestMsg(RequestMsgCodes::Home, mode));
+}
+
+void ProtocolLogic::PlanGenericRequest(RequestMsg rq) {
+    plannedRq = rq;
+    if( ! currentState->ExpectsResponse() ){
+        ActivatePlannedRequest();
+    } // otherwise wait for an empty window to activate the request
+}
+
+bool MMU2::ProtocolLogic::ActivatePlannedRequest(){
+    if( plannedRq.code == RequestMsgCodes::Button ){
+        // only issue the button to the MMU and do not restart the state machines
+        command.SendButton(plannedRq.value);
+        plannedRq = RequestMsg(RequestMsgCodes::unknown, 0);
+        return true;
+    } else if( plannedRq.code != RequestMsgCodes::unknown ){
+        currentState = &command;
+        command.SetRequestMsg(plannedRq);
+        plannedRq = RequestMsg(RequestMsgCodes::unknown, 0);
+        command.Restart();
+        return true;
+    }
+    return false;
+}
+
+void ProtocolLogic::SwitchFromIdleToCommand(){
+    currentState = &command;
+    command.SetRequestMsg(rsp.request);
+    // we are recovering from a communication drop out, the command is already running
+    // and we have just received a response to a Q0 message about a command progress
+    command.ContinueFromIdle();
+}
+
+void ProtocolLogic::SwitchToIdle() {
+    state = State::Running;
+    currentState = &idle;
+    idle.Restart();
+}
+
+void ProtocolLogic::HandleCommunicationTimeout() {
+    uart->flush(); // clear the output buffer
+    currentState = &startSeq;
+    state = State::InitSequence;
+    startSeq.Restart();
+}
+
+bool ProtocolLogic::Elapsed(uint32_t timeout) const {
+    return _millis() >= (lastUARTActivityMs + timeout);
+}
+
+void ProtocolLogic::RecordUARTActivity() {
+    lastUARTActivityMs = _millis();
+}
+
+void ProtocolLogic::RecordReceivedByte(uint8_t c){
+    lastReceivedBytes[lrb] = c;
+    lrb = (lrb+1) % lastReceivedBytes.size();
+}
+
+char NibbleToChar(uint8_t c){
+    switch (c) {
+    case 0:
+    case 1:
+    case 2:
+    case 3:
+    case 4:
+    case 5:
+    case 6:
+    case 7:
+    case 8:
+    case 9:
+        return c + '0';
+    case 10:
+    case 11:
+    case 12:
+    case 13:
+    case 14:
+    case 15:
+        return (c - 10) + 'a';
+    default:
+        return 0;
+    }
+}
+
+void ProtocolLogic::FormatLastReceivedBytes(char *dst){
+    for(uint8_t i = 0; i < lastReceivedBytes.size(); ++i){
+        uint8_t b = lastReceivedBytes[ (lrb-i-1) % lastReceivedBytes.size() ];
+        dst[i*3] = NibbleToChar(b >> 4);
+        dst[i*3+1] = NibbleToChar(b & 0xf);
+        dst[i*3+2] = ' ';
+    }
+    dst[ (lastReceivedBytes.size() - 1) * 3 + 2] = 0; // terminate properly
+}
+
+void ProtocolLogic::FormatLastResponseMsgAndClearLRB(char *dst){
+    *dst++ = '<';
+    for(uint8_t i = 0; i < lrb; ++i){
+        uint8_t b = lastReceivedBytes[ i ];
+        if( b < 32 )b = '.';
+        if( b > 127 )b = '.';
+        *dst++ = b;
+    }
+    *dst = 0; // terminate properly
+    lrb = 0; // reset the input buffer index in case of a clean message
+}
+
+void ProtocolLogic::LogRequestMsg(const uint8_t *txbuff, uint8_t size){
+    constexpr uint_fast8_t rqs = modules::protocol::Protocol::MaxRequestSize() + 2;
+    char tmp[rqs] = ">";
+    static char lastMsg[rqs] = "";
+    for(uint8_t i = 0; i < size; ++i){
+        uint8_t b = txbuff[i];
+        if( b < 32 )b = '.';
+        if( b > 127 )b = '.';
+        tmp[i+1] = b;
+    }
+    tmp[size+1] = '\n';
+    tmp[size+2] = 0;
+    if( !strncmp(tmp, ">S0.\n", rqs) && !strncmp(lastMsg, tmp, rqs) ){
+        // @@TODO we skip the repeated request msgs for now 
+        // to avoid spoiling the whole log just with ">S0" messages
+        // especially when the MMU is not connected.
+        // We'll lose the ability to see if the printer is actually
+        // trying to find the MMU, but since it has been reliable in the past
+        // we can live without it for now.
+    } else {
+        MMU2_ECHO_MSG(tmp);
+    }
+    memcpy(lastMsg, tmp, rqs);
+}
+
+void MMU2::ProtocolLogic::LogError(const char *reason){
+    char lrb[lastReceivedBytes.size() * 3];
+    FormatLastReceivedBytes(lrb);
+    
+    MMU2_ERROR_MSG(reason);
+    SERIAL_ECHO(", last bytes: ");
+    SERIAL_ECHOLN(lrb);
+}
+
+void ProtocolLogic::LogResponse(){
+    char lrb[lastReceivedBytes.size()];
+    FormatLastResponseMsgAndClearLRB(lrb);
+    MMU2_ECHO_MSG(lrb);
+    SERIAL_ECHOLN();
+}
+
+StepStatus MMU2::ProtocolLogic::HandleCommError(const char *msg, StepStatus ss){
+    protocol.ResetResponseDecoder();
+    HandleCommunicationTimeout();
+    if( dataTO.Record(ss) ){
+        LogError(msg);
+        return dataTO.InitialCause();
+    } else {
+        return Processing; // suppress short drop outs of communication
+    }
+}
+
+StepStatus ProtocolLogic::Step() {
+    if( ! currentState->ExpectsResponse() ){ // if not waiting for a response, activate a planned request immediately
+        ActivatePlannedRequest();
+    }
+    auto currentStatus = currentState->Step();
+    switch (currentStatus) {
+    case Processing:
+        // we are ok, the state machine continues correctly
+        break;
+    case Finished: {
+        // We are ok, switching to Idle if there is no potential next request planned.
+        // But the trouble is we must report a finished command if the previous command has just been finished
+        // i.e. only try to find some planned command if we just finished the Idle cycle
+        bool previousCommandFinished = currentState == &command; // @@TODO this is a nasty hack :( 
+        if( ! ActivatePlannedRequest() ){ // if nothing is planned, switch to Idle
+            SwitchToIdle();
+        } else {
+            // if the previous cycle was Idle and now we have planned a new command -> avoid returning Finished
+            if( ! previousCommandFinished && currentState == &command){
+                currentStatus = Processing;
+            }
+        }
+        }
+        break;
+    case CommandRejected:
+        // we have to repeat it - that's the only thing we can do
+        // no change in state
+        // @@TODO wait until Q0 returns command in progress finished, then we can send this one
+        LogError("Command rejected");
+        command.Restart();
+        break;
+    case CommandError:
+        LogError("Command Error");
+        // we shall probably transfer into the Idle state and await further instructions from the upper layer
+        // Idle state may solve the problem of keeping up the heart beat running
+        break;
+    case VersionMismatch:
+        LogError("Version mismatch");
+        Stop(); // cannot continue
+        break;
+    case ProtocolError:
+        currentStatus = HandleCommError("Protocol error", ProtocolError);
+        break;
+    case CommunicationTimeout:
+        currentStatus = HandleCommError("Communication timeout", CommunicationTimeout);
+        break;
+    default:
+        break;
+    }
+    return currentStatus;
+}
+
+uint8_t ProtocolLogic::CommandInProgress() const {
+    if( currentState != &command )
+        return 0;
+    return (uint8_t)command.ReqMsg().code; 
+}
+
+bool DropOutFilter::Record(StepStatus ss){
+    if( occurrences == maxOccurrences ){
+        cause = ss;
+    }
+    --occurrences;
+    return occurrences == 0;
+}
+
+} // namespace MMU2

+ 314 - 0
Firmware/mmu2_protocol_logic.h

@@ -0,0 +1,314 @@
+#pragma once
+#include <stdint.h>
+// #include <array> //@@TODO Don't we have STL for AVR somewhere?
+template<typename T, uint8_t N>
+class array {
+    T data[N];
+public:
+    array() = default;
+    inline constexpr T* begin()const { return data; }
+    inline constexpr T* end()const { return data + N; }
+    constexpr uint8_t size()const { return N; }
+    inline T &operator[](uint8_t i){
+        return data[i];
+    }
+};
+
+#include "mmu2/error_codes.h"
+#include "mmu2/progress_codes.h"
+#include "mmu2_protocol.h"
+
+#include "mmu2_serial.h"
+
+/// New MMU2 protocol logic
+namespace MMU2 {
+
+using namespace modules::protocol;
+
+class ProtocolLogic;
+
+/// ProtocolLogic stepping statuses
+enum StepStatus : uint_fast8_t {
+    Processing = 0,
+    MessageReady, ///< a message has been successfully decoded from the received bytes
+    Finished,
+    CommunicationTimeout, ///< the MMU failed to respond to a request within a specified time frame
+    ProtocolError, ///< bytes read from the MMU didn't form a valid response
+    CommandRejected, ///< the MMU rejected the command due to some other command in progress, may be the user is operating the MMU locally (button commands)
+    CommandError, ///< the command in progress stopped due to unrecoverable error, user interaction required
+    VersionMismatch, ///< the MMU reports its firmware version incompatible with our implementation
+    CommunicationRecovered,
+};
+
+
+static constexpr uint32_t linkLayerTimeout = 2000; ///< default link layer communication timeout
+static constexpr uint32_t dataLayerTimeout = linkLayerTimeout * 3; ///< data layer communication timeout
+static constexpr uint32_t heartBeatPeriod = linkLayerTimeout / 2; ///< period of heart beat messages (Q0)
+
+static_assert( heartBeatPeriod < linkLayerTimeout && linkLayerTimeout < dataLayerTimeout, "Incorrect ordering of timeouts");
+
+/// Base class for sub-automata of the ProtocolLogic class.
+/// Their operation should never block (wait inside).
+class ProtocolLogicPartBase {
+public:
+    inline ProtocolLogicPartBase(ProtocolLogic *logic)
+        : logic(logic)
+        , state(State::Ready) {}
+
+    /// Restarts the sub-automaton
+    virtual void Restart() = 0;
+
+    /// Makes one step in the sub-automaton
+    /// @returns StepStatus
+    virtual StepStatus Step() = 0;
+    
+    /// @returns true if the state machine is waiting for a response from the MMU
+    bool ExpectsResponse()const { return state != State::Ready && state != State::Wait; }
+
+protected:
+    ProtocolLogic *logic; ///< pointer to parent ProtocolLogic layer
+    friend class ProtocolLogic;
+    
+    /// Common internal states of the derived sub-automata
+    /// General rule of thumb: *Sent states are waiting for a response from the MMU
+    enum class State : uint_fast8_t { 
+        Ready,
+        Wait,
+
+        S0Sent,
+        S1Sent,
+        S2Sent,
+        QuerySent,
+        CommandSent,
+        FilamentSensorStateSent,
+        FINDAReqSent,
+        ButtonSent,
+
+        ContinueFromIdle
+    };
+
+    State state; ///< internal state of the sub-automaton
+
+    /// @returns the status of processing of the FINDA query response
+    /// @param finishedRV returned value in case the message was successfully received and processed
+    /// @param nextState is a state where the state machine should transfer to after the message was successfully received and processed
+    StepStatus ProcessFINDAReqSent(StepStatus finishedRV, State nextState);
+    
+    /// Called repeatedly while waiting for a query (Q0) period.
+    /// All event checks to report immediately from the printer to the MMU shall be done in this method.
+    /// So far, the only such a case is the filament sensor, but there can be more like this in the future.
+    void CheckAndReportAsyncEvents();
+    
+    void SendQuery();
+    
+    void SendFINDAQuery();
+    
+    void SendAndUpdateFilamentSensor();
+
+    void SendButton(uint8_t btn);
+};
+
+/// Starting sequence of the communication with the MMU.
+/// The printer shall ask for MMU's version numbers.
+/// If everything goes well and the MMU's version is good enough,
+/// the ProtocolLogic layer may continue talking to the MMU
+class StartSeq : public ProtocolLogicPartBase {
+public:
+    inline StartSeq(ProtocolLogic *logic)
+        : ProtocolLogicPartBase(logic) {}
+    void Restart() override;
+    StepStatus Step() override;
+};
+
+/// A command and its lifecycle.
+/// CommandSent:
+/// - the command was placed into the UART TX buffer, awaiting response from the MMU
+/// - if the MMU confirms the command, we'll wait for it to finish
+/// - if the MMU refuses the command, we report an error (should normally not happen unless someone is hacking the communication without waiting for the previous command to finish)
+/// Wait:
+/// - waiting for the MMU to process the command - may take several seconds, for example Tool change operation
+/// - meawhile, every 300ms we send a Q0 query to obtain the current state of the command being processed
+/// - as soon as we receive a response to Q0 from the MMU, we process it in the next state
+/// QuerySent - check the reply from the MMU - can be any of the following:
+/// - Processing: the MMU is still working
+/// - Error: the command failed on the MMU, we'll have the exact error report in the response message
+/// - Finished: the MMU finished the command successfully, another command may be issued now
+class Command : public ProtocolLogicPartBase {
+public:
+    inline Command(ProtocolLogic *logic)
+        : ProtocolLogicPartBase(logic)
+        , rq(RequestMsgCodes::unknown, 0) {}
+    void Restart() override;
+    StepStatus Step() override;
+    inline void SetRequestMsg(RequestMsg msg) {
+        rq = msg;
+    }
+    void ContinueFromIdle(){
+        state = State::ContinueFromIdle;
+    }
+    inline const RequestMsg &ReqMsg()const { return rq; }
+
+private:
+    RequestMsg rq;
+};
+
+/// Idle state - we have no command for the MMU, so we are only regularly querying its state with Q0 messages.
+/// The idle state can be interrupted any time to issue a command into the MMU
+class Idle : public ProtocolLogicPartBase {
+public:
+    inline Idle(ProtocolLogic *logic)
+        : ProtocolLogicPartBase(logic) {}
+    void Restart() override;
+    StepStatus Step() override;
+};
+
+/// The communication with the MMU is stopped/disabled (for whatever reason).
+/// Nothing is being put onto the UART.
+class Stopped : public ProtocolLogicPartBase {
+public:
+    inline Stopped(ProtocolLogic *logic)
+        : ProtocolLogicPartBase(logic) {}
+    void Restart() override {}
+    StepStatus Step() override { return Processing; }
+};
+
+///< Filter of short consecutive drop outs which are recovered instantly
+class DropOutFilter {
+    StepStatus cause;
+    uint8_t occurrences;
+public:
+    static constexpr uint8_t maxOccurrences = 3;
+    static_assert (maxOccurrences > 1, "we should really silently ignore at least 1 comm drop out if recovered immediately afterwards");
+    DropOutFilter() = default;
+    
+    /// @returns true if the error should be reported to higher levels (max. number of consecutive occurrences reached)
+    bool Record(StepStatus ss);
+    
+    /// @returns the initial cause which started this drop out event
+    inline StepStatus InitialCause()const { return cause; }
+    
+    /// Rearms the object for further processing - basically call this once the MMU responds with something meaningful (e.g. S0 A2)
+    inline void Reset(){ occurrences = maxOccurrences; }
+};
+
+/// Logic layer of the MMU vs. printer communication protocol
+class ProtocolLogic {
+public:
+    ProtocolLogic(MMU2Serial *uart);
+
+    /// Start/Enable communication with the MMU
+    void Start();
+
+    /// Stop/Disable communication with the MMU
+    void Stop();
+
+    // Issue commands to the MMU
+    void ToolChange(uint8_t slot);
+    void UnloadFilament();
+    void LoadFilament(uint8_t slot);
+    void EjectFilament(uint8_t slot);
+    void CutFilament(uint8_t slot);
+    void ResetMMU();
+    void Button(uint8_t index);
+    void Home(uint8_t mode);
+
+    /// Step the state machine
+    StepStatus Step();
+
+    /// @returns the current/latest error code as reported by the MMU
+    ErrorCode Error() const { return errorCode; }
+
+    /// @returns the current/latest process code as reported by the MMU
+    ProgressCode Progress() const { return progressCode; }
+    
+    uint8_t CommandInProgress()const;
+    
+    inline bool Running()const {
+        return state == State::Running;
+    }
+    
+    inline bool FindaPressed() const {
+        return findaPressed;
+    }
+
+#ifndef UNITTEST
+private:
+#endif
+    
+    StepStatus ProcessUARTByte(uint8_t c);
+    StepStatus ExpectingMessage(uint32_t timeout);
+    void SendMsg(RequestMsg rq);
+    void SwitchToIdle();
+    void HandleCommunicationTimeout();
+    StepStatus HandleCommError(const char *msg, StepStatus ss);
+    bool Elapsed(uint32_t timeout) const;
+    void RecordUARTActivity();
+    void RecordReceivedByte(uint8_t c);
+    void FormatLastReceivedBytes(char *dst);
+    void FormatLastResponseMsgAndClearLRB(char *dst);
+    void LogRequestMsg(const uint8_t *txbuff, uint8_t size);
+    void LogError(const char *reason);
+    void LogResponse();
+    void SwitchFromIdleToCommand();
+    
+    enum class State : uint_fast8_t {
+        Stopped,      ///< stopped for whatever reason
+        InitSequence, ///< initial sequence running
+        Running       ///< normal operation - Idle + Command processing
+    };
+
+    // individual sub-state machines - may be they can be combined into a union since only one is active at once
+    Stopped stopped;
+    StartSeq startSeq;
+    Idle idle;
+    Command command;
+    ProtocolLogicPartBase *currentState; ///< command currently being processed
+    
+    /// Records the next planned state, "unknown" msg code if no command is planned.
+    /// This is not intended to be a queue of commands to process, protocol_logic must not queue commands.
+    /// It exists solely to prevent breaking the Request-Response protocol handshake -
+    /// - during tests it turned out, that the commands from Marlin are coming in such an asynchronnous way, that
+    /// we could accidentally send T2 immediately after Q0 without waiting for reception of response to Q0.
+    /// 
+    /// Beware, if Marlin manages to call PlanGenericCommand multiple times before a response comes,
+    /// these variables will get overwritten by the last call.
+    /// However, that should not happen under normal circumstances as Marlin should wait for the Command to finish, 
+    /// which includes all responses (and error recovery if any).
+    RequestMsg plannedRq;
+
+    /// Plan a command to be processed once the immediate response to a sent request arrives
+    void PlanGenericRequest(RequestMsg rq);
+    /// Activate the planned state once the immediate response to a sent request arrived
+    bool ActivatePlannedRequest();
+
+    uint32_t lastUARTActivityMs; ///< timestamp - last ms when something occurred on the UART
+    DropOutFilter dataTO; ///< Filter of short consecutive drop outs which are recovered instantly
+
+    ResponseMsg rsp; ///< decoded response message from the MMU protocol
+
+    State state; ///< internal state of ProtocolLogic
+
+    Protocol protocol; ///< protocol codec
+    
+    array<uint8_t, 16> lastReceivedBytes; ///< remembers the last few bytes of incoming communication for diagnostic purposes
+    uint8_t lrb;
+
+    MMU2Serial *uart; ///< UART interface
+
+    ErrorCode errorCode;       ///< last received error code from the MMU
+    ProgressCode progressCode; ///< last received progress code from the MMU
+
+    uint8_t lastFSensor; ///< last state of filament sensor
+    
+    bool findaPressed;
+    
+    friend class ProtocolLogicPartBase;
+    friend class Stopped;
+    friend class Command;
+    friend class Idle;
+    friend class StartSeq;
+
+    friend class MMU2;
+};
+
+} // namespace MMU2

+ 21 - 0
Firmware/mmu2_reporting.cpp

@@ -0,0 +1,21 @@
+#include "mmu2_reporting.h"
+
+// @@TODO implement the interface for MK3
+
+namespace MMU2 {
+
+void BeginReport(CommandInProgress cip, uint16_t ec) { }
+
+void EndReport(CommandInProgress cip, uint16_t ec) { }
+
+void ReportErrorHook(CommandInProgress cip, uint16_t ec) { }
+
+void ReportProgressHook(CommandInProgress cip, uint16_t ec) { }
+
+Buttons ButtonPressed(uint16_t ec) { }
+
+bool MMUAvailable() { }
+
+bool UseMMU() { }
+
+} // namespace MMU2

+ 53 - 0
Firmware/mmu2_reporting.h

@@ -0,0 +1,53 @@
+/// @file mmu2_reporting.h
+
+#pragma once
+#include <stdint.h>
+
+namespace MMU2 {
+
+enum CommandInProgress : uint8_t {
+    NoCommand = 0,
+    CutFilament = 'C',
+    EjectFilament = 'E',
+    Homing = 'H',
+    LoadFilament = 'L',
+    Reset = 'X',
+    ToolChange = 'T',
+    UnloadFilament = 'U',
+};
+
+/// Called at the begin of every MMU operation
+void BeginReport(CommandInProgress cip, uint16_t ec);
+
+/// Called at the end of every MMU operation
+void EndReport(CommandInProgress cip, uint16_t ec);
+
+/// Called when the MMU sends operation error (even repeatedly)
+void ReportErrorHook(CommandInProgress cip, uint16_t ec);
+
+/// Called when the MMU sends operation progress update
+void ReportProgressHook(CommandInProgress cip, uint16_t ec);
+
+/// Button codes + extended actions performed on the printer's side
+enum Buttons : uint8_t {
+    Left = 0,
+    Middle,
+    Right,
+    
+    // performed on the printer's side
+    RestartMMU,
+    StopPrint,
+    
+    NoButton = 0xff // shall be kept last
+};
+
+Buttons ButtonPressed(uint16_t ec);
+
+/// @returns true if the MMU is communicating and available
+/// can change at runtime
+bool MMUAvailable();
+
+/// Global Enable/Disable use MMU (to be stored in EEPROM)
+bool UseMMU();
+
+} // namespace

+ 15 - 0
Firmware/mmu2_serial.cpp

@@ -0,0 +1,15 @@
+#include "mmu2_serial.h"
+
+//@@TODO implement for MK3
+
+namespace MMU2 {
+
+void MMU2Serial::begin(uint32_t baud){ }
+void MMU2Serial::close() { }
+int MMU2Serial::read() { }
+void MMU2Serial::flush() { }
+size_t MMU2Serial::write(const uint8_t *buffer, size_t size) { }
+
+MMU2Serial mmu2Serial;
+
+} // namespace MMU2

+ 21 - 0
Firmware/mmu2_serial.h

@@ -0,0 +1,21 @@
+#pragma once
+#include <stdint.h>
+#include <stddef.h>
+
+namespace MMU2 {
+
+/// A minimal serial interface for the MMU
+class MMU2Serial {
+public:
+    MMU2Serial() = default;
+//    bool available()const;
+    void begin(uint32_t baud);
+    void close();
+    int read();
+    void flush();
+    size_t write(const uint8_t *buffer, size_t size);
+};
+
+extern MMU2Serial mmu2Serial;
+
+} // namespace MMU2

+ 1 - 1
Firmware/stepper.cpp

@@ -38,7 +38,7 @@
 
 #include "Filament_sensor.h"
 
-#include "mmu.h"
+#include "mmu2.h"
 #include "ConfigurationStore.h"
 
 #include "Prusa_farm.h"

+ 5 - 0
Firmware/strlen_cx.h

@@ -0,0 +1,5 @@
+#pragma once
+
+constexpr inline int strlen_constexpr(const char* str){
+    return *str ? 1 + strlen_constexpr(str + 1) : 0;
+}

+ 93 - 91
Firmware/ultralcd.cpp

@@ -36,7 +36,7 @@
 
 #include "sound.h"
 
-#include "mmu.h"
+#include "mmu2.h"
 
 #include "static_assert.h"
 #include "first_lay_cal.h"
@@ -450,19 +450,20 @@ void lcdui_print_percent_done(void)
 }
 
 // Print extruder status (5 chars total)
-void lcdui_print_extruder(void)
-{
-	int chars = 0;
-	if (mmu_extruder == tmp_extruder) {
-		if (mmu_extruder == MMU_FILAMENT_UNKNOWN) chars = lcd_printf_P(_N(" F?"));
-		else chars = lcd_printf_P(_N(" F%u"), mmu_extruder + 1);
-	}
-	else
-	{
-		if (mmu_extruder == MMU_FILAMENT_UNKNOWN) chars = lcd_printf_P(_N(" ?>%u"), tmp_extruder + 1);
-		else chars = lcd_printf_P(_N(" %u>%u"), mmu_extruder + 1, tmp_extruder + 1);
-	}
-	lcd_space(5 - chars);
+void lcdui_print_extruder(void) {
+    uint8_t chars = 0;
+// @@TODO   if (MMU2::mmu2.get_current_tool() == tmp_extruder) {
+//        if (MMU2::mmu2.get_current_tool() == MMU2::FILAMENT_UNKNOWN)
+//            chars = lcd_printf_P(_N(" F?"));
+//        else
+//            chars = lcd_printf_P(_N(" F%u"), MMU2::mmu2.get_current_tool() + 1);
+//    } else {
+//        if (MMU2::mmu2.get_current_tool() == MMU2::FILAMENT_UNKNOWN)
+//            chars = lcd_printf_P(_N(" ?>%u"), tmp_extruder + 1);
+//        else
+//            chars = lcd_printf_P(_N(" %u>%u"), MMU2::mmu2.get_current_tool() + 1, tmp_extruder + 1);
+//    }
+    lcd_space(5 - chars);
 }
 
 // Print farm number (5 chars total)
@@ -719,7 +720,7 @@ void lcdui_print_status_screen(void)
 	//Print SD status (7 chars)
 	lcdui_print_percent_done();
 
-	if (mmu_enabled)
+	if (MMU2::mmu2.Enabled())
 		//Print extruder status (5 chars)
 		lcdui_print_extruder();
 	else if (farm_mode)
@@ -945,7 +946,7 @@ void lcd_commands()
                 enquecommand_P(PSTR("M140 S0")); // turn off heatbed
                 enquecommand_P(PSTR("G1 Z10 F1300.000")); //lift Z
                 enquecommand_P(PSTR("G1 X10 Y180 F4000")); //Go to parking position
-                if (mmu_enabled) enquecommand_P(PSTR("M702 C")); //unload from nozzle
+                if (MMU2::mmu2.Enabled()) enquecommand_P(PSTR("M702 C")); //unload from nozzle
                 enquecommand_P(PSTR("M84"));// disable motors
                 forceMenuExpire = true; //if user dont confirm live adjust Z value by pressing the knob, we are saving last value by timeout to status screen
                 lcd_commands_step = 1;
@@ -1188,14 +1189,14 @@ static void lcd_menu_fails_stats_mmu_print()
 //! @todo Positioning of the messages and values on LCD aren't fixed to their exact place. This causes issues with translations.
 static void lcd_menu_fails_stats_mmu_total()
 {
-	mmu_command(MmuCmd::S3);
+// @@TODO	mmu_command(MmuCmd::S3);
 	lcd_timeoutToStatus.stop(); //infinite timeout
     lcd_home();
-    lcd_printf_P(PSTR("%S\n" " %-16.16S%-3d\n" " %-16.16S%-3d\n" " %-16.16S%-3d"), 
-        _T(MSG_TOTAL_FAILURES),
-        _T(MSG_MMU_FAILS), clamp999( eeprom_read_word((uint16_t*)EEPROM_MMU_FAIL_TOT) ),
-        _T(MSG_MMU_LOAD_FAILS), clamp999( eeprom_read_word((uint16_t*)EEPROM_MMU_LOAD_FAIL_TOT) ),
-        _i("MMU power fails"), clamp999( mmu_power_failures )); ////MSG_MMU_POWER_FAILS c=15
+//    lcd_printf_P(PSTR("%S\n" " %-16.16S%-3d\n" " %-16.16S%-3d\n" " %-16.16S%-3d"), 
+//        _T(MSG_TOTAL_FAILURES),
+//        _T(MSG_MMU_FAILS), clamp999( eeprom_read_word((uint16_t*)EEPROM_MMU_FAIL_TOT) ),
+//        _T(MSG_MMU_LOAD_FAILS), clamp999( eeprom_read_word((uint16_t*)EEPROM_MMU_LOAD_FAIL_TOT) ),
+//        _i("MMU power fails"), clamp999( mmu_power_failures )); ////MSG_MMU_POWER_FAILS c=15
     menu_back_if_clicked_fb();
 }
 
@@ -1680,13 +1681,15 @@ static void lcd_support_menu()
 #endif // IR_SENSOR_ANALOG
 
 	MENU_ITEM_BACK_P(STR_SEPARATOR);
-	if (mmu_enabled)
+	if (MMU2::mmu2.Enabled())
 	{
 		MENU_ITEM_BACK_P(_i("MMU2 connected"));  ////MSG_MMU_CONNECTED c=18
 		MENU_ITEM_BACK_P(PSTR(" FW:"));  ////c=17
 		if (((menu_item - 1) == menu_line) && lcd_draw_update)
 		{
 		    lcd_set_cursor(6, menu_row);
+            uint8_t mmu_version = 200; // @@TODO
+            uint8_t mmu_buildnr = 0;
 			if ((mmu_version > 0) && (mmu_buildnr > 0))
 				lcd_printf_P(PSTR("%d.%d.%d-%d"), mmu_version/100, mmu_version%100/10, mmu_version%10, mmu_buildnr);
 			else
@@ -1919,7 +1922,7 @@ void mFilamentItem(uint16_t nTemp, uint16_t nTempBed)
             nLevel = bFilamentPreheatState ? 1 : 2;
             bFilamentAction = true;
             menu_back(nLevel);
-            extr_unload();
+            MMU2::mmu2.unload();
             break;
         case FilamentAction::MmuEject:
             nLevel = bFilamentPreheatState ? 1 : 2;
@@ -3430,15 +3433,15 @@ static void lcd_show_sensors_state()
 	uint8_t idler_state = STATE_NA;
 
 	pinda_state = READ(Z_MIN_PIN);
-	if (mmu_enabled && !mmu_last_finda_response.expired(1000))
+	if (MMU2::mmu2.Enabled())
 	{
-		finda_state = mmu_finda;
+		finda_state = MMU2::mmu2.FindaDetectsFilament();
 	}
 	lcd_puts_at_P(0, 0, MSG_PINDA);
 	lcd_set_cursor(LCD_WIDTH - 14, 0);
 	lcd_print_state(pinda_state);
 	
-	if (mmu_enabled == true)
+	if (MMU2::mmu2.Enabled())
 	{
 		lcd_puts_at_P(10, 0, _n("FINDA"));////MSG_FINDA c=5
 		lcd_set_cursor(LCD_WIDTH - 3, 0);
@@ -3775,7 +3778,7 @@ void lcd_first_layer_calibration_reset()
 
 void lcd_v2_calibration()
 {
-	if (mmu_enabled)
+	if (MMU2::mmu2.Enabled())
 	{
 	    const uint8_t filament = choose_menu_P(
             _T(MSG_SELECT_FILAMENT),
@@ -3882,22 +3885,19 @@ static void wait_preheat()
 	
 }
 
-static void lcd_wizard_load()
-{
-	if (mmu_enabled)
-	{
-		lcd_show_fullscreen_message_and_wait_P(_i("Please insert filament into the first tube of the MMU, then press the knob to load it."));////MSG_MMU_INSERT_FILAMENT_FIRST_TUBE c=20 r=6
-		tmp_extruder = 0;
-	} 
-	else
-	{
-		lcd_show_fullscreen_message_and_wait_P(_i("Please insert filament into the extruder, then press the knob to load it."));////MSG_WIZARD_LOAD_FILAMENT c=20 r=6
-	}	
-	lcd_update_enable(false);
-	lcd_clear();
-	lcd_puts_at_P(0, 2, _T(MSG_LOADING_FILAMENT));
-	loading_flag = true;
-	gcode_M701();
+static void lcd_wizard_load() {
+    if (MMU2::mmu2.Enabled()) {
+        lcd_show_fullscreen_message_and_wait_P(
+            _i("Please insert filament into the first tube of the MMU, then press the knob to load it.")); ////MSG_MMU_INSERT_FILAMENT_FIRST_TUBE c=20 r=6
+    } else {
+        lcd_show_fullscreen_message_and_wait_P(
+            _i("Please insert filament into the extruder, then press the knob to load it.")); ////MSG_WIZARD_LOAD_FILAMENT c=20 r=6
+    }
+    lcd_update_enable(false);
+    lcd_clear();
+    lcd_puts_at_P(0, 2, _T(MSG_LOADING_FILAMENT));
+    loading_flag = true;
+    gcode_M701(0);
 }
 
 bool lcd_autoDepleteEnabled()
@@ -3913,7 +3913,7 @@ static void wizard_lay1cal_message(bool cold)
 {
     lcd_show_fullscreen_message_and_wait_P(
             _i("Now I will calibrate distance between tip of the nozzle and heatbed surface.")); ////MSG_WIZARD_V2_CAL c=20 r=8
-    if (mmu_enabled)
+    if (MMU2::mmu2.Enabled())
     {
         lcd_show_fullscreen_message_and_wait_P(
                 _i("Select a filament for the First Layer Calibration and select it in the on-screen menu."));////MSG_SELECT_FIL_1ST_LAYERCAL c=20 r=7
@@ -4056,7 +4056,7 @@ void lcd_wizard(WizState state)
 		    //start to preheat nozzle and bed to save some time later
 			setTargetHotend(PLA_PREHEAT_HOTEND_TEMP, 0);
 			setTargetBed(PLA_PREHEAT_HPB_TEMP);
-			if (mmu_enabled)
+			if (MMU2::mmu2.Enabled())
 			{
 			    wizard_event = lcd_show_fullscreen_message_yes_no_and_wait_P(_T(MSG_FILAMENT_LOADED), true);
 			} else
@@ -4066,7 +4066,7 @@ void lcd_wizard(WizState state)
 			if (wizard_event) state = S::Lay1CalCold;
 			else
 			{
-			    if(mmu_enabled) state = S::LoadFilCold;
+			    if(MMU2::mmu2.Enabled()) state = S::LoadFilCold;
 			    else state = S::Preheat;
 			}
 			break;
@@ -4256,7 +4256,7 @@ static void auto_deplete_switch()
 
 static void settingsAutoDeplete()
 {
-    if (mmu_enabled)
+    if (MMU2::mmu2.Enabled())
     {
 #ifdef FILAMENT_SENSOR
         if (fsensor.isError()) {
@@ -4280,7 +4280,7 @@ while(0)\
 #ifdef MMU_HAS_CUTTER
 static void settingsCutter()
 {
-    if (mmu_enabled)
+    if (MMU2::mmu2.Enabled())
     {
         if (EEPROM_MMU_CUTTER_ENABLED_enabled == eeprom_read_byte((uint8_t*)EEPROM_MMU_CUTTER_ENABLED))
         {
@@ -4360,7 +4360,7 @@ while (0)
 #define SETTINGS_MMU_MODE \
 do\
 {\
-	if (mmu_enabled)\
+	if (MMU2::mmu2.Enabled())\
 	{\
 		if (SilentModeMenu_MMU == 0) MENU_ITEM_TOGGLE_P(_T(MSG_MMU_MODE), _T(MSG_NORMAL), lcd_silent_mode_mmu_set);\
 		else MENU_ITEM_TOGGLE_P(_T(MSG_MMU_MODE), _T(MSG_STEALTH), lcd_silent_mode_mmu_set);\
@@ -4716,7 +4716,7 @@ void lcd_hw_setup_menu(void)                      // can not be "static"
 #if defined(FILAMENT_SENSOR) && (FILAMENT_SENSOR_TYPE == FSENSOR_IR_ANALOG)
     //! Fsensor Detection isn't ready for mmu yet it is temporarily disabled.
     //! @todo Don't forget to remove this as soon Fsensor Detection works with mmu
-    if(!mmu_enabled) MENU_ITEM_FUNCTION_P(PSTR("Fsensor Detection"), lcd_detect_IRsensor);
+    if(!MMU2::mmu2.Enabled()) MENU_ITEM_FUNCTION_P(PSTR("Fsensor Detection"), lcd_detect_IRsensor);
 #endif //IR_SENSOR_ANALOG
 
     if (_md->experimental_menu_visibility)
@@ -4878,7 +4878,7 @@ static void lcd_calibration_menu()
 //!
 //! Create list of items with header. Header can not be selected.
 //! Each item has text description passed by function parameter and
-//! number. There are 5 numbered items, if mmu_enabled, 4 otherwise.
+//! number. There are 5 numbered items, if MMU2::mmu2.Enabled(), 4 otherwise.
 //! Items are numbered from 1 to 4 or 5. But index returned starts at 0.
 //! There can be last item with different text and no number.
 //!
@@ -4889,7 +4889,7 @@ static void lcd_calibration_menu()
 uint8_t choose_menu_P(const char *header, const char *item, const char *last_item)
 {
     //following code should handle 3 to 127 number of items well
-    const int8_t items_no = last_item?(mmu_enabled?6:5):(mmu_enabled?5:4);
+    const int8_t items_no = last_item?(MMU2::mmu2.Enabled()?6:5):(MMU2::mmu2.Enabled()?5:4);
     const uint8_t item_len = item?strlen_P(item):0;
 	int8_t first = 0;
 	int8_t enc_dif = lcd_encoder_diff;
@@ -5061,68 +5061,71 @@ static void lcd_disable_farm_mode()
 	
 }
 
+static inline void load_all_wrapper(){
+    for(uint8_t i = 0; i < 5; ++i){
+        MMU2::mmu2.load_filament(i);
+    }
+}
 
+static inline void load_filament_wrapper(uint8_t i){
+    MMU2::mmu2.load_filament(i);
+}
 
-static void mmu_load_filament_menu()
-{
+static void mmu_load_filament_menu() {
     MENU_BEGIN();
     MENU_ITEM_BACK_P(_T(MSG_MAIN));
-    MENU_ITEM_FUNCTION_P(_i("Load all"), load_all); ////MSG_LOAD_ALL c=18
+    MENU_ITEM_FUNCTION_P(_i("Load all"), load_all_wrapper); ////MSG_LOAD_ALL c=18
     for (uint8_t i = 0; i < MMU_FILAMENT_COUNT; i++)
-        MENU_ITEM_FUNCTION_NR_P(_T(MSG_LOAD_FILAMENT), i + '1', extr_adj, i); ////MSG_LOAD_FILAMENT c=16
+        MENU_ITEM_FUNCTION_NR_P(_T(MSG_LOAD_FILAMENT), i + '1', load_filament_wrapper, i); ////MSG_LOAD_FILAMENT c=16
     MENU_END();
 }
 
-static void mmu_load_to_nozzle_menu()
-{
-    if (bFilamentAction)
-    {
+static inline void lcd_mmu_load_to_nozzle_wrapper(uint8_t index){
+    MMU2::mmu2.load_filament_to_nozzle(index);
+}
+
+static void mmu_load_to_nozzle_menu() {
+    if (bFilamentAction) {
         MENU_BEGIN();
         MENU_ITEM_BACK_P(_T(MSG_MAIN));
         for (uint8_t i = 0; i < MMU_FILAMENT_COUNT; i++)
-            MENU_ITEM_FUNCTION_NR_P(_T(MSG_LOAD_FILAMENT), i + '1', lcd_mmu_load_to_nozzle, i); ////MSG_LOAD_FILAMENT c=16
+            MENU_ITEM_FUNCTION_NR_P(_T(MSG_LOAD_FILAMENT), i + '1', lcd_mmu_load_to_nozzle_wrapper, i); ////MSG_LOAD_FILAMENT c=16
         MENU_END();
-    }
-    else
-    {
+    } else {
         eFilamentAction = FilamentAction::MmuLoad;
         preheat_or_continue();
     }
 }
 
-static void mmu_eject_filament(uint8_t filament)
-{
+static void mmu_eject_filament(uint8_t filament) {
     menu_back();
-    mmu_eject_filament(filament, true);
+    MMU2::mmu2.eject_filament(filament, true);
 }
 
-static void mmu_fil_eject_menu()
-{
-    if (bFilamentAction)
-    {
+static void mmu_fil_eject_menu() {
+    if (bFilamentAction) {
         MENU_BEGIN();
         MENU_ITEM_BACK_P(_T(MSG_MAIN));
         for (uint8_t i = 0; i < MMU_FILAMENT_COUNT; i++)
             MENU_ITEM_FUNCTION_NR_P(_T(MSG_EJECT_FILAMENT), i + '1', mmu_eject_filament, i); ////MSG_EJECT_FILAMENT c=16
         MENU_END();
-    }
-    else
-    {
+    } else {
         eFilamentAction = FilamentAction::MmuEject;
         preheat_or_continue();
     }
 }
 
 #ifdef MMU_HAS_CUTTER
+static inline void mmu_cut_filament_wrapper(uint8_t index){
+    MMU2::mmu2.cut_filament(index);
+}
 
-static void mmu_cut_filament_menu()
-{
-    if(bFilamentAction)
-    {
+static void mmu_cut_filament_menu() {
+    if (bFilamentAction) {
         MENU_BEGIN();
         MENU_ITEM_BACK_P(_T(MSG_MAIN));
         for (uint8_t i = 0; i < MMU_FILAMENT_COUNT; i++)
-            MENU_ITEM_FUNCTION_NR_P(_T(MSG_CUT_FILAMENT), i + '1', mmu_cut_filament, i); ////MSG_CUT_FILAMENT c=16
+            MENU_ITEM_FUNCTION_NR_P(_T(MSG_CUT_FILAMENT), i + '1', mmu_cut_filament_wrapper, i); ////MSG_CUT_FILAMENT c=16
         MENU_END();
     }
     else
@@ -5522,7 +5525,7 @@ static void lcd_main_menu()
     }
 
     if ( ! ( IS_SD_PRINTING || usb_timer.running() || (lcd_commands_type == LcdCommands::Layer1Cal) ) ) {
-        if (mmu_enabled) {
+        if (MMU2::mmu2.Enabled()) {
             MENU_ITEM_SUBMENU_P(_T(MSG_LOAD_FILAMENT), mmu_load_filament_menu);
             MENU_ITEM_SUBMENU_P(_i("Load to nozzle"), mmu_load_to_nozzle_menu);////MSG_LOAD_TO_NOZZLE c=18
             MENU_ITEM_SUBMENU_P(_T(MSG_UNLOAD_FILAMENT), mmu_unload_filament);
@@ -5532,7 +5535,7 @@ static void lcd_main_menu()
 #endif //MMU_HAS_CUTTER
         } else {
 #ifdef FILAMENT_SENSOR
-            if (fsensor.getAutoLoadEnabled() && (mmu_enabled == false)) {
+            if (fsensor.getAutoLoadEnabled() && (MMU2::mmu2.Enabled() == false)) {
                 MENU_ITEM_SUBMENU_P(_i("AutoLoad filament"), lcd_menu_AutoLoadFilament);////MSG_AUTOLOAD_FILAMENT c=18
             }
             else
@@ -5553,7 +5556,7 @@ static void lcd_main_menu()
 #if defined(TMC2130) || defined(FILAMENT_SENSOR)
     MENU_ITEM_SUBMENU_P(_i("Fail stats"), lcd_menu_fails_stats);////MSG_FAIL_STATS c=18
 #endif
-    if (mmu_enabled) {
+    if (MMU2::mmu2.Enabled()) {
         MENU_ITEM_SUBMENU_P(_i("Fail stats MMU"), lcd_menu_fails_stats_mmu);////MSG_MMU_FAIL_STATS c=18
     }
     MENU_ITEM_SUBMENU_P(_i("Support"), lcd_support_menu);////MSG_SUPPORT c=18
@@ -5902,7 +5905,7 @@ void print_stop()
         fanSpeed = 0;
     }
 
-    if (mmu_enabled) extr_unload(); //M702 C
+    if (MMU2::mmu2.Enabled()) MMU2::mmu2.unload(); //M702 C
     finishAndDisableSteppers(); //M84
     axis_relative_modes = E_AXIS_MASK; //XYZ absolute, E relative
 }
@@ -6228,7 +6231,7 @@ bool lcd_selftest()
 	//!   As the Fsensor Detection isn't yet ready for the mmu2s we set temporarily the IR sensor 0.3 or older for mmu2s
 	//! @todo Don't forget to remove this as soon Fsensor Detection works with mmu
 	if(fsensor.getSensorRevision() == IR_sensor_analog::SensorRevision::_Undef) {
-		if (!mmu_enabled) {
+		if (!MMU2::mmu2.Enabled()) {
 			lcd_detect_IRsensor();
 		}
 		else {
@@ -6424,7 +6427,7 @@ bool lcd_selftest()
     if (_result)
     {
 #if (FILAMENT_SENSOR_TYPE == FSENSOR_IR) || (FILAMENT_SENSOR_TYPE == FSENSOR_IR_ANALOG)
-        if (mmu_enabled)
+        if (MMU2::mmu2.Enabled())
         {        
 			_progress = lcd_selftest_screen(TestScreen::Fsensor, _progress, 3, true, 2000); //check filaments sensor
             _result = selftest_irsensor();
@@ -7048,19 +7051,18 @@ static bool selftest_irsensor()
     {
         TempBackup tempBackup;
         setTargetHotend(ABS_PREHEAT_HOTEND_TEMP,active_extruder);
-        mmu_wait_for_heater_blocking();
+//@@TODO        mmu_wait_for_heater_blocking();
         progress = lcd_selftest_screen(TestScreen::Fsensor, 0, 1, true, 0);
-        mmu_filament_ramming();
+//@@TODO        mmu_filament_ramming();
     }
     progress = lcd_selftest_screen(TestScreen::Fsensor, progress, 1, true, 0);
-    mmu_command(MmuCmd::U0);
-    manage_response(false, false);
+    MMU2::mmu2.unload(); // mmu_command(MmuCmd::U0); manage_response(false, false);
 
     for(uint_least8_t i = 0; i < 200; ++i)
     {
         if (0 == (i % 32)) progress = lcd_selftest_screen(TestScreen::Fsensor, progress, 1, true, 0);
 
-        mmu_load_step(false);
+//@@TODO        mmu_load_step(false);
         while (blocks_queued())
         {
             if (fsensor.getFilamentPresent())