diff --git a/Marlin/Configuration.h b/Marlin/Configuration.h
index 404a1422ee8b..d926d4fc84ed 100644
--- a/Marlin/Configuration.h
+++ b/Marlin/Configuration.h
@@ -385,14 +385,15 @@
* PRUSA_MMU1 : Průša MMU1 (The "multiplexer" version)
* PRUSA_MMU2 : Průša MMU2
* PRUSA_MMU2S : Průša MMU2S (Requires MK3S extruder with motion sensor, EXTRUDERS = 5)
+ * PRUSA_MMU3 : Průša MMU3 (Requires MK3S extruder with motion sensor and MMU firmware version 3.x.x, EXTRUDERS = 5)
* EXTENDABLE_EMU_MMU2 : MMU with configurable number of filaments (ERCF, SMuFF or similar with Průša MMU2 compatible firmware)
* EXTENDABLE_EMU_MMU2S : MMUS with configurable number of filaments (ERCF, SMuFF or similar with Průša MMU2 compatible firmware)
*
* Requires NOZZLE_PARK_FEATURE to park print head in case MMU unit fails.
* See additional options in Configuration_adv.h.
- * :["PRUSA_MMU1", "PRUSA_MMU2", "PRUSA_MMU2S", "EXTENDABLE_EMU_MMU2", "EXTENDABLE_EMU_MMU2S"]
+ * :["PRUSA_MMU1", "PRUSA_MMU2", "PRUSA_MMU2S", "PRUSA_MMU3", "EXTENDABLE_EMU_MMU2", "EXTENDABLE_EMU_MMU2S"]
*/
-//#define MMU_MODEL PRUSA_MMU2
+//#define MMU_MODEL PRUSA_MMU3
// @section psu control
diff --git a/Marlin/Configuration_adv.h b/Marlin/Configuration_adv.h
index 0813b4525223..281edfa88d1a 100644
--- a/Marlin/Configuration_adv.h
+++ b/Marlin/Configuration_adv.h
@@ -1127,8 +1127,8 @@
#define FTM_DEFAULT_DYNFREQ_MODE dynFreqMode_DISABLED // Default mode of dynamic frequency calculation. (DISABLED, Z_BASED, MASS_BASED)
#define FTM_DEFAULT_SHAPER_X ftMotionShaper_NONE // Default shaper mode on X axis (NONE, ZV, ZVD, ZVDD, ZVDDD, EI, 2HEI, 3HEI, MZV)
#define FTM_DEFAULT_SHAPER_Y ftMotionShaper_NONE // Default shaper mode on Y axis
- #define FTM_SHAPING_DEFAULT_X_FREQ 37.0f // (Hz) Default peak frequency used by input shapers
- #define FTM_SHAPING_DEFAULT_Y_FREQ 37.0f // (Hz) Default peak frequency used by input shapers
+ #define FTM_SHAPING_DEFAULT_FREQ_X 37.0f // (Hz) Default peak frequency used by input shapers
+ #define FTM_SHAPING_DEFAULT_FREQ_Y 37.0f // (Hz) Default peak frequency used by input shapers
#define FTM_LINEAR_ADV_DEFAULT_ENA false // Default linear advance enable (true) or disable (false)
#define FTM_LINEAR_ADV_DEFAULT_K 0 // Default linear advance gain, integer value. (Acceleration-based scaling factor.)
#define FTM_SHAPING_ZETA_X 0.1f // Zeta used by input shapers for X axis
@@ -4389,44 +4389,89 @@
//#define E_MUX0_PIN 40 // Always Required
//#define E_MUX1_PIN 42 // Needed for 3 to 8 inputs
//#define E_MUX2_PIN 44 // Needed for 5 to 8 inputs
-#elif HAS_PRUSA_MMU2
- // Serial port used for communication with MMU2.
+#elif HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3
+ // Common settings for MMU2/MMU2S/MMU3
+ // Serial port used for communication with MMU2/MMU2S/MMU3.
#define MMU2_SERIAL_PORT 2
+ #define MMU_BAUD 115200
// Use hardware reset for MMU if a pin is defined for it
//#define MMU2_RST_PIN 23
- // Enable if the MMU2 has 12V stepper motors (MMU2 Firmware 1.0.2 and up)
- //#define MMU2_MODE_12V
+ #if HAS_PRUSA_MMU2
+ // Enable if the MMU2 has 12V stepper motors (MMU2 Firmware 1.0.2 and up)
+ //#define MMU2_MODE_12V
- // G-code to execute when MMU2 F.I.N.D.A. probe detects filament runout
- #define MMU2_FILAMENT_RUNOUT_SCRIPT "M600"
+ // G-code to execute when MMU2 F.I.N.D.A. probe detects filament runout
+ #define MMU2_FILAMENT_RUNOUT_SCRIPT "M600"
+ #endif
- // Add an LCD menu for MMU2
- //#define MMU2_MENUS
+ // Add an LCD menu for MMU2/MMU2S/MMU3
+ //#define MMU_MENUS
// Settings for filament load / unload from the LCD menu.
// This is for Průša MK3-style extruders. Customize for your hardware.
#define MMU2_FILAMENTCHANGE_EJECT_FEED 80.0
+
+ /**
+ * ------------
+ * MMU2 / MMU2S
+ * ------------
+ * MMU2 sequences use mm/min. Not compatible with MMU3 (see below).
+ * #define MMU2_LOAD_TO_NOZZLE_SEQUENCE \
+ * { 4.4, 871 }, \
+ * { 10.0, 1393 }, \
+ * { 4.4, 871 }, \
+ * { 10.0, 198 }
+ */
+
+ /* #define MMU2_RAMMING_SEQUENCE \
+ * { 1.0, 1000 }, \
+ * { 1.0, 1500 }, \
+ * { 2.0, 2000 }, \
+ * { 1.5, 3000 }, \
+ * { 2.5, 4000 }, \
+ * { -15.0, 5000 }, \
+ * { -14.0, 1200 }, \
+ * { -6.0, 600 }, \
+ * { 10.0, 700 }, \
+ * { -10.0, 400 }, \
+ * { -50.0, 2000 }
+ */
+
+ /**
+ * ----
+ * MMU3
+ * ----
+ * These values are compatible with MMU3 as they are defined in mm/s
+ */
+
+ #define MMU2_EXTRUDER_PTFE_LENGTH 42.3 // (mm)
+ #define MMU2_EXTRUDER_HEATBREAK_LENGTH 17.7 // (mm)
+
#define MMU2_LOAD_TO_NOZZLE_SEQUENCE \
- { 7.2, 1145 }, \
- { 14.4, 871 }, \
- { 36.0, 1393 }, \
- { 14.4, 871 }, \
- { 50.0, 198 }
+ { MMU2_EXTRUDER_PTFE_LENGTH, MMM_TO_MMS(810) }, /* (13.5 mm/s) Fast load ahead of heatbreak */ \
+ { MMU2_EXTRUDER_HEATBREAK_LENGTH, MMM_TO_MMS(198) } // ( 3.3 mm/s) Slow load after heatbreak
#define MMU2_RAMMING_SEQUENCE \
- { 1.0, 1000 }, \
- { 1.0, 1500 }, \
- { 2.0, 2000 }, \
- { 1.5, 3000 }, \
- { 2.5, 4000 }, \
- { -15.0, 5000 }, \
- { -14.0, 1200 }, \
- { -6.0, 600 }, \
- { 10.0, 700 }, \
- { -10.0, 400 }, \
- { -50.0, 2000 }
+ { 0.2816, MMM_TO_MMS(1339.0) }, \
+ { 0.3051, MMM_TO_MMS(1451.0) }, \
+ { 0.3453, MMM_TO_MMS(1642.0) }, \
+ { 0.3990, MMM_TO_MMS(1897.0) }, \
+ { 0.4761, MMM_TO_MMS(2264.0) }, \
+ { 0.5767, MMM_TO_MMS(2742.0) }, \
+ { 0.5691, MMM_TO_MMS(3220.0) }, \
+ { 0.1081, MMM_TO_MMS(3220.0) }, \
+ { 0.7644, MMM_TO_MMS(3635.0) }, \
+ { 0.8248, MMM_TO_MMS(3921.0) }, \
+ { 0.8483, MMM_TO_MMS(4033.0) }, \
+ { -15.0, MMM_TO_MMS(6000.0) }, \
+ { -24.5, MMM_TO_MMS(1200.0) }, \
+ { -7.0, MMM_TO_MMS( 600.0) }, \
+ { -3.5, MMM_TO_MMS( 360.0) }, \
+ { 20.0, MMM_TO_MMS( 454.0) }, \
+ { -20.0, MMM_TO_MMS( 303.0) }, \
+ { -35.0, MMM_TO_MMS(2000.0) }
/**
* Using a sensor like the MMU2S
@@ -4436,11 +4481,26 @@
#if HAS_PRUSA_MMU2S
#define MMU2_C0_RETRY 5 // Number of retries (total time = timeout*retries)
+ /**
+ * This is called after the filament runout sensor is triggered to check if
+ * the filament has been loaded properly by moving the filament back and
+ * forth to see if the filament runout sensor is going to get triggered
+ * again, which should not occur if the filament is properly loaded.
+ *
+ * Thus, the MMU2_CAN_LOAD_SEQUENCE should contain some forward and
+ * backward moves. The forward moves should be greater than the backward
+ * moves.
+ *
+ * This is useless if your filament runout sensor is way behind the gears.
+ * In that case use {0, MMU2_CAN_LOAD_FEEDRATE}
+ *
+ * Adjust MMU2_CAN_LOAD_SEQUENCE according to your setup.
+ */
#define MMU2_CAN_LOAD_FEEDRATE 800 // (mm/min)
#define MMU2_CAN_LOAD_SEQUENCE \
- { 0.1, MMU2_CAN_LOAD_FEEDRATE }, \
- { 60.0, MMU2_CAN_LOAD_FEEDRATE }, \
- { -52.0, MMU2_CAN_LOAD_FEEDRATE }
+ { 5.0, MMU2_CAN_LOAD_FEEDRATE }, \
+ { 15.0, MMU2_CAN_LOAD_FEEDRATE }, \
+ { -10.0, MMU2_CAN_LOAD_FEEDRATE }
#define MMU2_CAN_LOAD_RETRACT 6.0 // (mm) Keep under the distance between Load Sequence values
#define MMU2_CAN_LOAD_DEVIATION 0.8 // (mm) Acceptable deviation
@@ -4451,6 +4511,68 @@
// Continue unloading if sensor detects filament after the initial unload move
//#define MMU_IR_UNLOAD_MOVE
+
+ #elif HAS_PRUSA_MMU3
+
+ // MMU3 settings
+
+ #define MMU2_MAX_RETRIES 3 // Number of retries (total time = timeout*retries)
+
+ // Nominal distance from the extruder gear to the nozzle tip is 87mm
+ // However, some slipping may occur and we need separate distances for
+ // LoadToNozzle and ToolChange.
+ // - +5mm seemed good for LoadToNozzle,
+ // - but too much (made blobs) for a ToolChange
+ #define MMU2_LOAD_TO_NOZZLE_LENGTH 87.0 + 5.0
+
+ // As discussed with our PrusaSlicer profile specialist
+ // - ToolChange shall not try to push filament into the very tip of the nozzle
+ // to have some space for additional G-code to tune the extruded filament length
+ // in the profile
+ // Beware - this value is used to initialize the MMU logic layer - it will be sent to the MMU upon line up (written into its 8bit register 0x0b)
+ // However - in the G-code we can get a request to set the extra load distance at runtime to something else (M708 A0xb Xsomething).
+ // The printer intercepts such a call and sets its extra load distance to match the new value as well.
+ #define MMU2_FILAMENT_SENSOR_POSITION 0 // (mm)
+ #define MMU2_LOAD_DISTANCE_PAST_GEARS 5 // (mm)
+ #define MMU2_TOOL_CHANGE_LOAD_LENGTH MMU2_FILAMENT_SENSOR_POSITION + MMU2_LOAD_DISTANCE_PAST_GEARS // (mm)
+
+ #define MMU2_LOAD_TO_NOZZLE_FEED_RATE 20.0 // (mm/s)
+ #define MMU2_UNLOAD_TO_FINDA_FEED_RATE 120.0 // (mm/s)
+
+ #define MMU2_VERIFY_LOAD_TO_NOZZLE_FEED_RATE 50.0 // (mm/s)
+ #define MMU2_VERIFY_LOAD_TO_NOZZLE_TWEAK -5.0 // (mm) Amount to adjust the length for verifying load-to-nozzle
+
+ // The first thing the MMU does is initialize its axis.
+ // Meanwhile the E-motor will unload 20mm of filament in about 1 second.
+ #define MMU2_RETRY_UNLOAD_TO_FINDA_LENGTH 80.0 // (mm)
+ #define MMU2_RETRY_UNLOAD_TO_FINDA_FEED_RATE 80.0 // (mm/s)
+
+ // After loading a new filament, the printer will extrude this length of filament
+ // then retract to the original position. This is used to check if the filament sensor
+ // reading flickers or filament is jammed.
+ #define MMU2_CHECK_FILAMENT_PRESENCE_EXTRUSION_LENGTH (MMU2_EXTRUDER_PTFE_LENGTH + MMU2_EXTRUDER_HEATBREAK_LENGTH + MMU2_VERIFY_LOAD_TO_NOZZLE_TWEAK + MMU2_FILAMENT_SENSOR_POSITION) // (mm)
+
+ #define MMU_HAS_CUTTER // Enable cutter related functionalities
+ //#define MMU_FORCE_STEALTH_MODE // Force stealth mode and disable menu item
+
+ /**
+ * SpoolJoin Consumes All Filament -- EXPERIMENTAL
+ *
+ * SpoolJoin normally triggers when FINDA sensor untriggers while printing.
+ * This is the default behaviour and it doesn't consume all the filament
+ * before triggering a filament change. This leaves some filament in the
+ * current slot and before switching to the next slot it is unloaded.
+ *
+ * Enabling this option will trigger the filament change when both FINDA
+ * and Filament Runout Sensor triggers during the print and it allows the
+ * filament in the current slot to be completely consumed before doing the
+ * filament change. But this can cause problems as a little bit of filament
+ * will be left between the extruder gears (thinking that the filament
+ * sensor is triggered through the gears) and the end of the PTFE tube and
+ * can cause filament load issues.
+ */
+ //#define MMU_SPOOL_JOIN_CONSUMES_ALL_FILAMENT
+
#else
/**
@@ -4473,7 +4595,7 @@
//#define MMU2_DEBUG // Write debug info to serial output
-#endif // HAS_PRUSA_MMU2
+#endif // HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3
/**
* Advanced Print Counter settings
diff --git a/Marlin/Version.h b/Marlin/Version.h
index ac1c4f19f438..6feedc9c5966 100644
--- a/Marlin/Version.h
+++ b/Marlin/Version.h
@@ -41,7 +41,7 @@
* here we define this default string as the date where the latest release
* version was tagged.
*/
-//#define STRING_DISTRIBUTION_DATE "2024-08-23"
+//#define STRING_DISTRIBUTION_DATE "2024-08-25"
/**
* Defines a generic printer name to be output to the LCD after booting Marlin.
diff --git a/Marlin/src/MarlinCore.cpp b/Marlin/src/MarlinCore.cpp
index f1594b2ce640..08adb65134d1 100644
--- a/Marlin/src/MarlinCore.cpp
+++ b/Marlin/src/MarlinCore.cpp
@@ -229,12 +229,14 @@
#include "feature/controllerfan.h"
#endif
-#if HAS_PRUSA_MMU1
- #include "feature/mmu/mmu.h"
-#endif
-
-#if HAS_PRUSA_MMU2
+#if HAS_PRUSA_MMU3
+ #include "feature/mmu3/mmu2.h"
+ #include "feature/mmu3/mmu2_reporting.h"
+ #include "feature/mmu3/SpoolJoin.h"
+#elif HAS_PRUSA_MMU2
#include "feature/mmu/mmu2.h"
+#elif HAS_PRUSA_MMU1
+ #include "feature/mmu/mmu.h"
#endif
#if ENABLED(PASSWORD_FEATURE)
@@ -351,6 +353,7 @@ void startOrResumeJob() {
TERN_(CANCEL_OBJECTS, cancelable.reset());
TERN_(LCD_SHOW_E_TOTAL, e_move_accumulator = 0);
TERN_(SET_REMAINING_TIME, ui.reset_remaining_time());
+ TERN_(HAS_PRUSA_MMU3, MMU3::operation_statistics.reset_per_print_stats());
}
print_job_timer.start();
}
@@ -785,7 +788,7 @@ void idle(const bool no_stepper_sleep/*=false*/) {
// Handle filament runout sensors
#if HAS_FILAMENT_SENSOR
- if (TERN1(HAS_PRUSA_MMU2, !mmu2.enabled()))
+ if (TERN1(HAS_PRUSA_MMU2, !mmu2.enabled()) && TERN1(HAS_PRUSA_MMU3, !mmu3.enabled()))
runout.run();
#endif
@@ -850,7 +853,11 @@ void idle(const bool no_stepper_sleep/*=false*/) {
#endif
// Update the Průša MMU2
- TERN_(HAS_PRUSA_MMU2, mmu2.mmu_loop());
+ #if HAS_PRUSA_MMU3
+ mmu3.mmu_loop();
+ #elif HAS_PRUSA_MMU2
+ mmu2.mmu_loop();
+ #endif
// Handle Joystick jogging
TERN_(POLL_JOG, joystick.inject_jog_moves());
@@ -1586,7 +1593,11 @@ void setup() {
SETUP_RUN(stepper_driver_backward_report());
#endif
- #if HAS_PRUSA_MMU2
+ #if HAS_PRUSA_MMU3
+ if (mmu3.mmu_hw_enabled) SETUP_RUN(mmu3.start());
+ SETUP_RUN(mmu3.status());
+ SETUP_RUN(spooljoin.initStatus());
+ #elif HAS_PRUSA_MMU2
SETUP_RUN(mmu2.init());
#endif
diff --git a/Marlin/src/feature/mmu/mmu2.cpp b/Marlin/src/feature/mmu/mmu2.cpp
index 5ef56c7eacfc..562cc3303bcc 100644
--- a/Marlin/src/feature/mmu/mmu2.cpp
+++ b/Marlin/src/feature/mmu/mmu2.cpp
@@ -526,7 +526,7 @@ inline void beep_bad_cmd() { BUZZ(400, 40); }
switch (*special) {
case '?': {
- #if ENABLED(MMU2_MENUS)
+ #if ENABLED(MMU_MENUS)
const uint8_t index = mmu2_choose_filament();
while (!thermalManager.wait_for_hotend(active_extruder, false)) safe_delay(100);
load_to_nozzle(index);
@@ -536,7 +536,7 @@ inline void beep_bad_cmd() { BUZZ(400, 40); }
} break;
case 'x': {
- #if ENABLED(MMU2_MENUS)
+ #if ENABLED(MMU_MENUS)
planner.synchronize();
const uint8_t index = mmu2_choose_filament();
stepper.disable_extruder();
@@ -614,7 +614,7 @@ inline void beep_bad_cmd() { BUZZ(400, 40); }
switch (*special) {
case '?': {
DEBUG_ECHOLNPGM("case ?\n");
- #if ENABLED(MMU2_MENUS)
+ #if ENABLED(MMU_MENUS)
uint8_t index = mmu2_choose_filament();
while (!thermalManager.wait_for_hotend(active_extruder, false)) safe_delay(100);
load_to_nozzle(index);
@@ -625,7 +625,7 @@ inline void beep_bad_cmd() { BUZZ(400, 40); }
case 'x': {
DEBUG_ECHOLNPGM("case x\n");
- #if ENABLED(MMU2_MENUS)
+ #if ENABLED(MMU_MENUS)
planner.synchronize();
uint8_t index = mmu2_choose_filament();
stepper.disable_extruder();
@@ -729,7 +729,7 @@ inline void beep_bad_cmd() { BUZZ(400, 40); }
switch (*special) {
case '?': {
DEBUG_ECHOLNPGM("case ?\n");
- #if ENABLED(MMU2_MENUS)
+ #if ENABLED(MMU_MENUS)
uint8_t index = mmu2_choose_filament();
while (!thermalManager.wait_for_hotend(active_extruder, false)) safe_delay(100);
load_to_nozzle(index);
@@ -740,7 +740,7 @@ inline void beep_bad_cmd() { BUZZ(400, 40); }
case 'x': {
DEBUG_ECHOLNPGM("case x\n");
- #if ENABLED(MMU2_MENUS)
+ #if ENABLED(MMU_MENUS)
planner.synchronize();
uint8_t index = mmu2_choose_filament();
stepper.disable_extruder();
diff --git a/Marlin/src/feature/mmu3/SpoolJoin.cpp b/Marlin/src/feature/mmu3/SpoolJoin.cpp
new file mode 100644
index 000000000000..48495a225c1b
--- /dev/null
+++ b/Marlin/src/feature/mmu3/SpoolJoin.cpp
@@ -0,0 +1,73 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/**
+ * SpoolJoin.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "SpoolJoin.h"
+#include "../../module/settings.h"
+#include "../../core/language.h"
+
+SpoolJoin spooljoin;
+
+bool SpoolJoin::enabled; // Initialized by settings.load
+int SpoolJoin::epprom_addr; // Initialized by settings.load
+uint8_t SpoolJoin::currentMMUSlot;
+
+SpoolJoin::SpoolJoin() { setSlot(0); }
+
+void SpoolJoin::initStatus() {
+ // Useful information to see during bootup
+ SERIAL_ECHOLN(F("SpoolJoin is "), enabled ? F("On") : F("Off"));
+}
+
+void SpoolJoin::toggle() {
+ // Toggle enabled value.
+ enabled = !enabled;
+
+ // Following Prusa's implementation let's save the value to the EEPROM
+ // TODO: Move to settings.cpp
+ #if ENABLED(EEPROM_SETTINGS)
+ persistentStore.access_start();
+ persistentStore.write_data(epprom_addr, enabled);
+ persistentStore.access_finish();
+ settings.save();
+ #endif
+}
+
+bool SpoolJoin::isEnabled() { return enabled; }
+
+void SpoolJoin::setSlot(const uint8_t slot) { currentMMUSlot = slot; }
+
+uint8_t SpoolJoin::nextSlot() {
+ SERIAL_ECHOPGM("SpoolJoin: ", currentMMUSlot);
+ if (++currentMMUSlot >= 4) currentMMUSlot = 0;
+ SERIAL_ECHOLNPGM(" -> ", currentMMUSlot);
+ return currentMMUSlot;
+}
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/feature/mmu3/SpoolJoin.h b/Marlin/src/feature/mmu3/SpoolJoin.h
new file mode 100644
index 000000000000..b205d26ef501
--- /dev/null
+++ b/Marlin/src/feature/mmu3/SpoolJoin.h
@@ -0,0 +1,72 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * SpoolJoin.h
+ */
+
+#include "../../MarlinCore.h"
+
+#include
+
+// See documentation here: https://help.prusa3d.com/article/spooljoin-mmu2s_134252
+
+class SpoolJoin {
+public:
+ SpoolJoin();
+
+ enum class EEPROM : uint8_t {
+ Unknown, //!< SpoolJoin is unknown while printer is booting up
+ Enabled, //!< SpoolJoin is enabled in EEPROM
+ Disabled, //!< SpoolJoin is disabled in EEPROM
+ Empty = 0xFF //!< EEPROM has not been set before and all bits are 1 (0xFF) - either a new printer or user erased the memory
+ };
+
+ // @brief Contrary to Prusa's implementation we store the enabled status in a variable
+ static int epprom_addr;
+ static bool enabled;
+
+ // @brief Called when EEPROM is ready to be read
+ static void initStatus();
+
+ // @brief Toggle SpoolJoin
+ static void toggle();
+
+ // @brief Check if SpoolJoin is enabled
+ // @return true if enabled, false if disabled
+ static bool isEnabled();
+
+ // @brief Update the saved MMU slot number so SpoolJoin can determine the next slot to use
+ // @param slot number of the slot to set
+ static void setSlot(const uint8_t slot);
+
+ // @brief Fetch the next slot number (0 to 4).
+ // When filament slot 4 is depleted, the next slot should be 0.
+ // @return the next slot (0 to 4)
+ static uint8_t nextSlot();
+
+private:
+ static uint8_t currentMMUSlot; //!< Currently used slot (0 to 4)
+};
+
+extern SpoolJoin spooljoin;
diff --git a/Marlin/src/feature/mmu3/mmu2.cpp b/Marlin/src/feature/mmu3/mmu2.cpp
new file mode 100644
index 000000000000..ed4a04f080b3
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2.cpp
@@ -0,0 +1,1185 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/**
+ * mmu2.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "mmu2.h"
+#include "mmu2_error_converter.h"
+#include "mmu2_fsensor.h"
+#include "mmu2_log.h"
+#include "mmu2_marlin.h"
+#include "mmu2_marlin_macros.h"
+#include "mmu2_power.h"
+#include "mmu2_progress_converter.h"
+#include "mmu2_reporting.h"
+
+#include "strlen_cx.h"
+#include "SpoolJoin.h"
+
+#include "../../inc/MarlinConfig.h"
+
+#include "../../lcd/marlinui.h"
+#include "../../module/planner.h"
+#include "../../module/motion.h"
+#include "../../gcode/parser.h"
+#include "../../gcode/queue.h"
+#include "../runout.h"
+#if HAS_LEVELING
+ #include "../bedlevel/bedlevel.h"
+#endif
+#include "../pause.h"
+#include "../../libs/stopwatch.h"
+
+// As of FW 3.12 we only support building the FW with only one extruder, all the multi-extruder infrastructure will be removed.
+// Saves at least 800B of code size
+//#ifdef __AVR__
+//static_assert(EXTRUDERS == 1);
+//#endif
+
+#define MMU2_NO_TOOL 99
+
+MMU3::MMU3 mmu3;
+
+namespace MMU3 {
+
+ template
+ void waitForHotendTargetTemp(uint16_t delay, F f) {
+ while (((thermal_degTargetHotend() - thermal_degHotend()) > 5)) {
+ f();
+ safe_delay_keep_alive(delay);
+ }
+ }
+
+ void WaitForHotendTargetTempBeep() {
+ waitForHotendTargetTemp(3000, []{});
+ //MakeSound(Prompt);
+ }
+
+ uint8_t MMU3::cutter_mode; // Initialized by settings.load
+ int MMU3::cutter_mode_addr; // Initialized by settings.load
+ uint8_t MMU3::stealth_mode; // Initialized by settings.load
+ int MMU3::stealth_mode_addr; // Initialized by settings.load
+ // TODO: Currently, by logic, the value stored in the EEPROM for is ignored and
+ // mmu_hw_enabled is always overwritten by the MMU State. Thus restarting
+ // printer will always set the MMU as senabled.
+ bool MMU3::mmu_hw_enabled; // Initialized by settings.load
+ int MMU3::mmu_hw_enabled_addr; // Initialized by settings.load
+
+ MMU3::MMU3()
+ : logic(MMU2_TOOL_CHANGE_LOAD_LENGTH, MMU2_LOAD_TO_NOZZLE_FEED_RATE)
+ , extruder(MMU2_NO_TOOL)
+ , tool_change_extruder(MMU2_NO_TOOL)
+ , resume_position()
+ , resume_hotend_temp(0)
+ , logicStepLastStatus(StepStatus::Finished)
+ , _state(xState::Stopped)
+ , mmu_print_saved(SavedState::None)
+ , loadFilamentStarted(false)
+ , unloadFilamentStarted(false)
+ , toolchange_counter(0)
+ , _tmcFailures(0) { }
+
+ void MMU3::status() {
+ // Useful information to see during bootup and change state
+ SERIAL_ECHOLN(F("MMU is "), mmu_hw_enabled ? GET_TEXT_F(MSG_ON) : GET_TEXT_F(MSG_OFF));
+ }
+
+ void MMU3::start() {
+ mmu_hw_enabled = true;
+
+ #if ENABLED(EEPROM_SETTINGS)
+ // Save mmu_hw_enabled to EEPROM
+ // TODO: Move to settings.cpp (for now)
+ persistentStore.access_start();
+ persistentStore.write_data(mmu_hw_enabled_addr, mmu_hw_enabled);
+ persistentStore.access_finish();
+ settings.save();
+ #endif
+
+ MMU2_SERIAL.begin(MMU_BAUD);
+
+ powerOn();
+ MMU2_SERIAL.flush(); // Make sure the UART buffer is clear before starting communication
+
+ setCurrentTool(MMU2_NO_TOOL);
+ _state = xState::Connecting;
+
+ // Start communication
+ logic.start();
+ logic.ResetRetryAttempts();
+ logic.ResetCommunicationTimeoutAttempts();
+ }
+
+ void MMU3::stop() {
+ stopKeepPowered();
+ powerOff();
+ }
+
+ void MMU3::stopKeepPowered() {
+ mmu_hw_enabled = false;
+
+ #if ENABLED(EEPROM_SETTINGS)
+ // Save mmu_hw_enabled to EEPROM
+ persistentStore.access_start();
+ persistentStore.write_data(mmu_hw_enabled_addr, mmu_hw_enabled);
+ persistentStore.access_finish();
+ settings.save();
+ #endif
+
+ _state = xState::Stopped;
+ logic.stop();
+ MMU2_SERIAL.end();
+ }
+
+ void MMU3::tune() {
+ switch (lastErrorCode) {
+ case ErrorCode::HOMING_SELECTOR_FAILED:
+ case ErrorCode::HOMING_IDLER_FAILED: {
+ // Prompt a menu for different values
+ tuneIdlerStallguardThreshold();
+ break;
+ }
+ default: break;
+ }
+ }
+
+ void MMU3::reset(ResetForm level) {
+ switch (level) {
+ case Software: resetX0(); break;
+ case ResetPin: triggerResetPin(); break;
+ case CutThePower: powerCycle(); break;
+ case EraseEEPROM: resetX42(); break;
+ default: break;
+ }
+ }
+
+ void MMU3::resetX0() { logic.ResetMMU(); } // Send soft reset
+ void MMU3::resetX42() { logic.ResetMMU(42); }
+
+ void MMU3::triggerResetPin() { power_reset(); }
+
+ void MMU3::powerCycle() {
+ // cut the power to the MMU and after a while restore it
+ // Sadly, MK3/S/+ cannot do this
+ stop();
+ safe_delay_keep_alive(1000);
+ start();
+ }
+
+ void MMU3::powerOff() { power_off(); }
+ void MMU3::powerOn() { power_on(); }
+
+ bool MMU3::readRegister(uint8_t address) {
+ if (!waitForMMUReady()) return false;
+
+ do {
+ logic.readRegister(address); // we may signal the accepted/rejected status of the response as return value of this function
+ } while (!manage_response(false, false));
+
+ // Update cached value
+ lastReadRegisterValue = logic.rsp.paramValue;
+ return true;
+ }
+
+ bool __attribute__((noinline)) MMU3::writeRegister(uint8_t address, uint16_t data) {
+ if (!waitForMMUReady()) return false;
+
+ // special cases - intercept requests of registers which influence the printer's behaviour too + perform the change even on the printer's side
+ switch (address) {
+ case (uint8_t)Register::Extra_Load_Distance: logic.PlanExtraLoadDistance(data); break;
+ case (uint8_t)Register::Pulley_Slow_Feedrate: logic.PlanPulleySlowFeedRate(data); break;
+ default: break; // Don't intercept any other register writes
+ }
+
+ do {
+ logic.writeRegister(address, data); // we may signal the accepted/rejected status of the response as return value of this function
+ } while (!manage_response(false, false));
+
+ return true;
+ }
+
+ void MMU3::mmu_loop() {
+ // We only leave this method if the current command was successfully
+ // completed - that's the Marlin's way of blocking operation
+ // Atomic compare_exchange would have been the most appropriate solution
+ // here, but this gets called only in Marlin's task, so thread safety
+ // should be kept
+ static bool avoidRecursion = false;
+ if (avoidRecursion) return;
+ avoidRecursion = true;
+
+ mmu_loop_inner(true);
+
+ avoidRecursion = false;
+ }
+
+ void __attribute__((noinline)) MMU3::mmu_loop_inner(bool reportErrors) {
+ logicStepLastStatus = logicStep(reportErrors); // it looks like the mmu_loop doesn't need to be a blocking call
+ CheckErrorScreenUserInput();
+ }
+
+ /**
+ * Check if there are extruder moves planned ahead.
+ *
+ * TODO: This should go to the planner, but for now keep it here!
+ */
+ bool MMU3::e_active() {
+ unsigned char e_active = 0;
+ block_t *block;
+ if (planner.block_buffer_tail != planner.block_buffer_head) {
+ uint8_t block_index = planner.block_buffer_tail;
+ while (block_index != planner.block_buffer_head) {
+ block = &planner.block_buffer[block_index];
+ if (block->steps[E_AXIS] != 0) e_active++;
+ block_index = (block_index + 1) & (BLOCK_BUFFER_SIZE - 1);
+ }
+ }
+ return (e_active > 0);
+ }
+
+ /**
+ * Trigger an M600 or the SpoolJoin feature if the FINDA cannot detect any
+ * filament during the print.
+ *
+ * In case of SpoolJoin feature is triggered, Marlin's implementation is a
+ * little different than Prusa's, as we are completely consuming the filament
+ * before switching to the next slot. There will be a little bit of filament
+ * left when the new filament is extruded SpoolJoin is not intended to be used with
+ * multi color/material prints so this should be fine.
+ */
+ void MMU3::checkFINDARunout() {
+ if (!findaDetectsFilament()
+ //&& printJobOngoing()
+ && parser.codenum != 600
+ && TERN1(HAS_LEVELING, planner.leveling_active)
+ && xy_are_trusted()
+ && e_active()
+ #if ENABLED(MMU_SPOOL_JOIN_CONSUMES_ALL_FILAMENT)
+ && runout.enabled // to prevent M600 to be triggered during M600 AUTO
+ && !FILAMENT_PRESENT() // so the filament is totally consumed
+ #endif
+ ) {
+ SERIAL_ECHOLN_P("FINDA filament runout!");
+ if (spooljoin.isEnabled() && get_current_tool() != (uint8_t)FILAMENT_UNKNOWN) { // Can't auto if F=?
+ #if ENABLED(MMU_SPOOL_JOIN_CONSUMES_ALL_FILAMENT)
+ // set the current tool to FILAMENT_UNKNOWN so that we don't try to unload it
+ extruder = MMU2_NO_TOOL;
+ // disable the filament runout sensor (this is going to be re-enabled after the filament is loaded)
+ runout.reset();
+ runout.filament_ran_out = false; // trying to disable the purge more / continue message
+ runout.enabled = false;
+ #endif
+ queue.enqueue_now(F("M600A")); // Save print and run M600 command
+ }
+ else {
+ marlin_stop_and_save_print_to_ram();
+ resume_print();
+ queue.enqueue_now(F("M600")); // Save print and run M600 command
+ }
+ }
+ }
+
+ struct ReportingRAII {
+ CommandInProgress cip;
+ explicit inline __attribute__((always_inline)) ReportingRAII(CommandInProgress cip)
+ : cip(cip) {
+ BeginReport(cip, ProgressCode::EngagingIdler);
+ }
+ inline __attribute__((always_inline)) ~ReportingRAII() {
+ EndReport(cip, ProgressCode::OK);
+ }
+ };
+
+ bool MMU3::waitForMMUReady() {
+ switch (state()) {
+ case xState::Stopped: return false;
+ case xState::Connecting:
+ // Should we wait until the MMU reconnects?
+ // Fire up a fsm_dlg and show "MMU not responding"?
+ default: return true;
+ }
+ }
+
+ bool MMU3::retryIfPossible(const ErrorCode ec) {
+ if (logic.RetryAttempts()) {
+ SetButtonResponse(ButtonOperations::Retry);
+ // check, that Retry is actually allowed on that operation
+ if (ButtonAvailable(ec) != Buttons::NoButton) {
+ logic.SetInAutoRetry(true);
+ SERIAL_ECHOLN_P("RetryButtonPressed");
+ // We don't decrement until the button is acknowledged by the MMU.
+ // --retryAttempts; // "used" one retry attempt
+ return true;
+ }
+ }
+ logic.SetInAutoRetry(false);
+ return false;
+ }
+
+ bool MMU3::verifyFilamentEnteredPTFE() {
+ planner_synchronize();
+
+ if (WhereIsFilament() != FilamentState::AT_FSENSOR)
+ return false;
+
+ // MMU has finished its load, push the filament further by some defined constant length
+ // If the filament sensor reads 0 at any moment, then report FAILURE
+ const float tryload_length = MMU2_CHECK_FILAMENT_PRESENCE_EXTRUSION_LENGTH - logic.ExtraLoadDistance();
+ TryLoadUnloadReporter tlur(tryload_length);
+
+ /**
+ * The position is a triangle wave.
+ * Current position is not zero, it is an offset
+ *
+ * Keep in mind that the relationship between machine position
+ * and pixel index is not linear. The area around the amplitude
+ * needs to be taken care of carefully. The current implementation
+ * handles each move separately so there is no need to watch for the change
+ * in the slope's sign or check the last machine position.
+ * y(x)
+ * ▲
+ * │ ^◄────────── tryload_length + current_position
+ * machine │ / \
+ * position │ / \◄────────── stepper_position_mm + current_position
+ * (mm) │ / \
+ * │ / \
+ * │/ \◄───────current_position
+ * └──────────────► x
+ * 0 19
+ * pixel #
+ */
+
+ bool filament_inserted = true; // Expect success
+ // Pixel index will go from 0 to 10, then back from 10 to 0.
+ // A change in this value indicates a new pixel should be drawn on the display.
+ for (uint8_t move = 0; move < 2; move++) {
+ extruder_move(move == 0 ? tryload_length : -tryload_length, MMU2_VERIFY_LOAD_TO_NOZZLE_FEED_RATE);
+ while (planner_any_moves()) {
+ filament_inserted = filament_inserted && (WhereIsFilament() == FilamentState::AT_FSENSOR);
+ tlur.Progress(filament_inserted);
+ safe_delay_keep_alive(0);
+ }
+ }
+ Disable_E0();
+ if (!filament_inserted) IncrementLoadFails();
+ tlur.DumpToSerial();
+ return filament_inserted;
+ }
+
+ bool MMU3::toolChangeCommonOnce(uint8_t slot) {
+ static_assert(MMU2_MAX_RETRIES > 1); // Need >1 retries to do the cut in the last attempt
+ uint8_t retries = 0;
+ for (;;) {
+ for (;;) {
+ Disable_E0(); // It may seem counterintuitive to disable the E-motor, but it gets enabled in the planner whenever the E-motor is to move
+ tool_change_extruder = slot;
+ logic.ToolChange(slot); // Let the MMU pull the filament out and push a new one in
+
+ if (manage_response(true, true)) break;
+
+ // Otherwise: failed to perform the command - unload first and then let it run again
+ IncrementMMUFails();
+
+ // Just in case we stood in an error screen for too long and the hotend got cold
+ resumeHotendTemp();
+ // If the extruder has been parked, it will get unparked once the ToolChange command finishes OK
+ // - so no resumeUnpark() at this spot
+
+ unloadInner();
+ // If we run out of retries, we must do something ... maybe raise an error screen and allow the user to do something.
+ // But honestly - if the MMU restarts during every toolchange something else is seriously broken
+ // and stopping a print is probably our best option.
+ }
+ if (verifyFilamentEnteredPTFE()) return true; // success
+
+ // Prepare a retry attempt
+ unloadInner();
+ if (retries == (MMU2_MAX_RETRIES) - 1 && cutter_enabled()) {
+ cutFilamentInner(slot); // try cutting filament tip at the last attempt
+ retries = 0; // reset retries every MMU2_MAX_RETRIES
+ }
+
+ ++retries;
+ }
+ return false; // Couldn't accomplish the task
+ }
+
+ void MMU3::toolChangeCommon(uint8_t slot) {
+ while (!toolChangeCommonOnce(slot)) { // While not successfully fed into extruder's PTFE tube...
+ // Failed autoretry, report an error by forcing a "printer" error into the MMU infrastructure - it is a hack to leverage existing code
+ // @@TODO theoretically logic layer may not need to be spoiled with the printer error - maybe just the manage_response needs it...
+ logic.SetPrinterError(ErrorCode::LOAD_TO_EXTRUDER_FAILED);
+ // We only have to wait for the user to fix the issue and press "Retry".
+ // Please see checkUserInput() for details how we "leave" manage_response.
+ // If manage_response returns false at this spot (MMU operation interrupted aka MMU reset)
+ // we can safely continue because the MMU is not doing an operation now.
+ static_cast(manage_response(true, true)); // yes, I'd like to silence [[nodiscard]] warning at this spot by casting to void
+ }
+
+ setCurrentTool(slot); // filament change is finished
+ spooljoin.setSlot(slot);
+
+ ++toolchange_counter;
+
+ // Also increment the total number of tool changes
+ operation_statistics.increment_tool_change_counter();
+ }
+
+ bool MMU3::tool_change(uint8_t slot) {
+ if (!waitForMMUReady()) return false;
+
+ if (slot != extruder) {
+ if (
+ //findaDetectsFilament()
+ //!IS_SD_PRINTING() && !usb_timer.running()
+ !marlin_printingIsActive()
+ ) {
+ // If Tcodes are used manually through the serial
+ // we need to unload manually as well -- but only if FINDA detects filament
+ unload();
+ }
+
+ ReportingRAII rep(CommandInProgress::ToolChange);
+ FSensorBlockRunout blockRunout;
+ planner_synchronize();
+ toolChangeCommon(slot);
+ }
+ return true;
+ }
+
+ /**
+ * Handle special T?/Tx/Tc commands
+ *
+ * - T? Gcode to extrude shouldn't have to follow, load to extruder wheels is done automatically
+ * - Tx Same as T?, except nozzle doesn't have to be preheated. Tc must be placed after extruder nozzle is preheated to finish filament load.
+ * - Tc Load to nozzle after filament was prepared by Tx and extruder nozzle is already heated.
+ */
+ bool MMU3::tool_change(char code, uint8_t slot) {
+ if (!waitForMMUReady()) return false;
+
+ FSensorBlockRunout blockRunout;
+
+ switch (code) {
+ case '?': {
+ waitForHotendTargetTemp(100, []{});
+ load_to_nozzle(slot);
+ }
+ break;
+
+ case 'x': {
+ thermal_setExtrudeMintemp(0); // Allow cold extrusion since Tx only loads to the gears not nozzle
+ tool_change(slot);
+ thermal_setExtrudeMintemp(EXTRUDE_MINTEMP);
+ }
+ break;
+
+ case 'c': {
+ waitForHotendTargetTemp(100, []{});
+ execute_load_to_nozzle_sequence();
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ void MMU3::get_statistics() {
+ logic.Statistics();
+ }
+
+ uint8_t __attribute__((noinline)) MMU3::get_current_tool() const {
+ return extruder == MMU2_NO_TOOL ? (uint8_t)FILAMENT_UNKNOWN : extruder;
+ }
+
+ uint8_t MMU3::get_tool_change_tool() const {
+ return tool_change_extruder == MMU2_NO_TOOL ? (uint8_t)FILAMENT_UNKNOWN : tool_change_extruder;
+ }
+
+ void MMU3::setCurrentTool(uint8_t ex) {
+ extruder = ex;
+ MMU2_ECHO_MSGRPGM(PSTR("MMU2tool="));
+ SERIAL_ECHOLN((int)ex);
+ }
+
+ bool MMU3::set_filament_type(uint8_t /*slot*/, uint8_t /*type*/) {
+ if (!waitForMMUReady()) return false;
+
+ // @@TODO - this is not supported in the new MMU yet
+ // slot = slot; // @@TODO
+ // type = type; // @@TODO
+ // cmd_arg = filamentType;
+ // command(MMU_CMD_F0 + index);
+
+ if (!manage_response(false, false)) {
+ // @@TODO failed to perform the command - retry
+ // Comment: how is it possible for a filament type set to fail? manage_response(true, true)
+ }
+
+ return true;
+ }
+
+ void MMU3::unloadInner() {
+ FSensorBlockRunout blockRunout;
+ filament_ramming();
+
+ // we assume the printer managed to relieve filament tip from the gears,
+ // so repeating that part in case of an MMU restart is not necessary
+ for (;;) {
+ Disable_E0();
+ logic.UnloadFilament();
+ if (manage_response(false, true)) break;
+ IncrementMMUFails();
+ }
+ //MakeSound(Confirm);
+
+ // no active tool
+ setCurrentTool(MMU2_NO_TOOL);
+ tool_change_extruder = MMU2_NO_TOOL;
+ }
+
+ bool MMU3::unload() {
+ if (!waitForMMUReady()) return false;
+
+ WaitForHotendTargetTempBeep();
+
+ // Scope for ReportingRAII
+ {
+ ReportingRAII rep(CommandInProgress::UnloadFilament);
+ unloadInner();
+ }
+
+ ScreenUpdateEnable();
+ return true;
+ }
+
+ void MMU3::cutFilamentInner(uint8_t slot) {
+ for (;;) {
+ Disable_E0();
+ logic.CutFilament(slot);
+ if (manage_response(false, true)) break;
+ IncrementMMUFails();
+ }
+ }
+
+ bool MMU3::cut_filament(uint8_t slot, bool enableFullScreenMsg /*= true*/) {
+ if (!waitForMMUReady()) return false;
+
+ if (enableFullScreenMsg) fullScreenMsgCut(slot);
+
+ // Scope for ReportingRAII
+ {
+ if (findaDetectsFilament()) unload();
+
+ ReportingRAII rep(CommandInProgress::CutFilament);
+ cutFilamentInner(slot);
+ setCurrentTool(MMU2_NO_TOOL);
+ tool_change_extruder = MMU2_NO_TOOL;
+ //MakeSound(SoundType::Confirm);
+ }
+ ScreenUpdateEnable();
+ return true;
+ }
+
+ bool MMU3::loading_test(uint8_t slot) {
+ fullScreenMsgTest(slot);
+ tool_change(slot);
+ planner_synchronize();
+ unload();
+ ScreenUpdateEnable();
+ return true;
+ }
+
+ bool MMU3::load_to_feeder(uint8_t slot) {
+ if (!waitForMMUReady()) return false;
+
+ fullScreenMsgLoad(slot);
+
+ // Scope for ReportingRAII
+ {
+ ReportingRAII rep(CommandInProgress::LoadFilament);
+ for (;;) {
+ Disable_E0();
+ logic.LoadFilament(slot);
+ if (manage_response(false, false)) break;
+ IncrementMMUFails();
+ }
+ //MakeSound(SoundType::Confirm);
+ }
+ ScreenUpdateEnable();
+ return true;
+ }
+
+ bool MMU3::load_to_nozzle(uint8_t slot) {
+ if (!waitForMMUReady()) return false;
+
+ WaitForHotendTargetTempBeep();
+
+ fullScreenMsgLoad(slot);
+
+ // Scope for ReportingRAII
+ {
+ // Used for MMU-menu operation "Load to Nozzle"
+ ReportingRAII rep(CommandInProgress::ToolChange);
+ FSensorBlockRunout blockRunout;
+
+ // Filament already loaded? Free it and shape its tip properly.
+ if (extruder != MMU2_NO_TOOL) filament_ramming();
+
+ toolChangeCommon(slot);
+
+ // Finish loading to the nozzle with finely tuned steps.
+ execute_load_to_nozzle_sequence();
+ //MakeSound(Confirm);
+ }
+ ScreenUpdateEnable();
+ return true;
+ }
+
+ bool MMU3::eject_filament(uint8_t slot, bool enableFullScreenMsg /* = true */) {
+ if (!waitForMMUReady()) return false;
+
+ if (enableFullScreenMsg) fullScreenMsgEject(slot);
+
+ // Scope for ReportingRAII
+ {
+ if (findaDetectsFilament())
+ unload();
+
+ ReportingRAII rep(CommandInProgress::EjectFilament);
+ for (;;) {
+ Disable_E0();
+ logic.EjectFilament(slot);
+ if (manage_response(false, true))
+ break;
+ IncrementMMUFails();
+ }
+ setCurrentTool(MMU2_NO_TOOL);
+ tool_change_extruder = MMU2_NO_TOOL;
+ //MakeSound(Confirm);
+ }
+ ScreenUpdateEnable();
+ return true;
+ }
+
+ void MMU3::button(uint8_t index) {
+ LogEchoEvent(F("button"));
+ logic.button(index);
+ }
+
+ void MMU3::home(uint8_t mode) {
+ logic.home(mode);
+ }
+
+ void MMU3::saveHotendTemp(bool turn_off_nozzle) {
+ if (mmu_print_saved & SavedState::Cooldown) return;
+
+ if (turn_off_nozzle && !(mmu_print_saved & SavedState::CooldownPending)) {
+ Disable_E0();
+ resume_hotend_temp = thermal_degTargetHotend();
+ mmu_print_saved |= SavedState::CooldownPending;
+ LogEchoEvent(F("Heater cooldown pending"));
+ }
+ }
+
+ void MMU3::saveAndPark(bool move_axes) {
+ if (mmu_print_saved == SavedState::None) { // First occurrence. Save current position, park print head, disable nozzle heater.
+ LogEchoEvent(F("Saving and parking"));
+ Disable_E0();
+ planner_synchronize();
+
+ // In case a power panic happens while waiting for the user
+ // take a partial back up of print state into RAM (current position, etc.)
+ marlin_refresh_print_state_in_ram();
+
+ if (move_axes) {
+ mmu_print_saved |= SavedState::ParkExtruder;
+ resume_position = planner_current_position(); // save current pos
+
+ // Do not lift Z, as it will double lift if there is another error
+ // right after the current one is solved.
+
+ // Move XY aside
+ if (xy_are_trusted()) nozzle_park();
+ }
+ }
+ }
+
+ void MMU3::resumeHotendTemp() {
+ if ((mmu_print_saved & SavedState::CooldownPending)) {
+ // Clear the "pending" flag if we haven't cooled yet.
+ mmu_print_saved &= ~(SavedState::CooldownPending);
+ LogEchoEvent(F("Cooldown flag cleared"));
+ }
+ if ((mmu_print_saved & SavedState::Cooldown) && resume_hotend_temp) {
+ LogEchoEvent(F("Resuming Temp"));
+ // @@TODO MMU2_ECHO_MSGRPGM(PSTR("Restoring hotend temperature "));
+ SERIAL_ECHOLN(resume_hotend_temp);
+ mmu_print_saved &= ~(SavedState::Cooldown);
+ thermal_setTargetHotend(resume_hotend_temp);
+ fullScreenMsgRestoringTemperature();
+ // @todo better report the event and let the GUI do its work somewhere else
+ ReportErrorHookSensorLineRender();
+ waitForHotendTargetTemp(100, [] {
+ marlin_manage_inactivity(true);
+ mmu3.mmu_loop_inner(false);
+ ReportErrorHookDynamicRender();
+ });
+ ScreenUpdateEnable(); // temporary hack to stop this locking the printer...
+ LogEchoEvent(F("Hotend temperature reached"));
+ ScreenClear();
+ }
+ }
+
+ void MMU3::resumeUnpark() {
+ if (mmu_print_saved & SavedState::ParkExtruder) {
+ LogEchoEvent(F("Resuming XYZ"));
+
+ // Move XY to starting position, then Z
+ motion_do_blocking_move_to_xy(resume_position.x, resume_position.x, feedRate_t(NOZZLE_PARK_XY_FEEDRATE));
+
+ // Move Z_AXIS to saved position
+ motion_do_blocking_move_to_z(resume_position.z, feedRate_t(NOZZLE_PARK_Z_FEEDRATE));
+
+ // From this point forward, power panic should not use
+ // the partial backup in RAM since the extruder is no
+ // longer in parking position
+ marlin_clear_print_state_in_ram();
+
+ mmu_print_saved &= ~(SavedState::ParkExtruder);
+ }
+ }
+
+ void MMU3::checkUserInput() {
+ auto btn = ButtonPressed(lastErrorCode);
+
+ // Was a button pressed on the MMU itself instead of the LCD?
+ if (btn == Buttons::NoButton && lastButton != Buttons::NoButton) {
+ btn = lastButton;
+ lastButton = Buttons::NoButton; // Clear it.
+ }
+
+ if (mmuLastErrorSource() == MMU3::ErrorSourcePrinter && btn != Buttons::NoButton) {
+ // When the printer has raised an error screen, and a button was selected
+ // the error screen should always be dismissed.
+ clearPrinterError();
+ // A horrible hack - clear the explicit printer error allowing manage_response to recover on MMU's Finished state
+ // Moreover - if the MMU is currently doing something (like the LoadFilament - see comment above)
+ // we'll actually wait for it automagically in manage_response and after it finishes correctly,
+ // we'll issue another command (like toolchange)
+ }
+
+ switch (btn) {
+ case Buttons::Left:
+ case Buttons::Middle:
+ case Buttons::Right:
+ SERIAL_ECHOPGM("checkUserInput-btnLMR ");
+ SERIAL_ECHOLN((int)buttons_to_uint8t(btn));
+ resumeHotendTemp(); // Recover the hotend temp before we attempt to do anything else...
+
+ if (mmuLastErrorSource() == MMU3::ErrorSourceMMU)
+ // Do not send a button to the MMU unless the MMU is in error state
+ button(buttons_to_uint8t(btn));
+
+ // A quick hack: for specific error codes move the E-motor every time.
+ // Not sure if we can rely on the fsensor.
+ // Just plan the move, let the MMU take over when it is ready
+ switch (lastErrorCode) {
+ case ErrorCode::FSENSOR_DIDNT_SWITCH_OFF:
+ case ErrorCode::FSENSOR_TOO_EARLY: helpUnloadToFinda(); break;
+ default: break;
+ }
+ break;
+ case Buttons::TuneMMU:
+ tune();
+ break;
+ case Buttons::Load:
+ case Buttons::Eject:
+ // High level operation
+ setPrinterButtonOperation(btn);
+ break;
+ case Buttons::ResetMMU:
+ reset(ResetPin); // Cannot do power cycle on the MK3
+ // ... but mmu2_power.cpp knows this and triggers a soft-reset instead.
+ break;
+ case Buttons::DisableMMU:
+ stop();
+ //DisableMMUInSettings(); // stop() already does this...
+ status();
+ break;
+ case Buttons::StopPrint:
+ // @@TODO Unsure if we should handle this high level operation at this spot
+ break;
+ default: break;
+ }
+ }
+
+ /**
+ * Originally, this was used to wait for response and deal with timeout if necessary.
+ * The new protocol implementation enables much nicer and intense reporting, so this method will boil down
+ * just to verify the result of an issued command (which was basically the original idea)
+ *
+ * It is closely related to mmu_loop() (which corresponds to our ProtocolLogic::Step()), which does NOT perform any blocking wait for a command to finish.
+ * But - in case of an error, the command is not yet finished, but we must react accordingly - move the printhead elsewhere, stop heating, eat a cat or so.
+ * That's what's being done here...
+ */
+ bool MMU3::manage_response(const bool move_axes, const bool turn_off_nozzle) {
+ mmu_print_saved = SavedState::None;
+
+ MARLIN_KEEPALIVE_STATE_IN_PROCESS;
+
+ Stopwatch nozzle_timer;
+
+ for (;;) {
+ // in our new implementation, we know the exact state of the MMU at any moment, we do not have to wait for a timeout
+ // So in this case we should decide if the operation is:
+ // - still running -> wait normally in idle()
+ // - failed -> then do the safety moves on the printer like before
+ // - finished ok -> proceed with reading other commands
+ safe_delay_keep_alive(0); // calls logicStep() and remembers its return status
+
+ if (mmu_print_saved & SavedState::CooldownPending) {
+ if (!nozzle_timer.isRunning()) {
+ nozzle_timer.start();
+ LogEchoEvent(F("Cooling Timeout started"));
+ }
+ else if (nozzle_timer.duration() > (PAUSE_PARK_NOZZLE_TIMEOUT * 1000ul)) { // mins->msec.
+ mmu_print_saved &= ~(SavedState::CooldownPending);
+ mmu_print_saved |= SavedState::Cooldown;
+ thermal_setTargetHotend(0);
+ LogEchoEvent(F("Heater cooldown"));
+ }
+ }
+ else if (nozzle_timer.isRunning()) {
+ nozzle_timer.stop();
+ LogEchoEvent(F("Cooling timer stopped"));
+ }
+
+ switch (logicStepLastStatus) {
+ case Finished:
+ // command/operation completed, let Marlin continue its work
+ // the E may have some more moves to finish - wait for them
+ resumeHotendTemp();
+ resumeUnpark(); // We can now travel back to the tower or wherever we were when we saved.
+ if (!TuneMenuEntered()) {
+ // If the error screen is sleeping (running 'Tune' menu)
+ // then don't reset retry attempts because we this will trigger
+ // an automatic retry attempt when 'Tune' button is selected. We want the
+ // error screen to appear once more so the user can hit 'Retry' button manually.
+ logic.ResetRetryAttempts(); // Reset the retry counter.
+ }
+ planner_synchronize();
+ return true;
+ case Interrupted:
+ // now what :D ... big bad ... ramming, unload, retry the whole command originally issued
+ return false;
+ case VersionMismatch: // this basically means the MMU will be disabled until reconnected
+ checkUserInput();
+ return true;
+ case PrinterError:
+ saveAndPark(move_axes);
+ saveHotendTemp(turn_off_nozzle);
+ checkUserInput();
+ // if button pressed "Done", return true, otherwise stay within manage_response
+ // Please see checkUserInput() for details how we "leave" manage_response
+ break;
+ case CommandError:
+ case CommunicationTimeout:
+ case ProtocolError:
+ case ButtonPushed:
+ if (!logic.InAutoRetry()) {
+ // Don't proceed to the park/save if we are doing an autoretry.
+ saveAndPark(move_axes);
+ saveHotendTemp(turn_off_nozzle);
+ checkUserInput();
+ }
+ break;
+ case CommunicationRecovered: // @@TODO communication recovered and maybe an error recovered as well
+ // Maybe the logic layer can detect the change of state a respond with one "Recovered" to be handled here
+ resumeHotendTemp();
+ resumeUnpark();
+ break;
+ case Processing: // Wait for the MMU to respond
+ default: break;
+ }
+ }
+ }
+
+ StepStatus MMU3::logicStep(bool reportErrors) {
+ // Process any buttons before proceeding with another MMU Query
+ checkUserInput();
+
+ const StepStatus ss = logic.Step();
+ switch (ss) {
+
+ case Finished:
+ // At this point it is safe to trigger a runout and not interrupt the MMU protocol
+ checkFINDARunout();
+ break;
+
+ case Processing:
+ onMMUProgressMsg(logic.Progress());
+ break;
+
+ case ButtonPushed:
+ lastButton = logic.button();
+ LogEchoEvent(F("MMU button pushed"));
+ checkUserInput(); // Process the button immediately
+ break;
+
+ case Interrupted:
+ // can be silently handed over to a higher layer, no processing necessary at this spot
+ break;
+
+ default:
+ if (reportErrors) {
+ switch (ss) {
+
+ case CommandError:
+ reportError(logic.Error(), ErrorSourceMMU);
+ break;
+
+ case CommunicationTimeout:
+ _state = xState::Connecting;
+ reportError(ErrorCode::MMU_NOT_RESPONDING, ErrorSourcePrinter);
+ break;
+
+ case ProtocolError:
+ _state = xState::Connecting;
+ reportError(ErrorCode::PROTOCOL_ERROR, ErrorSourcePrinter);
+ break;
+
+ case VersionMismatch:
+ stopKeepPowered();
+ reportError(ErrorCode::VERSION_MISMATCH, ErrorSourcePrinter);
+ break;
+
+ case PrinterError:
+ reportError(logic.PrinterError(), ErrorSourcePrinter);
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ if (logic.Running()) _state = xState::Active;
+
+ return ss;
+ }
+
+ void MMU3::filament_ramming() {
+ execute_extruder_sequence(ramming_sequence, sizeof(ramming_sequence) / sizeof(E_Step));
+ }
+
+ void MMU3::execute_extruder_sequence(const E_Step *sequence, uint8_t steps) {
+ planner_synchronize();
+
+ const E_Step *step = sequence;
+ for (uint8_t i = steps; i > 0; --i) {
+ extruder_move(pgm_read_float(&(step->extrude)), pgm_read_float(&(step->feedRate)));
+ step++;
+ }
+ planner_synchronize(); // it looks like it's better to sync the moves at the end - smoother move (if the sequence is not too long).
+
+ Disable_E0();
+ }
+
+ void MMU3::execute_load_to_nozzle_sequence() {
+ planner_synchronize();
+ // Compensate for configurable Extra Loading Distance
+ planner_set_current_position_E(planner_get_current_position_E() - (logic.ExtraLoadDistance() - MMU2_FILAMENT_SENSOR_POSITION));
+ execute_extruder_sequence(load_to_nozzle_sequence, sizeof(load_to_nozzle_sequence) / sizeof(load_to_nozzle_sequence[0]));
+ }
+
+ void MMU3::reportError(ErrorCode ec, ErrorSource res) {
+ // Due to a potential lossy error reporting layers linked to this hook
+ // we'd better report everything to make sure especially the error states
+ // do not get lost.
+ // - The good news here is the fact, that the MMU reports the errors repeatedly until resolved.
+ // - The bad news is, that MMU not responding may repeatedly occur on printers not having the MMU at all.
+ //
+ // Not sure how to properly handle this situation, options:
+ // - skip reporting "MMU not responding" (at least for now)
+ // - report only changes of states (we can miss an error message)
+ // - maybe some combination of MMUAvailable + UseMMU flags and decide based on their state
+ // Right now the filtering of MMU_NOT_RESPONDING is done in ReportErrorHook() as it is not a problem if mmu2.cpp
+
+ // Depending on the Progress code, we may want to do some action when an error occurs
+ switch (logic.Progress()) {
+ case ProgressCode::UnloadingToFinda:
+ unloadFilamentStarted = false;
+ planner_abort_queued_moves(); // Abort excess E-moves to be safe
+ break;
+ case ProgressCode::FeedingToFSensor:
+ // FSENSOR error during load. Make sure E-motor stops moving.
+ loadFilamentStarted = false;
+ planner_abort_queued_moves(); // Abort excess E-moves to be safe
+ break;
+ default: break;
+ }
+
+ if (ec != lastErrorCode) { // deduplicate: only report changes in error codes into the log
+ lastErrorCode = ec;
+ lastErrorSource = res;
+ LogErrorEvent(PrusaErrorTitle(PrusaErrorCodeIndex(ec)));
+
+ if (ec != ErrorCode::OK && ec != ErrorCode::FILAMENT_EJECTED && ec != ErrorCode::FILAMENT_CHANGE) {
+ IncrementMMUFails();
+
+ // Check if it is a "power" failure. TMC-related errors are considered power failures.
+ static constexpr uint16_t tmcMask =
+ ( (uint16_t)ErrorCode::TMC_IOIN_MISMATCH
+ | (uint16_t)ErrorCode::TMC_RESET
+ | (uint16_t)ErrorCode::TMC_UNDERVOLTAGE_ON_CHARGE_PUMP
+ | (uint16_t)ErrorCode::TMC_SHORT_TO_GROUND
+ | (uint16_t)ErrorCode::TMC_OVER_TEMPERATURE_WARN
+ | (uint16_t)ErrorCode::TMC_OVER_TEMPERATURE_ERROR
+ | (uint16_t)ErrorCode::MMU_SOLDERING_NEEDS_ATTENTION ) & 0x7FFFU; // skip the top bit
+
+ static_assert(tmcMask == 0x7E00); // Just make sure we fail compilation if any of the TMC error codes change
+
+ if ((uint16_t)ec & tmcMask) { // @@TODO can be optimized to uint8_t operation
+ // TMC-related errors are from 0x8200 higher
+ incrementTMCFailures();
+ }
+ }
+ }
+
+ if (!retryIfPossible(ec))
+ // If retry attempts are all used up
+ // or if 'Retry' operation is not available
+ // raise the MMU error screen and wait for user input
+ ReportErrorHook((CommandInProgress)logic.CommandInProgress(), ec, uint8_t(lastErrorSource));
+ }
+
+ void MMU3::reportProgress(ProgressCode pc) {
+ ReportProgressHook((CommandInProgress)logic.CommandInProgress(), pc);
+ LogEchoEvent(ProgressCodeToText(pc));
+ }
+
+ void MMU3::onMMUProgressMsg(ProgressCode pc) {
+ if (pc != lastProgressCode)
+ onMMUProgressMsgChanged(pc);
+ else
+ onMMUProgressMsgSame(pc);
+ }
+
+ void MMU3::onMMUProgressMsgChanged(ProgressCode pc) {
+ reportProgress(pc);
+ lastProgressCode = pc;
+ switch (pc) {
+ case ProgressCode::UnloadingToFinda:
+ if ( (CommandInProgress)logic.CommandInProgress() == CommandInProgress::UnloadFilament
+ || ((CommandInProgress)logic.CommandInProgress() == CommandInProgress::ToolChange)
+ ) {
+ // If MK3S sent U0 command, ramming sequence takes care of releasing the filament.
+ // If Toolchange is done while printing, PrusaSlicer takes care of releasing the filament
+ // If printing is not in progress, ToolChange will issue a U0 command.
+ break;
+ }
+ else {
+ // We're likely recovering from an MMU error
+ planner_synchronize();
+ unloadFilamentStarted = true;
+ helpUnloadToFinda();
+ }
+ break;
+ case ProgressCode::FeedingToFSensor:
+ // prepare for the movement of the E-motor
+ planner_synchronize();
+ loadFilamentStarted = true;
+ break;
+ default: break; // do nothing yet
+ }
+ }
+
+ void __attribute__((noinline)) MMU3::helpUnloadToFinda() {
+ extruder_move(-MMU2_RETRY_UNLOAD_TO_FINDA_LENGTH, MMU2_RETRY_UNLOAD_TO_FINDA_FEED_RATE);
+ }
+
+ void MMU3::onMMUProgressMsgSame(ProgressCode pc) {
+ const uint8_t pulley_slow_feedrate = logic.PulleySlowFeedRate();
+ const float extrude_distance = _MIN(_MAX(EXTRUDE_MAXLENGTH - 1, 1), pulley_slow_feedrate);
+
+ switch (pc) {
+ case ProgressCode::UnloadingToFinda:
+ if (unloadFilamentStarted && !planner_any_moves()) { // Only plan a move if there is no move ongoing
+ switch (WhereIsFilament()) {
+ case FilamentState::AT_FSENSOR:
+ case FilamentState::IN_NOZZLE:
+ case FilamentState::UNAVAILABLE: // actually Unavailable makes sense as well to start the E-move to release the filament from the gears
+ helpUnloadToFinda();
+ break;
+ default:
+ unloadFilamentStarted = false;
+ }
+ }
+ break;
+
+ case ProgressCode::FeedingToFSensor:
+ if (loadFilamentStarted) {
+ switch (WhereIsFilament()) {
+ case FilamentState::AT_FSENSOR:
+ // fsensor triggered, finish FeedingToExtruder state
+ loadFilamentStarted = false;
+
+ // Abort any excess E-move from the planner queue
+ planner_abort_queued_moves();
+
+ // After the MMU knows the FSENSOR is triggered it will:
+ // 1. Push the filament by additional 30mm (see fsensorToNozzle)
+ // 2. Disengage the idler and push another 2mm.
+ extruder_move(logic.ExtraLoadDistance() + 2, logic.PulleySlowFeedRate());
+ break;
+ case FilamentState::NOT_PRESENT:
+ // fsensor not triggered, continue moving extruder
+ //
+ // Instead of doing a very long extrude as in PrusaFirmware,
+ // Marlin's own MMU2s code has a better approach to this by spinning
+ // the extruder indefinitelly...
+ //
+ // this ensures that while the MMU is pushing the filament,
+ // the extruder will keep rotating, preventing the filament to hit
+ // the extruder gears...
+ while (planner.movesplanned() < 3) {
+ extruder_move(extrude_distance, pulley_slow_feedrate, false);
+ }
+ break;
+ default: break; // Abort here?
+ }
+ }
+ break;
+
+ default: break; // do nothing yet
+ }
+ }
+
+} // MMU3
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2.h b/Marlin/src/feature/mmu3/mmu2.h
new file mode 100644
index 000000000000..f69e6aca1022
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2.h
@@ -0,0 +1,419 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * mmu2.h
+ */
+
+#include "mmu2_state.h"
+#include "mmu2_marlin.h"
+
+#include "mmu2_protocol_logic.h"
+
+#include "../../MarlinCore.h"
+
+ #ifdef __AVR__
+ typedef float feedRate_t;
+ #else
+ //#include
+ #endif
+
+ struct E_Step {
+ float extrude; //!< extrude distance in mm
+ float feedRate; //!< feed rate in mm/s
+ };
+
+ static constexpr E_Step ramming_sequence[] PROGMEM = { MMU2_RAMMING_SEQUENCE };
+ static constexpr E_Step load_to_nozzle_sequence[] PROGMEM = { MMU2_LOAD_TO_NOZZLE_SEQUENCE };
+
+ namespace MMU3 {
+
+ // general MMU setup for MK3
+ enum : uint8_t {
+ FILAMENT_UNKNOWN = 0xFFU
+ };
+
+ struct Version {
+ uint8_t major, minor, build;
+ };
+
+ // Top-level interface between Logic and Marlin.
+ // Intentionally named MMU3 to be (almost) a drop-in replacement for the previous implementation.
+ // Most of the public methods share the original naming convention as well.
+ class MMU3 {
+ public:
+ MMU3();
+
+ // Powers ON the MMU, then initializes the UART and protocol logic
+ void start();
+
+ // Stops the protocol logic, closes the UART, powers OFF the MMU
+ void stop();
+
+ // Serial output of MMU state
+ void status();
+
+ xState state() const { return _state; }
+
+ bool enabled() const { mmu_hw_enabled = state() == xState::Active; return mmu_hw_enabled; }
+
+ // Different levels of resetting the MMU
+ enum ResetForm : uint8_t {
+ Software = 0, //!< sends a X0 command into the MMU, the MMU will watchdog-reset itself
+ ResetPin = 1, //!< trigger the reset pin of the MMU
+ CutThePower = 2, //!< power off and power on (that includes +5V and +24V power lines)
+ EraseEEPROM = 42, //!< erase MMU EEPROM and then perform a software reset
+ };
+
+ // Saved print state on error.
+ enum SavedState : uint8_t {
+ None = 0, // No state saved.
+ ParkExtruder = 1, // The extruder was parked.
+ Cooldown = 2, // The extruder was allowed to cool.
+ CooldownPending = 4,
+ };
+
+ // Source of operation error
+ enum ErrorSource : uint8_t {
+ ErrorSourcePrinter = 0,
+ ErrorSourceMMU = 1,
+ ErrorSourceNone = 0xFF,
+ };
+
+ // Tune value in MMU registers as a way to recover from errors
+ // e.g. Idler Stallguard threshold
+ void tune();
+
+ // Perform a reset of the MMU
+ // @param level physical form of the reset
+ void reset(ResetForm level);
+
+ // Power off the MMU (cut the power)
+ void powerOff();
+
+ // Power on the MMU
+ void powerOn();
+
+ // Read from a MMU register (See gcode M707)
+ // @param address Address of register in hexidecimal
+ // @return true upon success
+ bool readRegister(uint8_t address);
+
+ // Write from a MMU register (See gcode M708)
+ // @param address Address of register in hexidecimal
+ // @param data Data to write to register
+ // @return true upon success
+ bool writeRegister(uint8_t address, uint16_t data);
+
+ // The main loop of MMU processing.
+ // Doesn't loop (block) inside, performs just one step of logic state machines.
+ // Also, internally it prevents recursive entries.
+ void mmu_loop();
+
+ // The main MMU command - select a different slot
+ // @param slot of the slot to be selected
+ // @return false if the operation cannot be performed (Stopped)
+ bool tool_change(uint8_t slot);
+
+ // Handling of special Tx, Tc, T? commands
+ bool tool_change(char code, uint8_t slot);
+
+ // Unload of filament in collaboration with the MMU.
+ // That includes rotating the printer's extruder in order to release filament.
+ // @return false if the operation cannot be performed (Stopped or cold extruder)
+ bool unload();
+
+ // Load (insert) filament just into the MMU (not into printer's nozzle)
+ // @return false if the operation cannot be performed (Stopped)
+ bool load_to_feeder(uint8_t slot);
+
+ // Load (push) filament from the MMU into the printer's nozzle
+ // @return false if the operation cannot be performed (Stopped or cold extruder)
+ bool load_to_nozzle(uint8_t slot);
+
+ // Move MMU's selector aside and push the selected filament forward.
+ // Usable for improving filament's tip or pulling the remaining piece of filament out completely.
+ bool eject_filament(uint8_t slot, bool enableFullScreenMsg=true);
+
+ // Issue a Cut command into the MMU
+ // Requires unloaded filament from the printer (obviously)
+ // @return false if the operation cannot be performed (Stopped)
+ bool cut_filament(uint8_t slot, bool enableFullScreenMsg=true);
+
+ // Issue a planned request for statistics data from MMU
+ void get_statistics();
+
+ // Issue a Try-Load command
+ // It behaves very similarly like a ToolChange, but it doesn't load the filament
+ // all the way down to the nozzle. The sole purpose of this operation
+ // is to check, that the filament will be ready for printing.
+ // @param slot index of slot to be tested
+ // @return true
+ bool loading_test(uint8_t slot);
+
+ // @return the active filament slot index (0-4) or 0xff in case of no active tool
+ uint8_t get_current_tool() const;
+
+ // @return The filament slot index (0 to 4) that will be loaded next, 0xff in case of no active tool change
+ uint8_t get_tool_change_tool() const;
+
+ bool set_filament_type(uint8_t slot, uint8_t type);
+
+ // Issue a "button" click into the MMU - to be used from Error screens of the MMU
+ // to select one of the 3 possible options to resolve the issue
+ void button(uint8_t index);
+
+ // Issue an explicit "homing" command into the MMU
+ void home(uint8_t mode);
+
+ // @return current state of FINDA (true=filament present, false=filament not present)
+ bool findaDetectsFilament() const { return logic.findaPressed(); }
+
+ uint16_t totalFailStatistics() const { return logic.FailStatistics(); }
+
+ // @return Current error code
+ ErrorCode mmuCurrentErrorCode() const { return logic.Error(); }
+
+ // @return Command in progress
+ uint8_t getCommandInProgress() const { return logic.CommandInProgress(); }
+
+ // @return Last error source
+ ErrorSource mmuLastErrorSource() const { return lastErrorSource; }
+
+ // @return Last error code
+ ErrorCode getLastErrorCode() const { return lastErrorCode; }
+
+ // @return the version of the connected MMU FW.
+ // In the future we'll return the trully detected FW version
+ Version getMMUFWVersion() const {
+ if (state() == xState::Active) {
+ return { logic.mmuFwVersionMajor(), logic.mmuFwVersionMinor(), logic.mmuFwVersionRevision() };
+ }
+ else {
+ return { 0, 0, 0 };
+ }
+ }
+
+ // Method to read-only mmu_print_saved
+ bool MMU_PRINT_SAVED() const { return mmu_print_saved != SavedState::None; }
+
+ // Automagically "press" a Retry button if we have any retry attempts left
+ // @param ec ErrorCode enum value
+ // @return true if auto-retry is ongoing, false when retry is unavailable or retry attempts are all used up
+ bool retryIfPossible(const ErrorCode ec);
+
+ // @return count for toolchange in current print
+ uint16_t toolChangeCounter() const { return toolchange_counter; }
+
+ // Set toolchange counter to zero
+ void resetToolChangeCounter() { toolchange_counter = 0; }
+
+ uint16_t tmcFailures() const { return _tmcFailures; }
+ void incrementTMCFailures() { ++_tmcFailures; }
+ void resetTMCFailures() { _tmcFailures = 0; }
+
+ // Retrieve cached value parsed from readRegister()
+ // or using M707
+ uint16_t getLastReadRegisterValue() const {
+ return lastReadRegisterValue;
+ }
+ void invokeErrorScreen(const ErrorCode ec) {
+ if (logic.CommandInProgress()) return; // MMU must not be busy
+ if (lastErrorCode == ec) return; // The error code is not a duplicate
+ if (mmuCurrentErrorCode() == ErrorCode::OK) { // The protocol must not be in error state
+ reportError(ec, ErrorSource::ErrorSourcePrinter);
+ }
+ }
+
+ void clearPrinterError() {
+ logic.clearPrinterError();
+ lastErrorCode = ErrorCode::OK;
+ lastErrorSource = ErrorSource::ErrorSourceNone;
+ }
+
+ // @brief Queue a button operation which the printer can act upon
+ // @param btn Button operation
+ void setPrinterButtonOperation(Buttons btn) {
+ printerButtonOperation = btn;
+ }
+
+ // @brief Get the printer button operation
+ // @return currently set printer button operation, it can be NoButton if nothing is queued
+ Buttons getPrinterButtonOperation() {
+ return printerButtonOperation;
+ }
+
+ void clearPrinterButtonOperation() {
+ printerButtonOperation = Buttons::NoButton;
+ }
+
+ static uint8_t cutter_mode; // mode 0:disabled | 1:enabled | 2:always (EXPERIMENTAL)
+ static int cutter_mode_addr; // EEPROM addr for cutter enabled setting
+ static uint8_t stealth_mode; // stealth mode
+ static int stealth_mode_addr; // EEPROM addr for stealth_mode setting
+ static bool mmu_hw_enabled; // MMU hardware can be Enabled/Disabled
+ // with the M709 S0 or M709 S1 commands
+ // and the last state is stored in the
+ // EEPROM
+
+ static int mmu_hw_enabled_addr; // EEPROM addr for mmu_hw_enabled
+
+ bool e_active();
+
+ #ifndef UNITTEST
+ private:
+ #endif
+
+ // Perform software self-reset of the MMU (sends an X0 command)
+ void resetX0();
+
+ // Perform software self-reset of the MMU + erase its EEPROM (sends X2a command)
+ void resetX42();
+
+ // Trigger reset pin of the MMU
+ void triggerResetPin();
+
+ // Perform power cycle of the MMU (cold boot)
+ // Please note this is a blocking operation (sleeps for some time inside while doing the power cycle)
+ void powerCycle();
+
+ // Stop the communication, but keep the MMU powered on (for scenarios with incorrect FW version)
+ void stopKeepPowered();
+
+ // Along with the mmu_loop method, this loops until a response from the MMU is received and acts upon.
+ // In case of an error, it parks the print head and turns off nozzle heating
+ // @return false if the command could not have been completed (MMU interrupted)
+ [[nodiscard]] bool manage_response(const bool move_axes, const bool turn_off_nozzle);
+
+ // The inner private implementation of mmu_loop()
+ // which is NOT (!!!) recursion-guarded. Use caution - but we do need it during waiting for hotend resume to keep comms alive!
+ // @param reportErrors true if Errors should raise MMU Error screen, false otherwise
+ void mmu_loop_inner(bool reportErrors);
+
+ // Performs one step of the protocol logic state machine
+ // and reports progress and errors if needed to attached ExtUIs.
+ // Updates the global state of MMU (Active/Connecting/Stopped) at runtime, see @ref State
+ // @param reportErrors true if Errors should raise MMU Error screen, false otherwise
+ StepStatus logicStep(bool reportErrors);
+
+ void filament_ramming();
+ void execute_extruder_sequence(const E_Step *sequence, uint8_t steps);
+ void execute_load_to_nozzle_sequence();
+
+ // Reports an error into attached ExtUIs
+ // @param ec error code, see ErrorCode
+ // @param res reporter error source, is either Printer (0) or MMU (1)
+ void reportError(ErrorCode ec, ErrorSource res);
+
+ // Reports progress of operations into attached ExtUIs
+ // @param pc progress code, see ProgressCode
+ void reportProgress(ProgressCode pc);
+
+ // Responds to a change of MMU's progress
+ // - plans additional steps, e.g. starts the E-motor after fsensor trigger
+ // The function is quite complex, because it needs to handle asynchronnous
+ // progress and error reports coming from the MMU without an explicit command
+ // - typically after MMU's start or after some HW issue on the MMU.
+ // It must ensure, that calls to @ref reportProgress and/or @ref reportError are
+ // only executed after @ref BeginReport has been called first.
+ void onMMUProgressMsg(ProgressCode pc);
+ // Progress code changed - act accordingly
+ void onMMUProgressMsgChanged(ProgressCode pc);
+ // Repeated calls when progress code remains the same
+ void onMMUProgressMsgSame(ProgressCode pc);
+
+ // @brief Save hotend temperature and set flag to cooldown hotend after 60 minutes
+ // @param turn_off_nozzle if true, the hotend temperature will be set to 0degC after 60 minutes
+ void saveHotendTemp(bool turn_off_nozzle);
+
+ // Save print and park the print head
+ void saveAndPark(bool move_axes);
+
+ // Resume hotend temperature, if it was cooled. Safe to call if we aren't saved.
+ void resumeHotendTemp();
+
+ // Resume position, if the extruder was parked. Safe to all if state was not saved.
+ void resumeUnpark();
+
+ // Check for any button/user input coming from the printer's UI
+ void checkUserInput();
+
+ // @brief Check whether to trigger a FINDA runout. If triggered this function will call M600 AUTO
+ // if SpoolJoin is enabled, otherwise M600 is called without AUTO which will prompt the user
+ // for the next filament slot to use
+ void checkFINDARunout();
+
+ // Entry check of all external commands.
+ // It can wait until the MMU becomes ready.
+ // Optionally, it can also emit/display an error screen and the user can decide what to do next.
+ // @return false if the MMU is not ready to perform the command (for whatever reason)
+ bool waitForMMUReady();
+
+ // After MMU completes a tool-change command
+ // the printer will push the filament by a constant distance. If the Fsensor untriggers
+ // at any moment the test fails. Else the test passes, and the E-motor retracts the
+ // filament back to its original position.
+ // @return false if test fails, true otherwise
+ bool verifyFilamentEnteredPTFE();
+
+ // Common processing of pushing filament into the extruder - shared by tool_change, load_to_nozzle and probably others
+ void toolChangeCommon(uint8_t slot);
+ bool toolChangeCommonOnce(uint8_t slot);
+
+ void helpUnloadToFinda();
+ void unloadInner();
+ void cutFilamentInner(uint8_t slot);
+
+ void setCurrentTool(uint8_t ex);
+
+ ProtocolLogic logic; //!< implementation of the protocol logic layer
+ uint8_t extruder; //!< currently active slot in the MMU ... somewhat... not sure where to get it from yet
+ uint8_t tool_change_extruder; //!< only used for UI purposes
+
+ xyz_pos_t resume_position;
+ int16_t resume_hotend_temp;
+
+ ProgressCode lastProgressCode = ProgressCode::OK;
+ ErrorCode lastErrorCode = ErrorCode::MMU_NOT_RESPONDING;
+ ErrorSource lastErrorSource = ErrorSource::ErrorSourceNone;
+ Buttons lastButton = Buttons::NoButton;
+ uint16_t lastReadRegisterValue = 0;
+ Buttons printerButtonOperation = Buttons::NoButton;
+
+ StepStatus logicStepLastStatus;
+
+ enum xState _state;
+
+ uint8_t mmu_print_saved;
+ bool loadFilamentStarted;
+ bool unloadFilamentStarted;
+
+ uint16_t toolchange_counter;
+ uint16_t _tmcFailures;
+ };
+
+ } // MMU3
+
+// following Marlin's way of doing stuff - one and only instance of MMU implementation in the code base
+// + avoiding buggy singletons on the AVR platform
+extern MMU3::MMU3 mmu3;
diff --git a/Marlin/src/feature/mmu3/mmu2_crc.cpp b/Marlin/src/feature/mmu3/mmu2_crc.cpp
new file mode 100644
index 000000000000..d94e2d9997c5
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_crc.cpp
@@ -0,0 +1,53 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/**
+ * mmu2_crc.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "mmu2_crc.h"
+
+#ifdef __AVR__
+ #include
+#endif
+
+namespace modules {
+
+namespace crc {
+
+uint8_t CRC8::CCITT_update(uint8_t crc, uint8_t b) {
+ #ifdef __AVR__
+ return _crc8_ccitt_update(crc, b);
+ #else
+ return CCITT_updateCX(crc, b);
+ #endif
+}
+
+} // namespace crc
+
+} // namespace modules
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_crc.h b/Marlin/src/feature/mmu3/mmu2_crc.h
new file mode 100644
index 000000000000..f7221b38f5a7
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_crc.h
@@ -0,0 +1,73 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * mmu2_crc.h
+ */
+
+#include
+
+namespace modules {
+
+// prevent silly indenting of the whole file
+
+// Contains all the necessary functions for computation of CRC
+namespace crc {
+
+class CRC8 {
+public:
+ // Compute/update CRC8 CCIIT from 8bits.
+ // Details: https://www.nongnu.org/avr-libc/user-manual/group__util__crc.html
+ static uint8_t CCITT_update(uint8_t crc, uint8_t b);
+
+ static constexpr uint8_t CCITT_updateCX(uint8_t crc, uint8_t b) {
+ uint8_t data = crc ^ b;
+ for (uint8_t i = 0; i < 8; i++) {
+ if ((data & 0x80U) != 0) {
+ data <<= 1U;
+ data ^= 0x07U;
+ }
+ else {
+ data <<= 1U;
+ }
+ }
+ return data;
+ }
+
+ // Compute/update CRC8 CCIIT from 16bits (convenience wrapper)
+ static constexpr uint8_t CCITT_updateW(uint8_t crc, uint16_t w) {
+ union U {
+ uint8_t b[2];
+ uint16_t w;
+ explicit constexpr inline U(uint16_t w)
+ : w(w) {}
+ }
+ u(w);
+ return CCITT_updateCX(CCITT_updateCX(crc, u.b[0]), u.b[1]);
+ }
+};
+
+} // namespace crc
+
+
+} // namespace modules
diff --git a/Marlin/src/feature/mmu3/mmu2_error_converter.cpp b/Marlin/src/feature/mmu3/mmu2_error_converter.cpp
new file mode 100644
index 000000000000..b37079807f8d
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_error_converter.cpp
@@ -0,0 +1,376 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/**
+ * mmu2_error_converter.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "../../core/language.h"
+#include "mmu2_error_converter.h"
+#include "mmu_hw/error_codes.h"
+#include "mmu_hw/errors_list.h"
+
+namespace MMU3 {
+
+ static ButtonOperations buttonSelectedOperation = ButtonOperations::NoOperation;
+
+ // we don't have a constexpr find_if in C++17/STL yet
+ template
+ constexpr InputIt find_if_cx(InputIt first, InputIt last, UnaryPredicate p) {
+ for (; first != last; ++first) {
+ if (p(*first)) return first;
+ }
+ return last;
+ }
+
+ // Making a constexpr FindError should instruct the compiler to optimize the
+ // PrusaErrorCodeIndex in such a way that no searching will ever be done at
+ // runtime. A call to FindError then compiles to a single instruction even on
+ // the AVR.
+ // static constexpr uint8_t FindErrorIndex(uint16_t pec) {
+ static uint8_t FindErrorIndex(uint16_t pec) {
+ constexpr uint16_t errorCodesSize = sizeof(errorCodes) / sizeof(errorCodes[0]);
+ constexpr const auto *errorCodesEnd = errorCodes + errorCodesSize;
+ const auto *i = find_if_cx(errorCodes, errorCodesEnd, [pec](uint16_t ed) {
+ return ed == pec;
+ });
+ return (i != errorCodesEnd) ? (i - errorCodes) : (errorCodesSize - 1);
+ }
+
+ // check that the searching algoritm works
+ // static_assert( FindErrorIndex(ERR_MECHANICAL_FINDA_DIDNT_TRIGGER) == 0);
+ // static_assert( FindErrorIndex(ERR_MECHANICAL_FINDA_FILAMENT_STUCK) == 1);
+ // static_assert( FindErrorIndex(ERR_MECHANICAL_FSENSOR_DIDNT_TRIGGER) == 2);
+ // static_assert( FindErrorIndex(ERR_MECHANICAL_FSENSOR_FILAMENT_STUCK) == 3);
+
+ constexpr ErrorCode operator&(ErrorCode a, ErrorCode b) {
+ return (ErrorCode)((uint16_t)a & (uint16_t)b);
+ }
+
+ constexpr bool ContainsBit(ErrorCode ec, ErrorCode mask) {
+ return (uint16_t)ec & (uint16_t)mask;
+ }
+
+ uint8_t PrusaErrorCodeIndex(const ErrorCode ec) {
+ switch (ec) {
+ case ErrorCode::FINDA_DIDNT_SWITCH_ON:
+ return FindErrorIndex(ERR_MECHANICAL_FINDA_DIDNT_TRIGGER);
+ case ErrorCode::FINDA_DIDNT_SWITCH_OFF:
+ return FindErrorIndex(ERR_MECHANICAL_FINDA_FILAMENT_STUCK);
+ case ErrorCode::FSENSOR_DIDNT_SWITCH_ON:
+ return FindErrorIndex(ERR_MECHANICAL_FSENSOR_DIDNT_TRIGGER);
+ case ErrorCode::FSENSOR_DIDNT_SWITCH_OFF:
+ return FindErrorIndex(ERR_MECHANICAL_FSENSOR_FILAMENT_STUCK);
+ case ErrorCode::FSENSOR_TOO_EARLY:
+ return FindErrorIndex(ERR_MECHANICAL_FSENSOR_TOO_EARLY);
+ case ErrorCode::FINDA_FLICKERS:
+ return FindErrorIndex(ERR_MECHANICAL_INSPECT_FINDA);
+ case ErrorCode::LOAD_TO_EXTRUDER_FAILED:
+ return FindErrorIndex(ERR_MECHANICAL_LOAD_TO_EXTRUDER_FAILED);
+ case ErrorCode::FILAMENT_EJECTED:
+ return FindErrorIndex(ERR_SYSTEM_FILAMENT_EJECTED);
+ case ErrorCode::FILAMENT_CHANGE:
+ return FindErrorIndex(ERR_SYSTEM_FILAMENT_CHANGE);
+
+ case ErrorCode::STALLED_PULLEY:
+ case ErrorCode::MOVE_PULLEY_FAILED:
+ return FindErrorIndex(ERR_MECHANICAL_PULLEY_CANNOT_MOVE);
+
+ case ErrorCode::HOMING_SELECTOR_FAILED:
+ return FindErrorIndex(ERR_MECHANICAL_SELECTOR_CANNOT_HOME);
+ case ErrorCode::MOVE_SELECTOR_FAILED:
+ return FindErrorIndex(ERR_MECHANICAL_SELECTOR_CANNOT_MOVE);
+
+ case ErrorCode::HOMING_IDLER_FAILED:
+ return FindErrorIndex(ERR_MECHANICAL_IDLER_CANNOT_HOME);
+ case ErrorCode::MOVE_IDLER_FAILED:
+ return FindErrorIndex(ERR_MECHANICAL_IDLER_CANNOT_MOVE);
+
+ case ErrorCode::MMU_NOT_RESPONDING:
+ return FindErrorIndex(ERR_CONNECT_MMU_NOT_RESPONDING);
+ case ErrorCode::PROTOCOL_ERROR:
+ return FindErrorIndex(ERR_CONNECT_COMMUNICATION_ERROR);
+ case ErrorCode::FILAMENT_ALREADY_LOADED:
+ return FindErrorIndex(ERR_SYSTEM_FILAMENT_ALREADY_LOADED);
+ case ErrorCode::INVALID_TOOL:
+ return FindErrorIndex(ERR_SYSTEM_INVALID_TOOL);
+ case ErrorCode::QUEUE_FULL:
+ return FindErrorIndex(ERR_SYSTEM_QUEUE_FULL);
+ case ErrorCode::VERSION_MISMATCH:
+ return FindErrorIndex(ERR_SYSTEM_FW_UPDATE_NEEDED);
+ case ErrorCode::INTERNAL:
+ return FindErrorIndex(ERR_SYSTEM_FW_RUNTIME_ERROR);
+ case ErrorCode::FINDA_VS_EEPROM_DISREPANCY:
+ return FindErrorIndex(ERR_SYSTEM_UNLOAD_MANUALLY);
+ case ErrorCode::MCU_UNDERVOLTAGE_VCC:
+ return FindErrorIndex(ERR_ELECTRICAL_MMU_MCU_ERROR);
+ default: break;
+ }
+
+ // Electrical issues which can be detected somehow.
+ // Need to be placed before TMC-related errors in order to process couples of error bits between single ones
+ // and to keep the code size down.
+ if (ContainsBit(ec, ErrorCode::TMC_PULLEY_BIT)) {
+ if ((ec & ErrorCode::MMU_SOLDERING_NEEDS_ATTENTION) == ErrorCode::MMU_SOLDERING_NEEDS_ATTENTION)
+ return FindErrorIndex(ERR_ELECTRICAL_MMU_PULLEY_SELFTEST_FAILED);
+ }
+ else if (ContainsBit(ec, ErrorCode::TMC_SELECTOR_BIT)) {
+ if ((ec & ErrorCode::MMU_SOLDERING_NEEDS_ATTENTION) == ErrorCode::MMU_SOLDERING_NEEDS_ATTENTION)
+ return FindErrorIndex(ERR_ELECTRICAL_MMU_SELECTOR_SELFTEST_FAILED);
+ }
+ else if (ContainsBit(ec, ErrorCode::TMC_IDLER_BIT)) {
+ if ((ec & ErrorCode::MMU_SOLDERING_NEEDS_ATTENTION) == ErrorCode::MMU_SOLDERING_NEEDS_ATTENTION)
+ return FindErrorIndex(ERR_ELECTRICAL_MMU_IDLER_SELFTEST_FAILED);
+ }
+
+ // TMC-related errors - multiple of these can occur at once
+ // - in such a case we report the first which gets found/converted into Prusa-Error-Codes (usually the fact, that one TMC has an issue is serious enough)
+ // By carefully ordering the checks here we can prioritize the errors being reported to the user.
+ if (ContainsBit(ec, ErrorCode::TMC_PULLEY_BIT)) {
+ if (ContainsBit(ec, ErrorCode::TMC_IOIN_MISMATCH))
+ return FindErrorIndex(ERR_ELECTRICAL_TMC_PULLEY_DRIVER_ERROR);
+ if (ContainsBit(ec, ErrorCode::TMC_RESET))
+ return FindErrorIndex(ERR_ELECTRICAL_TMC_PULLEY_DRIVER_RESET);
+ if (ContainsBit(ec, ErrorCode::TMC_UNDERVOLTAGE_ON_CHARGE_PUMP))
+ return FindErrorIndex(ERR_ELECTRICAL_TMC_PULLEY_UNDERVOLTAGE_ERROR);
+ if (ContainsBit(ec, ErrorCode::TMC_SHORT_TO_GROUND))
+ return FindErrorIndex(ERR_ELECTRICAL_TMC_PULLEY_DRIVER_SHORTED);
+ if (ContainsBit(ec, ErrorCode::TMC_OVER_TEMPERATURE_WARN))
+ return FindErrorIndex(ERR_TEMPERATURE_WARNING_TMC_PULLEY_TOO_HOT);
+ if (ContainsBit(ec, ErrorCode::TMC_OVER_TEMPERATURE_ERROR))
+ return FindErrorIndex(ERR_TEMPERATURE_TMC_PULLEY_OVERHEAT_ERROR);
+ }
+ else if (ContainsBit(ec, ErrorCode::TMC_SELECTOR_BIT)) {
+ if (ContainsBit(ec, ErrorCode::TMC_IOIN_MISMATCH))
+ return FindErrorIndex(ERR_ELECTRICAL_TMC_SELECTOR_DRIVER_ERROR);
+ if (ContainsBit(ec, ErrorCode::TMC_RESET))
+ return FindErrorIndex(ERR_ELECTRICAL_TMC_SELECTOR_DRIVER_RESET);
+ if (ContainsBit(ec, ErrorCode::TMC_UNDERVOLTAGE_ON_CHARGE_PUMP))
+ return FindErrorIndex(ERR_ELECTRICAL_TMC_SELECTOR_UNDERVOLTAGE_ERROR);
+ if (ContainsBit(ec, ErrorCode::TMC_SHORT_TO_GROUND))
+ return FindErrorIndex(ERR_ELECTRICAL_TMC_SELECTOR_DRIVER_SHORTED);
+ if (ContainsBit(ec, ErrorCode::TMC_OVER_TEMPERATURE_WARN))
+ return FindErrorIndex(ERR_TEMPERATURE_WARNING_TMC_SELECTOR_TOO_HOT);
+ if (ContainsBit(ec, ErrorCode::TMC_OVER_TEMPERATURE_ERROR))
+ return FindErrorIndex(ERR_TEMPERATURE_TMC_SELECTOR_OVERHEAT_ERROR);
+ }
+ else if (ContainsBit(ec, ErrorCode::TMC_IDLER_BIT)) {
+ if (ContainsBit(ec, ErrorCode::TMC_IOIN_MISMATCH))
+ return FindErrorIndex(ERR_ELECTRICAL_TMC_IDLER_DRIVER_ERROR);
+ if (ContainsBit(ec, ErrorCode::TMC_RESET))
+ return FindErrorIndex(ERR_ELECTRICAL_TMC_IDLER_DRIVER_RESET);
+ if (ContainsBit(ec, ErrorCode::TMC_UNDERVOLTAGE_ON_CHARGE_PUMP))
+ return FindErrorIndex(ERR_ELECTRICAL_TMC_IDLER_UNDERVOLTAGE_ERROR);
+ if (ContainsBit(ec, ErrorCode::TMC_SHORT_TO_GROUND))
+ return FindErrorIndex(ERR_ELECTRICAL_TMC_IDLER_DRIVER_SHORTED);
+ if (ContainsBit(ec, ErrorCode::TMC_OVER_TEMPERATURE_WARN))
+ return FindErrorIndex(ERR_TEMPERATURE_WARNING_TMC_IDLER_TOO_HOT);
+ if (ContainsBit(ec, ErrorCode::TMC_OVER_TEMPERATURE_ERROR))
+ return FindErrorIndex(ERR_TEMPERATURE_TMC_IDLER_OVERHEAT_ERROR);
+ }
+
+ // if nothing got caught, return a generic runtime error
+ return FindErrorIndex(ERR_OTHER_UNKNOWN_ERROR);
+ }
+
+ uint16_t PrusaErrorCode(const uint8_t i) { return (uint16_t)pgm_read_word(&errorCodes[i]); }
+
+ FSTR_P const PrusaErrorTitle(const uint8_t i) { return (FSTR_P const)pgm_read_ptr(&errorTitles[i]); }
+ FSTR_P const PrusaErrorDesc(const uint8_t i) { return (FSTR_P const)pgm_read_ptr(&errorDescs[i]); }
+
+ uint8_t PrusaErrorButtons(const uint8_t i) { return pgm_read_byte(errorButtons + i); }
+
+ FSTR_P const PrusaErrorButtonTitle(const uint8_t bi) {
+ // -1 represents the hidden NoOperation button which is not drawn in any way
+ return (FSTR_P const)pgm_read_ptr(&btnOperation[bi - 1]);
+ }
+
+ Buttons ButtonPressed(const ErrorCode ec) {
+ if (buttonSelectedOperation == ButtonOperations::NoOperation || buttonSelectedOperation == ButtonOperations::MoreInfo)
+ return Buttons::NoButton; // no button
+
+ const auto result = ButtonAvailable(ec);
+ buttonSelectedOperation = ButtonOperations::NoOperation; // Reset operation
+
+ return result;
+ }
+
+ Buttons ButtonAvailable(const ErrorCode ec) {
+ uint8_t ei = PrusaErrorCodeIndex(ec);
+
+ // The list of responses which occur in mmu error dialogs
+ // Return button index or perform some action on the MK3 by itself (like Reset MMU)
+ // Based on Prusa-Error-Codes errors_list.h
+ // So far hardcoded, but should be generated in the future
+ switch (PrusaErrorCode(ei)) {
+ case ERR_MECHANICAL_FINDA_DIDNT_TRIGGER:
+ case ERR_MECHANICAL_FINDA_FILAMENT_STUCK:
+ case ERR_MECHANICAL_FSENSOR_DIDNT_TRIGGER:
+ case ERR_MECHANICAL_FSENSOR_FILAMENT_STUCK:
+ case ERR_MECHANICAL_FSENSOR_TOO_EARLY:
+ case ERR_MECHANICAL_INSPECT_FINDA:
+ case ERR_MECHANICAL_SELECTOR_CANNOT_MOVE:
+ case ERR_MECHANICAL_IDLER_CANNOT_MOVE:
+ case ERR_MECHANICAL_PULLEY_CANNOT_MOVE:
+ case ERR_SYSTEM_UNLOAD_MANUALLY:
+ switch (buttonSelectedOperation) {
+ // may be allow move selector right and left in the future
+ case ButtonOperations::Retry: // "Repeat action"
+ return Buttons::Middle;
+ default:
+ break;
+ }
+ break;
+ case ERR_MECHANICAL_SELECTOR_CANNOT_HOME:
+ case ERR_MECHANICAL_IDLER_CANNOT_HOME:
+ switch (buttonSelectedOperation) {
+ // may be allow move selector right and left in the future
+ case ButtonOperations::Tune: // Tune Stallguard threshold
+ return Buttons::TuneMMU;
+ case ButtonOperations::Retry: // "Repeat action"
+ return Buttons::Middle;
+ default:
+ break;
+ }
+ break;
+ case ERR_MECHANICAL_LOAD_TO_EXTRUDER_FAILED:
+ case ERR_SYSTEM_FILAMENT_EJECTED:
+ switch (buttonSelectedOperation) {
+ case ButtonOperations::Continue: // User solved the serious mechanical problem by hand - there is no other way around
+ return Buttons::Middle;
+ default:
+ break;
+ }
+ break;
+ case ERR_SYSTEM_FILAMENT_CHANGE:
+ switch (buttonSelectedOperation) {
+ case ButtonOperations::Load:
+ return Buttons::Load;
+ case ButtonOperations::Eject:
+ return Buttons::Eject;
+ default:
+ break;
+ }
+ break;
+ case ERR_TEMPERATURE_WARNING_TMC_PULLEY_TOO_HOT:
+ case ERR_TEMPERATURE_WARNING_TMC_SELECTOR_TOO_HOT:
+ case ERR_TEMPERATURE_WARNING_TMC_IDLER_TOO_HOT:
+ switch (buttonSelectedOperation) {
+ case ButtonOperations::Continue: // "Continue"
+ return Buttons::Left;
+ case ButtonOperations::ResetMMU: // "Reset MMU"
+ return Buttons::ResetMMU;
+ default:
+ break;
+ }
+ break;
+
+ case ERR_TEMPERATURE_TMC_PULLEY_OVERHEAT_ERROR:
+ case ERR_TEMPERATURE_TMC_SELECTOR_OVERHEAT_ERROR:
+ case ERR_TEMPERATURE_TMC_IDLER_OVERHEAT_ERROR:
+
+ case ERR_ELECTRICAL_TMC_PULLEY_DRIVER_ERROR:
+ case ERR_ELECTRICAL_TMC_SELECTOR_DRIVER_ERROR:
+ case ERR_ELECTRICAL_TMC_IDLER_DRIVER_ERROR:
+
+ case ERR_ELECTRICAL_TMC_PULLEY_DRIVER_RESET:
+ case ERR_ELECTRICAL_TMC_SELECTOR_DRIVER_RESET:
+ case ERR_ELECTRICAL_TMC_IDLER_DRIVER_RESET:
+
+ case ERR_ELECTRICAL_TMC_PULLEY_UNDERVOLTAGE_ERROR:
+ case ERR_ELECTRICAL_TMC_SELECTOR_UNDERVOLTAGE_ERROR:
+ case ERR_ELECTRICAL_TMC_IDLER_UNDERVOLTAGE_ERROR:
+
+ case ERR_ELECTRICAL_TMC_PULLEY_DRIVER_SHORTED:
+ case ERR_ELECTRICAL_TMC_SELECTOR_DRIVER_SHORTED:
+ case ERR_ELECTRICAL_TMC_IDLER_DRIVER_SHORTED:
+
+ case ERR_ELECTRICAL_MMU_PULLEY_SELFTEST_FAILED:
+ case ERR_ELECTRICAL_MMU_SELECTOR_SELFTEST_FAILED:
+ case ERR_ELECTRICAL_MMU_IDLER_SELFTEST_FAILED:
+
+ case ERR_SYSTEM_QUEUE_FULL:
+ case ERR_SYSTEM_FW_RUNTIME_ERROR:
+ case ERR_ELECTRICAL_MMU_MCU_ERROR:
+ switch (buttonSelectedOperation) {
+ case ButtonOperations::ResetMMU: // "Reset MMU"
+ return Buttons::ResetMMU;
+ default:
+ break;
+ }
+ break;
+ case ERR_CONNECT_MMU_NOT_RESPONDING:
+ case ERR_CONNECT_COMMUNICATION_ERROR:
+ case ERR_SYSTEM_FW_UPDATE_NEEDED:
+ switch (buttonSelectedOperation) {
+ case ButtonOperations::DisableMMU: // "Disable"
+ return Buttons::DisableMMU;
+ case ButtonOperations::ResetMMU: // "ResetMMU"
+ return Buttons::ResetMMU;
+ default:
+ break;
+ }
+ break;
+ case ERR_SYSTEM_FILAMENT_ALREADY_LOADED:
+ switch (buttonSelectedOperation) {
+ case ButtonOperations::Unload: // "Unload"
+ return Buttons::Left;
+ case ButtonOperations::Continue: // "Proceed/Continue"
+ return Buttons::Right;
+ default:
+ break;
+ }
+ break;
+
+ case ERR_SYSTEM_INVALID_TOOL:
+ switch (buttonSelectedOperation) {
+ case ButtonOperations::StopPrint: // "Stop print"
+ return Buttons::StopPrint;
+ case ButtonOperations::ResetMMU: // "Reset MMU"
+ return Buttons::ResetMMU;
+ default:
+ break;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return Buttons::NoButton;
+ }
+
+ void SetButtonResponse(ButtonOperations rsp) {
+ buttonSelectedOperation = rsp;
+ }
+
+ ButtonOperations GetButtonResponse() {
+ return buttonSelectedOperation;
+ }
+
+} // MMU3
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_error_converter.h b/Marlin/src/feature/mmu3/mmu2_error_converter.h
new file mode 100644
index 000000000000..93a4d5e455ae
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_error_converter.h
@@ -0,0 +1,73 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * mmu2_error_converter.h
+ */
+
+#include
+#include
+#include "mmu_hw/buttons.h"
+#include "mmu_hw/error_codes.h"
+
+ namespace MMU3 {
+
+ // Translates MMU3::ErrorCode into an index of Prusa-Error-Codes
+ // Basically this is the way to obtain an index into all other functions in this API
+ uint8_t PrusaErrorCodeIndex(const ErrorCode ec);
+
+ // @return pointer to a PROGMEM string representing the Title of the Prusa-Error-Codes error
+ // @param i index of the error - obtained by calling ErrorCodeIndex
+ FSTR_P const PrusaErrorTitle(const uint8_t i);
+
+ // @return pointer to a PROGMEM string representing the multi-page Description of the Prusa-Error-Codes error
+ // @param i index of the error - obtained by calling ErrorCodeIndex
+ FSTR_P const PrusaErrorDesc(const uint8_t i);
+
+ // @return the actual numerical value of the Prusa-Error-Codes error
+ // @param i index of the error - obtained by calling ErrorCodeIndex
+ uint16_t PrusaErrorCode(const uint8_t i);
+
+ // @return Btns pair of buttons for a particular Prusa-Error-Codes error
+ // @param i index of the error - obtained by calling ErrorCodeIndex
+ uint8_t PrusaErrorButtons(const uint8_t i);
+
+ // @return pointer to a PROGMEM string representing the Title of a button
+ // @param i index of the error - obtained by calling PrusaErrorButtons + extracting low or high nibble from the Btns pair
+ FSTR_P const PrusaErrorButtonTitle(const uint8_t bi);
+
+ // Sets the selected button for later pick-up by the MMU state machine.
+ // Used to save the GUI selection/decoupling
+ void SetButtonResponse(const ButtonOperations rsp);
+ ButtonOperations GetButtonResponse();
+
+ // @return button index/code based on currently processed error/screen
+ // Clears the "pressed" button upon exit
+ Buttons ButtonPressed(const ErrorCode ec);
+
+ // @return button index/code based on currently processed error/screen
+ // Used as a subfunction of ButtonPressed.
+ // Does not clear the "pressed" button upon exit
+ Buttons ButtonAvailable(const ErrorCode ec);
+
+ } // MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_fsensor.cpp b/Marlin/src/feature/mmu3/mmu2_fsensor.cpp
new file mode 100644
index 000000000000..4252fb517441
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_fsensor.cpp
@@ -0,0 +1,65 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/**
+ * mmu2_fsensor.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "../../feature/runout.h"
+#include "mmu2_fsensor.h"
+
+namespace MMU3 {
+
+ #if HAS_FILAMENT_SENSOR
+
+ FSensorBlockRunout::FSensorBlockRunout() {
+ runout.enabled = false; // Suppress filament runouts while loading filament.
+ //fsensor.setAutoLoadEnabled(false); //suppress filament autoloads while loading filament.
+ }
+
+ FSensorBlockRunout::~FSensorBlockRunout() {
+ //fsensor.settings_init(); // restore filament runout state.
+ runout.reset();
+ runout.enabled = true;
+ //SERIAL_ECHOLNPGM("FSUnBlockRunout");
+ }
+
+ #else
+
+ FSensorBlockRunout::FSensorBlockRunout() { }
+ FSensorBlockRunout::~FSensorBlockRunout() { }
+
+ #endif
+
+
+ FilamentState WhereIsFilament() {
+ //return fsensor.getFilamentPresent() ? FilamentState::AT_FSENSOR : FilamentState::NOT_PRESENT;
+ return FILAMENT_PRESENT() ? FilamentState::AT_FSENSOR : FilamentState::NOT_PRESENT;
+ }
+
+} // MMU3
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_fsensor.h b/Marlin/src/feature/mmu3/mmu2_fsensor.h
new file mode 100644
index 000000000000..bda6bf2a7064
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_fsensor.h
@@ -0,0 +1,55 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * mmu2_fsensor.h
+ */
+
+#include "../../core/macros.h"
+#include
+
+#define FILAMENT_PRESENT() (READ(FIL_RUNOUT1_PIN) != FIL_RUNOUT1_STATE)
+
+namespace MMU3 {
+
+ // Can be used to block printer's filament sensor handling - to avoid errorneous injecting of M600
+ // while doing a toolchange with the MMU
+ // In case of "no filament sensor" these methods default to an empty implementation
+ class FSensorBlockRunout {
+ public:
+ FSensorBlockRunout();
+ ~FSensorBlockRunout();
+ };
+
+ // 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
+ UNAVAILABLE = 3 //!< Sensor not available (likely not connected due broken cable)
+ };
+
+ FilamentState WhereIsFilament();
+
+} // MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_log.cpp b/Marlin/src/feature/mmu3/mmu2_log.cpp
new file mode 100644
index 000000000000..4dd0d79bf0d6
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_log.cpp
@@ -0,0 +1,47 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/**
+ * mmu2_log.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "mmu2_log.h"
+
+namespace MMU3 {
+
+ void LogEchoEvent_P(PGM_P const pstr) {
+ SERIAL_ECHO_START(); // @@TODO Decide MMU errors on serial line
+ SERIAL_MMU2();
+ SERIAL_ECHOLN_P(pstr);
+ }
+
+ void LogErrorEvent_P(PGM_P const pstr) {
+ LogEchoEvent_P(pstr);
+ }
+
+} // MMU3
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_log.h b/Marlin/src/feature/mmu3/mmu2_log.h
new file mode 100644
index 000000000000..e4dbffeee2a4
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_log.h
@@ -0,0 +1,82 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * mmu2_log.h
+ */
+
+#include "../../inc/MarlinConfig.h"
+
+namespace MMU3 {
+
+ // Report the msg into the general logging subsystem (through Marlin's SERIAL_ECHO stuff)
+ // @param msg pointer to a string in PROGMEM
+ // On the AVR platform this variant reads the input string from PROGMEM.
+ // On the ARM platform it calls LogErrorEvent directly (silently expecting the compiler to optimize it away)
+ void LogErrorEvent_P(PGM_P const pstr);
+ inline void LogErrorEvent(FSTR_P const fstr) { LogErrorEvent_P(FTOP(fstr)); }
+
+ // Report the msg into the general logging subsystem (through Marlin's SERIAL_ECHO stuff)
+ // @param msg pointer to a string in PROGMEM
+ // On the AVR platform this variant reads the input string from PROGMEM.
+ // On the ARM platform it calls LogErrorEvent directly (silently expecting the compiler to optimize it away)
+ void LogEchoEvent_P(PGM_P const pstr);
+ inline void LogEchoEvent(FSTR_P const fstr) { LogEchoEvent_P(FTOP(fstr)); }
+
+} // MMU3
+
+#ifndef UNITTEST
+
+ #define SERIAL_MMU2() { SERIAL_ECHO(F("MMU3:")); }
+
+ #define MMU2_ECHO_MSGLN(S) do { \
+ SERIAL_ECHO_START(); \
+ SERIAL_MMU2(); \
+ SERIAL_ECHOLN(S); \
+ }while(0)
+ #define MMU2_ERROR_MSGLN(S) MMU2_ECHO_MSGLN(S) //! @todo Decide MMU errors on serial line
+ #define MMU2_ECHO_MSGRPGM(S) do { \
+ SERIAL_ECHO_START(); \
+ SERIAL_MMU2(); \
+ SERIAL_ECHO_P(S); \
+ }while(0)
+ #define MMU2_ERROR_MSGRPGM(S) MMU2_ECHO_MSGRPGM(S) //! @todo Decide MMU errors on serial line
+ #define MMU2_ECHO_MSG(S) do { \
+ SERIAL_ECHO_START(); \
+ SERIAL_MMU2(); \
+ SERIAL_ECHO(S); \
+ }while(0)
+ #define MMU2_ERROR_MSG(S) MMU2_ECHO_MSG(S) //! @todo Decide MMU errors on serial line
+
+#else // UNITTEST
+
+ #include "stubs/stub_interfaces.h"
+ #define MMU2_ECHO_MSGLN(S) marlinLogSim.AppendLine(S)
+ #define MMU2_ERROR_MSGLN(S) marlinLogSim.AppendLine(S)
+ #define MMU2_ECHO_MSGRPGM(S) /* marlinLogSim.AppendLine(S) */
+ #define MMU2_ERROR_MSGRPGM(S) /* marlinLogSim.AppendLine(S) */
+ #define SERIAL_ECHOLNPGM(S) /* marlinLogSim.AppendLine(S) */
+ #define SERIAL_ECHOPGM(S) /* */
+ #define SERIAL_ECHOLN(S) /* marlinLogSim.AppendLine(S) */
+
+#endif // UNITTEST
diff --git a/Marlin/src/feature/mmu3/mmu2_marlin.h b/Marlin/src/feature/mmu3/mmu2_marlin.h
new file mode 100644
index 000000000000..2754a2274be9
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_marlin.h
@@ -0,0 +1,74 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * mmu2_marlin.h
+ */
+
+#include "../../inc/MarlinConfig.h"
+
+namespace MMU3 {
+
+ // This interface separates Marlin1/Marlin2 from the MMU top logic layer.
+ // - Unify implementation among MK3 and Buddy FW
+ // - Enable unit testing of MMU top layer
+
+ void extruder_move(const_float_t distance, const_float_t feedRate_mm_s, const bool sync=true);
+ void extruder_schedule_turning(const_float_t feedRate_mm_s);
+
+ float move_raise_z(const_float_t delta);
+
+ void planner_abort_queued_moves();
+ void planner_synchronize();
+ bool planner_any_moves();
+ float stepper_get_machine_position_E_mm();
+ float planner_get_current_position_E();
+ void planner_set_current_position_E(float e);
+ xyz_pos_t planner_current_position();
+
+ void motion_do_blocking_move_to_xy(float rx, float ry, float feedRate_mm_s);
+ void motion_do_blocking_move_to_z(float z, float feedRate_mm_s);
+
+ void nozzle_park();
+
+ bool marlin_printingIsActive();
+ void marlin_manage_heater();
+ void marlin_manage_inactivity(bool b);
+ void marlin_idle(bool b);
+ void marlin_refresh_print_state_in_ram();
+ void marlin_clear_print_state_in_ram();
+ void marlin_stop_and_save_print_to_ram();
+
+ int16_t thermal_degTargetHotend();
+ int16_t thermal_degHotend();
+ void thermal_setExtrudeMintemp(int16_t t);
+ void thermal_setTargetHotend(int16_t t);
+
+ void safe_delay_keep_alive(uint16_t t);
+
+ void Enable_E0();
+ void Disable_E0();
+
+ bool xy_are_trusted();
+
+} // MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_marlin1.cpp b/Marlin/src/feature/mmu3/mmu2_marlin1.cpp
new file mode 100644
index 000000000000..3fae1c3e3ad6
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_marlin1.cpp
@@ -0,0 +1,190 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/**
+ * mmu2_marlin1.cpp
+ * MK3 / Marlin1 implementation of support routines for the MMU3
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "../../MarlinCore.h"
+#include "../../module/stepper.h"
+#include "../../module/planner.h"
+#include "../../module/temperature.h"
+
+#include "../../feature/pause.h"
+#include "../../libs/nozzle.h"
+#include "mmu2_marlin.h"
+
+namespace MMU3 {
+
+ static void planner_line_to_current_position(float feedRate_mm_s) {
+ line_to_current_position(feedRate_mm_s);
+ }
+
+ static void planner_line_to_current_position_sync(float feedRate_mm_s) {
+ planner_line_to_current_position(feedRate_mm_s);
+ planner_synchronize();
+ }
+
+ void extruder_move(const_float_t delta, const_float_t feedRate_mm_s, const bool sync/*=true*/) {
+ current_position.e += delta / planner.e_factor[active_extruder];
+ planner_line_to_current_position(feedRate_mm_s);
+ if (sync) planner.synchronize();
+ }
+
+ float move_raise_z(const_float_t delta) {
+ //return raise_z(delta);
+ xyze_pos_t current_position_before = current_position;
+ do_z_clearance_by(delta);
+ return (current_position - current_position_before).z;
+ }
+
+ void planner_abort_queued_moves() {
+ //planner_abort_hard();
+ quickstop_stepper();
+
+ // Unblock the planner. This should be safe in the
+ // toolchange context. Currently we are mainly aborting
+ // excess E-moves after detecting filament during toolchange.
+ // If a MMU error is reported, the planner must be unblocked
+ // as well so the extruder can be parked safely.
+ //planner_aborted = false;
+ // eoyilmaz: we don't need this part, the print is not aborted
+ }
+
+ void planner_synchronize() {
+ planner.synchronize();
+ }
+
+ bool planner_any_moves() {
+ return planner.has_blocks_queued();
+ }
+
+ float planner_get_machine_position_E_mm() {
+ return current_position.e;
+ }
+
+ float stepper_get_machine_position_E_mm() {
+ return planner.get_axis_position_mm(E_AXIS);
+ }
+
+ float planner_get_current_position_E() {
+ return current_position.e;
+ }
+
+ void planner_set_current_position_E(float e) {
+ current_position.e = e;
+ }
+
+ xyz_pos_t planner_current_position() {
+ return xyz_pos_t(current_position);
+ }
+
+ void motion_do_blocking_move_to_xy(float rx, float ry, float feedRate_mm_s) {
+ current_position[X_AXIS] = rx;
+ current_position[Y_AXIS] = ry;
+ planner_line_to_current_position_sync(feedRate_mm_s);
+ }
+
+ void motion_do_blocking_move_to_z(float z, float feedRate_mm_s) {
+ current_position[Z_AXIS] = z;
+ planner_line_to_current_position_sync(feedRate_mm_s);
+ }
+
+ void nozzle_park() {
+ #if ANY(NOZZLE_CLEAN_FEATURE, NOZZLE_PARK_FEATURE)
+ #if ALL(ADVANCED_PAUSE_FEATURE)
+ xyz_pos_t park_point = NOZZLE_PARK_POINT;
+ nozzle.park(0, park_point);
+ #endif
+ #endif
+ }
+
+ bool marlin_printingIsActive() { return printingIsActive(); }
+
+ void marlin_manage_heater() { thermalManager.task(); }
+
+ void marlin_manage_inactivity(const bool b) { idle(b); }
+
+ void marlin_idle(bool b) {
+ thermalManager.task();
+ idle(b);
+ }
+
+ void marlin_refresh_print_state_in_ram() {
+ // refresh_print_state_in_ram();
+ // TODO: I don't see a comparable implementation in Marlin.
+ }
+
+ void marlin_clear_print_state_in_ram() {
+ // clear_print_state_in_ram();
+ // TODO: I don't see a comparable implementation in Marlin.
+ }
+
+ void marlin_stop_and_save_print_to_ram() {
+ // stop_and_save_print_to_ram(0,0);
+ #if ENABLED(ADVANCED_PAUSE_FEATURE)
+ constexpr xyz_pos_t park_point = NOZZLE_PARK_POINT;
+ pause_print(0, park_point);
+ #endif
+ }
+
+ int16_t thermal_degTargetHotend() {
+ return thermalManager.degTargetHotend(0);
+ }
+
+ int16_t thermal_degHotend() {
+ return thermalManager.degHotend(0);
+ }
+
+ void thermal_setExtrudeMintemp(int16_t t) {
+ thermalManager.extrude_min_temp = t;
+ }
+
+ void thermal_setTargetHotend(int16_t t) {
+ thermalManager.setTargetHotend(t, 0);
+ }
+
+ void safe_delay_keep_alive(uint16_t t) {
+ idle(true);
+ safe_delay(t);
+ }
+
+ void Enable_E0() {
+ stepper.enable_extruder(TERN_(HAS_EXTRUDERS, 0));
+ }
+
+ void Disable_E0() {
+ stepper.disable_extruder(TERN_(HAS_EXTRUDERS, 0));
+ }
+
+ bool xy_are_trusted() {
+ return axis_is_trusted(X_AXIS) && axis_is_trusted(Y_AXIS);
+ }
+
+} // MMU3
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_marlin_macros.h b/Marlin/src/feature/mmu3/mmu2_marlin_macros.h
new file mode 100644
index 000000000000..b277ce98056e
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_marlin_macros.h
@@ -0,0 +1,49 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * mmu2_marlin_macros.h
+ */
+
+// This file will not be the same on Marlin1 and Marlin2.
+// Its purpose is to unify different macros in either of Marlin incarnations.
+
+#ifdef __AVR__
+ #include "../../MarlinCore.h"
+ // brings _O and _T macros into MMU
+ #include "../../core/language.h"
+ #include "../../gcode/gcode.h"
+ // we don't have these in Marlin 2.x so just define them here again
+ #define _O(x) x
+ #define _T(x) x
+ #define MARLIN_KEEPALIVE_STATE_IN_PROCESS KEEPALIVE_STATE(IN_PROCESS)
+#elif defined(UNITTEST)
+ #define _O(x) x
+ #define _T(x) x
+ #define MARLIN_KEEPALIVE_STATE_IN_PROCESS /*KEEPALIVE_STATE(IN_PROCESS) TODO*/
+#else
+ #include "../../gcode/gcode.h"
+ #define _O(x) x
+ #define _T(x) x
+ #define MARLIN_KEEPALIVE_STATE_IN_PROCESS KEEPALIVE_STATE(IN_PROCESS)
+#endif
diff --git a/Marlin/src/feature/mmu3/mmu2_power.cpp b/Marlin/src/feature/mmu3/mmu2_power.cpp
new file mode 100644
index 000000000000..418f32aef23e
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_power.cpp
@@ -0,0 +1,65 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/**
+ * mmu2_power.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "mmu2.h"
+#include "mmu2_power.h"
+
+#include "../../MarlinCore.h"
+
+#include "../../core/macros.h"
+#include "../../core/boards.h"
+#include "../../pins/pins.h"
+
+namespace MMU3 {
+
+// On MK3 we cannot do actual power cycle on HW. Instead trigger a hardware reset.
+void power_on() {
+ #if PIN_EXISTS(MMU2_RST)
+ OUT_WRITE(MMU2_RST_PIN, HIGH);
+ #endif
+ power_reset();
+}
+
+void power_off() {}
+
+void power_reset() {
+ #if PIN_EXISTS(MMU2_RST) // HW - pulse reset pin
+ WRITE(MMU2_RST_PIN, LOW);
+ safe_delay(100);
+ WRITE(MMU2_RST_PIN, HIGH);
+ #else
+ mmu3.reset(MMU3::Software); // TODO: Needs redesign. This power implementation shouldn't know anything about the MMU itself
+ #endif
+ // otherwise HW reset is not available
+}
+
+} // MMU3
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_power.h b/Marlin/src/feature/mmu3/mmu2_power.h
new file mode 100644
index 000000000000..4f6b94f01ed7
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_power.h
@@ -0,0 +1,36 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * mmu2_power.h
+ */
+
+namespace MMU3 {
+
+void power_on();
+
+void power_off();
+
+void power_reset();
+
+} // MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_progress_converter.cpp b/Marlin/src/feature/mmu3/mmu2_progress_converter.cpp
new file mode 100644
index 000000000000..0e8e258e5365
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_progress_converter.cpp
@@ -0,0 +1,82 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/**
+ * mmu2_progress_converter.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "../../core/language.h"
+#include "mmu2_progress_converter.h"
+#ifdef __AVR__
+ #include
+#endif
+#include "mmu_hw/progress_codes.h"
+#include "mmu_hw/errors_list.h"
+
+namespace MMU3 {
+
+ FSTR_P const progressTexts[] PROGMEM = {
+ GET_TEXT_F(MSG_PROGRESS_OK),
+ GET_TEXT_F(MSG_PROGRESS_ENGAGE_IDLER),
+ GET_TEXT_F(MSG_PROGRESS_DISENGAGE_IDLER),
+ GET_TEXT_F(MSG_PROGRESS_UNLOAD_FINDA),
+ GET_TEXT_F(MSG_PROGRESS_UNLOAD_PULLEY),
+ GET_TEXT_F(MSG_PROGRESS_FEED_FINDA),
+ GET_TEXT_F(MSG_PROGRESS_FEED_EXTRUDER),
+ GET_TEXT_F(MSG_PROGRESS_FEED_NOZZLE),
+ GET_TEXT_F(MSG_PROGRESS_AVOID_GRIND),
+ GET_TEXT_F(MSG_FINISHING_MOVEMENTS), // reuse from messages.cpp
+ GET_TEXT_F(MSG_PROGRESS_DISENGAGE_IDLER), // err disengaging idler is the same text
+ GET_TEXT_F(MSG_PROGRESS_ENGAGE_IDLER), // engage dtto.
+ GET_TEXT_F(MSG_PROGRESS_WAIT_USER),
+ GET_TEXT_F(MSG_PROGRESS_ERR_INTERNAL),
+ GET_TEXT_F(MSG_PROGRESS_ERR_HELP_FIL),
+ GET_TEXT_F(MSG_PROGRESS_ERR_TMC),
+ GET_TEXT_F(MSG_UNLOADING_FILAMENT), // reuse from messages.cpp
+ GET_TEXT_F(MSG_LOADING_FILAMENT), // reuse from messages.cpp
+ GET_TEXT_F(MSG_PROGRESS_SELECT_SLOT),
+ GET_TEXT_F(MSG_PROGRESS_PREPARE_BLADE),
+ GET_TEXT_F(MSG_PROGRESS_PUSH_FILAMENT),
+ GET_TEXT_F(MSG_PROGRESS_PERFORM_CUT),
+ GET_TEXT_F(MSG_PROGRESSPSTRETURN_SELECTOR),
+ GET_TEXT_F(MSG_PROGRESS_PARK_SELECTOR),
+ GET_TEXT_F(MSG_PROGRESS_EJECT_FILAMENT),
+ GET_TEXT_F(MSG_PROGRESSPSTRETRACT_FINDA),
+ GET_TEXT_F(MSG_PROGRESS_HOMING),
+ GET_TEXT_F(MSG_PROGRESS_MOVING_SELECTOR),
+ GET_TEXT_F(MSG_PROGRESS_FEED_FSENSOR)
+ };
+
+ FSTR_P const ProgressCodeToText(const ProgressCode pc) {
+ // @@TODO ?? a better fallback option?
+ return (int(pc) < COUNT(progressTexts))
+ ? static_cast(pgm_read_ptr(&progressTexts[(uint16_t)pc]))
+ : static_cast(pgm_read_ptr(&progressTexts[0]));
+ }
+
+} // MMU3
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_progress_converter.h b/Marlin/src/feature/mmu3/mmu2_progress_converter.h
new file mode 100644
index 000000000000..e7b1d8682153
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_progress_converter.h
@@ -0,0 +1,36 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * mmu2_progress_converter.h
+ */
+
+#include "mmu_hw/progress_codes.h"
+
+#include "../../HAL/shared/Marduino.h"
+
+namespace MMU3 {
+
+FSTR_P const ProgressCodeToText(const ProgressCode pc);
+
+}
diff --git a/Marlin/src/feature/mmu3/mmu2_protocol.cpp b/Marlin/src/feature/mmu3/mmu2_protocol.cpp
new file mode 100644
index 000000000000..6cc5423bce5b
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_protocol.cpp
@@ -0,0 +1,418 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/**
+ * mmu2_protocol.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#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]
+// P[0-9] : command being processed i.e. operation running, may contain a state number
+// E[0-9][0-9] : error 1-9 while doing a tool change
+// F[0-9] : 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': // write is gonna be a special one
+ case 'K':
+ case 'F':
+ case 'f':
+ case 'H':
+ case 'R':
+ requestMsg.code = (RequestMsgCodes)c;
+ requestMsg.value = 0;
+ requestMsg.value2 = 0;
+ requestMsg.crc8 = 0;
+ rqState = (c == 'W') ? RequestStates::Address : RequestStates::Value; // prepare special automaton path for Write commands
+ return DecodeStatus::NeedMoreData;
+ default:
+ requestMsg.code = RequestMsgCodes::unknown;
+ rqState = RequestStates::Error;
+ return DecodeStatus::Error;
+ }
+ case RequestStates::Value:
+ if (IsHexDigit(c)) {
+ requestMsg.value <<= 4U;
+ requestMsg.value |= Char2Nibble(c);
+ return DecodeStatus::NeedMoreData;
+ }
+ else if (IsCRCSeparator(c)) {
+ rqState = RequestStates::CRC;
+ return DecodeStatus::NeedMoreData;
+ }
+ else {
+ requestMsg.code = RequestMsgCodes::unknown;
+ rqState = RequestStates::Error;
+ return DecodeStatus::Error;
+ }
+ case RequestStates::Address:
+ if (IsHexDigit(c)) {
+ requestMsg.value <<= 4U;
+ requestMsg.value |= Char2Nibble(c);
+ return DecodeStatus::NeedMoreData;
+ }
+ else if (c == ' ') { // end of address, value coming
+ rqState = RequestStates::WriteValue;
+ return DecodeStatus::NeedMoreData;
+ }
+ else {
+ requestMsg.code = RequestMsgCodes::unknown;
+ rqState = RequestStates::Error;
+ return DecodeStatus::Error;
+ }
+ case RequestStates::WriteValue:
+ if (IsHexDigit(c)) {
+ requestMsg.value2 <<= 4U;
+ requestMsg.value2 |= Char2Nibble(c);
+ return DecodeStatus::NeedMoreData;
+ }
+ else if (IsCRCSeparator(c)) {
+ rqState = RequestStates::CRC;
+ return DecodeStatus::NeedMoreData;
+ }
+ else {
+ requestMsg.code = RequestMsgCodes::unknown;
+ rqState = RequestStates::Error;
+ return DecodeStatus::Error;
+ }
+ case RequestStates::CRC:
+ if (IsHexDigit(c)) {
+ requestMsg.crc8 <<= 4U;
+ requestMsg.crc8 |= Char2Nibble(c);
+ return DecodeStatus::NeedMoreData;
+ }
+ else if (IsNewLine(c)) {
+ // check CRC at this spot
+ if (requestMsg.crc8 != requestMsg.ComputeCRC8()) {
+ // CRC mismatch
+ requestMsg.code = RequestMsgCodes::unknown;
+ rqState = RequestStates::Error;
+ return DecodeStatus::Error;
+ }
+ else {
+ 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) {
+ txbuff[0] = (uint8_t)msg.code;
+ uint8_t i = 1 + UInt8ToHex(msg.value, txbuff + 1);
+
+ i += AppendCRC(msg.getCRC(), txbuff + i);
+
+ txbuff[i] = '\n';
+ ++i;
+ return i;
+ static_assert(7 <= MaxRequestSize(), "Request message length exceeded the maximum size, increase the magic constant in MaxRequestSize()");
+ }
+
+ uint8_t Protocol::EncodeWriteRequest(uint8_t address, uint16_t value, uint8_t *txbuff) {
+ const RequestMsg msg(RequestMsgCodes::Write, address, value);
+ uint8_t i = BeginEncodeRequest(msg, txbuff);
+ // dump the value
+ i += UInt16ToHex(value, txbuff + i);
+
+ i += AppendCRC(msg.getCRC(), txbuff + i);
+
+ txbuff[i] = '\n';
+ ++i;
+ return i;
+ }
+
+ 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':
+ case 'R':
+ responseMsg.request.code = (RequestMsgCodes)c;
+ responseMsg.request.value = 0;
+ responseMsg.request.value2 = 0;
+ responseMsg.request.crc8 = 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 (IsHexDigit(c)) {
+ responseMsg.request.value <<= 4U;
+ responseMsg.request.value += Char2Nibble(c);
+ 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':
+ case 'B':
+ 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 (IsHexDigit(c)) {
+ responseMsg.paramValue <<= 4U;
+ responseMsg.paramValue += Char2Nibble(c);
+ return DecodeStatus::NeedMoreData;
+ }
+ else if (IsCRCSeparator(c)) {
+ rspState = ResponseStates::CRC;
+ return DecodeStatus::NeedMoreData;
+ }
+ else {
+ responseMsg.paramCode = ResponseMsgParamCodes::unknown;
+ rspState = ResponseStates::Error;
+ return DecodeStatus::Error;
+ }
+ case ResponseStates::CRC:
+ if (IsHexDigit(c)) {
+ responseMsg.request.crc8 <<= 4U;
+ responseMsg.request.crc8 += Char2Nibble(c);
+ return DecodeStatus::NeedMoreData;
+ }
+ else if (IsNewLine(c)) {
+ // check CRC at this spot
+ if (responseMsg.request.crc8 != responseMsg.ComputeCRC8()) {
+ // CRC mismatch
+ responseMsg.paramCode = ResponseMsgParamCodes::unknown;
+ rspState = ResponseStates::Error;
+ return DecodeStatus::Error;
+ }
+ else {
+ 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) {
+ // BEWARE:
+ // ResponseMsg rsp(RequestMsg(msg.code, msg.value), ar, 0);
+ // ... is NOT the same as:
+ // ResponseMsg rsp(msg, ar, 0);
+ // ... because of the usually unused parameter value2 (which only comes non-zero in write requests).
+ // It took me a few hours to find out why the CRC from the MMU never matched all the other sides (unit tests and the MK3S)
+ // It is because this was the only place where the original request kept its value2 non-zero.
+ // In the response, we must make sure value2 is actually zero unless being sent along with it (which is not right now)
+ const ResponseMsg rsp(RequestMsg(msg.code, msg.value), ar, 0); // this needs some cleanup @@TODO - check assembly how bad is it
+ uint8_t i = BeginEncodeRequest(rsp.request, txbuff);
+ txbuff[i] = (uint8_t)ar;
+ ++i;
+ i += AppendCRC(rsp.getCRC(), txbuff + i);
+ txbuff[i] = '\n';
+ ++i;
+ return i;
+ }
+
+ uint8_t Protocol::EncodeResponseReadFINDA(const RequestMsg &msg, uint8_t findaValue, uint8_t *txbuff) {
+ return EncodeResponseRead(msg, true, findaValue, txbuff);
+ }
+
+ uint8_t Protocol::EncodeResponseQueryOperation(const RequestMsg &msg, ResponseCommandStatus rcs, uint8_t *txbuff) {
+ const ResponseMsg rsp(msg, rcs.code, rcs.value);
+ uint8_t i = BeginEncodeRequest(msg, txbuff);
+ txbuff[i] = (uint8_t)rsp.paramCode;
+ ++i;
+ i += UInt16ToHex(rsp.paramValue, txbuff + i);
+ i += AppendCRC(rsp.getCRC(), txbuff + i);
+ txbuff[i] = '\n';
+ return i + 1;
+ }
+
+ uint8_t Protocol::EncodeResponseRead(const RequestMsg &msg, bool accepted, uint16_t value2, uint8_t *txbuff) {
+ const ResponseMsg rsp(msg,
+ accepted ? ResponseMsgParamCodes::Accepted : ResponseMsgParamCodes::Rejected,
+ accepted ? value2 : 0 // be careful about this value for CRC computation - rejected status doesn't have any meaningful value which could be reconstructed from the textual form of the message
+ );
+ uint8_t i = BeginEncodeRequest(msg, txbuff);
+ txbuff[i] = (uint8_t)rsp.paramCode;
+ ++i;
+ if (accepted)
+ // dump the value
+ i += UInt16ToHex(value2, txbuff + i);
+ i += AppendCRC(rsp.getCRC(), txbuff + i);
+ txbuff[i] = '\n';
+ return i + 1;
+ }
+
+ uint8_t Protocol::UInt8ToHex(uint8_t value, uint8_t *dst) {
+ if (value == 0) {
+ *dst = '0';
+ return 1;
+ }
+
+ uint8_t v = value >> 4U;
+ uint8_t charsOut = 1;
+ if (v != 0) { // skip the first '0' if any
+ *dst = Nibble2Char(v);
+ ++dst;
+ charsOut = 2;
+ }
+ v = value & 0xfU;
+ *dst = Nibble2Char(v);
+ return charsOut;
+ }
+
+ uint8_t Protocol::UInt16ToHex(uint16_t value, uint8_t *dst) {
+ constexpr uint16_t topNibbleMask = 0xf000;
+ if (value == 0) {
+ *dst = '0';
+ return 1;
+ }
+ // skip initial zeros
+ uint8_t charsOut = 4;
+ while ((value & topNibbleMask) == 0) {
+ value <<= 4U;
+ --charsOut;
+ }
+ for (uint8_t i = 0; i < charsOut; ++i) {
+ uint8_t n = (value & topNibbleMask) >> (8U + 4U);
+ value <<= 4U;
+ *dst = Nibble2Char(n);
+ ++dst;
+ }
+ return charsOut;
+ }
+
+ uint8_t Protocol::BeginEncodeRequest(const RequestMsg &msg, uint8_t *dst) {
+ dst[0] = (uint8_t)msg.code;
+
+ uint8_t i = 1 + UInt8ToHex(msg.value, dst + 1);
+
+ dst[i] = ' ';
+ return i + 1;
+ }
+
+ uint8_t Protocol::AppendCRC(uint8_t crc, uint8_t *dst) {
+ dst[0] = '*'; // reprap-style separator of CRC
+ return 1 + UInt8ToHex(crc, dst + 1);
+ }
+
+} // namespace protocol
+} // namespace modules
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_protocol.h b/Marlin/src/feature/mmu3/mmu2_protocol.h
new file mode 100644
index 000000000000..712ba171986e
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_protocol.h
@@ -0,0 +1,318 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * mmu2_protocol.h
+ */
+
+#include "../../MarlinCore.h"
+
+#include
+#include "mmu2_crc.h"
+
+// prevent ARM HAL macros from breaking our code
+#undef CRC
+
+namespace modules {
+
+// @brief The MMU communication protocol implementation and related stuff.
+//
+// See description of the new protocol in the MMU 2021 doc
+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',
+ Write = 'W',
+ Cut = 'K',
+ FilamentType = 'F',
+ FilamentSensor = 'f',
+ Home = 'H',
+ Read = 'R'
+ };
+
+ // Definition of response message parameter codes
+ enum class ResponseMsgParamCodes : uint8_t {
+ unknown = 0,
+ Processing = 'P',
+ Error = 'E',
+ Finished = 'F',
+ Accepted = 'A',
+ Rejected = 'R',
+ Button = 'B'// the MMU registered a button press and is sending it to the printer for processing
+ };
+
+ // 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 or address of variable to read/write
+ uint16_t value2; //!< in case or write messages - value to be written into the register
+
+ // CRC8 check - please note we abuse this byte for CRC of ResponseMsgs as well.
+ // The crc8 byte itself is not added into the CRC computation (obviously ;) )
+ // Beware - adding any members of this data structure may need changing the way CRC is being computed!
+ uint8_t crc8;
+
+ constexpr uint8_t ComputeCRC8() const {
+ uint8_t crc = 0;
+ crc = modules::crc::CRC8::CCITT_updateCX(0, (uint8_t)code);
+ crc = modules::crc::CRC8::CCITT_updateCX(crc, value);
+ crc = modules::crc::CRC8::CCITT_updateW(crc, value2);
+ return crc;
+ }
+
+ // @param code of the request message
+ // @param value of the request message
+ inline constexpr RequestMsg(RequestMsgCodes code, uint8_t value)
+ : code(code)
+ , value(value)
+ , value2(0)
+ , crc8(ComputeCRC8()) {
+ }
+
+ // Intended for write requests
+ // @param code of the request message ('W')
+ // @param address of the register
+ // @param value to write into the register
+ inline constexpr RequestMsg(RequestMsgCodes code, uint8_t address, uint16_t value)
+ : code(code)
+ , value(address)
+ , value2(value)
+ , crc8(ComputeCRC8()) {}
+
+ constexpr uint8_t getCRC() const { return crc8; }
+ };
+
+ // 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
+
+ constexpr uint8_t ComputeCRC8() const {
+ uint8_t crc = request.ComputeCRC8();
+ crc = modules::crc::CRC8::CCITT_updateCX(crc, (uint8_t)paramCode);
+ crc = modules::crc::CRC8::CCITT_updateW(crc, paramValue);
+ return crc;
+ }
+
+ // @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 constexpr ResponseMsg(RequestMsg request, ResponseMsgParamCodes paramCode, uint16_t paramValue)
+ : request(request)
+ , paramCode(paramCode)
+ , paramValue(paramValue) {
+ this->request.crc8 = ComputeCRC8();
+ }
+
+ constexpr uint8_t getCRC() const { return request.crc8; }
+ };
+
+ // Combined commandStatus and its value into one data structure (optimization purposes)
+ struct ResponseCommandStatus {
+ ResponseMsgParamCodes code;
+ uint16_t value;
+ inline constexpr ResponseCommandStatus(ResponseMsgParamCodes code, uint16_t value)
+ : code(code)
+ , value(value) {}
+ };
+
+ // 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:
+ 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
+ // @return state of the message being decoded
+ DecodeStatus DecodeRequest(uint8_t c);
+
+ // Decodes response message in rxbuff
+ // @return 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
+ // @return number of bytes written into txbuff
+ static uint8_t EncodeRequest(const RequestMsg &msg, uint8_t *txbuff);
+
+ // Encodes Write request message msg into txbuff memory
+ // It is expected the txbuff is large enough to fit the message
+ // @return number of bytes written into txbuff
+ static uint8_t EncodeWriteRequest(uint8_t address, uint16_t value, uint8_t *txbuff);
+
+ // @return 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 13; }
+
+ // @return the maximum byte length necessary to encode a response message
+ // Beneficial in case of pre-allocating a buffer for enconding a ResponseMsg.
+ static constexpr uint8_t MaxResponseSize() { return 14; }
+
+ // 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
+ // @return 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
+ // @return 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
+ // @return number of bytes written into txbuff
+ static uint8_t EncodeResponseVersion(const RequestMsg &msg, uint16_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
+ // @return number of bytes written into txbuff
+ static uint8_t EncodeResponseQueryOperation(const RequestMsg &msg, ResponseCommandStatus rcs, uint8_t *txbuff);
+
+ // Encode response to Read query
+ // @param msg source request message for this response
+ // @param accepted true if the read query was accepted
+ // @param value2 variable value
+ // @param txbuff where to format the message
+ // @return number of bytes written into txbuff
+ static uint8_t EncodeResponseRead(const RequestMsg &msg, bool accepted, uint16_t value2, uint8_t *txbuff);
+
+ // @return the most recently lexed request message
+ inline const RequestMsg GetRequestMsg() const { return requestMsg; }
+
+ // @return 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;
+ }
+
+ #ifndef UNITTEST
+ private:
+ #endif
+
+ enum class RequestStates : uint8_t {
+ Code, //!< starting state - expects message code
+ Value, //!< expecting code value
+ Address, //!< expecting address for Write command
+ WriteValue, //!< value to be written (Write command)
+ CRC, //!< CRC
+ 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
+ CRC, //!< expecting CRC value
+ Error //!< automaton in error state
+ };
+
+ ResponseStates rspState;
+ ResponseMsg responseMsg;
+
+ static constexpr bool IsNewLine(uint8_t c) {
+ return c == '\n' || c == '\r';
+ }
+ static constexpr bool IsDigit(uint8_t c) {
+ return c >= '0' && c <= '9';
+ }
+ static constexpr bool IsCRCSeparator(uint8_t c) {
+ return c == '*';
+ }
+ static constexpr bool IsHexDigit(uint8_t c) {
+ return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
+ }
+ static constexpr uint8_t Char2Nibble(uint8_t c) {
+ switch (c) {
+ case '0' ... '9': return c - '0';
+ case 'a' ... 'f': return c - 'a' + 10;
+ default: return 0;
+ }
+ }
+
+ static constexpr uint8_t Nibble2Char(uint8_t n) {
+ switch (n) {
+ case 0x0 ... 0x9: return n + '0';
+ case 0xA ... 0xF: return n - 10 + 'a';
+ default: return 0;
+ }
+ }
+
+ // @return number of characters written
+ static uint8_t UInt8ToHex(uint8_t value, uint8_t *dst);
+
+ // @return number of characters written
+ static uint8_t UInt16ToHex(uint16_t value, uint8_t *dst);
+
+ static uint8_t BeginEncodeRequest(const RequestMsg &msg, uint8_t *dst);
+
+ static uint8_t AppendCRC(uint8_t crc, uint8_t *dst);
+ };
+
+} // namespace protocol
+} // namespace modules
+
diff --git a/Marlin/src/feature/mmu3/mmu2_protocol_logic.cpp b/Marlin/src/feature/mmu3/mmu2_protocol_logic.cpp
new file mode 100644
index 000000000000..9453e6713618
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_protocol_logic.cpp
@@ -0,0 +1,899 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/**
+ * mmu2_protocol_logic.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "mmu2_protocol_logic.h"
+#include "mmu2_log.h"
+#include "mmu2_fsensor.h"
+
+ #ifdef __AVR__
+ // on MK3/S/+ we shuffle the timers a bit, thus "_millis" may not equal "millis"
+ // #include "system_timer.h"
+ #define _millis millis
+ #else
+ // irrelevant on Buddy FW, just keep "_millis" as "millis"
+ // #include
+ #define _millis millis
+ #ifdef UNITTEST
+ #define strncmp_P strncmp
+ #else
+ #include "../../core/serial.h"
+ #endif
+ #endif
+
+ #include
+ #include "mmu2_supported_version.h"
+
+namespace MMU3 {
+
+ static constexpr uint8_t supportedMmuFWVersion[] PROGMEM = { mmuVersionMajor, mmuVersionMinor, mmuVersionPatch };
+
+ const Register ProtocolLogic::regs8Addrs[ProtocolLogic::regs8Count] PROGMEM = {
+ Register::FINDA_State, // FINDA state
+ Register::Set_Get_Selector_Slot, // Selector slot
+ Register::Set_Get_Idler_Slot, // Idler slot
+ };
+
+ const Register ProtocolLogic::regs16Addrs[ProtocolLogic::regs16Count] PROGMEM = {
+ Register::MMU_Errors, // MMU errors - aka statistics
+ Register::Get_Pulley_Position, // Pulley position [mm]
+ };
+
+ const Register ProtocolLogic::initRegs8Addrs[ProtocolLogic::initRegs8Count] PROGMEM = {
+ Register::Extra_Load_Distance, // Extra load distance [mm]
+ Register::Pulley_Slow_Feedrate, // Pulley slow feedrate [mm/s]
+ };
+
+ void ProtocolLogic::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 != lastFSensor)
+ SendAndUpdateFilamentSensor();
+ }
+
+ void ProtocolLogic::SendQuery() {
+ SendMsg(RequestMsg(RequestMsgCodes::Query, 0));
+ scopeState = ScopeState::QuerySent;
+ }
+
+ void ProtocolLogic::StartReading8bitRegisters() {
+ regIndex = 0;
+ SendReadRegister(pgm_read_byte(regs8Addrs + regIndex), ScopeState::Reading8bitRegisters);
+ }
+
+ void ProtocolLogic::ProcessRead8bitRegister() {
+ regs8[regIndex] = rsp.paramValue;
+ ++regIndex;
+ if (regIndex >= regs8Count)
+ // proceed with reading 16bit registers
+ StartReading16bitRegisters();
+ else
+ SendReadRegister(pgm_read_byte(regs8Addrs + regIndex), ScopeState::Reading8bitRegisters);
+ }
+
+ void ProtocolLogic::StartReading16bitRegisters() {
+ regIndex = 0;
+ SendReadRegister(pgm_read_byte(regs16Addrs + regIndex), ScopeState::Reading16bitRegisters);
+ }
+
+ ProtocolLogic::ScopeState __attribute__((noinline)) ProtocolLogic::ProcessRead16bitRegister(ProtocolLogic::ScopeState stateAtEnd) {
+ regs16[regIndex] = rsp.paramValue;
+ ++regIndex;
+ if (regIndex >= regs16Count)
+ return stateAtEnd;
+ else
+ SendReadRegister(pgm_read_byte(regs16Addrs + regIndex), ScopeState::Reading16bitRegisters);
+ return ScopeState::Reading16bitRegisters;
+ }
+
+ void ProtocolLogic::StartWritingInitRegisters() {
+ regIndex = 0;
+ SendWriteRegister(pgm_read_byte(initRegs8Addrs + regIndex), initRegs8[regIndex], ScopeState::WritingInitRegisters);
+ }
+
+ bool __attribute__((noinline)) ProtocolLogic::ProcessWritingInitRegister() {
+ ++regIndex;
+ if (regIndex >= initRegs8Count)
+ return true;
+ else
+ SendWriteRegister(pgm_read_byte(initRegs8Addrs + regIndex), initRegs8[regIndex], ScopeState::WritingInitRegisters);
+ return false;
+ }
+
+ void ProtocolLogic::SendAndUpdateFilamentSensor() {
+ SendMsg(RequestMsg(RequestMsgCodes::FilamentSensor, lastFSensor = (uint8_t)WhereIsFilament()));
+ scopeState = ScopeState::FilamentSensorStateSent;
+ }
+
+ void ProtocolLogic::SendButton(uint8_t btn) {
+ SendMsg(RequestMsg(RequestMsgCodes::Button, btn));
+ scopeState = ScopeState::ButtonSent;
+ }
+
+ void ProtocolLogic::SendVersion(uint8_t stage) {
+ SendMsg(RequestMsg(RequestMsgCodes::Version, stage));
+ scopeState = (ScopeState)((uint_fast8_t)ScopeState::S0Sent + stage);
+ }
+
+ void ProtocolLogic::SendReadRegister(uint8_t index, ScopeState nextState) {
+ SendMsg(RequestMsg(RequestMsgCodes::Read, index));
+ scopeState = nextState;
+ }
+
+ void ProtocolLogic::SendWriteRegister(uint8_t index, uint16_t value, ScopeState nextState) {
+ SendWriteMsg(RequestMsg(RequestMsgCodes::Write, index, value));
+ scopeState = nextState;
+ }
+
+ // searches for "ok\n" in the incoming serial data (that's the usual response of the old MMU FW)
+ struct OldMMUFWDetector {
+ uint8_t ok;
+ inline constexpr OldMMUFWDetector()
+ : ok(0) {}
+
+ enum class State : uint8_t {
+ MatchingPart,
+ SomethingElse,
+ Matched
+ };
+
+ // @return true when "ok\n" gets detected
+ State Detect(uint8_t c) {
+ // consume old MMU FW's data if any -> avoid confusion of protocol decoder
+ if (ok == 0 && c == 'o') {
+ ++ok;
+ return State::MatchingPart;
+ }
+ else if (ok == 1 && c == 'k') {
+ ++ok;
+ return State::Matched;
+ }
+ return State::SomethingElse;
+ }
+ };
+
+ StepStatus ProtocolLogic::ExpectingMessage() {
+ int bytesConsumed = 0;
+ int c = -1;
+
+ OldMMUFWDetector oldMMUh4x0r; // old MMU FW hacker ;)
+
+ // try to consume as many rx bytes as possible (until a message has been completed)
+ while ((c = MMU2_SERIAL.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: {
+ // consume old MMU FW's data if any -> avoid confusion of protocol decoder
+ auto old = oldMMUh4x0r.Detect(c);
+ if (old == OldMMUFWDetector::State::Matched)
+ // Old MMU FW 1.0.6 detected. Firmwares are incompatible.
+ return VersionMismatch;
+ else if (old == OldMMUFWDetector::State::MatchingPart)
+ break;
+ }
+ // [[fallthrough]]; // otherwise
+ // fall through
+ 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(linkLayerTimeout) && currentScope != Scope::Stopped) {
+ return CommunicationTimeout;
+ }
+ return Processing;
+ }
+
+ void ProtocolLogic::SendMsg(RequestMsg rq) {
+ #if defined(__AVR__) || defined(TARGET_LPC1768)
+ // Buddy FW cannot use stack-allocated txbuff - DMA doesn't work with CCMRAM
+ // No restrictions on MK3/S/+ though
+ uint8_t txbuff[Protocol::MaxRequestSize()];
+ #endif
+ uint8_t len = Protocol::EncodeRequest(rq, txbuff);
+ #if defined(__AVR__) || defined(TARGET_LPC1768)
+ // TODO: I'm not sure if this is the correct approach with AVR
+ for ( uint8_t i = 0; i < len; i++) {
+ MMU2_SERIAL.write(txbuff[i]);
+ }
+ #else
+ MMU2_SERIAL.write(txbuff, len);
+ #endif
+ LogRequestMsg(txbuff, len);
+ RecordUARTActivity();
+ }
+
+ void ProtocolLogic::SendWriteMsg(RequestMsg rq) {
+ #if defined(__AVR__) || defined(TARGET_LPC1768)
+ // Buddy FW cannot use stack-allocated txbuff - DMA doesn't work with CCMRAM
+ // No restrictions on MK3/S/+ though
+ uint8_t txbuff[Protocol::MaxRequestSize()];
+ #endif
+ uint8_t len = Protocol::EncodeWriteRequest(rq.value, rq.value2, txbuff);
+
+ #if defined(__AVR__) || defined(TARGET_LPC1768)
+ // TODO: I'm not sure if this is the correct approach with AVR
+ for ( uint8_t i = 0; i < len; i++) {
+ MMU2_SERIAL.write(txbuff[i]);
+ }
+ #else
+ MMU2_SERIAL.write(txbuff, len);
+ #endif
+ LogRequestMsg(txbuff, len);
+ RecordUARTActivity();
+ }
+
+ void ProtocolLogic::StartSeqRestart() {
+ retries = maxRetries;
+ SendVersion(0);
+ }
+
+ void ProtocolLogic::DelayedRestartRestart() {
+ scopeState = ScopeState::RecoveringProtocolError;
+ }
+
+ void ProtocolLogic::CommandRestart() {
+ scopeState = ScopeState::CommandSent;
+ SendMsg(rq);
+ }
+
+ void ProtocolLogic::IdleRestart() {
+ scopeState = ScopeState::Ready;
+ }
+
+ StepStatus ProtocolLogic::ProcessVersionResponse(uint8_t stage) {
+ if (rsp.request.code != RequestMsgCodes::Version || rsp.request.value != stage) {
+ // got a response to something else - protocol corruption probably, repeat the query OR restart the comm by issuing S0?
+ SendVersion(stage);
+ }
+ else {
+ mmuFwVersion[stage] = rsp.paramValue;
+ if (mmuFwVersion[stage] != pgm_read_byte(&supportedMmuFWVersion[stage])) {
+ if (--retries == 0) return VersionMismatch;
+ SendVersion(stage);
+ }
+ else {
+ ResetCommunicationTimeoutAttempts(); // got a meaningful response from the MMU, stop data layer timeout tracking
+ SendVersion(stage + 1);
+ }
+ }
+ return Processing;
+ }
+
+ StepStatus ProtocolLogic::ScopeStep() {
+ if (!ExpectsResponse()) {
+ // we are waiting for something
+ switch (currentScope) {
+ case Scope::DelayedRestart:
+ return DelayedRestartWait();
+ case Scope::Idle:
+ return IdleWait();
+ case Scope::Command:
+ return CommandWait();
+ case Scope::Stopped:
+ return StoppedStep();
+ default:
+ break;
+ }
+ }
+ else {
+ // we are expecting a message
+ auto expmsg = ExpectingMessage();
+ if (expmsg != MessageReady)
+ return expmsg;
+
+ // process message
+ switch (currentScope) {
+ case Scope::StartSeq:
+ return StartSeqStep(); // ~270B
+ case Scope::Idle:
+ return IdleStep(); // ~300B
+ case Scope::Command:
+ return CommandStep(); // ~430B
+ case Scope::Stopped:
+ return StoppedStep();
+ default:
+ break;
+ }
+ }
+ return Finished;
+ }
+
+ StepStatus ProtocolLogic::StartSeqStep() {
+ // solve initial handshake
+ switch (scopeState) {
+ case ScopeState::S0Sent: // received response to S0 - major
+ case ScopeState::S1Sent: // received response to S1 - minor
+ case ScopeState::S2Sent: // received response to S2 - patch
+ return ProcessVersionResponse((uint8_t)scopeState - (uint8_t)ScopeState::S0Sent);
+ case ScopeState::S3Sent: // received response to S3 - revision
+ if (rsp.request.code != RequestMsgCodes::Version || rsp.request.value != 3) {
+ // got a response to something else - protocol corruption probably, repeat the query OR restart the comm by issuing S0?
+ SendVersion(3);
+ }
+ else {
+ mmuFwVersionBuild = rsp.paramValue; // just register the build number
+ // Start General Interrogation after line up - initial parametrization is started
+ StartWritingInitRegisters();
+ }
+ return Processing;
+ case ScopeState::WritingInitRegisters:
+ if (ProcessWritingInitRegister())
+ SendAndUpdateFilamentSensor();
+ return Processing;
+ case ScopeState::FilamentSensorStateSent:
+ SwitchFromStartToIdle();
+ return Processing; // Returning Finished is not a good idea in case of a fast error recovery
+ // - it tells the printer, that the command which experienced a protocol error and recovered successfully actually terminated.
+ // In such a case we must return "Processing" in order to keep the MMU state machine running and prevent the printer from executing next G-codes.
+ default:
+ return VersionMismatch;
+ }
+ }
+
+ StepStatus ProtocolLogic::DelayedRestartWait() {
+ if (Elapsed(heartBeatPeriod)) { // this basically means, that we are waiting until there is some traffic on
+ while (MMU2_SERIAL.read() != -1); // clear the input buffer
+ // switch to StartSeq
+ start();
+ }
+ return Processing;
+ }
+
+ StepStatus ProtocolLogic::CommandWait() {
+ if (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();
+ return Processing;
+ }
+
+ StepStatus ProtocolLogic::ProcessCommandQueryResponse() {
+ switch (rsp.paramCode) {
+ case ResponseMsgParamCodes::Processing:
+ progressCode = static_cast(rsp.paramValue);
+ errorCode = ErrorCode::OK;
+ SendAndUpdateFilamentSensor(); // keep on reporting the state of fsensor regularly
+ return Processing;
+ case ResponseMsgParamCodes::Error:
+ // in case of an error the progress code remains as it has been before
+ progressCode = ProgressCode::ERRWaitingForUser;
+ errorCode = static_cast(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::Button:
+ // The user pushed a button on the MMU. Save it, do what we need to do
+ // to prepare, then pass it back to the MMU so it can work its magic.
+ buttonCode = static_cast(rsp.paramValue);
+ SendAndUpdateFilamentSensor();
+ return ButtonPushed;
+ case ResponseMsgParamCodes::Finished:
+ // We must check whether the "finished" is actually related to the command issued into the MMU
+ // It can also be an X0 F which means MMU just successfully restarted.
+ if (ReqMsg().code == rsp.request.code && ReqMsg().value == rsp.request.value) {
+ progressCode = ProgressCode::OK;
+ errorCode = ErrorCode::OK;
+ scopeState = ScopeState::Ready;
+ rq = RequestMsg(RequestMsgCodes::unknown, 0); // clear the successfully finished request
+ return Finished;
+ }
+ else {
+ // got response to some other command - the originally issued command was interrupted!
+ return Interrupted;
+ }
+ default:
+ return ProtocolError;
+ }
+ }
+
+ StepStatus ProtocolLogic::CommandStep() {
+ switch (scopeState) {
+ case ScopeState::CommandSent: {
+ switch (rsp.paramCode) { // the response should be either accepted or rejected
+ case ResponseMsgParamCodes::Accepted:
+ progressCode = ProgressCode::OK;
+ errorCode = ErrorCode::RUNNING;
+ scopeState = ScopeState::Wait;
+ break;
+ case ResponseMsgParamCodes::Rejected:
+ // rejected - should normally not happen, but report the error up
+ progressCode = ProgressCode::OK;
+ errorCode = ErrorCode::PROTOCOL_ERROR;
+ return CommandRejected;
+ default:
+ return ProtocolError;
+ }
+ }
+ break;
+ case ScopeState::QuerySent:
+ return ProcessCommandQueryResponse();
+ case ScopeState::FilamentSensorStateSent:
+ StartReading8bitRegisters();
+ return Processing;
+ case ScopeState::Reading8bitRegisters:
+ ProcessRead8bitRegister();
+ return Processing;
+ case ScopeState::Reading16bitRegisters:
+ scopeState = ProcessRead16bitRegister(ScopeState::Wait);
+ return Processing;
+ case ScopeState::ButtonSent:
+ if (rsp.paramCode == ResponseMsgParamCodes::Accepted)
+ // Button was accepted, decrement the retry.
+ DecrementRetryAttempts();
+ SendAndUpdateFilamentSensor();
+ break;
+ default:
+ return ProtocolError;
+ }
+ return Processing;
+ }
+
+ StepStatus ProtocolLogic::IdleWait() {
+ if (scopeState == ScopeState::Ready) { // check timeout
+ if (Elapsed(heartBeatPeriod)) {
+ SendQuery();
+ return Processing;
+ }
+ }
+ return Finished;
+ }
+
+ StepStatus ProtocolLogic::IdleStep() {
+ switch (scopeState) {
+ case ScopeState::QuerySent: // check UART
+ // 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 (rsp.request.code) {
+ case RequestMsgCodes::Cut:
+ case RequestMsgCodes::Eject:
+ case RequestMsgCodes::Load:
+ case RequestMsgCodes::Mode:
+ case RequestMsgCodes::Tool:
+ case RequestMsgCodes::Unload:
+ if (rsp.paramCode != ResponseMsgParamCodes::Finished)
+ return SwitchFromIdleToCommand();
+ break;
+ case RequestMsgCodes::Reset:
+ // this one is kind of special
+ // we do not transfer to any "running" command (i.e. we stay in Idle),
+ // but in case there is an error reported we must make sure it gets propagated
+ switch (rsp.paramCode) {
+ case ResponseMsgParamCodes::Button:
+ // The user pushed a button on the MMU. Save it, do what we need to do
+ // to prepare, then pass it back to the MMU so it can work its magic.
+ buttonCode = static_cast(rsp.paramValue);
+ StartReading8bitRegisters();
+ return ButtonPushed;
+ case ResponseMsgParamCodes::Finished:
+ if (ReqMsg().code != RequestMsgCodes::unknown) {
+ // got reset while doing some other command - the originally issued command was interrupted!
+ // this must be solved by the upper layer, protocol logic doesn't have all the context (like unload before trying again)
+ IdleRestart();
+ return Interrupted;
+ }
+ // [[fallthrough]];
+ // fall through
+ case ResponseMsgParamCodes::Processing:
+ // @@TODO we may actually use this branch to report progress of manual operation on the MMU
+ // The MMU sends e.g. X0 P27 after its restart when the user presses an MMU button to move the Selector
+ progressCode = static_cast(rsp.paramValue);
+ errorCode = ErrorCode::OK;
+ break;
+ default:
+ progressCode = ProgressCode::ERRWaitingForUser;
+ errorCode = static_cast(rsp.paramValue);
+ StartReading8bitRegisters(); // continue Idle state without restarting the communication
+ return CommandError;
+ }
+ break;
+ default:
+ return ProtocolError;
+ }
+ StartReading8bitRegisters();
+ return Processing;
+ case ScopeState::Reading8bitRegisters:
+ ProcessRead8bitRegister();
+ return Processing;
+ case ScopeState::Reading16bitRegisters:
+ scopeState = ProcessRead16bitRegister(ScopeState::Ready);
+ return scopeState == ScopeState::Ready ? Finished : Processing;
+ case ScopeState::ButtonSent:
+ if (rsp.paramCode == ResponseMsgParamCodes::Accepted)
+ // Button was accepted, decrement the retry.
+ DecrementRetryAttempts();
+ StartReading8bitRegisters();
+ return Processing;
+ case ScopeState::ReadRegisterSent:
+ if (rsp.paramCode == ResponseMsgParamCodes::Accepted) {
+ // @@TODO just dump the value onto the serial
+ }
+ return Finished;
+ case ScopeState::WriteRegisterSent:
+ if (rsp.paramCode == ResponseMsgParamCodes::Accepted) {
+ // @@TODO do something? Retry if not accepted?
+ }
+ return Finished;
+ 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(uint8_t extraLoadDistance, uint8_t pulleySlowFeedrate)
+ : explicitPrinterError(ErrorCode::OK)
+ , currentScope(Scope::Stopped)
+ , scopeState(ScopeState::Ready)
+ , plannedRq(RequestMsgCodes::unknown, 0)
+ , lastUARTActivityMs(0)
+ , dataTO()
+ , rsp(RequestMsg(RequestMsgCodes::unknown, 0), ResponseMsgParamCodes::unknown, 0)
+ , state(State::Stopped)
+ , lrb(0)
+ , errorCode(ErrorCode::OK)
+ , progressCode(ProgressCode::OK)
+ , buttonCode(Buttons::NoButton)
+ , lastFSensor((uint8_t)WhereIsFilament())
+ , regIndex(0)
+ , retryAttempts(MMU2_MAX_RETRIES)
+ , inAutoRetry(false) {
+ // @@TODO currently, I don't see a way of writing the initialization better :(
+ // I'd like to write something like: initRegs8 { extraLoadDistance, pulleySlowFeedrate }
+ // avr-gcc seems to like such a syntax, ARM gcc doesn't
+ initRegs8[0] = extraLoadDistance;
+ initRegs8[1] = pulleySlowFeedrate;
+ }
+
+ void ProtocolLogic::start() {
+ state = State::InitSequence;
+ currentScope = Scope::StartSeq;
+ protocol.ResetResponseDecoder(); // important - finished delayed restart relies on this
+ StartSeqRestart();
+ }
+
+ void ProtocolLogic::stop() {
+ state = State::Stopped;
+ currentScope = Scope::Stopped;
+ }
+
+ void ProtocolLogic::ToolChange(uint8_t slot) {
+ PlanGenericRequest(RequestMsg(RequestMsgCodes::Tool, slot));
+ }
+
+ void ProtocolLogic::Statistics() {
+ PlanGenericRequest(RequestMsg(RequestMsgCodes::Version, 3));
+ }
+
+ 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(uint8_t mode /* = 0 */) {
+ PlanGenericRequest(RequestMsg(RequestMsgCodes::Reset, mode));
+ }
+
+ void ProtocolLogic::button(uint8_t index) {
+ PlanGenericRequest(RequestMsg(RequestMsgCodes::Button, index));
+ }
+
+ void ProtocolLogic::home(uint8_t mode) {
+ PlanGenericRequest(RequestMsg(RequestMsgCodes::Home, mode));
+ }
+
+ void ProtocolLogic::readRegister(uint8_t address) {
+ PlanGenericRequest(RequestMsg(RequestMsgCodes::Read, address));
+ }
+
+ void ProtocolLogic::writeRegister(uint8_t address, uint16_t data) {
+ PlanGenericRequest(RequestMsg(RequestMsgCodes::Write, address, data));
+ }
+
+ void ProtocolLogic::PlanGenericRequest(RequestMsg rq) {
+ plannedRq = rq;
+ if (!ExpectsResponse())
+ ActivatePlannedRequest();
+ // otherwise wait for an empty window to activate the request
+ }
+
+ bool ProtocolLogic::ActivatePlannedRequest() {
+ switch (plannedRq.code) {
+ case RequestMsgCodes::Button:
+ // only issue the button to the MMU and do not restart the state machines
+ SendButton(plannedRq.value);
+ plannedRq = RequestMsg(RequestMsgCodes::unknown, 0);
+ return true;
+ case RequestMsgCodes::Read:
+ SendReadRegister(plannedRq.value, ScopeState::ReadRegisterSent);
+ plannedRq = RequestMsg(RequestMsgCodes::unknown, 0);
+ return true;
+ case RequestMsgCodes::Write:
+ SendWriteRegister(plannedRq.value, plannedRq.value2, ScopeState::WriteRegisterSent);
+ plannedRq = RequestMsg(RequestMsgCodes::unknown, 0);
+ return true;
+ case RequestMsgCodes::unknown:
+ return false;
+ default: // commands
+ currentScope = Scope::Command;
+ SetRequestMsg(plannedRq);
+ plannedRq = RequestMsg(RequestMsgCodes::unknown, 0);
+ CommandRestart();
+ return true;
+ }
+ }
+
+ StepStatus ProtocolLogic::SwitchFromIdleToCommand() {
+ currentScope = Scope::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
+ return ProcessCommandQueryResponse();
+ }
+
+ void ProtocolLogic::SwitchToIdle() {
+ state = State::Running;
+ currentScope = Scope::Idle;
+ IdleRestart();
+ }
+
+ void ProtocolLogic::SwitchFromStartToIdle() {
+ state = State::Running;
+ currentScope = Scope::Idle;
+ IdleRestart();
+ SendQuery(); // force sending Q0 immediately
+ }
+
+ 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();
+ }
+
+ constexpr char NibbleToChar(uint8_t c) {
+ switch (c) {
+ case 0x0 ... 0x9: return c + '0';
+ case 0xA ... 0xF: 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];
+ // Check for printable character, including space
+ if (b < 32 || 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() + 1;
+ char tmp[rqs] = ">";
+ static char lastMsg[rqs] = "";
+ for (uint8_t i = 0; i < size; ++i) {
+ uint8_t b = txbuff[i];
+ // Check for printable character, including space
+ if (b < 32 || b > 127)
+ b = '.';
+ tmp[i + 1] = b;
+ }
+ tmp[size + 1] = 0;
+ if (!strncmp_P(tmp, PSTR(">S0*c6."), 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_MSGLN(tmp);
+ }
+ strncpy(lastMsg, tmp, rqs);
+ }
+
+ void ProtocolLogic::LogError(const char *reason_P) {
+ char lrb[lastReceivedBytes.size() * 3];
+ FormatLastReceivedBytes(lrb);
+
+ MMU2_ERROR_MSGRPGM(reason_P);
+ SERIAL_ECHOPGM(", last bytes: ");
+ SERIAL_ECHOLN(lrb);
+ }
+
+ void ProtocolLogic::LogResponse() {
+ char lrb[lastReceivedBytes.size()];
+ FormatLastResponseMsgAndClearLRB(lrb);
+ MMU2_ECHO_MSGLN(lrb);
+ }
+
+ StepStatus ProtocolLogic::SuppressShortDropOuts(const char *msg_P, StepStatus ss) {
+ if (dataTO.Record(ss)) {
+ LogError(msg_P);
+ ResetCommunicationTimeoutAttempts(); // prepare for another run of consecutive retries before firing an error
+ return dataTO.InitialCause();
+ }
+ else {
+ return Processing; // suppress short drop outs of communication
+ }
+ }
+
+ StepStatus ProtocolLogic::HandleCommunicationTimeout() {
+ MMU2_SERIAL.flush(); // clear the output buffer
+ protocol.ResetResponseDecoder();
+ start();
+ return SuppressShortDropOuts(PSTR("Communication timeout"), CommunicationTimeout);
+ }
+
+ StepStatus ProtocolLogic::HandleProtocolError() {
+ MMU2_SERIAL.flush(); // clear the output buffer
+ state = State::InitSequence;
+ currentScope = Scope::DelayedRestart;
+ DelayedRestartRestart();
+ return SuppressShortDropOuts(PSTR("Protocol Error"), ProtocolError);
+ }
+
+ StepStatus ProtocolLogic::Step() {
+ if (!ExpectsResponse()) // if not waiting for a response, activate a planned request immediately
+ ActivatePlannedRequest();
+ auto currentStatus = ScopeStep();
+ 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
+ if (!ActivatePlannedRequest()) { // if nothing is planned, switch to Idle
+ SwitchToIdle();
+ }
+ else if (ExpectsResponse()) {
+ // if the previous cycle was Idle and now we have planned a new command -> avoid returning Finished
+ 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(PSTR("Command rejected"));
+ CommandRestart();
+ break;
+ case CommandError:
+ LogError(PSTR("Command Error"));
+ // we should 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(PSTR("Version mismatch"));
+ break;
+ case ProtocolError:
+ currentStatus = HandleProtocolError();
+ break;
+ case CommunicationTimeout:
+ currentStatus = HandleCommunicationTimeout();
+ break;
+ default:
+ break;
+ }
+ // special handling of explicit printer errors
+ return IsPrinterError() ? StepStatus::PrinterError : currentStatus;
+ }
+
+ uint8_t ProtocolLogic::CommandInProgress() const {
+ if (currentScope != Scope::Command) return 0;
+ return (uint8_t)ReqMsg().code;
+ }
+
+ void ProtocolLogic::DecrementRetryAttempts() {
+ if (inAutoRetry && retryAttempts) {
+ SERIAL_ECHOLNPGM("DecrementRetryAttempts");
+ retryAttempts--;
+ }
+ }
+
+ void ProtocolLogic::ResetRetryAttempts() {
+ SERIAL_ECHOLNPGM("ResetRetryAttempts");
+ retryAttempts = MMU2_MAX_RETRIES;
+ }
+
+ void __attribute__((noinline)) ProtocolLogic::ResetCommunicationTimeoutAttempts() {
+ SERIAL_ECHOLNPGM("RSTCommTimeout");
+ dataTO.reset();
+ }
+
+ bool DropOutFilter::Record(StepStatus ss) {
+ if (occurrences == maxOccurrences) cause = ss;
+ --occurrences;
+ return occurrences == 0;
+ }
+
+} // MMU3
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_protocol_logic.h b/Marlin/src/feature/mmu3/mmu2_protocol_logic.h
new file mode 100644
index 000000000000..6b240996b5c0
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_protocol_logic.h
@@ -0,0 +1,397 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * mmu2_protocol_logic.h
+ */
+
+#include "../../MarlinCore.h"
+
+#include
+
+#ifdef __AVR__
+ #include
+ #include "mmu_hw/error_codes.h"
+ #include "mmu_hw/progress_codes.h"
+ #include "mmu_hw/buttons.h"
+ #include "mmu_hw/registers.h"
+ #include "mmu2_protocol.h"
+
+ // #include std array is not available on AVR ... we need to "fake" it
+ namespace std {
+ template
+ class array {
+ T data[N];
+ public:
+ array() = default;
+ inline constexpr T *begin() const { return data; }
+ inline constexpr T *end() const { return data + N; }
+ static constexpr uint8_t size() { return N; }
+ inline T &operator[](uint8_t i) { return data[i]; }
+ };
+ } // std
+
+#else // !__AVR__
+
+ #include
+ #include "mmu_hw/error_codes.h"
+ #include "mmu_hw/progress_codes.h"
+
+ // Prevent ARM HAL macros from breaking our code
+ #undef CRC
+ #include "mmu2_protocol.h"
+ #include "mmu_hw/buttons.h"
+ #include "registers.h"
+
+#endif // !__AVR__
+
+// New MMU3 protocol logic
+namespace MMU3 {
+
+ 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, //!< Scope finished successfully
+ Interrupted, //!< Received "Finished" message related to a different command than originally issued (most likely the MMU restarted while doing something)
+ 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
+ PrinterError, //!< Printer's explicit error - MMU is fine, but the printer was unable to complete the requested operation
+ CommunicationRecovered,
+ ButtonPushed //!< The MMU reported the user pushed one of its three buttons.
+ };
+
+ /*inline*/ constexpr uint32_t linkLayerTimeout = 2000; //!< Default link layer communication timeout
+ /*inline*/ constexpr uint32_t dataLayerTimeout = linkLayerTimeout * 3; //!< Data layer communication timeout
+ /*inline*/ constexpr uint32_t heartBeatPeriod = linkLayerTimeout / 2; //!< Period of heart beat messages (Q0)
+
+ static_assert(heartBeatPeriod < linkLayerTimeout && linkLayerTimeout < dataLayerTimeout, "Incorrect ordering of timeouts");
+
+ //!< Filter of short consecutive drop outs which are recovered instantly
+ class DropOutFilter {
+ public:
+ static constexpr uint8_t maxOccurrences = 10; // ideally set this to >8 seconds -> 12x heartBeatPeriod
+ static_assert(maxOccurrences > 1, "we should really silently ignore at least 1 comm drop out if recovered immediately afterwards");
+ DropOutFilter() = default;
+
+ // @return true if the error should be reported to higher levels (max. number of consecutive occurrences reached)
+ bool Record(StepStatus ss);
+
+ // @return 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; }
+
+ private:
+ StepStatus cause;
+ uint8_t occurrences = maxOccurrences;
+ };
+
+ // Logic layer of the MMU vs. printer communication protocol
+ class ProtocolLogic {
+ public:
+ ProtocolLogic(uint8_t extraLoadDistance, uint8_t pulleySlowFeedrate);
+
+ // 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 Statistics();
+ void UnloadFilament();
+ void LoadFilament(uint8_t slot);
+ void EjectFilament(uint8_t slot);
+ void CutFilament(uint8_t slot);
+ void ResetMMU(uint8_t mode=0);
+ void button(uint8_t index);
+ void home(uint8_t mode);
+ void readRegister(uint8_t address);
+ void writeRegister(uint8_t address, uint16_t data);
+
+ // Set the extra load distance to be reported to the MMU.
+ // Beware - this call doesn't send anything to the MMU.
+ // The MMU gets the newly set value either by a communication restart or via an explicit writeRegister call
+ inline void PlanExtraLoadDistance(uint8_t eld_mm) { initRegs8[0] = eld_mm; }
+ // @return the currently preset extra load distance
+ inline uint8_t ExtraLoadDistance() const { return initRegs8[0]; }
+
+ // Sets the Pulley slow feed rate to be reported to the MMU.
+ // Beware - this call doesn't send anything to the MMU.
+ // The MMU gets the newly set value either by a communication restart or via an explicit writeRegister call
+ inline void PlanPulleySlowFeedRate(uint8_t psfr) {
+ initRegs8[1] = psfr;
+ }
+ // @return the currently preset Pulley slow feed rate
+ inline uint8_t PulleySlowFeedRate() const {
+ return initRegs8[1]; // even though MMU register 0x14 is 16bit, reasonable speeds are way below 255mm/s - saving space ;)
+ }
+
+ // Step the state machine
+ StepStatus Step();
+
+ // @return the current/latest error code as reported by the MMU
+ ErrorCode Error() const { return errorCode; }
+
+ // @return the current/latest process code as reported by the MMU
+ ProgressCode Progress() const { return progressCode; }
+
+ // @return the current/latest button code as reported by the MMU
+ Buttons button() const { return buttonCode; }
+
+ uint8_t CommandInProgress() const;
+
+ inline bool Running() const { return state == State::Running; }
+
+ inline bool findaPressed() const { return regs8[0]; }
+
+ inline uint16_t FailStatistics() const { return regs16[0]; }
+
+ inline uint8_t mmuFwVersionMajor() const { return mmuFwVersion[0]; }
+ inline uint8_t mmuFwVersionMinor() const { return mmuFwVersion[1]; }
+ inline uint8_t mmuFwVersionRevision() const { return mmuFwVersion[2]; }
+
+ // Current number of retry attempts left
+ constexpr uint8_t RetryAttempts() const { return retryAttempts; }
+
+ // Decrement the retry attempts, if in a retry.
+ // Called by the MMU protocol when a sent button is acknowledged.
+ void DecrementRetryAttempts();
+
+ // Reset the retryAttempts back to the default value
+ void ResetRetryAttempts();
+
+ void ResetCommunicationTimeoutAttempts();
+
+ constexpr bool InAutoRetry() const { return inAutoRetry; }
+ inline void SetInAutoRetry(const bool iar) { inAutoRetry = iar; }
+
+ inline void SetPrinterError(const ErrorCode ec) { explicitPrinterError = ec; }
+ inline void clearPrinterError() { explicitPrinterError = ErrorCode::OK; }
+ inline bool IsPrinterError() const { return explicitPrinterError != ErrorCode::OK; }
+ inline ErrorCode PrinterError() const { return explicitPrinterError; }
+
+ #ifndef UNITTEST
+ private:
+ #endif
+
+ StepStatus ExpectingMessage();
+ void SendMsg(RequestMsg rq);
+ void SendWriteMsg(RequestMsg rq);
+ void SwitchToIdle();
+ StepStatus SuppressShortDropOuts(const char *msg_P, StepStatus ss);
+ StepStatus HandleCommunicationTimeout();
+ StepStatus HandleProtocolError();
+ 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_P);
+ void LogResponse();
+ StepStatus SwitchFromIdleToCommand();
+ void SwitchFromStartToIdle();
+
+ ErrorCode explicitPrinterError;
+
+ enum class State : uint_fast8_t {
+ Stopped, //!< stopped for whatever reason
+ InitSequence, //!< initial sequence running
+ Running //!< normal operation - Idle + Command processing
+ };
+
+ enum class Scope : uint_fast8_t {
+ Stopped,
+ StartSeq,
+ DelayedRestart,
+ Idle,
+ Command
+ };
+ Scope currentScope;
+
+ // basic scope members
+ // @return true if the state machine is waiting for a response from the MMU
+ bool ExpectsResponse() const { return ((uint8_t)scopeState & (uint8_t)ScopeState::NotExpectsResponse) == 0; }
+
+ // Common internal states of the derived sub-automata
+ // General rule of thumb: *Sent states are waiting for a response from the MMU
+ enum class ScopeState : uint_fast8_t {
+ S0Sent, // beware - due to optimization reasons these SxSent must be kept one after another
+ S1Sent,
+ S2Sent,
+ S3Sent,
+ QuerySent,
+ CommandSent,
+ FilamentSensorStateSent,
+ Reading8bitRegisters,
+ Reading16bitRegisters,
+ WritingInitRegisters,
+ ButtonSent,
+ ReadRegisterSent, // standalone requests for reading registers - from higher layers
+ WriteRegisterSent,
+
+ // States which do not expect a message - MSb set
+ NotExpectsResponse = 0x80,
+ Wait = NotExpectsResponse + 1,
+ Ready = NotExpectsResponse + 2,
+ RecoveringProtocolError = NotExpectsResponse + 3,
+ };
+
+ ScopeState scopeState; //!< internal state of the sub-automaton
+
+ // @return 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);
+
+ // @return the status of processing of the statistics 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 ProcessStatisticsReqSent(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 should 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 StartReading8bitRegisters();
+ void ProcessRead8bitRegister();
+ void StartReading16bitRegisters();
+ ScopeState ProcessRead16bitRegister(ProtocolLogic::ScopeState stateAtEnd);
+ void StartWritingInitRegisters();
+ // @return true when all registers have been written into the MMU
+ bool ProcessWritingInitRegister();
+ void SendAndUpdateFilamentSensor();
+ void SendButton(uint8_t btn);
+ void SendVersion(uint8_t stage);
+ void SendReadRegister(uint8_t index, ScopeState nextState);
+ void SendWriteRegister(uint8_t index, uint16_t value, ScopeState nextState);
+
+ StepStatus ProcessVersionResponse(uint8_t stage);
+
+ // Top level split - calls the appropriate step based on current scope
+ StepStatus ScopeStep();
+
+ static constexpr uint8_t maxRetries = 6;
+ uint8_t retries;
+
+ void StartSeqRestart();
+ void DelayedRestartRestart();
+ void IdleRestart();
+ void CommandRestart();
+
+ StepStatus StartSeqStep();
+ StepStatus DelayedRestartWait();
+ StepStatus IdleStep();
+ StepStatus IdleWait();
+ StepStatus CommandStep();
+ StepStatus CommandWait();
+ StepStatus StoppedStep() { return Processing; }
+
+ StepStatus ProcessCommandQueryResponse();
+
+ inline void SetRequestMsg(const RequestMsg msg) { rq = msg; }
+ inline const RequestMsg &ReqMsg() const { return rq; }
+ RequestMsg rq = RequestMsg(RequestMsgCodes::unknown, 0);
+
+ // 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
+
+ std::array lastReceivedBytes; //!< remembers the last few bytes of incoming communication for diagnostic purposes
+ uint8_t lrb;
+
+ ErrorCode errorCode; //!< last received error code from the MMU
+ ProgressCode progressCode; //!< last received progress code from the MMU
+ Buttons buttonCode; //!< Last received button from the MMU.
+
+ uint8_t lastFSensor; //!< last state of filament sensor
+
+ #ifndef __AVR__
+ uint8_t txbuff[Protocol::MaxRequestSize()]; //!< In Buddy FW - a static transmit buffer needs to exist as DMA cannot be used from CCMRAM.
+ //!< On MK3/S/+ the transmit buffer is allocated on the stack without restrictions
+ #endif
+
+ // 8bit registers
+ static constexpr uint8_t regs8Count = 3;
+ static_assert(regs8Count > 0); // code is not ready for empty lists of registers
+ static const Register regs8Addrs[regs8Count] PROGMEM;
+ uint8_t regs8[regs8Count] = { 0, 0, 0 };
+
+ // 16bit registers
+ static constexpr uint8_t regs16Count = 2;
+ static_assert(regs16Count > 0); // code is not ready for empty lists of registers
+ static const Register regs16Addrs[regs16Count] PROGMEM;
+ uint16_t regs16[regs16Count] = { 0, 0 };
+
+ // 8bit init values to be sent to the MMU after line up
+ static constexpr uint8_t initRegs8Count = 2;
+ static_assert(initRegs8Count > 0); // code is not ready for empty lists of registers
+ static const Register initRegs8Addrs[initRegs8Count] PROGMEM;
+ uint8_t initRegs8[initRegs8Count];
+
+ uint8_t regIndex;
+
+ uint8_t mmuFwVersion[3] = { 0, 0, 0 };
+ uint16_t mmuFwVersionBuild;
+
+ uint8_t retryAttempts;
+ bool inAutoRetry;
+
+ friend class MMU3;
+ };
+
+} // MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_reporting.cpp b/Marlin/src/feature/mmu3/mmu2_reporting.cpp
new file mode 100644
index 000000000000..a51fa1b6470d
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_reporting.cpp
@@ -0,0 +1,704 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/**
+ * mmu2_reporting.cpp
+ */
+
+#include "../../inc/MarlinConfig.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "mmu2.h"
+#include "mmu2_log.h"
+#include "mmu2_fsensor.h"
+#include "mmu2_reporting.h"
+#include "mmu2_error_converter.h"
+#include "mmu2_marlin_macros.h"
+#include "mmu2_progress_converter.h"
+#include "mmu_hw/buttons.h"
+#include "mmu_hw/error_codes.h"
+#include "mmu_hw/errors_list.h"
+#include "ultralcd.h"
+#include "sound.h"
+
+#include "../../core/language.h"
+#include "../../gcode/gcode.h"
+#include "../../feature/host_actions.h"
+#include "../../lcd/marlinui.h"
+#include "../../lcd/menu/menu.h"
+#include "../../lcd/menu/menu_item.h"
+#include "../../module/temperature.h"
+
+namespace MMU3 {
+
+ OperationStatistics operation_statistics;
+
+ uint16_t OperationStatistics::fail_total_num; // total failures
+ uint8_t OperationStatistics::fail_num; // fails during print
+ uint16_t OperationStatistics::load_fail_total_num; // total load failures
+ uint8_t OperationStatistics::load_fail_num; // load failures during print
+ uint16_t OperationStatistics::tool_change_counter; // number of tool changes per print
+ uint32_t OperationStatistics::tool_change_total_counter; // number of total tool changes
+ int OperationStatistics::fail_total_num_addr; // total failures EEPROM addr
+ int OperationStatistics::fail_num_addr; // fails during print EEPROM addr
+ int OperationStatistics::load_fail_total_num_addr; // total load failures EEPROM addr
+ int OperationStatistics::load_fail_num_addr; // load failures during print EEPROM addr
+ int OperationStatistics::tool_change_counter_addr; // number of total tool changes EEPROM addr
+ int OperationStatistics::tool_change_total_counter_addr; // number of total tool changes EEPROM addr
+
+ /**
+ * Increment both the total load fails and Per print job load fails.
+ */
+ void OperationStatistics::increment_load_fails() {
+ load_fail_num += 1;
+ load_fail_total_num += 1;
+
+ #if ENABLED(EEPROM_SETTINGS)
+ // save load_fail_num to eeprom
+ persistentStore.access_start();
+ persistentStore.write_data(load_fail_num_addr, load_fail_num);
+
+ // save load_fail_total_num to eeprom
+ persistentStore.write_data(load_fail_total_num_addr, load_fail_total_num);
+ persistentStore.access_finish();
+ settings.save();
+ #endif
+ }
+
+ /**
+ * Increment both the total fails and the per print job fails.
+ */
+ void OperationStatistics::increment_mmu_fails() {
+ fail_num += 1;
+ fail_total_num += 1;
+
+ #if ENABLED(EEPROM_SETTINGS)
+ // save fail_num to eeprom
+ persistentStore.access_start();
+ persistentStore.write_data(fail_num_addr, fail_num);
+ // save fail_total_num to eeprom
+ persistentStore.write_data(fail_total_num_addr, fail_total_num);
+ persistentStore.access_finish();
+ settings.save();
+ #endif
+ }
+
+ /**
+ * Increment tool change counter
+ */
+ void OperationStatistics::increment_tool_change_counter() {
+ tool_change_counter += 1;
+ tool_change_total_counter += 1;
+
+ #if ENABLED(EEPROM_SETTINGS)
+ // save tool_change_total_counter to eeprom
+ persistentStore.access_start();
+ persistentStore.write_data(tool_change_total_counter_addr, tool_change_total_counter);
+ persistentStore.access_finish();
+ settings.save();
+ #endif
+ }
+
+
+ /**
+ * Reset only per print operation statistics and update EEPROM.
+ *
+ * @return true if everything went okay, false otherwise.
+ */
+ bool OperationStatistics::reset_per_print_stats() {
+ // Update data
+ load_fail_num = 0;
+ fail_num = 0;
+ tool_change_counter = 0;
+
+ #if ENABLED(EEPROM_SETTINGS)
+ // Update EEPROM
+ persistentStore.access_start();
+ persistentStore.write_data(load_fail_num_addr, load_fail_num);
+ persistentStore.write_data(fail_num_addr, fail_num);
+ persistentStore.write_data(tool_change_counter_addr, tool_change_counter);
+ persistentStore.access_finish();
+ return settings.save();
+ #else
+ return true;
+ #endif
+ }
+
+
+ /**
+ * Reset fail statistics and update EEPROM.
+ *
+ * This will keep the tool change counter change counters and delete anything
+ * else.
+ *
+ * @return true if everything went okay, false otherwise.
+ */
+ bool OperationStatistics::reset_fail_stats() {
+ // Update data
+ load_fail_num = 0;
+ load_fail_total_num = 0;
+ fail_num = 0;
+ fail_total_num = 0;
+
+ #if ENABLED(EEPROM_SETTINGS)
+ // Update EEPROM
+ persistentStore.access_start();
+ persistentStore.write_data(load_fail_num_addr, load_fail_num);
+ persistentStore.write_data(load_fail_total_num_addr, load_fail_total_num);
+ persistentStore.write_data(fail_num_addr, fail_num);
+ persistentStore.write_data(fail_total_num_addr, fail_total_num);
+ persistentStore.access_finish();
+ return settings.save();
+ #else
+ return true;
+ #endif
+ }
+
+
+ /**
+ * Reset all operation statistics and update EEPROM.
+ *
+ * @return true if everything went okay, false otherwise.
+ */
+ bool OperationStatistics::reset_stats() {
+ // Update data
+ load_fail_num = 0;
+ load_fail_total_num = 0;
+ fail_num = 0;
+ fail_total_num = 0;
+ tool_change_counter = 0;
+ tool_change_total_counter = 0;
+
+ #if ENABLED(EEPROM_SETTINGS)
+ // Update EEPROM
+ persistentStore.access_start();
+ persistentStore.write_data(load_fail_num_addr, load_fail_num);
+ persistentStore.write_data(load_fail_total_num_addr, load_fail_total_num);
+ persistentStore.write_data(fail_num_addr, fail_num);
+ persistentStore.write_data(fail_total_num_addr, fail_total_num);
+ persistentStore.write_data(tool_change_counter_addr, tool_change_counter);
+ persistentStore.write_data(tool_change_total_counter_addr, tool_change_total_counter);
+ persistentStore.access_finish();
+ return settings.save();
+ #else
+ return true;
+ #endif
+ }
+
+
+ void BeginReport(CommandInProgress /*cip*/, ProgressCode ec) {
+ // custom_message_type = CustomMsg::MMUProgress;
+ ui.set_status(ProgressCodeToText(ec));
+ }
+
+ void EndReport(CommandInProgress /*cip*/, ProgressCode /*ec*/) {
+ // clear the status msg line - let the printed filename get visible again
+ if (!printJobOngoing()) ui.reset_status();
+ //custom_message_type = CustomMsg::Status;
+ }
+
+ /**
+ * @brief Renders any characters that will be updated live on the MMU error screen.
+ *Currently, this is FINDA and Filament Sensor status and Extruder temperature.
+ */
+ extern void ReportErrorHookDynamicRender(void) {
+ #if HAS_WIRED_LCD
+ // beware - this optimization abuses the fact, that findaDetectsFilament returns 0 or 1 and '0' is followed by '1' in the ASCII table
+ lcd_put_int(3, LCD_HEIGHT - 1, mmu3.findaDetectsFilament() + '0');
+ lcd_put_int(8, LCD_HEIGHT - 1, FILAMENT_PRESENT() + '0');
+
+ // print active/changing filament slot
+ lcd_moveto(10, LCD_HEIGHT - 1);
+ lcdui_print_extruder();
+
+ // Print active extruder temperature
+ lcd_put_int(16, LCD_HEIGHT - 1, (int)(thermalManager.degHotend(0) + 0.5));
+ #endif
+ }
+
+ static bool drawing_more_info_screen = false;
+ static bool msg_next_is_consumed = false;
+ static FSTR_P msg_next = nullptr;
+
+ /**
+ * Display more info about the error. If the error message doesn't fit into
+ * the screen, clicking the LCD button will go to the next screen to display
+ * the rest of the message, until no messages left to display and a final
+ * click will return to the previous screen.
+ *
+ * This gets the message data from the "editable.uint8" which is set in the
+ * action item.
+ */
+ void show_more_info_screen() {
+ #if HAS_WIRED_LCD
+ if (drawing_more_info_screen) return;
+ drawing_more_info_screen = true;
+ FSTR_P fmsg = PrusaErrorDesc(editable.uint8);
+ if (ui.use_click()) {
+ if (msg_next_is_consumed) {
+ msg_next_is_consumed = false;
+ drawing_more_info_screen = false;
+ msg_next = nullptr;
+ // Prevent this function being triggered again...
+ SetButtonResponse(ButtonOperations::NoOperation);
+ return ui.go_back();
+ }
+ fmsg = msg_next;
+ }
+ else if (msg_next_is_consumed) {
+ fmsg = msg_next;
+ }
+
+ FSTR_P const msg_next_int = lcd_display_message_fullscreen(fmsg);
+ msg_next_is_consumed = strlen_P(FTOP(msg_next_int)) == 0;
+ if (!msg_next_is_consumed) msg_next = msg_next_int;
+ // Set the button response to MoreInfo so we keep coming back to this screen until all messages are consumed
+ SetButtonResponse(ButtonOperations::MoreInfo);
+ #else
+ // no lcd, no error display... just break the loop...
+ msg_next_is_consumed = false;
+ msg_next = nullptr;
+ SetButtonResponse(ButtonOperations::NoOperation);
+ #endif // HAS_WIRED_LCD
+ drawing_more_info_screen = false;
+ }
+
+ /**
+ * @brief Renders any characters that are static on the MMU error screen i.e. they don't change.
+ * @param[in] ei Error code index
+ */
+ static void ReportErrorHookStaticRender(uint8_t ei) {
+ #if HAS_WIRED_LCD
+ //! Show an error screen
+ //! When an MMU error occurs, the LCD content will look like this:
+ //! |01234567890123456789|
+ //! |MMU FW update needed| <- title/header of the error: max 20 characters
+ //! |prusa.io/04504 | <- URL max 20 characters
+ //! |FI:1 FS:1 5>3 t201°| <- status line, t is thermometer symbol
+ //! |>Retry >Done >W| <- buttons
+ bool two_choices = false;
+
+ // Read and determine what operations should be shown on the menu
+ const uint8_t button_operation = PrusaErrorButtons(ei),
+ button_op_right = BUTTON_OP_RIGHT(button_operation),
+ button_op_middle = BUTTON_OP_MIDDLE(button_operation);
+
+ // Check if the menu should have three or two choices
+ if (button_op_right == (uint8_t)ButtonOperations::NoOperation) {
+ // Two operations not specified, the error menu should only show two choices
+ two_choices = true;
+ }
+
+ START_MENU();
+ #ifndef __AVR__
+ // TODO: I couldn't make this work on AVR
+ STATIC_ITEM_F(PrusaErrorTitle(ei), SS_DEFAULT | SS_INVERT);
+
+ // Write the help page and error code
+ MString url("");
+ url.appendf("prusa.io/04%hu", PrusaErrorCode(ei));
+ STATIC_ITEM_F(nullptr, SS_DEFAULT, url.buffer());
+
+ //ReportErrorHookSensorLineRender();
+
+ editable.uint8 = button_op_middle;
+ ACTION_ITEM_F(
+ PrusaErrorButtonTitle(button_op_middle),
+ []{ SetButtonResponse((ButtonOperations)editable.uint8); }
+ );
+
+ if (!two_choices) {
+ editable.uint8 = button_op_right;
+ ACTION_ITEM_F(
+ PrusaErrorButtonTitle(button_op_right),
+ []{ SetButtonResponse((ButtonOperations)editable.uint8); }
+ );
+ }
+
+ // Add a More Info option
+ editable.uint8 = ei;
+ ACTION_ITEM_F(
+ GET_TEXT_F(MSG_BTN_MORE),
+ []{
+ // only when the menu item is used push the current screen back
+ ui.push_current_screen();
+ msg_next_is_consumed = false;
+ msg_next = nullptr;
+ SetButtonResponse(ButtonOperations::MoreInfo);
+ }
+ );
+
+ #endif // !__AVR__
+
+ // Render the choices
+ //if (two_choices) {
+ // lcd_show_choices_prompt_P(
+ // LCD_LEFT_BUTTON_CHOICE,
+ // PrusaErrorButtonTitle(button_op_middle),
+ // GET_TEXT(MSG_BTN_MORE),
+ // 18, nullptr
+ // );
+ //}
+ //else {
+ // lcd_show_choices_prompt_P(LCD_MIDDLE_BUTTON_CHOICE,
+ // PrusaErrorButtonTitle(button_op_middle),
+ // PrusaErrorButtonTitle(button_op_right),
+ // 9, GET_TEXT(MSG_BTN_MORE)
+ // );
+ //}
+
+ END_MENU();
+ //ui.refresh(LCDVIEW_CALL_REDRAW_NEXT);
+ #endif // HAS_WIRED_LCD
+ }
+
+ void ReportErrorHookSensorLineRender() {
+ #if HAS_WIRED_LCD
+ // Render static characters in third line
+ lcd_put_u8str(
+ 0,
+ LCD_HEIGHT - 1,
+ F("FI: FS: > " LCD_STR_THERMOMETER " " LCD_STR_DEGREE)
+ );
+ #endif
+ }
+
+ /**
+ * @brief Monitors the LCD button selection without blocking MMU communication
+ * @param[in] ei Error code index
+ * @return 0 if there is no knob click --
+ * 1 if user clicked 'More' and firmware should render
+ * the error screen when ReportErrorHook is called next --
+ * 2 if the user selects an operation and we would like
+ * to exit the error screen. The MMU will raise the menu
+ * again if the error is not solved.
+ */
+ static uint8_t ReportErrorHookMonitor(uint8_t ei) {
+ uint8_t ret = 0;
+ if (GetButtonResponse() == ButtonOperations::MoreInfo) {
+ SetButtonResponse(ButtonOperations::NoOperation);
+ ret = 1;
+ }
+ else if (GetButtonResponse() != ButtonOperations::NoOperation) {
+ ret = 2;
+ }
+ // Next MMU error screen should reset the choice selection
+ // reset_button_selection = 1;
+ return ret;
+ }
+
+ enum class ReportErrorHookStates : uint8_t {
+ RENDER_ERROR_SCREEN = 0,
+ MONITOR_SELECTION = 1,
+ DISMISS_ERROR_SCREEN = 2,
+ };
+
+ enum ReportErrorHookStates ReportErrorHookState = ReportErrorHookStates::RENDER_ERROR_SCREEN;
+
+ // Helper variable to monitor knob in MMU error screen in blocking functions e.g. manage_response
+ static bool is_mmu_error_monitor_active;
+
+ // Helper variable to stop rendering the error screen when the firmware is rendering complementary
+ // UI to resolve the error screen, for example tuning Idler Stallguard Threshold
+ // Set to false to allow the error screen to render again.
+ static bool putErrorScreenToSleep;
+
+ void CheckErrorScreenUserInput() {
+ if (is_mmu_error_monitor_active) {
+ // Call this every iteration to keep the knob rotation responsive
+ // This includes when mmu_loop is called within manage_response
+ ReportErrorHook((CommandInProgress)mmu3.getCommandInProgress(), mmu3.getLastErrorCode(), mmu3.mmuLastErrorSource());
+ }
+ }
+
+ bool TuneMenuEntered() {
+ return putErrorScreenToSleep;
+ }
+
+ void ReportErrorHook(CommandInProgress /*cip*/, ErrorCode ec, uint8_t /*es*/) {
+ if (putErrorScreenToSleep) return;
+
+ if (mmu3.mmuCurrentErrorCode() == ErrorCode::OK && mmu3.mmuLastErrorSource() == MMU3::ErrorSourceMMU) {
+ // If the error code suddenly changes to OK, that means
+ // a button was pushed on the MMU and the LCD should
+ // dismiss the error screen until MMU raises a new error
+ ReportErrorHookState = ReportErrorHookStates::DISMISS_ERROR_SCREEN;
+ drawing_more_info_screen = false;
+ msg_next_is_consumed = true;
+ }
+
+ const uint8_t ei = PrusaErrorCodeIndex((ErrorCode)ec);
+
+ // This should be the equivelent of the switch..case above...
+ if ((uint8_t)ReportErrorHookState == (uint8_t)ReportErrorHookStates::RENDER_ERROR_SCREEN) {
+ KEEPALIVE_STATE(PAUSED_FOR_USER);
+ #if HAS_WIRED_LCD
+ drawing_more_info_screen = false;
+ msg_next_is_consumed = false;
+ msg_next = nullptr;
+ editable.uint8 = ei;
+ ui.defer_status_screen();
+ ui.goto_screen([]{ ReportErrorHookStaticRender(editable.uint8); });
+ #endif
+ ReportErrorHookState = ReportErrorHookStates::MONITOR_SELECTION;
+ }
+
+ if ((uint8_t)ReportErrorHookState == (uint8_t)ReportErrorHookStates::MONITOR_SELECTION) {
+ is_mmu_error_monitor_active = true;
+ // ReportErrorHookDynamicRender(); // Render dynamic characters
+ sound_wait_for_user();
+ uint8_t result = ReportErrorHookMonitor(ei);
+ if (result == 0) {
+ // No choice selected, return to loop()
+ }
+ else if (result == 1) {
+ // More Info button selected, change state
+ editable.uint8 = ei;
+ //ui.refresh(LCDVIEW_CALL_REDRAW_NEXT);
+ ui.goto_screen(show_more_info_screen);
+ ReportErrorHookState = ReportErrorHookStates::MONITOR_SELECTION;
+ }
+ else if (result == 2) {
+ // Exit error screen and enable lcd updates
+ TERN_(HAS_WIRED_LCD, ui.return_to_status());
+ sound_wait_for_user_reset();
+ // Reset the state in case a new error is reported
+ is_mmu_error_monitor_active = false;
+ KEEPALIVE_STATE(IN_HANDLER);
+ ReportErrorHookState = ReportErrorHookStates::RENDER_ERROR_SCREEN;
+ }
+ return; // Always return to loop() to let MMU trigger a call to ReportErrorHook again
+ }
+ else if ((uint8_t)ReportErrorHookState == (uint8_t)ReportErrorHookStates::DISMISS_ERROR_SCREEN) {
+ TERN_(HAS_WIRED_LCD, ui.return_to_status());
+ sound_wait_for_user_reset();
+ // Reset the state in case a new error is reported
+ is_mmu_error_monitor_active = false;
+ KEEPALIVE_STATE(IN_HANDLER);
+ ReportErrorHookState = ReportErrorHookStates::RENDER_ERROR_SCREEN;
+ }
+ }
+
+ void ReportProgressHook(CommandInProgress cip, ProgressCode ec) {
+ if (cip != CommandInProgress::NoCommand) {
+ // custom_message_type = CustomMsg::MMUProgress;
+ ui.set_status(ProgressCodeToText(ec));
+ }
+ }
+
+ TryLoadUnloadReporter::TryLoadUnloadReporter(float delta_mm)
+ : dpixel0(0), dpixel1(0), lcd_cursor_col(0)
+ , pixel_per_mm(0.5F * float(LCD_WIDTH) / (delta_mm)
+ ) {
+ //lcd_clearstatus();
+ ui.reset_status();
+ }
+
+ TryLoadUnloadReporter::~TryLoadUnloadReporter() {
+ #if HAS_WIRED_LCD
+ // Delay the next status message just so
+ // the user can see the results clearly
+ ui.set_status_no_expire(ui.status_message);
+ #endif
+ }
+
+ void TryLoadUnloadReporter::Render(uint8_t col, bool sensorState) {
+ #if HAS_WIRED_LCD
+ // Set the cursor position each time in case some other
+ // part of the firmware changes the cursor position
+ lcd_insert_char_into_status(col, sensorState ? LCD_STR_SOLID_BLOCK[0] : '-');
+ if (ui.lcdDrawUpdate == LCDViewAction::LCDVIEW_NONE)
+ ui.draw_status_message(false);
+ #endif
+ }
+
+ void TryLoadUnloadReporter::Progress(bool sensorState) {
+ // Always round up, you can only have 'whole' pixels. (floor is also an option)
+ dpixel1 = ceil((stepper_get_machine_position_E_mm() - planner_get_current_position_E()) * pixel_per_mm);
+ if (dpixel1 - dpixel0) {
+ dpixel0 = dpixel1;
+ if (lcd_cursor_col > (LCD_WIDTH - 1)) lcd_cursor_col = LCD_WIDTH - 1;
+ Render(lcd_cursor_col++, sensorState);
+ }
+ }
+
+ void TryLoadUnloadReporter::DumpToSerial() {
+ char buf[LCD_WIDTH + 1];
+ TERN_(HAS_WIRED_LCD, ui.status_message.copyto(buf));
+ for (uint8_t i = 0; i < sizeof(buf); i++) {
+ // 0xFF is -1 when converting from unsigned to signed char
+ // If the number is negative, that means filament is present
+ buf[i] = (buf[i] < 0) ? '1' : '0';
+ }
+ buf[LCD_WIDTH] = 0;
+ MMU2_ECHO_MSGLN(buf);
+ }
+
+ void IncrementLoadFails() {
+ operation_statistics.increment_load_fails();
+ }
+
+ void IncrementMMUFails() {
+ operation_statistics.increment_mmu_fails();
+ }
+
+ bool cutter_enabled() {
+ return mmu3.cutter_mode > 0;
+ }
+
+ void MakeSound(SoundType s) {
+ Sound_MakeSound((eSOUND_TYPE)s);
+ }
+
+ static void fullScreenMsg(FSTR_P const fstr, uint8_t slot) {
+ #if HAS_WIRED_LCD
+ ui.clear_lcd();
+ #ifndef __AVR__
+ SETCURSOR(0, 1);
+ lcd_put_u8str(fstr);
+ lcd_put_lchar(' ');
+ lcd_put_int(slot + 1);
+ #else
+ UNUSED(fstr);
+ #endif
+ ui.refresh(LCDVIEW_CALL_REDRAW_NEXT);
+ ui.screen_changed = true;
+ #endif
+ }
+
+ void fullScreenMsgCut(uint8_t slot) { fullScreenMsg(GET_TEXT_F(MSG_CUT_FILAMENT), slot); }
+ void fullScreenMsgEject(uint8_t slot) { fullScreenMsg(GET_TEXT_F(MSG_EJECT_FROM_MMU), slot); }
+ void fullScreenMsgTest(uint8_t slot) { fullScreenMsg(GET_TEXT_F(MSG_TESTING_FILAMENT), slot); }
+ void fullScreenMsgLoad(uint8_t slot) { fullScreenMsg(GET_TEXT_F(MSG_LOADING_FILAMENT), slot); }
+
+ void fullScreenMsgRestoringTemperature() {
+ #if HAS_WIRED_LCD
+ lcd_display_message_fullscreen(F("MMU Retry: Restoring temperature..."));
+ #endif
+ }
+
+ void ScreenUpdateEnable() {
+ TERN_(HAS_WIRED_LCD, ui.refresh(LCDVIEW_CALL_REDRAW_NEXT));
+ }
+
+ void ScreenClear() { ui.clear_lcd(); }
+
+ struct TuneItem {
+ uint8_t address;
+ uint8_t minValue;
+ uint8_t maxValue;
+ }
+ __attribute__((packed));
+
+ static const TuneItem TuneItems[] PROGMEM = {
+ { (uint8_t)Register::Selector_sg_thrs_R, 1, 4 },
+ { (uint8_t)Register::Idler_sg_thrs_R, 2, 10 },
+ };
+
+ static_assert(COUNT(TuneItems) == 2);
+
+ typedef struct {
+ // Variables used when editing values.
+ const char* editLabel;
+ uint8_t editValueBits; // 8 or 16
+ void* editValuePtr;
+ int16_t currentValue;
+ int16_t minEditValue;
+ int16_t maxEditValue;
+ int16_t minJumpValue;
+ } menu_data_edit_t;
+
+ struct _menu_tune_data_t {
+ menu_data_edit_t reserved; // 13 bytes reserved for number editing functions
+ int8_t status; // 1 byte
+ uint8_t currentValue; // 1 byte
+ TuneItem item; // 3 bytes
+ };
+
+ //static_assert(sizeof(_menu_tune_data_t) == 18);
+ //static_assert(sizeof(menu_data)>= sizeof(_menu_tune_data_t), "_menu_tune_data_t doesn't fit into menu_data");
+
+ void tuneIdlerStallguardThresholdMenu() {
+ // const uint8_t menu_data[32] = "Set Stallguard Threshold";
+ // //static constexpr _menu_tune_data_t * const _md = (_menu_tune_data_t*)&(menu_data[0]);
+ // static constexpr _menu_tune_data_t * const _md = (_menu_tune_data_t*)&(menu_data[0]);
+
+ // // Do not timeout the screen, otherwise there will be FW crash (menu recursion)
+ // //lcd_timeoutToStatus.stop();
+ //if (_md->status == 0) {
+ // _md->status = 1; // Menu entered for the first time
+
+ // // Fetch the TuneItem from PROGMEM
+ // const uint8_t offset = (mmu3.mmuCurrentErrorCode() == ErrorCode::HOMING_IDLER_FAILED) ? 1 : 0;
+ // memcpy_P(&(_md->item), &TuneItems[offset], sizeof(TuneItem));
+
+ // // Fetch the value which is currently in MMU EEPROM
+ // mmu3.readRegister(_md->item.address);
+ // _md->currentValue = mmu3.getLastReadRegisterValue();
+ //}
+
+ // //MENU_BEGIN();
+ // //ON_MENU_LEAVE(
+ // // mmu3.writeRegister(_md->item.address, (uint16_t)_md->currentValue);
+ // // putErrorScreenToSleep = false;
+ // // lcd_return_to_status();
+ // // return;
+ // //);
+ // //MENU_ITEM_BACK(MSG_DONE);
+ // //MENU_ITEM_EDIT_int3_P(
+ // // _i("Sensitivity"), ////MSG_MMU_SENSITIVITY c=18
+ // // &_md->currentValue,
+ // // _md->item.minValue,
+ // // _md->item.maxValue
+ // //);
+ // //MENU_END();
+
+ //START_MENU();
+ //BACK_ITEM(MSG_BACK);
+ //EDIT_ITEM(
+ // int8,
+ // MSG_MMU_SENSITIVITY,
+ // &_md->currentValue,
+ // _md->item.minValue,
+ // _md->item.maxValue,
+ // []{
+ // write_register_and_return_to_status_menu(_md->item.address, _md->currentValue);
+ // }
+ // );
+ //END_MENU();
+ }
+
+ void write_register_and_return_to_status_menu(uint8_t address, uint8_t value) {
+ mmu3.writeRegister(address, (uint16_t)value);
+ putErrorScreenToSleep = false;
+ ui.return_to_status();
+ }
+
+ void tuneIdlerStallguardThreshold() {
+ putErrorScreenToSleep = true;
+ //menu_submenu(tuneIdlerStallguardThresholdMenu);
+ }
+
+} // MMU3
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_reporting.h b/Marlin/src/feature/mmu3/mmu2_reporting.h
new file mode 100644
index 000000000000..d3e8db9c5eea
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_reporting.h
@@ -0,0 +1,168 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * mmu2_reporting.h
+ */
+
+#include "../../MarlinCore.h"
+
+#include
+#include "mmu_hw/error_codes.h"
+#include "mmu_hw/progress_codes.h"
+
+namespace MMU3 {
+
+ enum CommandInProgress : uint8_t {
+ NoCommand = 0,
+ CutFilament = 'K',
+ EjectFilament = 'E',
+ Homing = 'H',
+ LoadFilament = 'L',
+ Reset = 'X',
+ ToolChange = 'T',
+ UnloadFilament = 'U',
+ };
+
+ /**
+ * Data class for MMU operation statistics.
+ *
+ * This is used to load/save data from/to EEPROM.
+ * The data is initialized by the settings.load() method.
+ */
+ class OperationStatistics {
+ public:
+ void increment_load_fails();
+ void increment_mmu_fails();
+ void increment_tool_change_counter();
+ bool reset_per_print_stats(); // Reset only the per print stats.
+ bool reset_fail_stats(); // Reset only fail stats and keep tool change counters
+ bool reset_stats(); // Reset MMU stats and update EEPROM
+
+ static uint16_t fail_total_num; // total failures
+ static uint8_t fail_num; // fails during print
+ static uint16_t load_fail_total_num; // total load failures
+ static uint8_t load_fail_num; // load failures during print
+ static uint16_t tool_change_counter; // number of tool changes during print
+ static uint32_t tool_change_total_counter; // number of total tool changes
+ static int fail_total_num_addr; // total failures EEPROM addr
+ static int fail_num_addr; // fails during print EEPROM addr
+ static int load_fail_total_num_addr; // total load failures EEPROM addr
+ static int load_fail_num_addr; // load failures during print EEPROM addr
+ static int tool_change_counter_addr; // number of tool changes EEPROM addr
+ static int tool_change_total_counter_addr; // number of total tool changes EEPROM addr
+ };
+
+ extern OperationStatistics operation_statistics;
+
+ // Called at the begin of every MMU operation
+ void BeginReport(CommandInProgress cip, ProgressCode ec);
+
+ // Called at the end of every MMU operation
+ void EndReport(CommandInProgress cip, ProgressCode ec);
+
+ // Checks for error screen user input, if the error screen is open
+ void CheckErrorScreenUserInput();
+
+ // Return true if the error screen is sleeping in the background
+ // Error screen sleeps when the firmware is rendering complementary
+ // UI to resolve the error screen, for example tuning Idler Stallguard Threshold
+ bool TuneMenuEntered();
+
+ // @brief Called when the MMU or MK3S sends operation error (even repeatedly).
+ // Render MMU error screen on the LCD. This must be non-blocking
+ // and allow the MMU and printer to communicate with each other.
+ // @param[in] ec error code
+ // @param[in] es error source
+ void ReportErrorHook(CommandInProgress cip, ErrorCode ec, uint8_t es);
+
+ // Called when the MMU sends operation progress update
+ void ReportProgressHook(CommandInProgress cip, ProgressCode ec);
+
+ struct TryLoadUnloadReporter {
+ TryLoadUnloadReporter(float delta_mm);
+ ~TryLoadUnloadReporter();
+ void Progress(bool sensorState);
+ void DumpToSerial();
+
+ private:
+ // @brief Add one block to the progress bar
+ // @param col pixel position on the LCD status line, should range from 0 to (LCD_WIDTH - 1)
+ // @param sensorState if true, filament is not present, else filament is present. This controls which character to render
+ void Render(uint8_t col, bool sensorState);
+
+ uint8_t dpixel0, dpixel1;
+ uint8_t lcd_cursor_col;
+ // The total length is twice delta_mm. Divide that length by number of pixels
+ // available to get length per pixel.
+ // Note: Below is the reciprocal of (2 * delta_mm) / LCD_WIDTH [mm/pixel]
+ float pixel_per_mm;
+ };
+
+ // Remders the sensor status line. Also used by the "resume temperature" screen.
+ void ReportErrorHookDynamicRender();
+
+ // Renders the static part of the sensor state line. Also used by "resuming temperature screen"
+ void ReportErrorHookSensorLineRender();
+
+ // @return 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();
+
+ // Disable MMU in EEPROM
+ //void DisableMMUInSettings();
+
+ // Increments EEPROM cell - number of failed loads into the nozzle
+ // Note: technically, this is not an MMU error but an error of the printer.
+ void IncrementLoadFails();
+
+ // Increments EEPROM cell - number of MMU errors
+ void IncrementMMUFails();
+
+ // @return true when Cutter is enabled in the menus
+ bool cutter_enabled();
+
+ // Beware: enum values intentionally chosen to match the 8bit FW to save code size
+ enum SoundType {
+ Prompt = 2,
+ Confirm = 3
+ };
+
+ void MakeSound(SoundType s);
+
+ void fullScreenMsgCut(uint8_t slot);
+ void fullScreenMsgEject(uint8_t slot);
+ void fullScreenMsgTest(uint8_t slot);
+ void fullScreenMsgLoad(uint8_t slot);
+ void fullScreenMsgRestoringTemperature();
+
+ void ScreenUpdateEnable();
+ void ScreenClear();
+
+ void tuneIdlerStallguardThreshold();
+
+ void write_register_and_return_to_status_menu(uint8_t address, uint8_t value);
+
+} // MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_state.h b/Marlin/src/feature/mmu3/mmu2_state.h
new file mode 100644
index 000000000000..036d3ae25500
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_state.h
@@ -0,0 +1,48 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * mmu2_state.h
+ */
+
+#include
+
+namespace MMU3 {
+ /**
+ * @brief status of mmu
+ *
+ * States of a printer with the MMU:
+ * - Active
+ * - Connecting
+ * - Stopped
+ *
+ * When the printer's FW starts, the MMU 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.
+ };
+
+} // MMU3
diff --git a/Marlin/src/feature/mmu3/mmu2_supported_version.h b/Marlin/src/feature/mmu3/mmu2_supported_version.h
new file mode 100644
index 000000000000..fe21c79d5a2c
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu2_supported_version.h
@@ -0,0 +1,32 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * mmu2_supported_version.h
+ */
+
+#include
+
+#define mmuVersionMajor 3
+#define mmuVersionMinor 0
+#define mmuVersionPatch 2
diff --git a/Marlin/src/feature/mmu3/mmu3-serial-protocol.md b/Marlin/src/feature/mmu3/mmu3-serial-protocol.md
new file mode 100644
index 000000000000..fd0f1fbcbaad
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu3-serial-protocol.md
@@ -0,0 +1,159 @@
+# MMU3 Messages
+
+Starting with the version 2.0.19 of the MMU firmware, requests and responses have a trailing section that contains the CRC8 of the original message. The general structure is as follows:
+
+```
+Requests (what Marlin requests):
+MMU3:>{RequestMsgCode}{Value}*{CRC8}\n
+
+Responses (what MMU responds with):
+MMU3:<{RequestMsgCode}{Value} {ResponseMsgParamCode}{paramValue}*{CRC8}\n
+```
+
+An example of that would be:
+
+```
+MMU3:>S0*c6\n
+MMU3:S1*ad\n
+MMU3:S2*10\n
+MMU3:S0*c6\n
+MMU3:>S0*c6\n
+MMU3:>S0*c6\n
+...
+```
+
+Once communication is established the MMU responds with:
+```
+MMU3:S1*ad\n
+MMU3:S2*10\n
+MMU3:M1*{CRC8};
+MMU3:<---nothing---
+```
+
+```
+MMU3:>P0
+MMU3:T{Filament index}*{CRC8}\n
+MMU3:T0*{CRC8}\n
+
+MMU3:>Q0*{CRC8}\n
+MMU3: FeedingToFinda
+
+MMU3:>Q0*{CRC8}\n
+MMU3: FeedingToNozzle
+```
+
+As soon as the filament is fed down to the extruder we follow with:
+
+```
+MMU3:>C0*{CRC8}\n
+```
+
+The MMU will feed a few more millimeters of filament for the extruder gears to grab. When done, the MMU sends:
+```
+MMU3:>Q0*{CRC8}\n
+MMU3: FinishingMoves
+```
+
+After the `T0*P9` response we immediately continue with the next G-code which should be one or more extruder moves to feed the filament into the hotend.
+
+
+## FINDA status
+```
+MMU3:>P0*{CRC8}\n
+```
+
+If the filament is loaded to the extruder, FINDA status is 1 and the MMU responds with:
+```
+MMU3:L{Filament index}*{CRC8}\n
+MMU3:Q0*{CRC8}\n
+```
+
+The MMU will respond with status messages:
+```
+MMU3:Q0*{CRC8}\n
+MMU3: 'ok\n'`
+
+## Eject filament
+
+- `MMU <= 'E*Filament index*\n'`
+- `MMU => 'ok\n'`
diff --git a/Marlin/src/feature/mmu3/mmu_hw/buttons.h b/Marlin/src/feature/mmu3/mmu_hw/buttons.h
new file mode 100644
index 000000000000..2b35d6339c3b
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu_hw/buttons.h
@@ -0,0 +1,73 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * buttons.h
+ */
+
+#include
+
+// Helper macros to parse the operations from Btns()
+#define BUTTON_OP_RIGHT(X) ((X & 0xF0) >> 4)
+#define BUTTON_OP_MIDDLE(X) (X & 0x0F)
+
+namespace MMU3 {
+
+// Will be mapped onto dialog button responses in the FW
+// Those responses have their unique+translated texts as well
+enum class ButtonOperations : uint8_t {
+ NoOperation = 0,
+ Retry = 1,
+ Continue = 2,
+ ResetMMU = 3,
+ Unload = 4,
+ Load = 5,
+ Eject = 6,
+ Tune = 7,
+ StopPrint = 8,
+ DisableMMU = 9,
+ MoreInfo = 10,
+};
+
+// Button codes + extended actions performed on the printer's side
+enum class Buttons : uint_least8_t {
+ Right = 0,
+ Middle,
+ Left,
+
+ // Performed on the printer's side
+ ResetMMU,
+ Load,
+ Eject,
+ StopPrint,
+ DisableMMU,
+ TuneMMU, // Printer changes MMU register value
+
+ NoButton = 0xFF // should be kept last
+};
+
+constexpr uint_least8_t buttons_to_uint8t(Buttons b) {
+ return static_cast(b);
+}
+
+} // MMU3
diff --git a/Marlin/src/feature/mmu3/mmu_hw/check-pce.sh b/Marlin/src/feature/mmu3/mmu_hw/check-pce.sh
new file mode 100755
index 000000000000..197bc6dcc50b
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu_hw/check-pce.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+# download Prusa Error Codes for MMU
+#wget https://raw.githubusercontent.com/3d-gussner/Prusa-Error-Codes/master/04_MMU/error-codes.yaml --output-document=error-codes.yaml
+wget https://raw.githubusercontent.com/prusa3d/Prusa-Error-Codes/master/04_MMU/error-codes.yaml --output-document=error-codes.yaml
+
+oifs="$IFS" ## save original IFS
+IFS=$'\n' ## set IFS to break on newline
+codes=($(cat error-codes.yaml |grep "code:" |cut -d '"' -f2))
+titles=($(cat error-codes.yaml |grep 'title:' |cut -d '"' -f2))
+texts=($(cat error-codes.yaml |grep "text:" |cut -d '"' -f2))
+actions=($(cat error-codes.yaml |grep "action:" |cut -d ':' -f2))
+ids=($(cat error-codes.yaml |grep "id:" |cut -d '"' -f2))
+IFS="$oifs" ## restore original IFS
+
+filename=errors_list.h
+
+clear
+for ((i = 0; i < ${#codes[@]}; i++)) do
+ code=${codes[i]}
+ id=$(cat $filename |grep "${code#04*}" | cut -d "=" -f1 | cut -d "_" -f3- |cut -d " " -f1)
+ title=$(cat $filename |grep "${id}" |grep --max-count=1 "MSG_TITLE" |cut -d '"' -f2)
+ text=$(cat $filename |grep "${id}" |grep --max-count=1 "MSG_DESC" |cut -d '"' -f2)
+ action1=$(cat $filename |grep "),//$id"| cut -d "," -f1)
+ action2=$(cat $filename |grep "),//$id"| cut -d "," -f2)
+ action1=$(echo $action1 | cut -d ":" -f2- |cut -d ":" -f2)
+ action2=$(echo $action2 | cut -d ":" -f2- |cut -d ":" -f2 |cut -d ")" -f1)
+ if [ "$action2" == "NoOperation" ]; then
+ action=" [$action1]"
+ else
+ action=" [$action1,$action2]"
+ fi
+ echo -n "code: $code |"
+ if [ "$id" != "${ids[i]}" ]; then
+ echo -n "$(tput setaf 1) $id $(tput sgr0) # $(tput setaf 2)${ids[i]}$(tput sgr0)|"
+ else
+ echo -n " $id |"
+ fi
+ if [ "$title" != "${titles[i]}" ]; then
+ echo -n "$(tput setaf 1) $title $(tput sgr0) # $(tput setaf 2)${titles[i]}$(tput sgr0)|"
+ else
+ echo -n " $title |"
+ fi
+ if [ "$text" != "${texts[i]}" ]; then
+ echo -n "$(tput setaf 1) $text $(tput sgr0) # $(tput setaf 2)${texts[i]}$(tput sgr0)|"
+ else
+ echo -n " $text |"
+ fi
+ if [ "$action" != "${actions[i]}" ]; then
+ echo -n "$(tput setaf 1) $action $(tput sgr0) # $(tput setaf 2)${actions[i]}$(tput sgr0)|"
+ else
+ echo -n " $action |"
+ fi
+ echo
+done
diff --git a/Marlin/src/feature/mmu3/mmu_hw/error_codes.h b/Marlin/src/feature/mmu3/mmu_hw/error_codes.h
new file mode 100644
index 000000000000..cc583efbe6f2
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu_hw/error_codes.h
@@ -0,0 +1,151 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * error_codes.h
+ */
+
+#include
+
+/**
+ * A complete set of error codes which may be a result of a high-level command/operation.
+ * This header file should 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)
+
+ FINDA_VS_EEPROM_DISREPANCY = 0x8008, //!< E32776 FINDA is pressed but we have no such record in EEPROM - this can only happen at the start of the MMU and can be resolved by issuing an Unload command
+
+ FSENSOR_TOO_EARLY = 0x8009, //!< E32777 FSensor triggered while doing FastFeedToBondtech - that means either:
+ //!< - the PTFE is too short
+ //!< - a piece of filament was left inside - pushed in front of the loaded filament causing the fsensor trigger too early
+ //!< - fsensor is faulty producing bogus triggers
+
+ FINDA_FLICKERS = 0x800A, //!< FINDA flickers - seems to be badly calibrated and happens to be pressed at spots where it used to be not pressed before.
+ //!< The user is obliged to inspect FINDA and tune its switching
+
+ MOVE_FAILED = 0x800B, //!< generic move failed error - always reported with the corresponding axis bit set (Idler or Selector) as follows:
+ MOVE_SELECTOR_FAILED = MOVE_FAILED | TMC_SELECTOR_BIT, //!< E32905 the Selector was unable to move to desired position properly - that means something is blocking its movement, e.g. a piece of filament got out of pulley body
+ MOVE_IDLER_FAILED = MOVE_FAILED | TMC_IDLER_BIT, //!< E33033 the Idler was unable to move - unused at the time of creation, but added for completeness
+ MOVE_PULLEY_FAILED = MOVE_FAILED | TMC_PULLEY_BIT, //!< E32841 the Pulley was unable to move - unused at the time of creation, but added for completeness
+
+ FILAMENT_EJECTED = 0x800C, //!< Filament was ejected, waiting for user input - technically, this is not an error
+
+ MCU_UNDERVOLTAGE_VCC = 0x800D, //!< MCU VCC rail undervoltage.
+
+ FILAMENT_CHANGE = 0x8029, //!< E32809 internal error of the printer - try-load-unload sequence detected missing filament -> failed load into the nozzle
+ LOAD_TO_EXTRUDER_FAILED = 0x802A, //!< E32810 internal error of the printer - try-load-unload sequence detected missing filament -> failed load into the nozzle
+ 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
+ // - E33408 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,
+
+ // TMC driver - IO pins are unreliable. While in theory it's recoverable, in practice it most likely
+ // means your hardware is borked (we can't command the drivers reliably via STEP/EN/DIR due to electrical
+ // issues or hardware fault. Possible "fixable" cause is undervoltage on the 5v logic line.
+ // Unfixable possible cause: bad or cracked solder joints on the PCB, failed shift register, failed driver.
+ MMU_SOLDERING_NEEDS_ATTENTION = 0xC200,
+
+};
diff --git a/Marlin/src/feature/mmu3/mmu_hw/errors_list.h b/Marlin/src/feature/mmu3/mmu_hw/errors_list.h
new file mode 100644
index 000000000000..c4965791e67c
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu_hw/errors_list.h
@@ -0,0 +1,352 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * errors_list.h
+ */
+
+/**
+ * Extracted from Prusa-Error-Codes repo
+ * Subject to automation and optimization
+ * BEWARE - This file should only be included by mmu2_error_converter.cpp!
+ */
+#include "inttypes.h"
+#include "../../../core/language.h"
+#include "../../../lcd/marlinui.h"
+#ifdef __AVR__
+ #include
+#endif
+#include "buttons.h"
+#include "../strlen_cx.h"
+#include "../ultralcd.h"
+
+namespace MMU3 {
+
+static constexpr uint8_t ERR_MMU_CODE = 4;
+
+typedef enum : uint16_t {
+ ERR_UNDEF = 0,
+
+ ERR_MECHANICAL = 100,
+ ERR_MECHANICAL_FINDA_DIDNT_TRIGGER = 101,
+ ERR_MECHANICAL_FINDA_FILAMENT_STUCK = 102,
+ ERR_MECHANICAL_FSENSOR_DIDNT_TRIGGER = 103,
+ ERR_MECHANICAL_FSENSOR_FILAMENT_STUCK = 104,
+
+ ERR_MECHANICAL_PULLEY_CANNOT_MOVE = 105,
+ ERR_MECHANICAL_FSENSOR_TOO_EARLY = 106,
+ ERR_MECHANICAL_INSPECT_FINDA = 107,
+ ERR_MECHANICAL_LOAD_TO_EXTRUDER_FAILED = 108,
+ ERR_MECHANICAL_SELECTOR_CANNOT_HOME = 115,
+ ERR_MECHANICAL_SELECTOR_CANNOT_MOVE = 116,
+ ERR_MECHANICAL_IDLER_CANNOT_HOME = 125,
+ ERR_MECHANICAL_IDLER_CANNOT_MOVE = 126,
+
+ ERR_TEMPERATURE = 200,
+ ERR_TEMPERATURE_WARNING_TMC_PULLEY_TOO_HOT = 201,
+ ERR_TEMPERATURE_WARNING_TMC_SELECTOR_TOO_HOT = 211,
+ ERR_TEMPERATURE_WARNING_TMC_IDLER_TOO_HOT = 221,
+
+ ERR_TEMPERATURE_TMC_PULLEY_OVERHEAT_ERROR = 202,
+ ERR_TEMPERATURE_TMC_SELECTOR_OVERHEAT_ERROR = 212,
+ ERR_TEMPERATURE_TMC_IDLER_OVERHEAT_ERROR = 222,
+
+
+ ERR_ELECTRICAL = 300,
+ ERR_ELECTRICAL_TMC_PULLEY_DRIVER_ERROR = 301,
+ ERR_ELECTRICAL_TMC_SELECTOR_DRIVER_ERROR = 311,
+ ERR_ELECTRICAL_TMC_IDLER_DRIVER_ERROR = 321,
+
+ ERR_ELECTRICAL_TMC_PULLEY_DRIVER_RESET = 302,
+ ERR_ELECTRICAL_TMC_SELECTOR_DRIVER_RESET = 312,
+ ERR_ELECTRICAL_TMC_IDLER_DRIVER_RESET = 322,
+
+ ERR_ELECTRICAL_TMC_PULLEY_UNDERVOLTAGE_ERROR = 303,
+ ERR_ELECTRICAL_TMC_SELECTOR_UNDERVOLTAGE_ERROR = 313,
+ ERR_ELECTRICAL_TMC_IDLER_UNDERVOLTAGE_ERROR = 323,
+
+ ERR_ELECTRICAL_TMC_PULLEY_DRIVER_SHORTED = 304,
+ ERR_ELECTRICAL_TMC_SELECTOR_DRIVER_SHORTED = 314,
+ ERR_ELECTRICAL_TMC_IDLER_DRIVER_SHORTED = 324,
+
+ ERR_ELECTRICAL_MMU_PULLEY_SELFTEST_FAILED = 305,
+ ERR_ELECTRICAL_MMU_SELECTOR_SELFTEST_FAILED = 315,
+ ERR_ELECTRICAL_MMU_IDLER_SELFTEST_FAILED = 325,
+
+ ERR_ELECTRICAL_MMU_MCU_ERROR = 306,
+
+ ERR_CONNECT = 400,
+ ERR_CONNECT_MMU_NOT_RESPONDING = 401,
+ ERR_CONNECT_COMMUNICATION_ERROR = 402,
+
+
+ ERR_SYSTEM = 500,
+ ERR_SYSTEM_FILAMENT_ALREADY_LOADED = 501,
+ ERR_SYSTEM_INVALID_TOOL = 502,
+ ERR_SYSTEM_QUEUE_FULL = 503,
+ ERR_SYSTEM_FW_UPDATE_NEEDED = 504,
+ ERR_SYSTEM_FW_RUNTIME_ERROR = 505,
+ ERR_SYSTEM_UNLOAD_MANUALLY = 506,
+ ERR_SYSTEM_FILAMENT_EJECTED = 507,
+ ERR_SYSTEM_FILAMENT_CHANGE = 508,
+
+ ERR_OTHER_UNKNOWN_ERROR = 900
+} err_num_t;
+
+// Avr gcc has serious trouble understanding static data structures in PROGMEM
+// and inadvertedly falls back to copying the whole structure into RAM (which is obviously unwanted).
+// But since this file ought to be generated in the future from yaml prescription,
+// it really makes no difference if there are "nice" data structures or plain arrays.
+static const constexpr err_num_t errorCodes[] PROGMEM = {
+ ERR_MECHANICAL_FINDA_DIDNT_TRIGGER,
+ ERR_MECHANICAL_FINDA_FILAMENT_STUCK,
+ ERR_MECHANICAL_FSENSOR_DIDNT_TRIGGER,
+ ERR_MECHANICAL_FSENSOR_FILAMENT_STUCK,
+ ERR_MECHANICAL_PULLEY_CANNOT_MOVE,
+ ERR_MECHANICAL_FSENSOR_TOO_EARLY,
+ ERR_MECHANICAL_INSPECT_FINDA,
+ ERR_MECHANICAL_LOAD_TO_EXTRUDER_FAILED,
+ ERR_MECHANICAL_SELECTOR_CANNOT_HOME,
+ ERR_MECHANICAL_SELECTOR_CANNOT_MOVE,
+ ERR_MECHANICAL_IDLER_CANNOT_HOME,
+ ERR_MECHANICAL_IDLER_CANNOT_MOVE,
+ ERR_TEMPERATURE_WARNING_TMC_PULLEY_TOO_HOT,
+ ERR_TEMPERATURE_WARNING_TMC_SELECTOR_TOO_HOT,
+ ERR_TEMPERATURE_WARNING_TMC_IDLER_TOO_HOT,
+ ERR_TEMPERATURE_TMC_PULLEY_OVERHEAT_ERROR,
+ ERR_TEMPERATURE_TMC_SELECTOR_OVERHEAT_ERROR,
+ ERR_TEMPERATURE_TMC_IDLER_OVERHEAT_ERROR,
+ ERR_ELECTRICAL_TMC_PULLEY_DRIVER_ERROR,
+ ERR_ELECTRICAL_TMC_SELECTOR_DRIVER_ERROR,
+ ERR_ELECTRICAL_TMC_IDLER_DRIVER_ERROR,
+ ERR_ELECTRICAL_TMC_PULLEY_DRIVER_RESET,
+ ERR_ELECTRICAL_TMC_SELECTOR_DRIVER_RESET,
+ ERR_ELECTRICAL_TMC_IDLER_DRIVER_RESET,
+ ERR_ELECTRICAL_TMC_PULLEY_UNDERVOLTAGE_ERROR,
+ ERR_ELECTRICAL_TMC_SELECTOR_UNDERVOLTAGE_ERROR,
+ ERR_ELECTRICAL_TMC_IDLER_UNDERVOLTAGE_ERROR,
+ ERR_ELECTRICAL_TMC_PULLEY_DRIVER_SHORTED,
+ ERR_ELECTRICAL_TMC_SELECTOR_DRIVER_SHORTED,
+ ERR_ELECTRICAL_TMC_IDLER_DRIVER_SHORTED,
+ ERR_ELECTRICAL_MMU_PULLEY_SELFTEST_FAILED,
+ ERR_ELECTRICAL_MMU_SELECTOR_SELFTEST_FAILED,
+ ERR_ELECTRICAL_MMU_IDLER_SELFTEST_FAILED,
+ ERR_ELECTRICAL_MMU_MCU_ERROR,
+ ERR_CONNECT_MMU_NOT_RESPONDING,
+ ERR_CONNECT_COMMUNICATION_ERROR,
+ ERR_SYSTEM_FILAMENT_ALREADY_LOADED,
+ ERR_SYSTEM_INVALID_TOOL,
+ ERR_SYSTEM_QUEUE_FULL,
+ ERR_SYSTEM_FW_UPDATE_NEEDED,
+ ERR_SYSTEM_FW_RUNTIME_ERROR,
+ ERR_SYSTEM_UNLOAD_MANUALLY,
+ ERR_SYSTEM_FILAMENT_EJECTED,
+ ERR_SYSTEM_FILAMENT_CHANGE,
+ ERR_OTHER_UNKNOWN_ERROR
+};
+
+FSTR_P const errorTitles[] PROGMEM = {
+ GET_TEXT_F(MSG_TITLE_FINDA_DIDNT_TRIGGER),
+ GET_TEXT_F(MSG_TITLE_FINDA_FILAMENT_STUCK),
+ GET_TEXT_F(MSG_TITLE_FSENSOR_DIDNT_TRIGGER),
+ GET_TEXT_F(MSG_TITLE_FSENSOR_FILAMENT_STUCK),
+ GET_TEXT_F(MSG_TITLE_PULLEY_CANNOT_MOVE),
+ GET_TEXT_F(MSG_TITLE_FSENSOR_TOO_EARLY),
+ GET_TEXT_F(MSG_TITLE_INSPECT_FINDA),
+ GET_TEXT_F(MSG_TITLE_LOAD_TO_EXTRUDER_FAILED),
+ GET_TEXT_F(MSG_TITLE_SELECTOR_CANNOT_HOME),
+ GET_TEXT_F(MSG_TITLE_SELECTOR_CANNOT_MOVE),
+ GET_TEXT_F(MSG_TITLE_IDLER_CANNOT_HOME),
+ GET_TEXT_F(MSG_TITLE_IDLER_CANNOT_MOVE),
+ GET_TEXT_F(MSG_TITLE_TMC_WARNING_TMC_TOO_HOT),
+ GET_TEXT_F(MSG_TITLE_TMC_WARNING_TMC_TOO_HOT),
+ GET_TEXT_F(MSG_TITLE_TMC_WARNING_TMC_TOO_HOT),
+ GET_TEXT_F(MSG_TITLE_TMC_OVERHEAT_ERROR),
+ GET_TEXT_F(MSG_TITLE_TMC_OVERHEAT_ERROR),
+ GET_TEXT_F(MSG_TITLE_TMC_OVERHEAT_ERROR),
+ GET_TEXT_F(MSG_TITLE_TMC_DRIVER_ERROR),
+ GET_TEXT_F(MSG_TITLE_TMC_DRIVER_ERROR),
+ GET_TEXT_F(MSG_TITLE_TMC_DRIVER_ERROR),
+ GET_TEXT_F(MSG_TITLE_TMC_DRIVER_RESET),
+ GET_TEXT_F(MSG_TITLE_TMC_DRIVER_RESET),
+ GET_TEXT_F(MSG_TITLE_TMC_DRIVER_RESET),
+ GET_TEXT_F(MSG_TITLE_TMC_UNDERVOLTAGE_ERROR),
+ GET_TEXT_F(MSG_TITLE_TMC_UNDERVOLTAGE_ERROR),
+ GET_TEXT_F(MSG_TITLE_TMC_UNDERVOLTAGE_ERROR),
+ GET_TEXT_F(MSG_TITLE_TMC_DRIVER_SHORTED),
+ GET_TEXT_F(MSG_TITLE_TMC_DRIVER_SHORTED),
+ GET_TEXT_F(MSG_TITLE_TMC_DRIVER_SHORTED),
+ GET_TEXT_F(MSG_TITLE_SELFTEST_FAILED),
+ GET_TEXT_F(MSG_TITLE_SELFTEST_FAILED),
+ GET_TEXT_F(MSG_TITLE_SELFTEST_FAILED),
+ GET_TEXT_F(MSG_TITLE_MMU_MCU_ERROR),
+ GET_TEXT_F(MSG_TITLE_MMU_NOT_RESPONDING),
+ GET_TEXT_F(MSG_TITLE_COMMUNICATION_ERROR),
+ GET_TEXT_F(MSG_TITLE_FILAMENT_ALREADY_LOADED),
+ GET_TEXT_F(MSG_TITLE_INVALID_TOOL),
+ GET_TEXT_F(MSG_TITLE_QUEUE_FULL),
+ GET_TEXT_F(MSG_TITLE_FW_UPDATE_NEEDED),
+ GET_TEXT_F(MSG_TITLE_FW_RUNTIME_ERROR),
+ GET_TEXT_F(MSG_TITLE_UNLOAD_MANUALLY),
+ GET_TEXT_F(MSG_TITLE_FILAMENT_EJECTED),
+ GET_TEXT_F(MSG_TITLE_FILAMENT_CHANGE),
+ GET_TEXT_F(MSG_TITLE_UNKNOWN_ERROR)
+};
+
+// @@TODO Looking at the texts, they can be composed of several parts and/or parameterized (could save a lot of space) )
+// Moreover, some of them have been disabled in favour of saving some more code size.
+
+FSTR_P const errorDescs[] PROGMEM = {
+ GET_TEXT_F(MSG_DESC_FINDA_DIDNT_TRIGGER),
+ GET_TEXT_F(MSG_DESC_FINDA_FILAMENT_STUCK),
+ GET_TEXT_F(MSG_DESC_FSENSOR_DIDNT_TRIGGER),
+ GET_TEXT_F(MSG_DESC_FSENSOR_FILAMENT_STUCK),
+ GET_TEXT_F(MSG_DESC_PULLEY_CANNOT_MOVE),
+ GET_TEXT_F(MSG_DESC_FSENSOR_TOO_EARLY),
+ GET_TEXT_F(MSG_DESC_INSPECT_FINDA),
+ GET_TEXT_F(MSG_DESC_LOAD_TO_EXTRUDER_FAILED),
+ GET_TEXT_F(MSG_DESC_SELECTOR_CANNOT_HOME),
+ GET_TEXT_F(MSG_DESC_CANNOT_MOVE),
+ GET_TEXT_F(MSG_DESC_IDLER_CANNOT_HOME),
+ GET_TEXT_F(MSG_DESC_CANNOT_MOVE),
+ GET_TEXT_F(MSG_DESC_TMC), // WARNING_TMC_PULLEY_TOO_HOT
+ GET_TEXT_F(MSG_DESC_TMC), // WARNING_TMC_SELECTOR_TOO_HOT
+ GET_TEXT_F(MSG_DESC_TMC), // WARNING_TMC_IDLER_TOO_HOT
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_PULLEY_OVERHEAT_ERROR
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_SELECTOR_OVERHEAT_ERROR
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_IDLER_OVERHEAT_ERROR
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_PULLEY_DRIVER_ERROR
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_SELECTOR_DRIVER_ERROR
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_IDLER_DRIVER_ERROR
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_PULLEY_DRIVER_RESET
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_SELECTOR_DRIVER_RESET
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_IDLER_DRIVER_RESET
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_PULLEY_UNDERVOLTAGE_ERROR
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_SELECTOR_UNDERVOLTAGE_ERROR
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_IDLER_UNDERVOLTAGE_ERROR
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_PULLEY_DRIVER_SHORTED
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_SELECTOR_DRIVER_SHORTED
+ GET_TEXT_F(MSG_DESC_TMC), // TMC_IDLER_DRIVER_SHORTED
+ GET_TEXT_F(MSG_DESC_TMC), // MMU_PULLEY_SELFTEST_FAILED
+ GET_TEXT_F(MSG_DESC_TMC), // MMU_SELECTOR_SELFTEST_FAILED
+ GET_TEXT_F(MSG_DESC_TMC), // MMU_IDLER_SELFTEST_FAILED
+ GET_TEXT_F(MSG_DESC_TMC), // MSG_DESC_MMU_MCU_ERROR
+ GET_TEXT_F(MSG_DESC_MMU_NOT_RESPONDING),
+ GET_TEXT_F(MSG_DESC_COMMUNICATION_ERROR),
+ GET_TEXT_F(MSG_DESC_FILAMENT_ALREADY_LOADED),
+ GET_TEXT_F(MSG_DESC_INVALID_TOOL),
+ GET_TEXT_F(MSG_DESC_QUEUE_FULL),
+ GET_TEXT_F(MSG_DESC_FW_UPDATE_NEEDED),
+ GET_TEXT_F(MSG_DESC_FW_RUNTIME_ERROR),
+ GET_TEXT_F(MSG_DESC_UNLOAD_MANUALLY),
+ GET_TEXT_F(MSG_DESC_FILAMENT_EJECTED),
+ GET_TEXT_F(MSG_DESC_FILAMENT_CHANGE),
+ GET_TEXT_F(MSG_DESC_UNKNOWN_ERROR)
+};
+
+// We have max 3 buttons/operations to select from.
+// One of them is "More" to show the explanation text normally hidden in the next screens.
+// It is displayed with W (Double down arrow, special character from CGRAM)
+// 01234567890123456789
+// >bttxt >bttxt >W
+// Therefore at least some of the buttons, which can occur on the screen together, can only be 8-chars long max @@TODO.
+// Beware - we only have space for 2 buttons on the LCD while the MMU has 3 buttons
+// -> the left button on the MMU is not used/rendered on the LCD (it is also almost unused on the MMU side)
+
+// Used to parse the buttons from Btns().
+FSTR_P const btnOperation[] PROGMEM = {
+ GET_TEXT_F(MSG_BTN_RETRY),
+ GET_TEXT_F(MSG_DONE),
+ GET_TEXT_F(MSG_BTN_RESET_MMU),
+ GET_TEXT_F(MSG_BTN_UNLOAD),
+ GET_TEXT_F(MSG_BTN_LOAD),
+ GET_TEXT_F(MSG_BTN_EJECT),
+ GET_TEXT_F(MSG_TUNE),
+ GET_TEXT_F(MSG_BTN_STOP),
+ GET_TEXT_F(MSG_BTN_DISABLE_MMU)
+};
+
+// We have 8 different operations/buttons at this time, so we need at least 4 bits to encode each.
+// Since one of the buttons is always "More", we can skip that one.
+// Therefore we need just 1 byte to describe the necessary buttons for each screen.
+uint8_t constexpr Btns(ButtonOperations bMiddle, ButtonOperations bRight) {
+ return ((uint8_t)bRight) << 4 | ((uint8_t)bMiddle);
+}
+
+static const uint8_t errorButtons[] PROGMEM = {
+ Btns(ButtonOperations::Retry, ButtonOperations::NoOperation), // FINDA_DIDNT_TRIGGER
+ Btns(ButtonOperations::Retry, ButtonOperations::NoOperation), // FINDA_FILAMENT_STUCK
+ Btns(ButtonOperations::Retry, ButtonOperations::NoOperation), // FSENSOR_DIDNT_TRIGGER
+ Btns(ButtonOperations::Retry, ButtonOperations::NoOperation), // FSENSOR_FILAMENT_STUCK
+
+ Btns(ButtonOperations::Retry, ButtonOperations::NoOperation), // PULLEY_CANNOT_MOVE
+ Btns(ButtonOperations::Retry, ButtonOperations::NoOperation), // FSENSOR_TOO_EARLY
+ Btns(ButtonOperations::Retry, ButtonOperations::NoOperation), // INSPECT_FINDA
+ Btns(ButtonOperations::Continue, ButtonOperations::NoOperation), // LOAD_TO_EXTRUDER_FAILED
+ Btns(ButtonOperations::Retry, ButtonOperations::Tune), // SELECTOR_CANNOT_HOME
+ Btns(ButtonOperations::Retry, ButtonOperations::NoOperation), // SELECTOR_CANNOT_MOVE
+ Btns(ButtonOperations::Retry, ButtonOperations::Tune), // IDLER_CANNOT_HOME
+ Btns(ButtonOperations::Retry, ButtonOperations::NoOperation), // IDLER_CANNOT_MOVE
+
+ Btns(ButtonOperations::Continue, ButtonOperations::ResetMMU), // WARNING_TMC_PULLEY_TOO_HOT
+ Btns(ButtonOperations::Continue, ButtonOperations::ResetMMU), // WARNING_TMC_SELECTOR_TOO_HOT
+ Btns(ButtonOperations::Continue, ButtonOperations::ResetMMU), // WARNING_TMC_IDLER_TOO_HOT
+
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_PULLEY_OVERHEAT_ERROR
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_SELECTOR_OVERHEAT_ERROR
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_IDLER_OVERHEAT_ERROR
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_PULLEY_DRIVER_ERROR
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_SELECTOR_DRIVER_ERROR
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_IDLER_DRIVER_ERROR
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_PULLEY_DRIVER_RESET
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_SELECTOR_DRIVER_RESET
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_IDLER_DRIVER_RESET
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_PULLEY_UNDERVOLTAGE_ERROR
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_SELECTOR_UNDERVOLTAGE_ERROR
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_IDLER_UNDERVOLTAGE_ERROR
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_PULLEY_DRIVER_SHORTED
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_SELECTOR_DRIVER_SHORTED
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // TMC_IDLER_DRIVER_SHORTED
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // MMU_PULLEY_SELFTEST_FAILED
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // MMU_SELECTOR_SELFTEST_FAILED
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // MMU_IDLER_SELFTEST_FAILED
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // MMU_MCU_ERROR
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::DisableMMU), // MMU_NOT_RESPONDING
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::DisableMMU), // COMMUNICATION_ERROR
+
+ Btns(ButtonOperations::Unload, ButtonOperations::Continue), // FILAMENT_ALREADY_LOADED
+ Btns(ButtonOperations::StopPrint, ButtonOperations::ResetMMU), // INVALID_TOOL
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // QUEUE_FULL
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::DisableMMU), // FW_UPDATE_NEEDED
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // FW_RUNTIME_ERROR
+ Btns(ButtonOperations::Retry, ButtonOperations::NoOperation), // UNLOAD_MANUALLY
+ Btns(ButtonOperations::Continue, ButtonOperations::NoOperation), // FILAMENT_EJECTED
+ Btns(ButtonOperations::Eject, ButtonOperations::Load), // FILAMENT_CHANGE
+ Btns(ButtonOperations::ResetMMU, ButtonOperations::NoOperation), // UNKOWN_ERROR
+};
+
+static_assert(COUNT(errorCodes) == COUNT(errorDescs));
+static_assert(COUNT(errorCodes) == COUNT(errorTitles));
+static_assert(COUNT(errorCodes) == COUNT(errorButtons));
+
+} // MMU3
diff --git a/Marlin/src/feature/mmu3/mmu_hw/progress_codes.h b/Marlin/src/feature/mmu3/mmu_hw/progress_codes.h
new file mode 100644
index 000000000000..16e63a856432
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu_hw/progress_codes.h
@@ -0,0 +1,72 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * progress_codes.h
+ */
+
+#include
+
+/**
+ * A complete set of progress codes which may be reported while running a high-level command/operation.
+ * This header file should be included in the printer's firmware as well as a reference, so the progress
+ * codes are extracted to one place.
+ */
+enum class ProgressCode : uint_fast8_t {
+ OK = 0, //!< finished ok
+
+ EngagingIdler, // P1
+ DisengagingIdler, // P2
+ UnloadingToFinda, // P3
+ UnloadingToPulley, // P4
+ FeedingToFinda, // P5
+ FeedingToExtruder, // 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, // P26
+ MovingSelector, // P27
+
+ FeedingToFSensor, // P28
+
+ Empty = 0xFF // dummy empty state
+};
diff --git a/Marlin/src/feature/mmu3/mmu_hw/registers.h b/Marlin/src/feature/mmu3/mmu_hw/registers.h
new file mode 100644
index 000000000000..3e423c479dc7
--- /dev/null
+++ b/Marlin/src/feature/mmu3/mmu_hw/registers.h
@@ -0,0 +1,70 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * registers.h
+ */
+
+#include
+
+namespace MMU3 {
+
+// Register map for MMU
+enum class Register : uint8_t {
+ Project_Major = 0x00,
+ Project_Minor = 0x01,
+ Project_Revision = 0x02,
+ Project_Build_Number = 0x03,
+ MMU_Errors = 0x04,
+ Current_Progress_Code = 0x05,
+ Current_Error_Code = 0x06,
+ Filament_State = 0x07,
+ FINDA_State = 0x08,
+ FSensor_State = 0x09,
+ Motor_Mode = 0x0A,
+ Extra_Load_Distance = 0x0B,
+ FSensor_Unload_Check_Distance = 0xC,
+ Pulley_Unload_Feedrate = 0x0D,
+ Pulley_Acceleration = 0x0E,
+ Selector_Acceleration = 0x0F,
+ Idler_Acceleration = 0x10,
+ Pulley_Load_Feedrate = 0x11,
+ Selector_Nominal_Feedrate = 0x12,
+ Idler_Nominal_Feedrate = 0x13,
+ Pulley_Slow_Feedrate = 0x14,
+ Selector_Homing_Feedrate = 0x15,
+ Idler_Homing_Feedrate = 0x16,
+ Pulley_sg_thrs_R = 0x17,
+ Selector_sg_thrs_R = 0x18,
+ Idler_sg_thrs_R = 0x19,
+ Get_Pulley_Position = 0x1A,
+ Set_Get_Selector_Slot = 0x1B,
+ Set_Get_Idler_Slot = 0x1C,
+ Set_Get_Selector_Cut_iRun = 0x1D,
+ Set_Get_Pulley_iRun = 0x1E,
+ Set_Get_Selector_iRun = 0x1F,
+ Set_Get_Idler_iRun = 0x20,
+ Reserved = 0x21,
+};
+
+} // MMU3
diff --git a/Marlin/src/feature/mmu3/registers.h b/Marlin/src/feature/mmu3/registers.h
new file mode 100644
index 000000000000..3e423c479dc7
--- /dev/null
+++ b/Marlin/src/feature/mmu3/registers.h
@@ -0,0 +1,70 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * registers.h
+ */
+
+#include
+
+namespace MMU3 {
+
+// Register map for MMU
+enum class Register : uint8_t {
+ Project_Major = 0x00,
+ Project_Minor = 0x01,
+ Project_Revision = 0x02,
+ Project_Build_Number = 0x03,
+ MMU_Errors = 0x04,
+ Current_Progress_Code = 0x05,
+ Current_Error_Code = 0x06,
+ Filament_State = 0x07,
+ FINDA_State = 0x08,
+ FSensor_State = 0x09,
+ Motor_Mode = 0x0A,
+ Extra_Load_Distance = 0x0B,
+ FSensor_Unload_Check_Distance = 0xC,
+ Pulley_Unload_Feedrate = 0x0D,
+ Pulley_Acceleration = 0x0E,
+ Selector_Acceleration = 0x0F,
+ Idler_Acceleration = 0x10,
+ Pulley_Load_Feedrate = 0x11,
+ Selector_Nominal_Feedrate = 0x12,
+ Idler_Nominal_Feedrate = 0x13,
+ Pulley_Slow_Feedrate = 0x14,
+ Selector_Homing_Feedrate = 0x15,
+ Idler_Homing_Feedrate = 0x16,
+ Pulley_sg_thrs_R = 0x17,
+ Selector_sg_thrs_R = 0x18,
+ Idler_sg_thrs_R = 0x19,
+ Get_Pulley_Position = 0x1A,
+ Set_Get_Selector_Slot = 0x1B,
+ Set_Get_Idler_Slot = 0x1C,
+ Set_Get_Selector_Cut_iRun = 0x1D,
+ Set_Get_Pulley_iRun = 0x1E,
+ Set_Get_Selector_iRun = 0x1F,
+ Set_Get_Idler_iRun = 0x20,
+ Reserved = 0x21,
+};
+
+} // MMU3
diff --git a/Marlin/src/feature/mmu3/sound.cpp b/Marlin/src/feature/mmu3/sound.cpp
new file mode 100644
index 000000000000..63edd2dd5e27
--- /dev/null
+++ b/Marlin/src/feature/mmu3/sound.cpp
@@ -0,0 +1,203 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/**
+ * sound.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+ //#include "backlight.h"
+ #include "../../libs/buzzer.h"
+ #include "sound.h"
+
+ // eSOUND_MODE eSoundMode=e_SOUND_MODE_LOUD;
+ // doesn't matter if Sound_Init is called (i.e. the value is in the EEPROM)
+ // !?! eSOUND_MODE eSoundMode; in ultraldc.cpp :: cd_settings_menu() it appears as a local variable like this
+ eSOUND_MODE eSoundMode; // =e_SOUND_MODE_DEFAULT;
+
+
+ static void Sound_SaveMode(void);
+ static void Sound_DoSound_Echo(void);
+ static void Sound_DoSound_Prompt(void);
+ static void Sound_DoSound_Alert(bool bOnce);
+ static void Sound_DoSound_Encoder_Move(void);
+ static void Sound_DoSound_Blind_Alert(void);
+
+ void Sound_Init(void) {
+ // SET_OUTPUT(BEEPER);
+ // eSoundMode = static_cast(eeprom_init_default_byte((uint8_t*)EEPROM_SOUND_MODE, e_SOUND_MODE_DEFAULT));
+ }
+
+ void Sound_SaveMode(void) {
+ // eeprom_update_byte((uint8_t*)EEPROM_SOUND_MODE,(uint8_t)eSoundMode);
+ }
+
+ void Sound_CycleState(void) {
+ switch (eSoundMode) {
+ case e_SOUND_MODE_LOUD: eSoundMode = e_SOUND_MODE_ONCE; break;
+ case e_SOUND_MODE_ONCE: eSoundMode = e_SOUND_MODE_SILENT; break;
+ case e_SOUND_MODE_SILENT: eSoundMode = e_SOUND_MODE_BLIND; break;
+ case e_SOUND_MODE_BLIND: eSoundMode = e_SOUND_MODE_LOUD; break;
+ default: eSoundMode = e_SOUND_MODE_LOUD;
+ }
+ Sound_SaveMode();
+ }
+
+ // if critical is true then silent and once mode is ignored
+ void __attribute__((noinline)) Sound_MakeCustom(uint16_t ms, uint16_t tone_, bool critical) {
+ if (critical || eSoundMode != e_SOUND_MODE_SILENT)
+ //if (!tone_) {
+ // WRITE(BEEPER, HIGH);
+ // _delay(ms);
+ // WRITE(BEEPER, LOW);
+ //}
+ //else {
+ // _tone(BEEPER, tone_);
+ // _delay(ms);
+ // _noTone(BEEPER);
+ //}
+ BUZZ(ms, tone_);
+ }
+
+ void Sound_MakeSound(eSOUND_TYPE eSoundType) {
+ switch (eSoundMode) {
+ case e_SOUND_MODE_LOUD:
+ if (eSoundType == e_SOUND_TYPE_ButtonEcho)
+ Sound_DoSound_Echo();
+ if (eSoundType == e_SOUND_TYPE_StandardPrompt)
+ Sound_DoSound_Prompt();
+ if (eSoundType == e_SOUND_TYPE_StandardAlert)
+ Sound_DoSound_Alert(false);
+ break;
+ case e_SOUND_MODE_ONCE:
+ if (eSoundType == e_SOUND_TYPE_ButtonEcho)
+ Sound_DoSound_Echo();
+ if (eSoundType == e_SOUND_TYPE_StandardPrompt)
+ Sound_DoSound_Prompt();
+ if (eSoundType == e_SOUND_TYPE_StandardAlert)
+ Sound_DoSound_Alert(true);
+ break;
+ case e_SOUND_MODE_SILENT:
+ if (eSoundType == e_SOUND_TYPE_StandardAlert)
+ Sound_DoSound_Alert(true);
+ break;
+ case e_SOUND_MODE_BLIND:
+ if (eSoundType == e_SOUND_TYPE_ButtonEcho)
+ Sound_DoSound_Echo();
+ if (eSoundType == e_SOUND_TYPE_StandardPrompt)
+ Sound_DoSound_Prompt();
+ if (eSoundType == e_SOUND_TYPE_StandardAlert)
+ Sound_DoSound_Alert(false);
+ if (eSoundType == e_SOUND_TYPE_EncoderMove)
+ Sound_DoSound_Encoder_Move();
+ if (eSoundType == e_SOUND_TYPE_BlindAlert)
+ Sound_DoSound_Blind_Alert();
+ break;
+ default:
+ break;
+ }
+ }
+
+ static void Sound_DoSound_Blind_Alert(void) {
+ // backlight_wake(1);
+ uint8_t nI;
+ for (nI = 0; nI < 20; nI++) {
+ BUZZ(94, 404);
+ BUZZ(94, 0);
+ }
+ }
+
+ static void Sound_DoSound_Encoder_Move(void) {
+ uint8_t nI;
+ for (nI = 0; nI < 5; nI++) {
+ BUZZ(75, 404);
+ BUZZ(75, 0);
+ }
+ }
+
+ static void Sound_DoSound_Echo(void) {
+ uint8_t nI;
+ for (nI = 0; nI < 10; nI++) {
+ BUZZ(100, 404);
+ BUZZ(100, 0);
+ }
+ }
+
+ static void Sound_DoSound_Prompt(void) {
+ // backlight_wake(2);
+ BUZZ(500, 404);
+ }
+
+ static void Sound_DoSound_Alert(bool bOnce) {
+ uint8_t nI, nMax;
+ nMax = bOnce ? 1 : 3;
+ for (nI = 0; nI < nMax; nI++) {
+ BUZZ(200, 404);
+ BUZZ(500, 0);
+ }
+ }
+
+ static int16_t constexpr CONTINOUS_BEEP_PERIOD = 2000; // in ms
+ // static ShortTimer beep_timer; // Timer to keep track of continous beeping
+ static bool bFirst; // true if the first beep has occurred, e_SOUND_MODE_ONCE
+
+ // @brief Handles sound when waiting for user input
+ // the function must be non-blocking. It is up to the caller
+ // to call this function repeatedly.
+ // Make sure to call sound_wait_for_user_reset() when the user has clicked the knob
+ // Loud - should continuously beep
+ // Silent - should be silent
+ // Once - should beep once
+ // Assist/Blind - as loud with beep and click on knob rotation and press
+ void sound_wait_for_user() {
+ #if BEEPER > 0
+ if (eSoundMode == e_SOUND_MODE_SILENT) return;
+
+ // Handle case where only one beep is needed
+ if (eSoundMode == e_SOUND_MODE_ONCE) {
+ if (bFirst) return;
+ Sound_MakeCustom(80, 0, false);
+ bFirst = true;
+ }
+
+ // Handle case where there should be continous beeps
+ if (beep_timer.expired_cont(CONTINOUS_BEEP_PERIOD)) {
+ beep_timer.start();
+ if (eSoundMode == e_SOUND_MODE_LOUD)
+ Sound_MakeCustom(80, 0, false);
+ else
+ // Assist (lower volume sound)
+ Sound_MakeSound(e_SOUND_TYPE_ButtonEcho);
+ }
+ #endif // BEEPER > 0
+ }
+
+ // @brief Resets the global state of sound_wait_for_user()
+ void sound_wait_for_user_reset() {
+ // beep_timer.stop();
+ bFirst = false;
+ }
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/feature/mmu3/sound.h b/Marlin/src/feature/mmu3/sound.h
new file mode 100644
index 000000000000..f72e6ecafe53
--- /dev/null
+++ b/Marlin/src/feature/mmu3/sound.h
@@ -0,0 +1,69 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * sound.h
+ */
+
+#include
+
+#define e_SOUND_MODE_NULL 0xFF
+typedef enum : uint8_t {
+ e_SOUND_MODE_LOUD,
+ e_SOUND_MODE_ONCE,
+ e_SOUND_MODE_SILENT,
+ e_SOUND_MODE_BLIND
+} eSOUND_MODE;
+
+#define e_SOUND_MODE_DEFAULT e_SOUND_MODE_LOUD
+
+typedef enum : uint8_t {
+ e_SOUND_TYPE_ButtonEcho,
+ e_SOUND_TYPE_EncoderEcho,
+ e_SOUND_TYPE_StandardPrompt,
+ e_SOUND_TYPE_StandardConfirm,
+ e_SOUND_TYPE_StandardWarning,
+ e_SOUND_TYPE_StandardAlert,
+ e_SOUND_TYPE_EncoderMove,
+ e_SOUND_TYPE_BlindAlert
+} eSOUND_TYPE;
+
+typedef enum : uint8_t {
+ e_SOUND_CLASS_Echo,
+ e_SOUND_CLASS_Prompt,
+ e_SOUND_CLASS_Confirm,
+ e_SOUND_CLASS_Warning,
+ e_SOUND_CLASS_Alert
+} eSOUND_CLASS;
+
+extern eSOUND_MODE eSoundMode;
+
+extern void Sound_Init(void);
+extern void Sound_CycleState(void);
+extern void Sound_MakeSound(eSOUND_TYPE eSoundType);
+extern void Sound_MakeCustom(uint16_t ms, uint16_t tone_, bool critical);
+void sound_wait_for_user();
+void sound_wait_for_user_reset();
+
+//static void Sound_DoSound_Echo(void);
+//static void Sound_DoSound_Prompt(void);
diff --git a/Marlin/src/feature/mmu3/strlen_cx.h b/Marlin/src/feature/mmu3/strlen_cx.h
new file mode 100644
index 000000000000..6ac2a84b427a
--- /dev/null
+++ b/Marlin/src/feature/mmu3/strlen_cx.h
@@ -0,0 +1,30 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * strlen_cx.h
+ */
+
+constexpr inline int strlen_constexpr(const char *str) {
+ return *str ? 1 + strlen_constexpr(str + 1) : 0;
+}
diff --git a/Marlin/src/feature/mmu3/ultralcd.cpp b/Marlin/src/feature/mmu3/ultralcd.cpp
new file mode 100644
index 000000000000..fdf43672ff3e
--- /dev/null
+++ b/Marlin/src/feature/mmu3/ultralcd.cpp
@@ -0,0 +1,217 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/**
+ * ultralcd.cpp
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "mmu2.h"
+#include "mmu2_marlin_macros.h"
+#include "mmu_hw/errors_list.h"
+#include "ultralcd.h"
+
+#include "../../lcd/menu/menu_item.h"
+#include "../../gcode/gcode.h"
+#include "../../lcd/marlinui.h"
+
+ //! @brief Show a two-choice prompt on the last line of the LCD
+ //! @param selected Show first choice as selected if true, the second otherwise
+ //! @param first_choice text caption of first possible choice
+ //! @param second_choice text caption of second possible choice
+ //! @param second_col column on LCD where second choice is rendered.
+ //! @param third_choice text caption of third, optional, choice.
+ void lcd_show_choices_prompt_P(uint8_t selected, const char *first_choice, const char *second_choice, uint8_t second_col, const char *third_choice) {
+ lcd_put_lchar(0, 3, selected == LCD_LEFT_BUTTON_CHOICE ? '>' : ' ');
+ lcd_put_u8str(first_choice);
+ lcd_put_lchar(second_col, 3, selected == LCD_MIDDLE_BUTTON_CHOICE ? '>' : ' ');
+ lcd_put_u8str(second_choice);
+ if (third_choice) {
+ lcd_put_lchar(18, 3, selected == LCD_RIGHT_BUTTON_CHOICE ? '>' : ' ');
+ lcd_put_u8str(third_choice);
+ }
+ }
+
+ void lcd_space(uint8_t n) {
+ while (n--) lcd_put_lchar(' ');
+ }
+
+ // Print extruder status (5 chars total)
+ // Scenario 1: "F?"
+ // There is no filament loaded and no tool change is in progress
+ // Scenario 2: "F[nr.]"
+ // [nr.] ranges from 1 to 5.
+ // Shows which filament is loaded. No tool change is in progress
+ // Scenario 3: "?>[nr.]"
+ // [nr.] ranges from 1 to 5.
+ // There is no filament currently loaded, but [nr.] is currently being loaded via tool change
+ // Scenario 4: "[nr.]>?"
+ // [nr.] ranges from 1 to 5.
+ // This scenario indicates a bug in the firmware if ? is on the right side
+ // Scenario 5: "[nr1.]>[nr2.]"
+ // [nr1.] ranges from 1 to 5.
+ // [nr2.] ranges from 1 to 5.
+ // Filament [nr1.] was loaded, but [nr2.] is currently being loaded via tool change
+ // Scenario 6: "?>?"
+ // This scenario should not be possible and indicates a bug in the firmware
+ uint8_t lcdui_print_extruder(void) {
+ uint8_t chars = 1;
+ lcd_space(1);
+ if (mmu3.get_current_tool() == mmu3.get_tool_change_tool()) {
+ lcd_put_lchar('F');
+ lcd_put_lchar(mmu3.get_current_tool() == (uint8_t)MMU3::FILAMENT_UNKNOWN ? '?' : mmu3.get_current_tool() + '1');
+ chars += 2;
+ }
+ else {
+ lcd_put_lchar(mmu3.get_current_tool() == (uint8_t)MMU3::FILAMENT_UNKNOWN ? '?' : mmu3.get_current_tool() + '1');
+ lcd_put_lchar('>');
+ lcd_put_lchar(mmu3.get_tool_change_tool() == (uint8_t)MMU3::FILAMENT_UNKNOWN ? '?' : mmu3.get_tool_change_tool() + '1');
+ chars += 3;
+ }
+ return chars;
+ }
+
+ bool pgm_is_whitespace(const char *c_addr) {
+ const char c = pgm_read_byte(c_addr);
+ return c == ' ' || c == '\t' || c == '\r' || c == '\n';
+ }
+
+ bool pgm_is_interpunction(const char *c_addr) {
+ const char c = pgm_read_byte(c_addr);
+ return c == '.' || c == ',' || c == ':' || c == ';' || c == '?' || c == '!' || c == '/';
+ }
+
+ /**
+ * @brief show full screen message
+ *
+ * This function is non-blocking
+ * @param msg message to be displayed from PROGMEM
+ * @return rest of the text (to be displayed on next page)
+ */
+ static FSTR_P const lcd_display_message_fullscreen_nonBlocking(FSTR_P const fmsg) {
+ PGM_P msg = FTOP(fmsg);
+ PGM_P msgend = msg;
+ //bool multi_screen = false;
+ for (uint8_t row = 0; row < LCD_HEIGHT; ++row) {
+ if (pgm_read_byte(msgend) == 0) break;
+ SETCURSOR(0, row);
+
+ // Previous row ended with a complete word, so the first character in the
+ // next row is a whitespace. We can skip the whitespace on a new line.
+ if (pgm_is_whitespace(msg) && ++msg == nullptr) break; // End of the message.
+
+ uint8_t linelen = (strlen_P(msg) > LCD_WIDTH) ? LCD_WIDTH : strlen_P(msg);
+ PGM_P const msgend2 = msg + linelen;
+ msgend = msgend2;
+ if (row == 3 && linelen == LCD_WIDTH) {
+ // Last line of the display, full line should be displayed.
+ // Find out, whether this message will be split into multiple screens.
+ //multi_screen = pgm_read_byte(msgend) != 0;
+ // We do not need this...
+ //if (multi_screen) msgend = (msgend2 -= 2);
+ }
+ if (pgm_read_byte(msgend) != 0 && !pgm_is_whitespace(msgend) && !pgm_is_interpunction(msgend)) {
+ // Splitting a word. Find the start of the current word.
+ while (msgend > msg && !pgm_is_whitespace(msgend - 1)) --msgend;
+ if (msgend == msg) msgend = msgend2; // Found a single long word, which cannot be split. Just cut it.
+ }
+ for (; msg < msgend; ++msg) {
+ const char c = char(pgm_read_byte(msg));
+ if (c == '\n') {
+ // Abort early if '\n' is encountered.
+ // This character is used to force the following words to be printed on the next line.
+ break;
+ }
+ lcd_put_lchar(c);
+ }
+ }
+ // We do not need this part...
+ //if (multi_screen) {
+ // // Display the double down arrow.
+ // lcd_put_lchar(LCD_WIDTH - 2, LCD_HEIGHT - 2, LCD_STR_ARROW_2_DOWN[0]);
+ //}
+ //return multi_screen ? msgend : nullptr;
+ return FPSTR(msgend);
+ }
+
+ FSTR_P const lcd_display_message_fullscreen(FSTR_P const fmsg) {
+ // Disable update of the screen by the usual lcd_update(0) routine.
+ #if HAS_WIRED_LCD
+ //ui.lcdDrawUpdate = LCDViewAction::LCDVIEW_NONE;
+ ui.clear_lcd();
+ return lcd_display_message_fullscreen_nonBlocking(fmsg);
+ #else
+ return fmsg
+ #endif
+ }
+
+ /**
+ * @brief show full screen message and wait
+ *
+ * This function is blocking.
+ * @param msg message to be displayed from PROGMEM
+ */
+ void lcd_show_fullscreen_message_and_wait(FSTR_P const fmsg) {
+ LcdUpdateDisabler lcdUpdateDisabler;
+ FSTR_P fmsg_next = lcd_display_message_fullscreen(fmsg);
+ const bool multi_screen = fmsg_next != nullptr;
+ ui.use_click();
+ KEEPALIVE_STATE(PAUSED_FOR_USER);
+ // Until confirmed by a button click.
+ for (;;) {
+ if (fmsg_next == nullptr) {
+ // Display the confirm char.
+ //lcd_put_lchar(LCD_WIDTH - 2, LCD_HEIGHT - 2, LCD_STR_CONFIRM[0]);
+ }
+ // Wait for 5 seconds before displaying the next text.
+ for (uint8_t i = 0; i < 100; ++i) {
+ idle(true);
+ safe_delay(50);
+ if (ui.use_click()) {
+ if (fmsg_next == nullptr) {
+ KEEPALIVE_STATE(IN_HANDLER);
+ return ui.go_back();
+ }
+ if (!multi_screen) break;
+ if (fmsg_next == nullptr) fmsg_next = fmsg;
+ fmsg_next = lcd_display_message_fullscreen(fmsg_next);
+ }
+ }
+ //if (multi_screen) {
+ // if (fmsg_next == nullptr) fmsg_next = fmsg;
+ // fmsg_next = lcd_display_message_fullscreen(fmsg_next);
+ //}
+ }
+ }
+
+ void lcd_insert_char_into_status(uint8_t position, const char message) {
+ if (position >= LCD_WIDTH) return;
+ //int size = ui.status_message.length();
+ char *str = ui.status_message.buffer();
+ str[position] = message;
+ ui.refresh(LCDVIEW_REDRAW_NOW); // force redraw
+ }
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/feature/mmu3/ultralcd.h b/Marlin/src/feature/mmu3/ultralcd.h
new file mode 100644
index 000000000000..2f17b61d0a0e
--- /dev/null
+++ b/Marlin/src/feature/mmu3/ultralcd.h
@@ -0,0 +1,72 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+#pragma once
+
+/**
+ * ultralcd.h
+ */
+
+#include "../../MarlinCore.h"
+#include "../../lcd/marlinui.h"
+
+#define LCD_LEFT_BUTTON_CHOICE 0
+#define LCD_MIDDLE_BUTTON_CHOICE 1
+#define LCD_RIGHT_BUTTON_CHOICE 2
+
+#define LCD_STR_ARROW_2_DOWN "\x88"
+#define LCD_STR_CONFIRM "\x89"
+#define LCD_STR_SOLID_BLOCK "\xFF" // from the default character set
+
+/**
+ * @brief Helper class to temporarily disable LCD updates
+ *
+ * When constructed (on stack), original state state of lcd_update_enabled is stored
+ * and LCD updates are disabled.
+ * When destroyed (gone out of scope), original state of LCD update is restored.
+ * It has zero overhead compared to storing bool saved = lcd_update_enabled
+ * and calling lcd_update_enable(false) and lcd_update_enable(saved).
+ */
+class LcdUpdateDisabler {
+ public:
+ LcdUpdateDisabler() : m_updateEnabled(ui.lcdDrawUpdate) {
+ TERN_(HAS_WIRED_LCD, ui.lcdDrawUpdate = LCDViewAction::LCDVIEW_NONE);
+ }
+ ~LcdUpdateDisabler() {
+ #if HAS_WIRED_LCD
+ ui.lcdDrawUpdate = m_updateEnabled;
+ ui.clear_lcd();
+ ui.update();
+ #endif
+ }
+
+ private:
+ LCDViewAction m_updateEnabled;
+};
+
+bool pgm_is_whitespace(const char *c_addr);
+bool pgm_is_interpunction(const char *c_addr);
+FSTR_P const lcd_display_message_fullscreen(FSTR_P const pmsg);
+void lcd_show_choices_prompt_P(uint8_t selected, const char *first_choice, const char *second_choice, uint8_t second_col, const char *third_choice=nullptr);
+void lcd_show_fullscreen_message_and_wait(FSTR_P const fmsg);
+uint8_t lcdui_print_extruder(void);
+void lcd_space(uint8_t n);
+void lcd_insert_char_into_status(uint8_t position, const char message);
diff --git a/Marlin/src/gcode/control/T.cpp b/Marlin/src/gcode/control/T.cpp
index 3c13fe231a79..3b0c173195d1 100644
--- a/Marlin/src/gcode/control/T.cpp
+++ b/Marlin/src/gcode/control/T.cpp
@@ -31,7 +31,9 @@
#include "../../module/motion.h"
#endif
-#if HAS_PRUSA_MMU2
+#if HAS_PRUSA_MMU3
+ #include "../../feature/mmu3/mmu2.h"
+#elif HAS_PRUSA_MMU2
#include "../../feature/mmu/mmu2.h"
#endif
@@ -66,7 +68,12 @@ void GcodeSuite::T(const int8_t tool_index) {
// Count this command as movement / activity
reset_stepper_timeout();
- #if HAS_PRUSA_MMU2
+ #if HAS_PRUSA_MMU3
+ if (parser.string_arg) {
+ mmu3.tool_change(parser.string_arg[0], uint8_t(tool_index)); // Special commands T?/Tx/Tc
+ return;
+ }
+ #elif HAS_PRUSA_MMU2
if (parser.string_arg) {
mmu2.tool_change(parser.string_arg); // Special commands T?/Tx/Tc
return;
diff --git a/Marlin/src/gcode/feature/pause/M600.cpp b/Marlin/src/gcode/feature/pause/M600.cpp
index c42db203b675..a87f14c258fb 100644
--- a/Marlin/src/gcode/feature/pause/M600.cpp
+++ b/Marlin/src/gcode/feature/pause/M600.cpp
@@ -34,9 +34,14 @@
#include "../../../module/tool_change.h"
#endif
-#if HAS_PRUSA_MMU2
+#if HAS_PRUSA_MMU3
+ #include "../../../feature/mmu3/mmu2.h"
+ #if ENABLED(MMU_MENUS)
+ #include "../../../lcd/menu/menu_mmu2.h"
+ #endif
+#elif HAS_PRUSA_MMU2
#include "../../../feature/mmu/mmu2.h"
- #if ENABLED(MMU2_MENUS)
+ #if ENABLED(MMU_MENUS)
#include "../../../lcd/menu/menu_mmu2.h"
#endif
#endif
@@ -68,6 +73,9 @@
* T[toolhead] - Select extruder for filament change
* R[temp] - Resume temperature (in current units)
*
+ * With MMU_MENUS:
+ * A - Automatic
+ *
* Default values are used for omitted arguments.
*/
void GcodeSuite::M600() {
@@ -101,7 +109,7 @@ void GcodeSuite::M600() {
}
#endif
- const bool standardM600 = TERN1(MMU2_MENUS, !mmu2.enabled());
+ const bool standardM600 = TERN1(MMU_MENUS, TERN1(HAS_PRUSA_MMU2, !mmu2.enabled()) && TERN1(HAS_PRUSA_MMU3, !mmu3.mmu_hw_enabled));
// Show initial "wait for start" message
if (standardM600)
@@ -157,14 +165,17 @@ void GcodeSuite::M600() {
ABS(parser.axisunitsval('L', E_AXIS, fc_settings[active_extruder].load_length)),
ADVANCED_PAUSE_PURGE_LENGTH,
beep_count,
- parser.celsiusval('R')
+ parser.celsiusval('R'),
+ true,
+ false
DXC_PASS
);
}
else {
- #if ENABLED(MMU2_MENUS)
- mmu2_M600();
- resume_print(0, 0, 0, beep_count, 0 DXC_PASS);
+ #if ENABLED(MMU_MENUS)
+ const bool automatic = parser.seen_test('A');
+ mmu2_M600(automatic);
+ resume_print(0, 0, 0, beep_count, 0, !automatic, false DXC_PASS);
#endif
}
}
diff --git a/Marlin/src/gcode/feature/pause/M701_M702.cpp b/Marlin/src/gcode/feature/pause/M701_M702.cpp
index aec3a16a2a02..29c8fe9a4524 100644
--- a/Marlin/src/gcode/feature/pause/M701_M702.cpp
+++ b/Marlin/src/gcode/feature/pause/M701_M702.cpp
@@ -35,7 +35,9 @@
#include "../../../module/tool_change.h"
#endif
-#if HAS_PRUSA_MMU2
+#if HAS_PRUSA_MMU3
+ #include "../../../feature/mmu3/mmu2.h"
+#elif HAS_PRUSA_MMU2
#include "../../../feature/mmu/mmu2.h"
#endif
@@ -101,7 +103,9 @@ void GcodeSuite::M701() {
move_z_by(park_raise);
// Load filament
- #if HAS_PRUSA_MMU2
+ #if HAS_PRUSA_MMU3
+ mmu3.load_to_nozzle(target_extruder);
+ #elif HAS_PRUSA_MMU2
mmu2.load_to_nozzle(target_extruder);
#else
constexpr float purge_length = ADVANCED_PAUSE_PURGE_LENGTH,
@@ -196,7 +200,9 @@ void GcodeSuite::M702() {
do_blocking_move_to_z(_MIN(current_position.z + park_point.z, Z_MAX_POS), feedRate_t(NOZZLE_PARK_Z_FEEDRATE));
// Unload filament
- #if HAS_PRUSA_MMU2
+ #if HAS_PRUSA_MMU3
+ mmu3.unload();
+ #elif HAS_PRUSA_MMU2
mmu2.unload();
#else
#if ALL(HAS_MULTI_EXTRUDER, FILAMENT_UNLOAD_ALL_EXTRUDERS)
diff --git a/Marlin/src/gcode/feature/prusa_MMU2/M403.cpp b/Marlin/src/gcode/feature/prusa_MMU2/M403.cpp
index bca2013e88ba..23665ac51dc2 100644
--- a/Marlin/src/gcode/feature/prusa_MMU2/M403.cpp
+++ b/Marlin/src/gcode/feature/prusa_MMU2/M403.cpp
@@ -22,10 +22,15 @@
#include "../../../inc/MarlinConfigPre.h"
-#if HAS_PRUSA_MMU2
+#if HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3
#include "../../gcode.h"
-#include "../../../feature/mmu/mmu2.h"
+
+#if HAS_PRUSA_MMU3
+ #include "../../../feature/mmu3/mmu2.h"
+#elif HAS_PRUSA_MMU2
+ #include "../../../feature/mmu/mmu2.h"
+#endif
/**
* M403: Set filament type for MMU2
@@ -37,13 +42,17 @@
* 2 PVA
*/
void GcodeSuite::M403() {
- int8_t index = parser.intval('E', -1),
- type = parser.intval('F', -1);
+ const int8_t index = parser.intval('E', -1),
+ type = parser.intval('F', -1);
if (WITHIN(index, 0, EXTRUDERS - 1) && WITHIN(type, 0, 2))
- mmu2.set_filament_type(index, type);
+ #if HAS_PRUSA_MMU3
+ mmu3.set_filament_type(index, type);
+ #else
+ mmu2.set_filament_type(index, type);
+ #endif
else
SERIAL_ECHO_MSG("M403 - bad arguments.");
}
-#endif // HAS_PRUSA_MMU2
+#endif // HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3
diff --git a/Marlin/src/gcode/feature/prusa_MMU2/M704-M709.cpp b/Marlin/src/gcode/feature/prusa_MMU2/M704-M709.cpp
new file mode 100644
index 000000000000..8229959cc0b2
--- /dev/null
+++ b/Marlin/src/gcode/feature/prusa_MMU2/M704-M709.cpp
@@ -0,0 +1,204 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#include "../../../inc/MarlinConfigPre.h"
+
+#if HAS_PRUSA_MMU3
+
+#include "../../gcode.h"
+#include "../../../module/settings.h"
+#include "../../../feature/mmu3/mmu2.h"
+#include "../../../feature/mmu3/mmu2_reporting.h"
+#include "../../../feature/mmu3/SpoolJoin.h"
+
+// Shared by the G-codes below to save flash memory.
+static void gcodes_M704_M705_M706(uint16_t gcode) {
+ const int8_t mmuSlotIndex = parser.intval('P', -1);
+
+ if (mmu3.enabled() && WITHIN(mmuSlotIndex, 0, EXTRUDERS - 1)) {
+ switch (gcode) {
+ case 704: mmu3.load_to_feeder(mmuSlotIndex); break;
+ case 705: mmu3.eject_filament(mmuSlotIndex, false); break;
+ case 706:
+ #if ENABLED(MMU_HAS_CUTTER)
+ if (mmu3.cutter_mode > 0) mmu3.cut_filament(mmuSlotIndex);
+ #endif
+ break;
+ default: break;
+ }
+ }
+}
+
+/**
+ * ### M704 - Preload to MMU
+ * #### Usage
+ *
+ * M704 [ P ]
+ *
+ * #### Parameters
+ * - `P` - n index of slot (zero based, so 0-4 like T0 and T4)
+ */
+void GcodeSuite::M704() {
+ gcodes_M704_M705_M706(704);
+}
+
+/**
+ * ### M705 - Eject filament
+ * #### Usage
+ *
+ * M705 [ P ]
+ *
+ * #### Parameters
+ * - `P` - n index of slot (zero based, so 0-4 like T0 and T4)
+ */
+void GcodeSuite::M705() {
+ gcodes_M704_M705_M706(705);
+}
+
+/*!
+ * ### M706 - Cut filament
+ * #### Usage
+ *
+ * M706 [ P ]
+ *
+ * #### Parameters
+ * - `P` - n index of slot (zero based, so 0-4 like T0 and T4)
+ */
+void GcodeSuite::M706() {
+ gcodes_M704_M705_M706(706);
+}
+
+/**
+ * ### M707 - Read from MMU register
+ * #### Usage
+ *
+ * M707 [ A ]
+ *
+ * #### Parameters
+ * - `A` - Address of register in hexidecimal.
+ *
+ * #### Example
+ *
+ * M707 A0x1b - Read a 8bit integer from register 0x1b and prints the result onto the serial line.
+ *
+ * Does nothing if the A parameter is not present or if MMU is not enabled.
+ *
+ */
+void GcodeSuite::M707() {
+ if (mmu3.enabled() && parser.seenval('A')) {
+ char *address = parser.value_string();
+ mmu3.readRegister(uint8_t(strtol(address, NULL, 16)));
+ }
+}
+
+/**
+ * ### M708 - Write to MMU register
+ * #### Usage
+ *
+ * M708 [ A | X ]
+ *
+ * #### Parameters
+ * - `A` - Address of register in hexidecimal.
+ * - `X` - Data to write (16-bit integer). Default value 0.
+ *
+ * #### Example
+ * M708 A0x1b X05 - Write to register 0x1b the value 05.
+ *
+ * Does nothing if A parameter is missing or if MMU is not enabled.
+ */
+void GcodeSuite::M708() {
+ if (mmu3.enabled() && parser.seenval('A')) {
+ char *address = parser.value_string();
+ const uint8_t addr = uint8_t(strtol(address, NULL, 16));
+ if (addr) {
+ const uint16_t data = parser.ushortval('X', 0);
+ mmu3.writeRegister(addr, data);
+ }
+ }
+}
+
+/**
+ * ### M709 - MMU power & reset
+ * The MK3S cannot not power off the MMU, but we can en- and disable the MMU.
+ *
+ * The new state of the MMU is stored in printer's EEPROM.
+ * i.e., If you disable the MMU via M709, it will not be activated after the printer resets.
+ * Usage
+ *
+ * M709 [ S | X ]
+ *
+ * Parameters
+ * - `X` - Reset MMU (0:soft reset | 1:hardware reset | 42: erase MMU eeprom)
+ * - `S` - En-/disable the MMU (0:off | 1:on)
+ *
+ * Examples
+ *
+ * M709 X0 ; issue an X0 command via communication into the MMU (soft reset)
+ * M709 X1 ; toggle the MMU's reset pin (hardware reset)
+ * M709 X42 ; erase MMU EEPROM
+ * M709 S1 ; enable MMU
+ * M709 S0 ; disable MMU
+ * M709 ; Serial message if en- or disabled
+ */
+void GcodeSuite::M709() {
+ if (parser.seenval('S')) {
+ if (parser.value_bool())
+ mmu3.start();
+ else
+ mmu3.stop();
+ }
+
+ if (mmu3.enabled() && parser.seenval('X')) {
+ switch (parser.value_byte()) {
+ case 0: mmu3.reset(MMU3::MMU3::Software); break;
+ case 1: mmu3.reset(MMU3::MMU3::ResetPin); break;
+ case 42: mmu3.reset(MMU3::MMU3::EraseEEPROM); break;
+ default: break;
+ }
+ }
+ mmu3.status();
+}
+
+/**
+ * Report for M503.
+ * TODO: Report MMU3 G-code settings here, status via a different G-code.
+ */
+void GcodeSuite::MMU3_report(const bool forReplay/*=true*/) {
+ using namespace MMU3;
+ report_heading(forReplay, F("MMU3 Operational Stats"));
+ SERIAL_ECHOPGM(" MMU "); serialprintln_onoff(mmu3.mmu_hw_enabled);
+ SERIAL_ECHOPGM(" Stealth Mode "); serialprintln_onoff(mmu3.stealth_mode);
+ #if ENABLED(MMU_HAS_CUTTER)
+ SERIAL_ECHOPGM(" Cutter ");
+ serialprintln_onoff(mmu3.cutter_mode != 0);
+ #endif
+ SERIAL_ECHOPGM(" SpoolJoin "); serialprintln_onoff(spooljoin.enabled);
+ SERIAL_ECHOLNPGM(" Tool Changes ", operation_statistics.tool_change_counter);
+ SERIAL_ECHOLNPGM(" Total Tool Changes ", operation_statistics.tool_change_total_counter);
+ SERIAL_ECHOLNPGM(" Fails ", operation_statistics.fail_num);
+ SERIAL_ECHOLNPGM(" Total Fails ", operation_statistics.fail_total_num);
+ SERIAL_ECHOLNPGM(" Load Fails ", operation_statistics.load_fail_num);
+ SERIAL_ECHOLNPGM(" Total Load Fails ", operation_statistics.load_fail_total_num);
+ SERIAL_ECHOLNPGM(" Power Fails ", mmu3.tmcFailures());
+}
+
+#endif // HAS_PRUSA_MMU3
diff --git a/Marlin/src/gcode/gcode.cpp b/Marlin/src/gcode/gcode.cpp
index 11b02ded6831..595312e6ae17 100644
--- a/Marlin/src/gcode/gcode.cpp
+++ b/Marlin/src/gcode/gcode.cpp
@@ -859,7 +859,7 @@ void GcodeSuite::process_parsed_command(const bool no_ok/*=false*/) {
case 402: M402(); break; // M402: Stow probe
#endif
- #if HAS_PRUSA_MMU2
+ #if HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3
case 403: M403(); break;
#endif
@@ -988,6 +988,15 @@ void GcodeSuite::process_parsed_command(const bool no_ok/*=false*/) {
case 702: M702(); break; // M702: Unload Filament
#endif
+ #if HAS_PRUSA_MMU3
+ case 704: M704(); break; // M704: Preload to MMU
+ case 705: M705(); break; // M705: Eject filament
+ case 706: M706(); break; // M706: Cut filament
+ case 707: M707(); break; // M707: Read from MMU register
+ case 708: M708(); break; // M708: Write to MMU register
+ case 709: M709(); break; // M709: MMU power & reset
+ #endif
+
#if ENABLED(CONTROLLER_FAN_EDITABLE)
case 710: M710(); break; // M710: Set Controller Fan settings
#endif
diff --git a/Marlin/src/gcode/gcode.h b/Marlin/src/gcode/gcode.h
index 7ec3e0088817..421dda9aec0f 100644
--- a/Marlin/src/gcode/gcode.h
+++ b/Marlin/src/gcode/gcode.h
@@ -272,6 +272,14 @@
* M672 - Set/Reset Duet Smart Effector's sensitivity. (Requires DUET_SMART_EFFECTOR and SMART_EFFECTOR_MOD_PIN)
* M701 - Load filament (Requires FILAMENT_LOAD_UNLOAD_GCODES)
* M702 - Unload filament (Requires FILAMENT_LOAD_UNLOAD_GCODES)
+ *
+ * M704 - Preload to MMU (Requires PRUSA_MMU3)
+ * M705 - Eject filament (Requires PRUSA_MMU3)
+ * M706 - Cut filament (Requires PRUSA_MMU3)
+ * M707 - Read from MMU register (Requires PRUSA_MMU3)
+ * M708 - Write to MMU register (Requires PRUSA_MMU3)
+ * M709 - MMU power & reset (Requires PRUSA_MMU3)
+ *
* M808 - Set or Goto a Repeat Marker (Requires GCODE_REPEAT_MARKERS)
* M810-M819 - Define/execute a G-code macro (Requires GCODE_MACROS)
* M851 - Set Z probe's XYZ offsets in current units. (Negative values: X=left, Y=front, Z=below)
@@ -1024,7 +1032,7 @@ class GcodeSuite {
static void M402();
#endif
- #if HAS_PRUSA_MMU2
+ #if HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3
static void M403();
#endif
@@ -1162,6 +1170,16 @@ class GcodeSuite {
static void M702();
#endif
+ #if HAS_PRUSA_MMU3
+ static void M704();
+ static void M705();
+ static void M706();
+ static void M707();
+ static void M708();
+ static void M709();
+ static void MMU3_report(const bool forReplay=true);
+ #endif
+
#if ENABLED(GCODE_REPEAT_MARKERS)
static void M808();
#endif
diff --git a/Marlin/src/gcode/parser.cpp b/Marlin/src/gcode/parser.cpp
index 4b8bbb925fde..3975cea50488 100644
--- a/Marlin/src/gcode/parser.cpp
+++ b/Marlin/src/gcode/parser.cpp
@@ -177,7 +177,7 @@ void GCodeParser::parse(char *p) {
// Skip spaces to get the numeric part
while (*p == ' ') p++;
- #if HAS_PRUSA_MMU2
+ #if HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3
if (letter == 'T') {
// check for special MMU2 T?/Tx/Tc commands
if (*p == '?' || *p == 'x' || *p == 'c') {
diff --git a/Marlin/src/inc/BaseConfiguration_adv.h b/Marlin/src/inc/BaseConfiguration_adv.h
index b3ff6a584afb..e3a39421dd1f 100644
--- a/Marlin/src/inc/BaseConfiguration_adv.h
+++ b/Marlin/src/inc/BaseConfiguration_adv.h
@@ -752,11 +752,11 @@
#ifndef FTM_DEFAULT_SHAPER_Y
#define FTM_DEFAULT_SHAPER_Y ftMotionShaper_NONE
#endif
- #ifndef FTM_SHAPING_DEFAULT_X_FREQ
- #define FTM_SHAPING_DEFAULT_X_FREQ 37.0f
+ #ifndef FTM_SHAPING_DEFAULT_FREQ_X
+ #define FTM_SHAPING_DEFAULT_FREQ_X 37.0f
#endif
- #ifndef FTM_SHAPING_DEFAULT_Y_FREQ
- #define FTM_SHAPING_DEFAULT_Y_FREQ 37.0f
+ #ifndef FTM_SHAPING_DEFAULT_FREQ_Y
+ #define FTM_SHAPING_DEFAULT_FREQ_Y 37.0f
#endif
#ifndef FTM_LINEAR_ADV_DEFAULT_ENA
#define FTM_LINEAR_ADV_DEFAULT_ENA false
diff --git a/Marlin/src/inc/Changes.h b/Marlin/src/inc/Changes.h
index a5319e46544b..4d81b229499e 100644
--- a/Marlin/src/inc/Changes.h
+++ b/Marlin/src/inc/Changes.h
@@ -699,6 +699,10 @@
#error "WIFI_SERIAL is now WIFI_SERIAL_PORT."
#elif defined(CALIBRATION_MEASUREMENT_RESOLUTION)
#error "CALIBRATION_MEASUREMENT_RESOLUTION is no longer needed and should be removed."
+#elif defined(MMU2_MENUS)
+ #error "MMU2_MENUS is now MMU_MENUS."
+#elif defined(FTM_SHAPING_DEFAULT_X_FREQ) || defined(FTM_SHAPING_DEFAULT_Y_FREQ)
+ #error "FTM_SHAPING_DEFAULT_[XY]_FREQ is now FTM_SHAPING_DEFAULT_FREQ_[XY]."
#endif
// Changes to Probe Temp Compensation (#17392)
diff --git a/Marlin/src/inc/Conditionals_LCD.h b/Marlin/src/inc/Conditionals_LCD.h
index d58f82a7a956..78164ade7b3e 100644
--- a/Marlin/src/inc/Conditionals_LCD.h
+++ b/Marlin/src/inc/Conditionals_LCD.h
@@ -95,8 +95,10 @@
#define _PRUSA_MMU1 1
#define _PRUSA_MMU2 2
#define _PRUSA_MMU2S 3
+ #define _PRUSA_MMU3 4
#define _EXTENDABLE_EMU_MMU2 12
#define _EXTENDABLE_EMU_MMU2S 13
+ #define _EXTENDABLE_EMU_MMU3 14
#define _MMU CAT(_,MMU_MODEL)
#if _MMU == _PRUSA_MMU1
@@ -106,6 +108,8 @@
#elif _MMU % 10 == _PRUSA_MMU2S
#define HAS_PRUSA_MMU2 1
#define HAS_PRUSA_MMU2S 1
+ #elif _MMU % 10 == _PRUSA_MMU3
+ #define HAS_PRUSA_MMU3 1
#endif
#if _MMU == _EXTENDABLE_EMU_MMU2 || _MMU == _EXTENDABLE_EMU_MMU2S
#define HAS_EXTENDABLE_MMU 1
@@ -115,8 +119,10 @@
#undef _PRUSA_MMU1
#undef _PRUSA_MMU2
#undef _PRUSA_MMU2S
+ #undef _PRUSA_MMU3
#undef _EXTENDABLE_EMU_MMU2
#undef _EXTENDABLE_EMU_MMU2S
+ #undef _EXTENDABLE_EMU_MMU3
#endif
#if ENABLED(E_DUAL_STEPPER_DRIVERS) // E0/E1 steppers act in tandem as E0
@@ -150,7 +156,7 @@
#define E_STEPPERS EXTRUDERS
#define E_MANUAL EXTRUDERS
-#elif HAS_PRUSA_MMU2 // Průša Multi-Material Unit v2
+#elif HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3 // Průša Multi-Material Unit v2/v3
#define E_STEPPERS 1
#define E_MANUAL 1
diff --git a/Marlin/src/inc/Conditionals_adv.h b/Marlin/src/inc/Conditionals_adv.h
index c0a659774499..4675051c5f05 100644
--- a/Marlin/src/inc/Conditionals_adv.h
+++ b/Marlin/src/inc/Conditionals_adv.h
@@ -865,7 +865,7 @@
#undef COOLER_MAXTEMP
#endif
-#if HAS_MULTI_EXTRUDER || HAS_MULTI_HOTEND || HAS_PRUSA_MMU2 || (ENABLED(MIXING_EXTRUDER) && MIXING_VIRTUAL_TOOLS > 1)
+#if HAS_MULTI_EXTRUDER || HAS_MULTI_HOTEND || HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3 || (ENABLED(MIXING_EXTRUDER) && MIXING_VIRTUAL_TOOLS > 1)
#define HAS_TOOLCHANGE 1
#endif
@@ -1396,9 +1396,14 @@
#endif
// FT Motion unified window and batch size
-#if ALL(FT_MOTION, FTM_UNIFIED_BWS)
- #define FTM_WINDOW_SIZE FTM_BW_SIZE
- #define FTM_BATCH_SIZE FTM_BW_SIZE
+#if ENABLED(FT_MOTION)
+ #if HAS_X_AXIS
+ #define HAS_FTM_SHAPING 1
+ #endif
+ #if ENABLED(FTM_UNIFIED_BWS)
+ #define FTM_WINDOW_SIZE FTM_BW_SIZE
+ #define FTM_BATCH_SIZE FTM_BW_SIZE
+ #endif
#endif
// Toolchange Event G-code
diff --git a/Marlin/src/inc/SanityCheck.h b/Marlin/src/inc/SanityCheck.h
index 3155a7aa0917..0fda662d23a4 100644
--- a/Marlin/src/inc/SanityCheck.h
+++ b/Marlin/src/inc/SanityCheck.h
@@ -507,8 +507,8 @@ static_assert(COUNT(arm) == LOGICAL_AXES, "AXIS_RELATIVE_MODES must contain " _L
#if HAS_FILAMENT_SENSOR
#if !PIN_EXISTS(FIL_RUNOUT)
#error "FILAMENT_RUNOUT_SENSOR requires FIL_RUNOUT_PIN."
- #elif HAS_PRUSA_MMU2 && NUM_RUNOUT_SENSORS != 1
- #error "NUM_RUNOUT_SENSORS must be 1 with MMU2 / MMU2S."
+ #elif (HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3) && NUM_RUNOUT_SENSORS != 1
+ #error "NUM_RUNOUT_SENSORS must be 1 with MMU2 / MMU2S / MMU3."
#elif NUM_RUNOUT_SENSORS != 1 && NUM_RUNOUT_SENSORS != E_STEPPERS
#error "NUM_RUNOUT_SENSORS must be either 1 or number of E steppers."
#elif NUM_RUNOUT_SENSORS >= 8 && !PIN_EXISTS(FIL_RUNOUT8)
@@ -599,7 +599,7 @@ static_assert(COUNT(arm) == LOGICAL_AXES, "AXIS_RELATIVE_MODES must contain " _L
/**
* Multi-Material-Unit 2 / EXTENDABLE_EMU_MMU2 requirements
*/
-#if HAS_PRUSA_MMU2
+#if HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3
#if !HAS_EXTENDABLE_MMU && EXTRUDERS != 5
#undef SINGLENOZZLE
#error "PRUSA_MMU2(S) requires exactly 5 EXTRUDERS. Please update your Configuration."
@@ -607,14 +607,20 @@ static_assert(COUNT(arm) == LOGICAL_AXES, "AXIS_RELATIVE_MODES must contain " _L
#error "EXTRUDERS is too large for MMU(S) emulation mode. The maximum value is 15."
#elif DISABLED(NOZZLE_PARK_FEATURE)
#error "PRUSA_MMU2(S) requires NOZZLE_PARK_FEATURE. Enable it to continue."
- #elif HAS_PRUSA_MMU2S && DISABLED(FILAMENT_RUNOUT_SENSOR)
- #error "PRUSA_MMU2S requires FILAMENT_RUNOUT_SENSOR. Enable it to continue."
+ #elif (HAS_PRUSA_MMU2S || HAS_PRUSA_MMU3) && DISABLED(FILAMENT_RUNOUT_SENSOR)
+ #error "PRUSA_MMU2S and HAS_PRUSA_MMU3 requires FILAMENT_RUNOUT_SENSOR. Enable it to continue."
#elif ENABLED(MMU_EXTRUDER_SENSOR) && DISABLED(FILAMENT_RUNOUT_SENSOR)
#error "MMU_EXTRUDER_SENSOR requires FILAMENT_RUNOUT_SENSOR. Enable it to continue."
#elif ENABLED(MMU_EXTRUDER_SENSOR) && !HAS_MARLINUI_MENU
#error "MMU_EXTRUDER_SENSOR requires an LCD supporting MarlinUI."
- #elif ENABLED(MMU2_MENUS) && !HAS_MARLINUI_MENU
- #error "MMU2_MENUS requires an LCD supporting MarlinUI."
+ #elif ENABLED(MMU_MENUS) && !HAS_MARLINUI_MENU
+ #error "MMU_MENUS requires an LCD supporting MarlinUI."
+ #elif HAS_PRUSA_MMU3 && !HAS_MARLINUI_MENU
+ #error "MMU3 requires an LCD supporting MarlinUI."
+ #elif HAS_PRUSA_MMU3 && DISABLED(MMU_MENUS)
+ #error "MMU3 requires MMU_MENUS."
+ #elif HAS_PRUSA_MMU3 && DISABLED(EEPROM_SETTINGS)
+ #error "MMU3 requires EEPROM_SETTINGS."
#elif DISABLED(ADVANCED_PAUSE_FEATURE)
static_assert(nullptr == strstr(MMU2_FILAMENT_RUNOUT_SCRIPT, "M600"), "MMU2_FILAMENT_RUNOUT_SCRIPT cannot make use of M600 unless ADVANCED_PAUSE_FEATURE is enabled");
#endif
@@ -690,8 +696,8 @@ static_assert(COUNT(arm) == LOGICAL_AXES, "AXIS_RELATIVE_MODES must contain " _L
#error "MECHANICAL_SWITCHING_NOZZLE and DUAL_X_CARRIAGE are incompatible."
#elif ENABLED(SINGLENOZZLE)
#error "MECHANICAL_SWITCHING_NOZZLE and SINGLENOZZLE are incompatible."
- #elif HAS_PRUSA_MMU2
- #error "MECHANICAL_SWITCHING_NOZZLE and PRUSA_MMU2(S) are incompatible."
+ #elif HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3
+ #error "MECHANICAL_SWITCHING_NOZZLE and PRUSA_MMU2(2S,3) are incompatible."
#elif !defined(EVENT_GCODE_TOOLCHANGE_T0)
#error "MECHANICAL_SWITCHING_NOZZLE requires EVENT_GCODE_TOOLCHANGE_T0."
#elif !defined(EVENT_GCODE_TOOLCHANGE_T1)
@@ -704,8 +710,8 @@ static_assert(COUNT(arm) == LOGICAL_AXES, "AXIS_RELATIVE_MODES must contain " _L
#error "SWITCHING_NOZZLE and DUAL_X_CARRIAGE are incompatible."
#elif ENABLED(SINGLENOZZLE)
#error "SWITCHING_NOZZLE and SINGLENOZZLE are incompatible."
- #elif HAS_PRUSA_MMU2
- #error "SWITCHING_NOZZLE and PRUSA_MMU2(S) are incompatible."
+ #elif HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3
+ #error "SWITCHING_NOZZLE and PRUSA_MMU2(2S,3) are incompatible."
#elif NUM_SERVOS < 1
#error "SWITCHING_NOZZLE requires NUM_SERVOS >= 1."
#elif !defined(SWITCHING_NOZZLE_SERVO_NR)
diff --git a/Marlin/src/inc/Version.h b/Marlin/src/inc/Version.h
index 34887da142e7..35d3b9e37521 100644
--- a/Marlin/src/inc/Version.h
+++ b/Marlin/src/inc/Version.h
@@ -42,7 +42,7 @@
* version was tagged.
*/
#ifndef STRING_DISTRIBUTION_DATE
- #define STRING_DISTRIBUTION_DATE "2024-08-23"
+ #define STRING_DISTRIBUTION_DATE "2024-08-25"
#endif
/**
diff --git a/Marlin/src/inc/Warnings.cpp b/Marlin/src/inc/Warnings.cpp
index b76d3497c0d0..52dc1444f5f5 100644
--- a/Marlin/src/inc/Warnings.cpp
+++ b/Marlin/src/inc/Warnings.cpp
@@ -737,25 +737,25 @@
#warning "High homing currents can lead to damage if a sensor fails or is set up incorrectly."
#endif
-#if USE_SENSORLESS
- #if defined(X_CURRENT_HOME) && !HAS_CURRENT_HOME(X)
- #warning "It's recommended to set X_CURRENT_HOME lower than X_CURRENT with SENSORLESS_HOMING."
- #elif defined(X2_CURRENT_HOME) && !HAS_CURRENT_HOME(X2)
- #warning "It's recommended to set X2_CURRENT_HOME lower than X2_CURRENT with SENSORLESS_HOMING."
- #endif
- #if defined(Y_CURRENT_HOME) && !HAS_CURRENT_HOME(Y)
- #warning "It's recommended to set Y_CURRENT_HOME lower than Y_CURRENT with SENSORLESS_HOMING."
- #elif defined(Y2_CURRENT_HOME) && !HAS_CURRENT_HOME(Y2)
- #warning "It's recommended to set Y2_CURRENT_HOME lower than Y2_CURRENT with SENSORLESS_HOMING."
- #endif
- #if defined(Z_CURRENT_HOME) && !HAS_CURRENT_HOME(Z)
- #warning "It's recommended to set Z_CURRENT_HOME lower than Z_CURRENT with SENSORLESS_HOMING."
- #elif defined(Z2_CURRENT_HOME) && !HAS_CURRENT_HOME(Z2)
- #warning "It's recommended to set Z2_CURRENT_HOME lower than Z2_CURRENT with SENSORLESS_HOMING."
- #elif defined(Z3_CURRENT_HOME) && !HAS_CURRENT_HOME(Z3)
- #warning "It's recommended to set Z3_CURRENT_HOME lower than Z3_CURRENT with SENSORLESS_HOMING."
- #elif defined(Z4_CURRENT_HOME) && !HAS_CURRENT_HOME(Z4)
- #warning "It's recommended to set Z4_CURRENT_HOME lower than Z4_CURRENT with SENSORLESS_HOMING."
+#if USE_SENSORLESS && DISABLED(NO_HOMING_CURRENT_WARNING)
+ #if ENABLED(X_SENSORLESS) && defined(X_CURRENT_HOME) && !HAS_CURRENT_HOME(X)
+ #warning "It's recommended to set X_CURRENT_HOME lower than X_CURRENT with SENSORLESS_HOMING. (Define NO_HOMING_CURRENT_WARNING to suppress this warning.)"
+ #elif ENABLED(X2_SENSORLESS) && defined(X2_CURRENT_HOME) && !HAS_CURRENT_HOME(X2)
+ #warning "It's recommended to set X2_CURRENT_HOME lower than X2_CURRENT with SENSORLESS_HOMING. (Define NO_HOMING_CURRENT_WARNING to suppress this warning.)"
+ #endif
+ #if ENABLED(Y_SENSORLESS) && defined(Y_CURRENT_HOME) && !HAS_CURRENT_HOME(Y)
+ #warning "It's recommended to set Y_CURRENT_HOME lower than Y_CURRENT with SENSORLESS_HOMING. (Define NO_HOMING_CURRENT_WARNING to suppress this warning.)"
+ #elif ENABLED(Y2_SENSORLESS) && defined(Y2_CURRENT_HOME) && !HAS_CURRENT_HOME(Y2)
+ #warning "It's recommended to set Y2_CURRENT_HOME lower than Y2_CURRENT with SENSORLESS_HOMING. (Define NO_HOMING_CURRENT_WARNING to suppress this warning.)"
+ #endif
+ #if ENABLED(Z_SENSORLESS) && defined(Z_CURRENT_HOME) && !HAS_CURRENT_HOME(Z)
+ #warning "It's recommended to set Z_CURRENT_HOME lower than Z_CURRENT with SENSORLESS_HOMING. (Define NO_HOMING_CURRENT_WARNING to suppress this warning.)"
+ #elif ENABLED(Z2_SENSORLESS) && defined(Z2_CURRENT_HOME) && !HAS_CURRENT_HOME(Z2)
+ #warning "It's recommended to set Z2_CURRENT_HOME lower than Z2_CURRENT with SENSORLESS_HOMING. (Define NO_HOMING_CURRENT_WARNING to suppress this warning.)"
+ #elif ENABLED(Z3_SENSORLESS) && defined(Z3_CURRENT_HOME) && !HAS_CURRENT_HOME(Z3)
+ #warning "It's recommended to set Z3_CURRENT_HOME lower than Z3_CURRENT with SENSORLESS_HOMING. (Define NO_HOMING_CURRENT_WARNING to suppress this warning.)"
+ #elif ENABLED(Z4_SENSORLESS) && defined(Z4_CURRENT_HOME) && !HAS_CURRENT_HOME(Z4)
+ #warning "It's recommended to set Z4_CURRENT_HOME lower than Z4_CURRENT with SENSORLESS_HOMING. (Define NO_HOMING_CURRENT_WARNING to suppress this warning.)"
#endif
#endif
diff --git a/Marlin/src/lcd/language/language_en.h b/Marlin/src/lcd/language/language_en.h
index 586e3bc34418..286c386a44a0 100644
--- a/Marlin/src/lcd/language/language_en.h
+++ b/Marlin/src/lcd/language/language_en.h
@@ -761,24 +761,52 @@ namespace LanguageNarrow_en {
LSTR MSG_MMU2_MENU = _UxGT("MMU");
LSTR MSG_KILL_MMU2_FIRMWARE = _UxGT("Update MMU Firmware!");
LSTR MSG_MMU2_NOT_RESPONDING = _UxGT("MMU Needs Attention.");
- LSTR MSG_MMU2_RESUME = _UxGT("MMU Resume");
+ LSTR MSG_MMU2_RESUME = _UxGT("Resume");
LSTR MSG_MMU2_RESUMING = _UxGT("MMU Resuming...");
- LSTR MSG_MMU2_LOAD_FILAMENT = _UxGT("MMU Load");
- LSTR MSG_MMU2_LOAD_ALL = _UxGT("MMU Load All");
- LSTR MSG_MMU2_LOAD_TO_NOZZLE = _UxGT("MMU Load to Nozzle");
- LSTR MSG_MMU2_EJECT_FILAMENT = _UxGT("MMU Eject");
- LSTR MSG_MMU2_EJECT_FILAMENT_N = _UxGT("MMU Eject ~");
- LSTR MSG_MMU2_UNLOAD_FILAMENT = _UxGT("MMU Unload");
+ LSTR MSG_MMU2_LOAD_FILAMENT = _UxGT("Load");
+ LSTR MSG_MMU2_LOAD_ALL = _UxGT("Load All");
+ LSTR MSG_MMU2_LOAD_TO_NOZZLE = _UxGT("Load to Nozzle");
+ LSTR MSG_MMU2_CUT_FILAMENT = _UxGT("Cut");
+ LSTR MSG_MMU2_EJECT_FILAMENT = _UxGT("Eject");
+ LSTR MSG_MMU2_EJECT_FILAMENT_N = _UxGT("Eject ~");
+ LSTR MSG_MMU2_UNLOAD_FILAMENT = _UxGT("Unload");
LSTR MSG_MMU2_LOADING_FILAMENT = _UxGT("Filament %i Load...");
- LSTR MSG_MMU2_EJECTING_FILAMENT = _UxGT("Filament Eject...");
- LSTR MSG_MMU2_UNLOADING_FILAMENT = _UxGT("Filament Unload...");
+ LSTR MSG_MMU2_CUTTING_FILAMENT = _UxGT("Filament %i Cut...");
+ LSTR MSG_MMU2_EJECTING_FILAMENT = _UxGT("Filament %i Eject...");
+ LSTR MSG_MMU2_UNLOADING_FILAMENT = _UxGT("Filament %i Unload...");
LSTR MSG_MMU2_ALL = _UxGT("All");
LSTR MSG_MMU2_FILAMENT_N = _UxGT("Filament ~");
LSTR MSG_MMU2_RESET = _UxGT("Reset MMU");
- LSTR MSG_MMU2_RESETTING = _UxGT("MMU Resetting...");
- LSTR MSG_MMU2_EJECT_RECOVER = _UxGT("MMU2 Eject Recover");
+ LSTR MSG_MMU2_RESETTING = _UxGT("Resetting...");
+ LSTR MSG_MMU2_EJECT_RECOVER = _UxGT("Eject Recover");
LSTR MSG_MMU2_REMOVE_AND_CLICK = _UxGT("Remove and click...");
+ LSTR MSG_MMU_SENSITIVITY = _UxGT("Sensitivity");
+ LSTR MSG_MMU_CUTTER = _UxGT("Cutter");
+ LSTR MSG_MMU_CUTTER_MODE = _UxGT("Cutter Mode");
+ LSTR MSG_MMU_CUTTER_MODE_DISABLE = _UxGT("Disable");
+ LSTR MSG_MMU_CUTTER_MODE_ENABLE = _UxGT("Enable");
+ LSTR MSG_MMU_CUTTER_MODE_ALWAYS = _UxGT("Always");
+ LSTR MSG_MMU_SPOOL_JOIN = _UxGT("Spool Join");
+ LSTR MSG_MMU_STEALTH = _UxGT("Stealth Mode");
+
+ LSTR MSG_MMU_FAIL_STATS = _UxGT("Fail stats");
+ LSTR MSG_MMU_STATISTICS = _UxGT("Statistics");
+ LSTR MSG_MMU_RESET_FAIL_STATS = _UxGT("Reset Fail Stats");
+ LSTR MSG_MMU_RESET_STATS = _UxGT("Reset All Stats");
+ LSTR MSG_MMU_CURRENT_PRINT = _UxGT("Curr. print");
+ LSTR MSG_MMU_CURRENT_PRINT_FAILURES = _UxGT("Curr. print failures");
+ LSTR MSG_MMU_LAST_PRINT = _UxGT("Last print");
+ LSTR MSG_MMU_LAST_PRINT_FAILURES = _UxGT("Last print failures");
+ LSTR MSG_MMU_TOTAL = _UxGT("Total");
+ LSTR MSG_MMU_TOTAL_FAILURES = _UxGT("Total failures");
+ LSTR MSG_MMU_DEV_INCREMENT_FAILS = _UxGT("Increment fails");
+ LSTR MSG_MMU_DEV_INCREMENT_LOAD_FAILS = _UxGT("Increment load fails");
+ LSTR MSG_MMU_FAILS = _UxGT("MMU fails");
+ LSTR MSG_MMU_LOAD_FAILS = _UxGT("MMU load fails");
+ LSTR MSG_MMU_POWER_FAILS = _UxGT("MMU power fails");
+ LSTR MSG_MMU_MATERIAL_CHANGES = _UxGT("Material changes");
+
LSTR MSG_MIX = _UxGT("Mix");
LSTR MSG_MIX_COMPONENT_N = _UxGT("Component {");
LSTR MSG_MIXER = _UxGT("Mixer");
@@ -937,6 +965,251 @@ namespace LanguageNarrow_en {
LSTR DGUS_MSG_WRITE_EEPROM_FAILED = _UxGT("EEPROM write failed");
LSTR DGUS_MSG_READ_EEPROM_FAILED = _UxGT("EEPROM read failed");
LSTR DGUS_MSG_FILAMENT_RUNOUT = _UxGT("Filament runout E%d");
+
+ //
+ // MMU3 Translatable Strings
+ //
+
+ LSTR MSG_TITLE_FINDA_DIDNT_TRIGGER = _UxGT("FINDA DIDNT TRIGGER");
+ LSTR MSG_TITLE_FINDA_FILAMENT_STUCK = _UxGT("FINDA FILAM. STUCK");
+ LSTR MSG_TITLE_FSENSOR_DIDNT_TRIGGER = _UxGT("FSENSOR DIDNT TRIGG.");
+ LSTR MSG_TITLE_FSENSOR_FILAMENT_STUCK = _UxGT("FSENSOR FIL. STUCK");
+ LSTR MSG_TITLE_PULLEY_CANNOT_MOVE = _UxGT("PULLEY CANNOT MOVE");
+ LSTR MSG_TITLE_FSENSOR_TOO_EARLY = _UxGT("FSENSOR TOO EARLY");
+ LSTR MSG_TITLE_INSPECT_FINDA = _UxGT("INSPECT FINDA");
+ LSTR MSG_TITLE_LOAD_TO_EXTRUDER_FAILED = _UxGT("LOAD TO EXTR. FAILED");
+ LSTR MSG_TITLE_SELECTOR_CANNOT_MOVE = _UxGT("SELECTOR CANNOT MOVE");
+ LSTR MSG_TITLE_SELECTOR_CANNOT_HOME = _UxGT("SELECTOR CANNOT HOME");
+ LSTR MSG_TITLE_IDLER_CANNOT_MOVE = _UxGT("IDLER CANNOT MOVE");
+ LSTR MSG_TITLE_IDLER_CANNOT_HOME = _UxGT("IDLER CANNOT HOME");
+ LSTR MSG_TITLE_TMC_WARNING_TMC_TOO_HOT = _UxGT("WARNING TMC TOO HOT");
+ LSTR MSG_TITLE_TMC_OVERHEAT_ERROR = _UxGT("TMC OVERHEAT ERROR");
+ LSTR MSG_TITLE_TMC_DRIVER_ERROR = _UxGT("TMC DRIVER ERROR");
+ LSTR MSG_TITLE_TMC_DRIVER_RESET = _UxGT("TMC DRIVER RESET");
+ LSTR MSG_TITLE_TMC_UNDERVOLTAGE_ERROR = _UxGT("TMC UNDERVOLTAGE ERR");
+ LSTR MSG_TITLE_TMC_DRIVER_SHORTED = _UxGT("TMC DRIVER SHORTED");
+ LSTR MSG_TITLE_SELFTEST_FAILED = _UxGT("MMU SELFTEST FAILED");
+ LSTR MSG_TITLE_MMU_MCU_ERROR = _UxGT("MMU MCU ERROR");
+ LSTR MSG_TITLE_MMU_NOT_RESPONDING = _UxGT("MMU NOT RESPONDING");
+ LSTR MSG_TITLE_COMMUNICATION_ERROR = _UxGT("COMMUNICATION ERROR");
+ LSTR MSG_TITLE_FILAMENT_ALREADY_LOADED = _UxGT("FIL. ALREADY LOADED");
+ LSTR MSG_TITLE_INVALID_TOOL = _UxGT("INVALID TOOL");
+ LSTR MSG_TITLE_QUEUE_FULL = _UxGT("QUEUE FULL");
+ LSTR MSG_TITLE_FW_UPDATE_NEEDED = _UxGT("MMU FW UPDATE NEEDED");
+ LSTR MSG_TITLE_FW_RUNTIME_ERROR = _UxGT("FW RUNTIME ERROR");
+ LSTR MSG_TITLE_UNLOAD_MANUALLY = _UxGT("UNLOAD MANUALLY");
+ LSTR MSG_TITLE_FILAMENT_EJECTED = _UxGT("FILAMENT EJECTED");
+ LSTR MSG_TITLE_FILAMENT_CHANGE = _UxGT("FILAMENT CHANGE");
+ LSTR MSG_TITLE_UNKNOWN_ERROR = _UxGT("UNKNOWN ERROR");
+
+ LSTR MSG_DESC_FINDA_DIDNT_TRIGGER = _UxGT("FINDA didn't trigger while loading the filament. Ensure the filament can move and FINDA works.");
+ LSTR MSG_DESC_FINDA_FILAMENT_STUCK = _UxGT("FINDA didn't switch off while unloading filament. Try unloading manually. Ensure filament can move and FINDA works.");
+ LSTR MSG_DESC_FSENSOR_DIDNT_TRIGGER = _UxGT("Filament sensor didn't trigger while loading the filament. Ensure the sensor is calibrated and the filament reached it.");
+ LSTR MSG_DESC_FSENSOR_FILAMENT_STUCK = _UxGT("Filament sensor didn't switch off while unloading filament. Ensure filament can move and the sensor works.");
+ LSTR MSG_DESC_PULLEY_CANNOT_MOVE = _UxGT("Pulley motor stalled. Ensure the pulley can move and check the wiring.");
+ LSTR MSG_DESC_FSENSOR_TOO_EARLY = _UxGT("Filament sensor triggered too early while loading to extruder. Check there isn't anything stuck in PTFE tube. Check that sensor reads properly.");
+ LSTR MSG_DESC_INSPECT_FINDA = _UxGT("Selector can't move due to FINDA detecting a filament. Make sure no filament is in Selector and FINDA works properly.");
+ LSTR MSG_DESC_LOAD_TO_EXTRUDER_FAILED = _UxGT("Loading to extruder failed. Inspect the filament tip shape. Refine the sensor calibration, if needed.");
+ LSTR MSG_DESC_SELECTOR_CANNOT_HOME = _UxGT("The Selector cannot home properly. Check for anything blocking its movement.");
+ LSTR MSG_DESC_CANNOT_MOVE = _UxGT("Can't move Selector or Idler.");
+ LSTR MSG_DESC_IDLER_CANNOT_HOME = _UxGT("The Idler cannot home properly. Check for anything blocking its movement.");
+ LSTR MSG_DESC_TMC = _UxGT("More details online.");
+ LSTR MSG_DESC_MMU_NOT_RESPONDING = _UxGT("MMU not responding. Check the wiring and connectors.");
+ LSTR MSG_DESC_COMMUNICATION_ERROR = _UxGT("MMU not responding correctly. Check the wiring and connectors.");
+ LSTR MSG_DESC_FILAMENT_ALREADY_LOADED = _UxGT("Cannot perform the action, filament is already loaded. Unload it first.");
+ LSTR MSG_DESC_INVALID_TOOL = _UxGT("Requested filament tool is not available on this hardware. Check the G-code for tool index out of range (T0-T4).");
+ LSTR MSG_DESC_QUEUE_FULL = _UxGT("MMU Firmware internal error, please reset the MMU.");
+ LSTR MSG_DESC_FW_RUNTIME_ERROR = _UxGT("Internal runtime error. Try resetting the MMU or updating the firmware.");
+ LSTR MSG_DESC_UNLOAD_MANUALLY = _UxGT("Filament detected unexpectedly. Ensure no filament is loaded. Check the sensors and wiring.");
+ LSTR MSG_DESC_FILAMENT_EJECTED = _UxGT("Remove the ejected filament from the front of the MMU.");
+ LSTR MSG_DESC_FILAMENT_CHANGE = _UxGT("M600 Filament Change. Load a new filament or eject the old one.");
+ LSTR MSG_DESC_UNKNOWN_ERROR = _UxGT("Unexpected error occurred.");
+
+ LSTR MSG_DESC_FW_UPDATE_NEEDED = _UxGT("MMU FW version is not supported. Update to version " STRINGIFY(mmuVersionMajor) "." STRINGIFY(mmuVersionMinor) "." STRINGIFY(mmuVersionPatch) ".");
+
+ LSTR MSG_BTN_RETRY = _UxGT("Retry");
+ LSTR MSG_BTN_RESET_MMU = _UxGT("ResetMMU");
+ LSTR MSG_BTN_UNLOAD = _UxGT("Unload");
+ LSTR MSG_BTN_LOAD = _UxGT("Load");
+ LSTR MSG_BTN_EJECT = _UxGT("Eject");
+ LSTR MSG_BTN_STOP = _UxGT("Stop");
+ LSTR MSG_BTN_DISABLE_MMU = _UxGT("Disable");
+ LSTR MSG_BTN_MORE = _UxGT("More Info");
+
+ LSTR MSG_ALWAYS = _UxGT("Always");
+ LSTR MSG_BABYSTEP_Z_NOT_SET = _UxGT("Distance between tip of the nozzle and the bed surface has not been set yet. Please follow the manual, chapter First steps, section First layer calibration.");
+ LSTR MSG_BED_DONE = _UxGT("Bed done");
+ LSTR MSG_BED_LEVELING_FAILED_POINT_LOW = _UxGT("Bed leveling failed. Sensor didn't trigger. Debris on nozzle? Waiting for reset.");
+ LSTR MSG_BED_SKEW_OFFSET_DETECTION_FITTING_FAILED = _UxGT("XYZ calibration failed. Please consult the manual.");
+ LSTR MSG_BELT_STATUS = _UxGT("Belt status");
+ LSTR MSG_CANCEL = _UxGT(">Cancel");
+ LSTR MSG_CALIBRATE_Z_AUTO = _UxGT("Calibrating Z");
+ LSTR MSG_CARD_MENU = _UxGT("Print from SD");
+ LSTR MSG_CHECKING_X = _UxGT("Checking X axis");
+ LSTR MSG_CHECKING_Y = _UxGT("Checking Y axis");
+ LSTR MSG_COMMUNITY_MADE = _UxGT("Community made");
+ LSTR MSG_CONFIRM_NOZZLE_CLEAN = _UxGT("Please clean the nozzle for calibration. Click when done.");
+ LSTR MSG_CRASH = _UxGT("Crash");
+ LSTR MSG_CRASH_DETECTED = _UxGT("Crash detected.");
+ LSTR MSG_CRASHDETECT = _UxGT("Crash det.");
+ LSTR MSG_DONE = _UxGT("Done");
+ LSTR MSG_EXTRUDER = _UxGT("Extruder");
+ LSTR MSG_FANS_CHECK = _UxGT("Fans check");
+ LSTR MSG_FIL_RUNOUTS = _UxGT("Fil. runouts");
+ LSTR MSG_HOTEND_FAN_SPEED = _UxGT("Hotend fan:");
+ LSTR MSG_PRINT_FAN_SPEED = _UxGT("Print fan:");
+ LSTR MSG_FILAMENT_CLEAN = _UxGT("Filament extruding & with correct color?");
+ LSTR MSG_FILAMENT_LOADED = _UxGT("Is filament loaded?");
+ LSTR MSG_FIND_BED_OFFSET_AND_SKEW_LINE1 = _UxGT("Searching bed calibration point");
+ LSTR MSG_FINISHING_MOVEMENTS = _UxGT("Finishing movements");
+ LSTR MSG_FOLLOW_CALIBRATION_FLOW = _UxGT("Printer has not been calibrated yet. Please follow the manual, chapter First steps, section Calibration flow.");
+ LSTR MSG_FOLLOW_Z_CALIBRATION_FLOW = _UxGT("There is still a need to make Z calibration. Please follow the manual, chapter First steps, section Calibration flow.");
+ LSTR MSG_FSENSOR_RUNOUT = _UxGT("F. runout");
+ LSTR MSG_FSENSOR_AUTOLOAD = _UxGT("F. autoload");
+ LSTR MSG_FSENSOR_JAM_DETECTION = _UxGT("F. jam detect");
+ LSTR MSG_FSENSOR = _UxGT("Fil. sensor");
+ LSTR MSG_HEATING_COMPLETE = _UxGT("Heating done.");
+ LSTR MSG_HOMEYZ = _UxGT("Calibrate Z");
+ LSTR MSG_SELECT_FILAMENT = _UxGT("Select filament:");
+ LSTR MSG_LAST_PRINT = _UxGT("Last print");
+ LSTR MSG_LAST_PRINT_FAILURES = _UxGT("Last print failures");
+ LSTR MSG_PRELOAD_TO_MMU = _UxGT("Preload to MMU");
+ LSTR MSG_LOAD_FILAMENT = _UxGT("Load filament");
+ LSTR MSG_LOADING_TEST = _UxGT("Loading Test");
+ LSTR MSG_LOADING_FILAMENT = _UxGT("Loading filament");
+ LSTR MSG_TESTING_FILAMENT = _UxGT("Testing filament");
+ LSTR MSG_EJECT_FROM_MMU = _UxGT("Eject from MMU");
+ LSTR MSG_CUT_FILAMENT = _UxGT("Cut filament");
+ LSTR MSG_SHEET = _UxGT("Sheet");
+ LSTR MSG_STEEL_SHEETS = _UxGT("Steel sheets");
+ LSTR MSG_MEASURE_BED_REFERENCE_HEIGHT_LINE1 = _UxGT("Measuring reference height of calibration point");
+ LSTR MSG_CALIBRATION = _UxGT("Calibration");
+ LSTR MSG_PAPER = _UxGT("Place a sheet of paper under the nozzle during the calibration of first 4 points. If the nozzle catches the paper, power off the printer immediately.");
+ LSTR MSG_PLACE_STEEL_SHEET = _UxGT("Please place steel sheet on heatbed.");
+ LSTR MSG_POWER_FAILURES = _UxGT("Power failures");
+ LSTR MSG_PREHEAT_NOZZLE = _UxGT("Preheat the nozzle!");
+ LSTR MSG_PRESS_TO_UNLOAD = _UxGT("Please press the knob to unload filament");
+ LSTR MSG_PULL_OUT_FILAMENT = _UxGT("Please pull out filament immediately");
+ LSTR MSG_RECOVER_PRINT = _UxGT("Blackout occurred. Recover print?");
+ LSTR MSG_REMOVE_STEEL_SHEET = _UxGT("Please remove steel sheet from heatbed.");
+ LSTR MSG_RESET = _UxGT("Reset");
+ LSTR MSG_RESUMING_PRINT = _UxGT("Resuming print");
+ LSTR MSG_SELFTEST_PART_FAN = _UxGT("Front print fan?");
+ LSTR MSG_SELFTEST_HOTEND_FAN = _UxGT("Left hotend fan?");
+ LSTR MSG_SELFTEST_FAILED = _UxGT("Selftest failed");
+ LSTR MSG_SELFTEST_FAN = _UxGT("Fan test");
+ LSTR MSG_SELFTEST_FAN_NO = _UxGT("Not spinning");
+ LSTR MSG_SELFTEST_FAN_YES = _UxGT("Spinning");
+ LSTR MSG_SELFTEST_CHECK_BED = _UxGT("Checking bed");
+ LSTR MSG_SELFTEST_CHECK_FSENSOR = _UxGT("Checking sensors");
+ LSTR MSG_SELFTEST_MOTOR = _UxGT("Motor");
+ LSTR MSG_SELFTEST_FILAMENT_SENSOR = _UxGT("Filament sensor");
+ LSTR MSG_SELFTEST_WIRINGERROR = _UxGT("Wiring error");
+ LSTR MSG_SETTINGS = _UxGT("Settings");
+ LSTR MSG_SET_READY = _UxGT("Set Ready");
+ LSTR MSG_SET_NOT_READY = _UxGT("Set not Ready");
+ LSTR MSG_SELECT_LANGUAGE = _UxGT("Select language");
+ LSTR MSG_SORTING_FILES = _UxGT("Sorting files");
+ LSTR MSG_TOTAL = _UxGT("Total");
+ LSTR MSG_MATERIAL_CHANGES = _UxGT("Material changes");
+ LSTR MSG_TOTAL_FAILURES = _UxGT("Total failures");
+ LSTR MSG_HW_SETUP = _UxGT("HW Setup");
+ LSTR MSG_MODE = _UxGT("Mode");
+ LSTR MSG_HIGH_POWER = _UxGT("High power");
+ LSTR MSG_AUTO_POWER = _UxGT("Auto power");
+ LSTR MSG_SILENT = _UxGT("Silent");
+ LSTR MSG_NORMAL = _UxGT("Normal");
+ LSTR MSG_STEALTH = _UxGT("Stealth");
+ LSTR MSG_STEEL_SHEET_CHECK = _UxGT("Is steel sheet on heatbed?");
+ LSTR MSG_PINDA_CALIBRATION = _UxGT("PINDA cal.");
+ LSTR MSG_PINDA_CALIBRATION_DONE = _UxGT("PINDA calibration is finished and active. It can be disabled in menu Settings->PINDA cal.");
+ LSTR MSG_UNLOAD_FILAMENT = _UxGT("Unload filament");
+ LSTR MSG_UNLOADING_FILAMENT = _UxGT("Unloading filament");
+ LSTR MSG_WIZARD_CALIBRATION_FAILED = _UxGT("Please check our handbook and fix the problem. Then resume the Wizard by rebooting the printer.");
+ LSTR MSG_WIZARD_DONE = _UxGT("All done. Happy printing!");
+ LSTR MSG_WIZARD_HEATING = _UxGT("Preheating nozzle. Please wait.");
+ LSTR MSG_WIZARD_QUIT = _UxGT("You can always resume the Wizard from Calibration -> Wizard.");
+ LSTR MSG_WIZARD_WELCOME = _UxGT("Hi, I am your Original Prusa i3 printer. Would you like me to guide you through the setup process?");
+ LSTR MSG_WIZARD_WELCOME_SHIPPING = _UxGT("Hi, I am your Original Prusa i3 printer. I will guide you through a short setup process, in which the Z-axis will be calibrated. Then, you will be ready to print.");
+ LSTR MSG_V2_CALIBRATION = _UxGT("First layer cal.");
+ LSTR MSG_OFF = _UxGT("Off");
+ LSTR MSG_ON = _UxGT("On");
+ LSTR MSG_NA = _UxGT("N/A");
+ LSTR MSG_NONE = _UxGT("None");
+ LSTR MSG_WARN = _UxGT("Warn");
+ LSTR MSG_STRICT = _UxGT("Strict");
+ LSTR MSG_MODEL = _UxGT("Model");
+ LSTR MSG_GCODE_DIFF_PRINTER_CONTINUE = _UxGT("G-code sliced for a different printer type. Continue?");
+ LSTR MSG_GCODE_DIFF_PRINTER_CANCELLED = _UxGT("G-code sliced for a different printer type. Please re-slice the model again. Print cancelled.");
+ LSTR MSG_GCODE_NEWER_FIRMWARE_CONTINUE = _UxGT("G-code sliced for a newer firmware. Continue?");
+ LSTR MSG_GCODE_NEWER_FIRMWARE_CANCELLED = _UxGT("G-code sliced for a newer firmware. Please update the firmware. Print cancelled.");
+ LSTR MSG_GCODE_DIFF_CONTINUE = _UxGT("G-code sliced for a different level. Continue?");
+ LSTR MSG_GCODE_DIFF_CANCELLED = _UxGT("G-code sliced for a different level. Please re-slice the model again. Print cancelled.");
+ LSTR MSG_NOZZLE_DIFFERS_CONTINUE = _UxGT("Nozzle diameter differs from the G-code. Continue?");
+ LSTR MSG_NOZZLE_DIFFERS_CANCELLED = _UxGT("Nozzle diameter differs from the G-code. Please check the value in settings. Print cancelled.");
+ LSTR MSG_NOZZLE_DIAMETER = _UxGT("Nozzle d.");
+ LSTR MSG_MMU_MODE = _UxGT("MMU Mode");
+ LSTR MSG_SORT = _UxGT("Sort");
+ LSTR MSG_SORT_TIME = _UxGT("Time");
+ LSTR MSG_SORT_ALPHA = _UxGT("Alphabet");
+ LSTR MSG_RPI_PORT = _UxGT("RPi port");
+ LSTR MSG_SOUND_LOUD = _UxGT("Loud");
+ LSTR MSG_SOUND_ONCE = _UxGT("Once");
+ LSTR MSG_SOUND_BLIND = _UxGT("Assist");
+ LSTR MSG_MESH = _UxGT("Mesh");
+ LSTR MSG_MESH_BED_LEVELING = _UxGT("Mesh Bed Leveling");
+ LSTR MSG_Z_PROBE_NR = _UxGT("Z-probe nr.");
+ LSTR MSG_MAGNETS_COMP = _UxGT("Magnets comp.");
+ LSTR MSG_FS_ACTION = _UxGT("FS Action");
+ LSTR MSG_CONTINUE_SHORT = _UxGT("Cont.");
+ LSTR MSG_PAUSE = _UxGT("Pause");
+ LSTR MSG_BL_HIGH = _UxGT("Level Bright");
+ LSTR MSG_BL_LOW = _UxGT("Level Dimmed");
+ LSTR MSG_BRIGHT = _UxGT("Bright");
+ LSTR MSG_DIM = _UxGT("Dim");
+ LSTR MSG_AUTO = _UxGT("Auto");
+ #if FILAMENT_SENSOR_TYPE == FSENSOR_IR_ANALOG
+ // Beware - The space at the beginning is necessary since it is reused in LCD menu items which are to be with a space
+ LSTR MSG_IR_04_OR_NEWER = _UxGT(" 0.4 or newer");
+ LSTR MSG_IR_03_OR_OLDER = _UxGT(" 0.3 or older");
+ LSTR MSG_IR_UNKNOWN = _UxGT("unknown state");
+ #endif
+ LSTR MSG_PAUSED_THERMAL_ERROR = _UxGT("PAUSED THERMAL ERROR");
+ #if ENABLED(THERMAL_MODEL)
+ LSTR MSG_THERMAL_ANOMALY = _UxGT("THERMAL ANOMALY");
+ LSTR MSG_TM_NOT_CAL = _UxGT("Thermal model not calibrated yet.");
+ LSTR MSG_TM_ACK_ERROR = _UxGT("Clear TM error");
+ #endif
+ LSTR MSG_LOAD_ALL = _UxGT("Load All");
+ LSTR MSG_NOZZLE_CNG_MENU = _UxGT("Nozzle change");
+ LSTR MSG_NOZZLE_CNG_READ_HELP = _UxGT("For a Nozzle change please read\nprusa.io/nozzle-mk3s");
+ LSTR MSG_NOZZLE_CNG_CHANGED = _UxGT("Hotend at 280C! Nozzle changed and tightened to specs?");
+ LSTR MSG_REPRINT = _UxGT("Reprint");
+
+ LSTR MSG_PROGRESS_OK = _UxGT("OK");
+ LSTR MSG_PROGRESS_ENGAGE_IDLER = _UxGT("Engaging idler");
+ LSTR MSG_PROGRESS_DISENGAGE_IDLER = _UxGT("Disengaging idler");
+ LSTR MSG_PROGRESS_UNLOAD_FINDA = _UxGT("Unloading to FINDA");
+ LSTR MSG_PROGRESS_UNLOAD_PULLEY = _UxGT("Unloading to pulley");
+ LSTR MSG_PROGRESS_FEED_FINDA = _UxGT("Feeding to FINDA");
+ LSTR MSG_PROGRESS_FEED_EXTRUDER = _UxGT("Feeding to extruder");
+ LSTR MSG_PROGRESS_FEED_NOZZLE = _UxGT("Feeding to nozzle");
+ LSTR MSG_PROGRESS_AVOID_GRIND = _UxGT("Avoiding grind");
+ LSTR MSG_PROGRESS_WAIT_USER = _UxGT("ERR Wait for User");
+ LSTR MSG_PROGRESS_ERR_INTERNAL = _UxGT("ERR Internal");
+ LSTR MSG_PROGRESS_ERR_HELP_FIL = _UxGT("ERR Help filament");
+ LSTR MSG_PROGRESS_ERR_TMC = _UxGT("ERR TMC failed");
+ LSTR MSG_PROGRESS_SELECT_SLOT = _UxGT("Selecting fil. slot");
+ LSTR MSG_PROGRESS_PREPARE_BLADE = _UxGT("Preparing blade");
+ LSTR MSG_PROGRESS_PUSH_FILAMENT = _UxGT("Pushing filament");
+ LSTR MSG_PROGRESS_PERFORM_CUT = _UxGT("Performing cut");
+ LSTR MSG_PROGRESSPSTRETURN_SELECTOR = _UxGT("Returning selector");
+ LSTR MSG_PROGRESS_PARK_SELECTOR = _UxGT("Parking selector");
+ LSTR MSG_PROGRESS_EJECT_FILAMENT = _UxGT("Ejecting filament");
+ LSTR MSG_PROGRESSPSTRETRACT_FINDA = _UxGT("Retract from FINDA");
+ LSTR MSG_PROGRESS_HOMING = _UxGT("Homing");
+ LSTR MSG_PROGRESS_MOVING_SELECTOR = _UxGT("Moving selector");
+ LSTR MSG_PROGRESS_FEED_FSENSOR = _UxGT("Feeding to FSensor");
}
namespace LanguageWide_en {
diff --git a/Marlin/src/lcd/menu/menu_main.cpp b/Marlin/src/lcd/menu/menu_main.cpp
index 4d239d595f78..218e37740a1a 100644
--- a/Marlin/src/lcd/menu/menu_main.cpp
+++ b/Marlin/src/lcd/menu/menu_main.cpp
@@ -50,7 +50,7 @@
#define MACHINE_CAN_PAUSE 1
#endif
-#if ENABLED(MMU2_MENUS)
+#if ENABLED(MMU_MENUS)
#include "menu_mmu2.h"
#endif
@@ -355,8 +355,10 @@ void menu_main() {
SUBMENU(MSG_MIXER, menu_mixer);
#endif
- #if ENABLED(MMU2_MENUS)
- if (!busy) SUBMENU(MSG_MMU2_MENU, menu_mmu2);
+ #if ENABLED(MMU_MENUS)
+ // MMU3 can show print stats which can be useful during
+ // the print, so MMU menus are required for MMU3.
+ if (TERN1(HAS_PRUSA_MMU2, !busy)) SUBMENU(MSG_MMU2_MENU, menu_mmu2);
#endif
SUBMENU(MSG_CONFIGURATION, menu_configuration);
diff --git a/Marlin/src/lcd/menu/menu_mmu2.cpp b/Marlin/src/lcd/menu/menu_mmu2.cpp
index c9d163357bbd..c1a330cb8124 100644
--- a/Marlin/src/lcd/menu/menu_mmu2.cpp
+++ b/Marlin/src/lcd/menu/menu_mmu2.cpp
@@ -22,10 +22,18 @@
#include "../../inc/MarlinConfig.h"
-#if ALL(HAS_MARLINUI_MENU, MMU2_MENUS)
+#if ENABLED(MMU_MENUS)
#include "../../MarlinCore.h"
-#include "../../feature/mmu/mmu2.h"
+
+#if HAS_PRUSA_MMU3
+ #include "../../feature/mmu3/mmu2.h"
+ #include "../../feature/mmu3/mmu2_reporting.h"
+ #include "../../feature/mmu3/SpoolJoin.h"
+#else
+ #include "../../feature/mmu/mmu2.h"
+#endif
+
#include "menu_mmu2.h"
#include "menu_item.h"
@@ -34,21 +42,23 @@
//
inline void action_mmu2_load_to_nozzle(const uint8_t tool) {
+ ui.reset_status();
ui.return_to_status();
ui.status_printf(0, GET_TEXT_F(MSG_MMU2_LOADING_FILAMENT), int(tool + 1));
- if (mmu2.load_to_nozzle(tool)) ui.reset_status();
+ TERN(HAS_PRUSA_MMU3, mmu3.load_to_nozzle(tool), mmu2.load_to_nozzle(tool));
+ ui.reset_status();
}
-void _mmu2_load_to_feeder(const uint8_t index) {
+void _mmu2_load_to_feeder(const uint8_t tool) {
+ ui.reset_status();
ui.return_to_status();
- ui.status_printf(0, GET_TEXT_F(MSG_MMU2_LOADING_FILAMENT), int(index + 1));
- mmu2.load_to_feeder(index);
+ ui.status_printf(0, GET_TEXT_F(MSG_MMU2_LOADING_FILAMENT), int(tool + 1));
+ TERN(HAS_PRUSA_MMU3, mmu3.load_to_feeder(tool), mmu2.load_to_feeder(tool));
ui.reset_status();
}
void action_mmu2_load_all() {
EXTRUDER_LOOP() _mmu2_load_to_feeder(e);
- ui.return_to_status();
}
void menu_mmu2_load_filament() {
@@ -74,15 +84,26 @@ void _mmu2_eject_filament(uint8_t index) {
ui.reset_status();
ui.return_to_status();
ui.status_printf(0, GET_TEXT_F(MSG_MMU2_EJECTING_FILAMENT), int(index + 1));
- if (mmu2.eject_filament(index, true)) ui.reset_status();
+ if (mmu3.eject_filament(index, true)) ui.reset_status();
+}
+
+void _mmu2_cut_filament(uint8_t index) {
+ ui.reset_status();
+ ui.return_to_status();
+ ui.status_printf(0, GET_TEXT_F(MSG_MMU2_CUTTING_FILAMENT), int(index + 1));
+ if (TERN0(HAS_PRUSA_MMU3, mmu3.cut_filament(index, true)))
+ ui.reset_status();
}
void action_mmu2_unload_filament() {
ui.reset_status();
ui.return_to_status();
LCD_MESSAGE(MSG_MMU2_UNLOADING_FILAMENT);
- idle();
- if (mmu2.unload()) ui.reset_status();
+ while (!TERN(HAS_PRUSA_MMU3, mmu3.unload(), mmu2.unload())) {
+ safe_delay(50);
+ TERN(HAS_PRUSA_MMU3, MMU3::marlin_idle(true), idle());
+ }
+ ui.reset_status();
}
void menu_mmu2_eject_filament() {
@@ -92,23 +113,214 @@ void menu_mmu2_eject_filament() {
END_MENU();
}
+// Cutter
+
+#if HAS_PRUSA_MMU3
+
+void menu_mmu3_cutter_set_mode(uint8_t mode) { mmu3.cutter_mode = mode; }
+void menu_mmu3_cutter_disable() { menu_mmu3_cutter_set_mode(0); }
+void menu_mmu3_cutter_enable() { menu_mmu3_cutter_set_mode(1); }
+void menu_mmu3_cutter_always() { menu_mmu3_cutter_set_mode(2); }
+
+void menu_mmu3_cutter() {
+ START_MENU();
+ BACK_ITEM(MSG_MMU2_MENU);
+ ACTION_ITEM(MSG_MMU_CUTTER_MODE_DISABLE, menu_mmu3_cutter_disable);
+ ACTION_ITEM(MSG_MMU_CUTTER_MODE_ENABLE, menu_mmu3_cutter_enable);
+ ACTION_ITEM(MSG_MMU_CUTTER_MODE_ALWAYS, menu_mmu3_cutter_always);
+ END_MENU();
+}
+
+void menu_mmu3_cut_filament() {
+ START_MENU();
+ BACK_ITEM(MSG_MMU2_MENU);
+ EXTRUDER_LOOP() ACTION_ITEM_N(e, MSG_MMU2_FILAMENT_N, []{ _mmu2_cut_filament(MenuItemBase::itemIndex); });
+ END_MENU();
+}
+
+// SpoolJoin
+void spool_join_status() { spooljoin.initStatus(); }
+
+// Fail Stats Menu
+void menu_mmu3_fail_stats_last_print() {
+ if (ui.use_click()) return ui.go_back();
+ char buffer1[LCD_WIDTH], buffer2[LCD_WIDTH];
+
+ // had to cast the uint8_t values to uint16_t before formatting them.
+ const uint16_t fail_num = MMU3::operation_statistics.fail_num;
+ const uint16_t load_fail_num = MMU3::operation_statistics.load_fail_num;
+
+ sprintf_P(buffer1, PSTR("%hu"), fail_num);
+ sprintf_P(buffer2, PSTR("%hu"), load_fail_num);
+
+ START_SCREEN();
+ STATIC_ITEM(
+ TERN(printJobOngoing(), MSG_MMU_CURRENT_PRINT_FAILURES, MSG_MMU_LAST_PRINT_FAILURES),
+ SS_INVERT
+ );
+ #ifndef __AVR__
+ // TODO: I couldn't make this work on AVR
+ PSTRING_ITEM(MSG_MMU_FAILS, buffer1, SS_FULL);
+ PSTRING_ITEM(MSG_MMU_LOAD_FAILS, buffer2, SS_FULL);
+ #endif
+ END_SCREEN();
+}
+
+void menu_mmu3_fail_stas_total() {
+ if (ui.use_click()) return ui.go_back();
+ char buffer1[LCD_WIDTH], buffer2[LCD_WIDTH], buffer3[LCD_WIDTH];
+
+ sprintf_P(buffer1, PSTR("%hu"), MMU3::operation_statistics.fail_total_num);
+ sprintf_P(buffer2, PSTR("%hu"), MMU3::operation_statistics.load_fail_total_num);
+ sprintf_P(buffer3, PSTR("%hu"), mmu3.tmcFailures());
+
+ START_SCREEN();
+ STATIC_ITEM(MSG_MMU_TOTAL_FAILURES, SS_INVERT);
+ #ifndef __AVR__
+ // TODO: I couldn't make this work on AVR
+ PSTRING_ITEM(MSG_MMU_FAILS, buffer1, SS_FULL);
+ PSTRING_ITEM(MSG_MMU_LOAD_FAILS, buffer2, SS_FULL);
+ PSTRING_ITEM(MSG_MMU_POWER_FAILS, buffer3, SS_FULL);
+ #endif
+ END_SCREEN();
+}
+
+#if ENABLED(MARLIN_DEV_MODE)
+ void menu_mmu3_dev_increment_fail_stat() {
+ MMU3::operation_statistics.increment_mmu_fails();
+ }
+
+ void menu_mmu3_dev_increment_load_fail_stat() {
+ MMU3::operation_statistics.increment_load_fails();
+ }
+#endif
+
+static void mmu3_reset_fail_stats() {
+ bool result = MMU3::operation_statistics.reset_fail_stats();
+ ui.go_back();
+ MarlinUI::completion_feedback(result);
+}
+
+static void mmu3_reset_stats() {
+ bool result = MMU3::operation_statistics.reset_stats();
+ ui.go_back();
+ MarlinUI::completion_feedback(result);
+}
+
+void menu_mmu3_toolchange_stat_total() {
+ if (ui.use_click()) return ui.go_back();
+ char buffer1[LCD_WIDTH];
+ sprintf_P(buffer1, PSTR("%u"), MMU3::operation_statistics.tool_change_counter);
+
+ char buffer2[LCD_WIDTH];
+ sprintf_P(buffer2, PSTR("%lu"), MMU3::operation_statistics.tool_change_total_counter);
+
+ START_SCREEN();
+ STATIC_ITEM(MSG_MMU_MATERIAL_CHANGES, SS_INVERT);
+ #ifndef __AVR__
+ // TODO: I couldn't make this work on AVR
+ if (printJobOngoing())
+ PSTRING_ITEM(MSG_MMU_CURRENT_PRINT, buffer1, SS_FULL);
+ else
+ PSTRING_ITEM(MSG_MMU_LAST_PRINT, buffer1, SS_FULL);
+ PSTRING_ITEM(MSG_MMU_TOTAL, buffer2, SS_FULL);
+ #endif
+ END_SCREEN();
+}
+
+void menu_mmu3_statistics() {
+ START_MENU();
+ BACK_ITEM(MSG_MMU2_MENU);
+ #if ENABLED(MARLIN_DEV_MODE)
+ ACTION_ITEM(MSG_MMU_DEV_INCREMENT_FAILS, menu_mmu3_dev_increment_fail_stat);
+ ACTION_ITEM(MSG_MMU_DEV_INCREMENT_LOAD_FAILS, menu_mmu3_dev_increment_load_fail_stat);
+ #endif
+
+ SUBMENU(
+ TERN(printJobOngoing(), MSG_MMU_CURRENT_PRINT_FAILURES, MSG_MMU_LAST_PRINT_FAILURES),
+ menu_mmu3_fail_stats_last_print
+ );
+ SUBMENU(MSG_MMU_TOTAL_FAILURES, menu_mmu3_fail_stas_total);
+ SUBMENU(MSG_MMU_MATERIAL_CHANGES, menu_mmu3_toolchange_stat_total);
+ CONFIRM_ITEM(MSG_MMU_RESET_FAIL_STATS,
+ MSG_BUTTON_RESET, MSG_BUTTON_CANCEL,
+ mmu3_reset_fail_stats, nullptr,
+ GET_TEXT_F(MSG_MMU_RESET_FAIL_STATS), (const char *)nullptr, F("?")
+ );
+ CONFIRM_ITEM(MSG_MMU_RESET_STATS,
+ MSG_BUTTON_RESET, MSG_BUTTON_CANCEL,
+ mmu3_reset_stats, nullptr,
+ GET_TEXT_F(MSG_MMU_RESET_STATS), (const char *)nullptr, F("?")
+ );
+ END_MENU();
+}
+
+#endif // HAS_PRUSA_MMU3
+
//
// MMU2 Menu
//
void action_mmu2_reset() {
- mmu2.init();
+ #if HAS_PRUSA_MMU3
+ #if PIN_EXISTS(MMU2_RST)
+ mmu3.reset(MMU3::MMU3::ResetForm::ResetPin);
+ #else
+ mmu3.reset(MMU3::MMU3::ResetForm::Software);
+ #endif
+ #else
+ mmu2.init();
+ #endif
ui.reset_status();
}
void menu_mmu2() {
+ const bool busy = printJobOngoing(); // printingIsActive();
+
START_MENU();
BACK_ITEM(MSG_MAIN_MENU);
- SUBMENU(MSG_MMU2_LOAD_FILAMENT, menu_mmu2_load_filament);
- SUBMENU(MSG_MMU2_LOAD_TO_NOZZLE, menu_mmu2_load_to_nozzle);
- SUBMENU(MSG_MMU2_EJECT_FILAMENT, menu_mmu2_eject_filament);
- ACTION_ITEM(MSG_MMU2_UNLOAD_FILAMENT, action_mmu2_unload_filament);
- ACTION_ITEM(MSG_MMU2_RESET, action_mmu2_reset);
+
+ // MMU2/MMU3 Commands
+ if (!busy && TERN1(HAS_PRUSA_MMU3, mmu3.mmu_hw_enabled)) {
+ SUBMENU(MSG_MMU2_LOAD_FILAMENT, menu_mmu2_load_filament);
+ SUBMENU(MSG_MMU2_LOAD_TO_NOZZLE, menu_mmu2_load_to_nozzle);
+ SUBMENU(MSG_MMU2_EJECT_FILAMENT, menu_mmu2_eject_filament);
+ ACTION_ITEM(MSG_MMU2_UNLOAD_FILAMENT, action_mmu2_unload_filament);
+ }
+
+ #if HAS_PRUSA_MMU3
+ // MMU3 Enable/Disable
+ #ifndef __AVR__
+ editable.state = mmu3.mmu_hw_enabled;
+ EDIT_ITEM_F(bool, F("MMU"), &mmu3.mmu_hw_enabled, []{
+ if (editable.state)
+ mmu3.stop();
+ else
+ mmu3.start();
+ });
+ #endif
+
+ // SpoolJoin Enable/Disable
+ EDIT_ITEM(bool, MSG_MMU_SPOOL_JOIN, &spooljoin.enabled, spool_join_status);
+
+ // Cutter Enable/Disable
+ bool cutter_enabled = mmu3.cutter_mode != 0;
+ editable.state = cutter_enabled;
+ EDIT_ITEM(bool, MSG_MMU_CUTTER, &cutter_enabled, []{
+ menu_mmu3_cutter_set_mode((uint8_t)!editable.state);
+ });
+ if (!busy && MMU3::cutter_enabled() && mmu3.mmu_hw_enabled) {
+ SUBMENU(MSG_MMU2_CUT_FILAMENT, menu_mmu3_cut_filament);
+ }
+
+ // Statistics
+ SUBMENU(MSG_MMU_STATISTICS, menu_mmu3_statistics);
+ #endif
+
+ if (TERN1(HAS_PRUSA_MMU3, mmu3.mmu_hw_enabled)) {
+ ACTION_ITEM(MSG_MMU2_RESET, action_mmu2_reset);
+ }
+
END_MENU();
}
@@ -138,19 +350,36 @@ void menu_mmu2_choose_filament() {
//
void menu_mmu2_pause() {
- feeder_index = mmu2.get_current_tool();
+ feeder_index = mmu3.get_current_tool();
START_MENU();
#if LCD_HEIGHT > 2
STATIC_ITEM(MSG_FILAMENT_CHANGE_HEADER, SS_DEFAULT|SS_INVERT);
#endif
ACTION_ITEM(MSG_MMU2_RESUME, []{ wait_for_mmu_menu = false; });
- ACTION_ITEM(MSG_MMU2_UNLOAD_FILAMENT, []{ mmu2.unload(); });
- ACTION_ITEM(MSG_MMU2_LOAD_FILAMENT, []{ mmu2.load_to_feeder(feeder_index); });
- ACTION_ITEM(MSG_MMU2_LOAD_TO_NOZZLE, []{ mmu2.load_to_nozzle(feeder_index); });
+ #if HAS_PRUSA_MMU3
+ ACTION_ITEM(MSG_MMU2_UNLOAD_FILAMENT, []{ mmu3.unload(); });
+ ACTION_ITEM(MSG_MMU2_LOAD_FILAMENT, []{ mmu3.load_to_feeder(feeder_index); });
+ ACTION_ITEM(MSG_MMU2_LOAD_TO_NOZZLE, []{ mmu3.load_to_nozzle(feeder_index); });
+ #else
+ ACTION_ITEM(MSG_MMU2_UNLOAD_FILAMENT, []{ mmu2.unload(); });
+ ACTION_ITEM(MSG_MMU2_LOAD_FILAMENT, []{ mmu2.load_to_feeder(feeder_index); });
+ ACTION_ITEM(MSG_MMU2_LOAD_TO_NOZZLE, []{ mmu2.load_to_nozzle(feeder_index); });
+ #endif
END_MENU();
}
-void mmu2_M600() {
+void mmu2_M600(const bool automatic/*=false*/) {
+ // Disable automatic switching if MMU3 is not enabled or spool join is disabled
+ #if HAS_PRUSA_MMU3
+ if (automatic && spooljoin.enabled) {
+ uint8_t slot;
+ slot = spooljoin.nextSlot();
+ mmu3.load_to_nozzle(slot);
+ return;
+ }
+ #else
+ UNUSED(automatic);
+ #endif
ui.defer_status_screen();
ui.goto_screen(menu_mmu2_pause);
wait_for_mmu_menu = true;
@@ -166,4 +395,4 @@ uint8_t mmu2_choose_filament() {
return feeder_index;
}
-#endif // HAS_MARLINUI_MENU && MMU2_MENUS
+#endif // MMU_MENUS
diff --git a/Marlin/src/lcd/menu/menu_mmu2.h b/Marlin/src/lcd/menu/menu_mmu2.h
index 4230c0146466..65506852fe6a 100644
--- a/Marlin/src/lcd/menu/menu_mmu2.h
+++ b/Marlin/src/lcd/menu/menu_mmu2.h
@@ -24,5 +24,5 @@
#include
void menu_mmu2();
-void mmu2_M600();
+void mmu2_M600(const bool automatic=false);
uint8_t mmu2_choose_filament();
diff --git a/Marlin/src/lcd/menu/menu_motion.cpp b/Marlin/src/lcd/menu/menu_motion.cpp
index 0965e0e48278..926d64ccca1a 100644
--- a/Marlin/src/lcd/menu/menu_motion.cpp
+++ b/Marlin/src/lcd/menu/menu_motion.cpp
@@ -44,6 +44,9 @@
#include "../../feature/bedlevel/bedlevel.h"
#endif
+// Always show configurable options regardless of FT Motion active
+//#define FT_MOTION_NO_MENU_TOGGLE
+
constexpr bool has_large_area() {
return TERN0(HAS_X_AXIS, (X_BED_SIZE) >= 1000) || TERN0(HAS_Y_AXIS, (Y_BED_SIZE) >= 1000) || TERN0(HAS_Z_AXIS, (Z_MAX_POS) >= 1000);
}
@@ -323,7 +326,6 @@ void menu_move() {
#if ENABLED(FT_MOTION_MENU)
#include "../../module/ft_motion.h"
- #include "../../gcode/gcode.h"
FSTR_P get_shaper_name(const AxisEnum axis=X_AXIS) {
switch (ftMotion.cfg.shaper[axis]) {
@@ -436,7 +438,8 @@ void menu_move() {
ftMotion.update_shaping_params();
});
- if (c.active) {
+ // Show only when FT Motion is active (or optionally always show)
+ if (c.active || ENABLED(FT_MOTION_NO_MENU_TOGGLE)) {
#if HAS_X_AXIS
SUBMENU_N(X_AXIS, MSG_FTM_CMPN_MODE, menu_ftm_shaper_x);
MENU_ITEM_ADDON_START_RJ(5); lcd_put_u8str(shaper_name[X_AXIS]); MENU_ITEM_ADDON_END();
@@ -475,7 +478,8 @@ void menu_move() {
#if HAS_EXTRUDERS
EDIT_ITEM(bool, MSG_LINEAR_ADVANCE, &c.linearAdvEna);
- if (c.linearAdvEna) EDIT_ITEM(float42_52, MSG_ADVANCE_K, &c.linearAdvK, 0, 10);
+ if (c.linearAdvEna || ENABLED(FT_MOTION_NO_MENU_TOGGLE))
+ EDIT_ITEM(float42_52, MSG_ADVANCE_K, &c.linearAdvK, 0, 10);
#endif
}
END_MENU();
@@ -492,6 +496,10 @@ void menu_move() {
MString<20> dmode = get_dyn_freq_mode_name();
#endif
+ #if HAS_EXTRUDERS
+ ft_config_t &c = ftMotion.cfg;
+ #endif
+
START_MENU();
#if HAS_X_AXIS
@@ -502,13 +510,14 @@ void menu_move() {
SUBMENU_N(Y_AXIS, MSG_FTM_CMPN_MODE, menu_ftm_shaper_y);
MENU_ITEM_ADDON_START_RJ(5); lcd_put_u8str(shaper_name[Y_AXIS]); MENU_ITEM_ADDON_END();
#endif
-
#if HAS_DYNAMIC_FREQ
SUBMENU(MSG_FTM_DYN_MODE, menu_ftm_dyn_mode);
MENU_ITEM_ADDON_START_RJ(dmode.length()); lcd_put_u8str(dmode); MENU_ITEM_ADDON_END();
#endif
#if HAS_EXTRUDERS
- EDIT_ITEM(bool, MSG_LINEAR_ADVANCE, &ftMotion.cfg.linearAdvEna);
+ EDIT_ITEM(bool, MSG_LINEAR_ADVANCE, &c.linearAdvEna);
+ if (c.linearAdvEna || ENABLED(FT_MOTION_NO_MENU_TOGGLE))
+ EDIT_ITEM(float42_52, MSG_ADVANCE_K, &c.linearAdvK, 0, 10);
#endif
END_MENU();
diff --git a/Marlin/src/module/endstops.cpp b/Marlin/src/module/endstops.cpp
index 8d6f384689a7..cb2a8f283a85 100644
--- a/Marlin/src/module/endstops.cpp
+++ b/Marlin/src/module/endstops.cpp
@@ -31,8 +31,9 @@
#include "temperature.h"
#include "../lcd/marlinui.h"
-#define DEBUG_OUT ALL(USE_SENSORLESS, DEBUG_LEVELING_FEATURE)
-#include "../core/debug_out.h"
+#if ENABLED(FT_MOTION)
+ #include "ft_motion.h"
+#endif
#if ENABLED(ENDSTOP_INTERRUPTS_FEATURE)
#include HAL_PATH(.., endstop_interrupts.h)
@@ -54,6 +55,9 @@
#include "probe.h"
#endif
+#define DEBUG_OUT ALL(USE_SENSORLESS, DEBUG_LEVELING_FEATURE)
+#include "../core/debug_out.h"
+
Endstops endstops;
// private:
@@ -830,9 +834,13 @@ void Endstops::update() {
// Signal, after validation, if an endstop limit is pressed or not
+ #define AXIS_IS_MOVING(A) TERN(FT_MOTION, ftMotion, stepper).axis_is_moving(_AXIS(A))
+ #define AXIS_DIR_REV(A) !TERN(FT_MOTION, ftMotion, stepper).motor_direction(A)
+
#if HAS_X_AXIS
- if (stepper.axis_is_moving(X_AXIS)) {
- if (!stepper.motor_direction(X_AXIS_HEAD)) {
+ if (AXIS_IS_MOVING(X)) {
+ const AxisEnum x_head = TERN0(FT_MOTION, ftMotion.cfg.active) ? X_AXIS : X_AXIS_HEAD;
+ if (AXIS_DIR_REV(x_head)) {
#if HAS_X_MIN_STATE
PROCESS_ENDSTOP_X(MIN);
#if CORE_DIAG(XY, Y, MIN)
@@ -864,8 +872,9 @@ void Endstops::update() {
#endif // HAS_X_AXIS
#if HAS_Y_AXIS
- if (stepper.axis_is_moving(Y_AXIS)) {
- if (!stepper.motor_direction(Y_AXIS_HEAD)) {
+ if (AXIS_IS_MOVING(Y)) {
+ const AxisEnum y_head = TERN0(FT_MOTION, ftMotion.cfg.active) ? Y_AXIS : Y_AXIS_HEAD;
+ if (AXIS_DIR_REV(y_head)) {
#if HAS_Y_MIN_STATE
PROCESS_ENDSTOP_Y(MIN);
#if CORE_DIAG(XY, X, MIN)
@@ -897,8 +906,9 @@ void Endstops::update() {
#endif // HAS_Y_AXIS
#if HAS_Z_AXIS
- if (stepper.axis_is_moving(Z_AXIS)) {
- if (!stepper.motor_direction(Z_AXIS_HEAD)) {
+ if (AXIS_IS_MOVING(Z)) {
+ const AxisEnum z_head = TERN0(FT_MOTION, ftMotion.cfg.active) ? Z_AXIS : Z_AXIS_HEAD;
+ if (AXIS_DIR_REV(z_head)) {
// Z- : Gantry down, bed up
#if HAS_Z_MIN_STATE
// If the Z_MIN_PIN is being used for the probe there's no
@@ -944,8 +954,8 @@ void Endstops::update() {
#endif // HAS_Z_AXIS
#if HAS_I_AXIS && HAS_I_STATE
- if (stepper.axis_is_moving(I_AXIS)) {
- if (!stepper.motor_direction(I_AXIS_HEAD)) {
+ if (AXIS_IS_MOVING(I)) {
+ if (AXIS_DIR_REV(I_AXIS_HEAD)) {
#if HAS_I_MIN_STATE
PROCESS_ENDSTOP(I, MIN);
#endif
@@ -959,8 +969,8 @@ void Endstops::update() {
#endif // HAS_I_AXIS
#if HAS_J_AXIS && HAS_J_STATE
- if (stepper.axis_is_moving(J_AXIS)) {
- if (!stepper.motor_direction(J_AXIS_HEAD)) {
+ if (AXIS_IS_MOVING(J)) {
+ if (AXIS_DIR_REV(J_AXIS_HEAD)) {
#if HAS_J_MIN_STATE
PROCESS_ENDSTOP(J, MIN);
#endif
@@ -974,8 +984,8 @@ void Endstops::update() {
#endif // HAS_J_AXIS
#if HAS_K_AXIS && HAS_K_STATE
- if (stepper.axis_is_moving(K_AXIS)) {
- if (!stepper.motor_direction(K_AXIS_HEAD)) {
+ if (AXIS_IS_MOVING(K)) {
+ if (AXIS_DIR_REV(K_AXIS_HEAD)) {
#if HAS_K_MIN_STATE
PROCESS_ENDSTOP(K, MIN);
#endif
@@ -989,8 +999,8 @@ void Endstops::update() {
#endif // HAS_K_AXIS
#if HAS_U_AXIS && HAS_U_STATE
- if (stepper.axis_is_moving(U_AXIS)) {
- if (!stepper.motor_direction(U_AXIS_HEAD)) {
+ if (AXIS_IS_MOVING(U)) {
+ if (AXIS_DIR_REV(U_AXIS_HEAD)) {
#if HAS_U_MIN_STATE
PROCESS_ENDSTOP(U, MIN);
#endif
@@ -1004,8 +1014,8 @@ void Endstops::update() {
#endif // HAS_U_AXIS
#if HAS_V_AXIS && HAS_V_STATE
- if (stepper.axis_is_moving(V_AXIS)) {
- if (!stepper.motor_direction(V_AXIS_HEAD)) {
+ if (AXIS_IS_MOVING(V)) {
+ if (AXIS_DIR_REV(V_AXIS_HEAD)) {
#if HAS_V_MIN_STATE
PROCESS_ENDSTOP(V, MIN);
#endif
@@ -1019,8 +1029,8 @@ void Endstops::update() {
#endif // HAS_V_AXIS
#if HAS_W_AXIS && HAS_W_STATE
- if (stepper.axis_is_moving(W_AXIS)) {
- if (!stepper.motor_direction(W_AXIS_HEAD)) {
+ if (AXIS_IS_MOVING(W)) {
+ if (AXIS_DIR_REV(W_AXIS_HEAD)) {
#if HAS_W_MIN_STATE
PROCESS_ENDSTOP(W, MIN);
#endif
diff --git a/Marlin/src/module/ft_motion.cpp b/Marlin/src/module/ft_motion.cpp
index fc8b6a14e37c..d63ca9f25df2 100644
--- a/Marlin/src/module/ft_motion.cpp
+++ b/Marlin/src/module/ft_motion.cpp
@@ -44,6 +44,8 @@ int32_t FTMotion::stepperCmdBuff_produceIdx = 0, // Index of next stepper comman
bool FTMotion::sts_stepperBusy = false; // The stepper buffer has items and is in use.
+XYZEval FTMotion::axis_move_end_ti = { 0 };
+AxisBits FTMotion::axis_move_dir;
// Private variables.
@@ -88,12 +90,14 @@ xyze_long_t FTMotion::steps = { 0 }; // Step count accumulator.
uint32_t FTMotion::interpIdx = 0; // Index of current data point being interpolated.
// Shaping variables.
-#if HAS_X_AXIS
+#if HAS_FTM_SHAPING
FTMotion::shaping_t FTMotion::shaping = {
0,
- x:{ false, { 0.0f }, { 0.0f }, { 0 }, { 0 } }, // ena, d_zi, Ai, Ni, max_i
+ #if HAS_X_AXIS
+ x:{ false, { 0.0f }, { 0.0f }, { 0 }, 0 } // ena, d_zi[], Ai[], Ni[], max_i
+ #endif
#if HAS_Y_AXIS
- y:{ false, { 0.0f }, { 0.0f }, { 0 }, { 0 } } // ena, d_zi, Ai, Ni, max_i
+ y:{ false, { 0.0f }, { 0.0f }, { 0 }, 0 } // ena, d_zi[], Ai[], Ni[], max_i
#endif
};
#endif
@@ -112,8 +116,6 @@ constexpr uint32_t BATCH_SIDX_IN_WINDOW = (FTM_WINDOW_SIZE) - (FTM_BATCH_SIZE);
// Public functions.
-static bool markBlockStart = false;
-
// Controller main, to be invoked from non-isr task.
void FTMotion::loop() {
@@ -141,7 +143,6 @@ void FTMotion::loop() {
continue;
}
loadBlockData(stepper.current_block);
- markBlockStart = true;
blockProcRdy = true;
// Some kinematics track axis motion in HX, HY, HZ
#if ANY(CORE_IS_XY, CORE_IS_XZ, MARKFORGED_XY, MARKFORGED_YX)
@@ -211,7 +212,7 @@ void FTMotion::loop() {
}
-#if HAS_X_AXIS
+#if HAS_FTM_SHAPING
// Refresh the gains used by shaping functions.
void FTMotion::AxisShaping::set_axis_shaping_A(const ftMotionShaper_t shaper, const_float_t zeta, const_float_t vtol) {
@@ -362,7 +363,7 @@ void FTMotion::loop() {
#endif
}
-#endif // HAS_X_AXIS
+#endif // HAS_FTM_SHAPING
// Reset all trajectory processing variables.
void FTMotion::reset() {
@@ -383,13 +384,15 @@ void FTMotion::reset() {
stepper.axis_did_move.reset();
- #if HAS_X_AXIS
- ZERO(shaping.x.d_zi);
+ #if HAS_FTM_SHAPING
+ TERN_(HAS_X_AXIS, ZERO(shaping.x.d_zi));
TERN_(HAS_Y_AXIS, ZERO(shaping.y.d_zi));
shaping.zi_idx = 0;
#endif
TERN_(HAS_EXTRUDERS, e_raw_z1 = e_advanced_z1 = 0.0f);
+
+ axis_move_end_ti.reset();
}
// Private functions.
@@ -429,7 +432,11 @@ void FTMotion::runoutBlock() {
const int32_t n_to_settle_and_fill_batch = n_to_settle_shaper + n_to_fill_batch_after_settling;
- max_intervals = (PROP_BATCHES) * (FTM_BATCH_SIZE) + n_to_settle_and_fill_batch;
+ const int32_t N_needed_to_propagate_to_stepper = PROP_BATCHES;
+
+ const int32_t n_to_use = N_needed_to_propagate_to_stepper * (FTM_BATCH_SIZE) + n_to_settle_and_fill_batch;
+
+ max_intervals = n_to_use;
blockProcRdy = true;
}
@@ -549,6 +556,26 @@ void FTMotion::loadBlockData(block_t * const current_block) {
endPosn_prevBlock += moveDist;
+ // Watch endstops until the move ends
+ const millis_t move_end_ti = millis() + SEC_TO_MS((FTM_TS) * float(max_intervals + num_samples_shaper_settle() + ((PROP_BATCHES) + 1) * (FTM_BATCH_SIZE)) + (float(FTM_STEPPERCMD_BUFF_SIZE) / float(FTM_STEPPER_FS)));
+
+ #define __SET_MOVE_END(A,V) do{ if (V) { axis_move_end_ti.A = move_end_ti; axis_move_dir.A = (V > 0); } }while(0);
+ #define _SET_MOVE_END(A) __SET_MOVE_END(A, moveDist[_AXIS(A)])
+ #if CORE_IS_XY
+ __SET_MOVE_END(X, moveDist.x + moveDist.y);
+ __SET_MOVE_END(Y, moveDist.x - moveDist.y);
+ #else
+ _SET_MOVE_END(X);
+ _SET_MOVE_END(Y);
+ #endif
+ TERN_(HAS_Z_AXIS, _SET_MOVE_END(Z));
+ SECONDARY_AXIS_MAP(_SET_MOVE_END);
+
+ // If the endstop is already pressed, endstop interrupts won't invoke
+ // endstop_triggered and the move will grind. So check here for a
+ // triggered endstop, which shortly marks the block for discard.
+ endstops.update();
+
}
// Generate data points of the trajectory.
@@ -565,6 +592,7 @@ void FTMotion::makeVector() {
else if (makeVector_idx < (N1 + N2)) {
// Coasting phase
dist = s_1e + F_P * (tau - N1 * (FTM_TS)); // (mm) Distance traveled for coasting phase since start of block
+ //accel_k = 0.0f;
}
else {
// Deceleration phase
@@ -626,15 +654,17 @@ void FTMotion::makeVector() {
}
// Apply shaping if active on each axis
- #if HAS_X_AXIS
- if (shaping.x.ena) {
- shaping.x.d_zi[shaping.zi_idx] = traj.x[makeVector_batchIdx];
- traj.x[makeVector_batchIdx] *= shaping.x.Ai[0];
- for (uint32_t i = 1U; i <= shaping.x.max_i; i++) {
- const uint32_t udiffx = shaping.zi_idx - shaping.x.Ni[i];
- traj.x[makeVector_batchIdx] += shaping.x.Ai[i] * shaping.x.d_zi[shaping.x.Ni[i] > shaping.zi_idx ? (FTM_ZMAX) + udiffx : udiffx];
+ #if HAS_FTM_SHAPING
+ #if HAS_X_AXIS
+ if (shaping.x.ena) {
+ shaping.x.d_zi[shaping.zi_idx] = traj.x[makeVector_batchIdx];
+ traj.x[makeVector_batchIdx] *= shaping.x.Ai[0];
+ for (uint32_t i = 1U; i <= shaping.x.max_i; i++) {
+ const uint32_t udiffx = shaping.zi_idx - shaping.x.Ni[i];
+ traj.x[makeVector_batchIdx] += shaping.x.Ai[i] * shaping.x.d_zi[shaping.x.Ni[i] > shaping.zi_idx ? (FTM_ZMAX) + udiffx : udiffx];
+ }
}
- }
+ #endif
#if HAS_Y_AXIS
if (shaping.y.ena) {
@@ -647,7 +677,7 @@ void FTMotion::makeVector() {
}
#endif
if (++shaping.zi_idx == (FTM_ZMAX)) shaping.zi_idx = 0;
- #endif // HAS_X_AXIS
+ #endif // HAS_FTM_SHAPING
// Filled up the queue with regular and shaped steps
if (++makeVector_batchIdx == FTM_WINDOW_SIZE) {
@@ -717,12 +747,6 @@ void FTMotion::convertToSteps(const uint32_t idx) {
// Init all step/dir bits to 0 (defaulting to reverse/negative motion)
cmd = 0;
- // Mark the start of a new block
- if (markBlockStart) {
- cmd = _BV(FT_BIT_START);
- markBlockStart = false;
- }
-
// Accumulate the errors for all axes
err_P += delta;
diff --git a/Marlin/src/module/ft_motion.h b/Marlin/src/module/ft_motion.h
index 8d60552d9069..bff3ba1fd423 100644
--- a/Marlin/src/module/ft_motion.h
+++ b/Marlin/src/module/ft_motion.h
@@ -23,6 +23,7 @@
#include "../inc/MarlinConfigPre.h" // Access the top level configurations.
#include "../module/planner.h" // Access block type from planner.
+#include "../module/stepper.h" // For stepper motion and direction
#include "ft_types.h"
@@ -39,23 +40,23 @@
typedef struct FTConfig {
bool active = ENABLED(FTM_IS_DEFAULT_MOTION); // Active (else standard motion)
- #if HAS_X_AXIS
+ #if HAS_FTM_SHAPING
ft_shaped_shaper_t shaper = // Shaper type
{ SHAPED_ELEM(FTM_DEFAULT_SHAPER_X, FTM_DEFAULT_SHAPER_Y) };
ft_shaped_float_t baseFreq = // Base frequency. [Hz]
- { SHAPED_ELEM(FTM_SHAPING_DEFAULT_X_FREQ, FTM_SHAPING_DEFAULT_Y_FREQ) };
+ { SHAPED_ELEM(FTM_SHAPING_DEFAULT_FREQ_X, FTM_SHAPING_DEFAULT_FREQ_Y) };
ft_shaped_float_t zeta = // Damping factor
{ SHAPED_ELEM(FTM_SHAPING_ZETA_X, FTM_SHAPING_ZETA_Y) };
ft_shaped_float_t vtol = // Vibration Level
{ SHAPED_ELEM(FTM_SHAPING_V_TOL_X, FTM_SHAPING_V_TOL_Y) };
- #endif
- #if HAS_DYNAMIC_FREQ
- dynFreqMode_t dynFreqMode = FTM_DEFAULT_DYNFREQ_MODE; // Dynamic frequency mode configuration.
- ft_shaped_float_t dynFreqK = { 0.0f }; // Scaling / gain for dynamic frequency. [Hz/mm] or [Hz/g]
- #else
- static constexpr dynFreqMode_t dynFreqMode = dynFreqMode_DISABLED;
- #endif
+ #if HAS_DYNAMIC_FREQ
+ dynFreqMode_t dynFreqMode = FTM_DEFAULT_DYNFREQ_MODE; // Dynamic frequency mode configuration.
+ ft_shaped_float_t dynFreqK = { 0.0f }; // Scaling / gain for dynamic frequency. [Hz/mm] or [Hz/g]
+ #else
+ static constexpr dynFreqMode_t dynFreqMode = dynFreqMode_DISABLED;
+ #endif
+ #endif // HAS_FTM_SHAPING
#if HAS_EXTRUDERS
bool linearAdvEna = FTM_LINEAR_ADV_DEFAULT_ENA; // Linear advance enable configuration.
@@ -74,33 +75,37 @@ class FTMotion {
static void set_defaults() {
cfg.active = ENABLED(FTM_IS_DEFAULT_MOTION);
- #if HAS_X_AXIS
- cfg.shaper.x = FTM_DEFAULT_SHAPER_X;
- cfg.baseFreq.x = FTM_SHAPING_DEFAULT_X_FREQ;
- cfg.zeta.x = FTM_SHAPING_ZETA_X;
- cfg.vtol.x = FTM_SHAPING_V_TOL_X;
- #endif
+ #if HAS_FTM_SHAPING
- #if HAS_Y_AXIS
- cfg.shaper.y = FTM_DEFAULT_SHAPER_Y;
- cfg.baseFreq.y = FTM_SHAPING_DEFAULT_Y_FREQ;
- cfg.zeta.y = FTM_SHAPING_ZETA_Y;
- cfg.vtol.y = FTM_SHAPING_V_TOL_Y;
- #endif
+ #if HAS_X_AXIS
+ cfg.shaper.x = FTM_DEFAULT_SHAPER_X;
+ cfg.baseFreq.x = FTM_SHAPING_DEFAULT_FREQ_X;
+ cfg.zeta.x = FTM_SHAPING_ZETA_X;
+ cfg.vtol.x = FTM_SHAPING_V_TOL_X;
+ #endif
- #if HAS_DYNAMIC_FREQ
- cfg.dynFreqMode = FTM_DEFAULT_DYNFREQ_MODE;
- TERN_(HAS_X_AXIS, cfg.dynFreqK.x = 0.0f);
- TERN_(HAS_Y_AXIS, cfg.dynFreqK.y = 0.0f);
- #endif
+ #if HAS_Y_AXIS
+ cfg.shaper.y = FTM_DEFAULT_SHAPER_Y;
+ cfg.baseFreq.y = FTM_SHAPING_DEFAULT_FREQ_Y;
+ cfg.zeta.y = FTM_SHAPING_ZETA_Y;
+ cfg.vtol.y = FTM_SHAPING_V_TOL_Y;
+ #endif
+
+ #if HAS_DYNAMIC_FREQ
+ cfg.dynFreqMode = FTM_DEFAULT_DYNFREQ_MODE;
+ TERN_(HAS_X_AXIS, cfg.dynFreqK.x = 0.0f);
+ TERN_(HAS_Y_AXIS, cfg.dynFreqK.y = 0.0f);
+ #endif
+
+ update_shaping_params();
+
+ #endif // HAS_FTM_SHAPING
#if HAS_EXTRUDERS
cfg.linearAdvEna = FTM_LINEAR_ADV_DEFAULT_ENA;
cfg.linearAdvK = FTM_LINEAR_ADV_DEFAULT_K;
#endif
- TERN_(HAS_X_AXIS, update_shaping_params());
-
reset();
}
@@ -110,17 +115,27 @@ class FTMotion {
static bool sts_stepperBusy; // The stepper buffer has items and is in use.
+ static XYZEval axis_move_end_ti;
+ static AxisBits axis_move_dir;
+
// Public methods
static void init();
static void loop(); // Controller main, to be invoked from non-isr task.
- #if HAS_X_AXIS
+ #if HAS_FTM_SHAPING
// Refresh gains and indices used by shaping functions.
static void update_shaping_params(void);
#endif
static void reset(); // Reset all states of the fixed time conversion to defaults.
+ FORCE_INLINE static bool axis_is_moving(const AxisEnum axis) {
+ return cfg.active ? PENDING(millis(), axis_move_end_ti[axis]) : stepper.axis_is_moving(axis);
+ }
+ FORCE_INLINE static bool motor_direction(const AxisEnum axis) {
+ return cfg.active ? axis_move_dir[axis] : stepper.last_direction_bits[axis];
+ }
+
private:
static xyze_trajectory_t traj;
@@ -155,7 +170,7 @@ class FTMotion {
static xyze_long_t steps;
// Shaping variables.
- #if HAS_X_AXIS
+ #if HAS_FTM_SHAPING
typedef struct AxisShaping {
bool ena = false; // Enabled indication.
@@ -171,16 +186,17 @@ class FTMotion {
typedef struct Shaping {
uint32_t zi_idx; // Index of storage in the data point delay vectors.
- axis_shaping_t x;
+ #if HAS_X_AXIS
+ axis_shaping_t x;
+ #endif
#if HAS_Y_AXIS
axis_shaping_t y;
#endif
-
} shaping_t;
static shaping_t shaping; // Shaping data
- #endif // HAS_X_AXIS
+ #endif // HAS_FTM_SHAPING
// Linear advance variables.
#if HAS_EXTRUDERS
diff --git a/Marlin/src/module/ft_types.h b/Marlin/src/module/ft_types.h
index 7659974409d4..1f7277b37283 100644
--- a/Marlin/src/module/ft_types.h
+++ b/Marlin/src/module/ft_types.h
@@ -49,7 +49,6 @@ typedef struct XYZEarray xyze_trajectoryMod_t;
// TODO: Convert ft_command_t to a struct with bitfields instead of using a primitive type
enum {
- FT_BIT_START,
LIST_N(DOUBLE(LOGICAL_AXES),
FT_BIT_DIR_E, FT_BIT_STEP_E,
FT_BIT_DIR_X, FT_BIT_STEP_X, FT_BIT_DIR_Y, FT_BIT_STEP_Y, FT_BIT_DIR_Z, FT_BIT_STEP_Z,
@@ -59,8 +58,13 @@ enum {
FT_BIT_COUNT
};
-#define NUM_AXES_SHAPED TERN(HAS_Y_AXIS, 2, 1)
-#define SHAPED_ELEM(A, B) A OPTARG(HAS_Y_AXIS, B)
+#if HAS_FTM_SHAPING
+ #define NUM_AXES_SHAPED TERN(HAS_Y_AXIS, 2, 1)
+ #define SHAPED_ELEM(A, B) A OPTARG(HAS_Y_AXIS, B)
+#else
+ #define NUM_AXES_SHAPED 0
+ #define SHAPED_ELEM(A, B)
+#endif
template
struct FTShapedAxes {
diff --git a/Marlin/src/module/motion.cpp b/Marlin/src/module/motion.cpp
index d79298874068..bdf99b0ed5d9 100644
--- a/Marlin/src/module/motion.cpp
+++ b/Marlin/src/module/motion.cpp
@@ -1125,7 +1125,7 @@ void do_blocking_move_to(NUM_AXIS_ARGS_(const_float_t) const_feedRate_t fr_mm_s/
if (DEBUGGING(LEVELING)) DEBUG_XYZ("> ", NUM_AXIS_ARGS_LC());
#endif
- const feedRate_t xy_feedrate = fr_mm_s ?: feedRate_t(PLANNER_XY_FEEDRATE_MM_S);
+ const feedRate_t xy_feedrate = fr_mm_s ?: feedRate_t(XY_PROBE_FEEDRATE_MM_S);
#if HAS_Z_AXIS
const feedRate_t z_feedrate = fr_mm_s ?: homing_feedrate(Z_AXIS);
@@ -1213,7 +1213,8 @@ void do_blocking_move_to(NUM_AXIS_ARGS_(const_float_t) const_feedRate_t fr_mm_s/
}
void do_blocking_move_to(const xy_pos_t &raw, const_feedRate_t fr_mm_s/*=0.0f*/) {
- do_blocking_move_to(NUM_AXIS_LIST_(raw.x, raw.y, current_position.z, current_position.i, current_position.j, current_position.k,
+ do_blocking_move_to(NUM_AXIS_LIST_(raw.x, raw.y, current_position.z,
+ current_position.i, current_position.j, current_position.k,
current_position.u, current_position.v, current_position.w) fr_mm_s);
}
void do_blocking_move_to(const xyz_pos_t &raw, const_feedRate_t fr_mm_s/*=0.0f*/) {
@@ -1227,7 +1228,8 @@ void do_blocking_move_to(const xyze_pos_t &raw, const_feedRate_t fr_mm_s/*=0.0f*
void do_blocking_move_to_x(const_float_t rx, const_feedRate_t fr_mm_s/*=0.0*/) {
if (DEBUGGING(LEVELING)) DEBUG_ECHOLNPGM("do_blocking_move_to_x(", rx, ", ", fr_mm_s, ")");
do_blocking_move_to(
- NUM_AXIS_LIST_(rx, current_position.y, current_position.z, current_position.i, current_position.j, current_position.k,
+ NUM_AXIS_LIST_(rx, current_position.y, current_position.z,
+ current_position.i, current_position.j, current_position.k,
current_position.u, current_position.v, current_position.w)
fr_mm_s
);
@@ -1238,7 +1240,8 @@ void do_blocking_move_to(const xyze_pos_t &raw, const_feedRate_t fr_mm_s/*=0.0f*
void do_blocking_move_to_y(const_float_t ry, const_feedRate_t fr_mm_s/*=0.0*/) {
if (DEBUGGING(LEVELING)) DEBUG_ECHOLNPGM("do_blocking_move_to_y(", ry, ", ", fr_mm_s, ")");
do_blocking_move_to(
- NUM_AXIS_LIST_(current_position.x, ry, current_position.z, current_position.i, current_position.j, current_position.k,
+ NUM_AXIS_LIST_(current_position.x, ry, current_position.z,
+ current_position.i, current_position.j, current_position.k,
current_position.u, current_position.v, current_position.w)
fr_mm_s
);
@@ -1246,7 +1249,8 @@ void do_blocking_move_to(const xyze_pos_t &raw, const_feedRate_t fr_mm_s/*=0.0f*
void do_blocking_move_to_xy(const_float_t rx, const_float_t ry, const_feedRate_t fr_mm_s/*=0.0*/) {
if (DEBUGGING(LEVELING)) DEBUG_ECHOLNPGM("do_blocking_move_to_xy(", rx, ", ", ry, ", ", fr_mm_s, ")");
do_blocking_move_to(
- NUM_AXIS_LIST_(rx, ry, current_position.z, current_position.i, current_position.j, current_position.k,
+ NUM_AXIS_LIST_(rx, ry, current_position.z,
+ current_position.i, current_position.j, current_position.k,
current_position.u, current_position.v, current_position.w)
fr_mm_s
);
@@ -1263,7 +1267,8 @@ void do_blocking_move_to(const xyze_pos_t &raw, const_feedRate_t fr_mm_s/*=0.0f*
}
void do_blocking_move_to_xy_z(const xy_pos_t &raw, const_float_t z, const_feedRate_t fr_mm_s/*=0.0f*/) {
do_blocking_move_to(
- NUM_AXIS_LIST_(raw.x, raw.y, z, current_position.i, current_position.j, current_position.k,
+ NUM_AXIS_LIST_(raw.x, raw.y, z,
+ current_position.i, current_position.j, current_position.k,
current_position.u, current_position.v, current_position.w)
fr_mm_s
);
diff --git a/Marlin/src/module/settings.cpp b/Marlin/src/module/settings.cpp
index 92f08969df6e..52057ceab51e 100644
--- a/Marlin/src/module/settings.cpp
+++ b/Marlin/src/module/settings.cpp
@@ -178,6 +178,12 @@
#include "../feature/hotend_idle.h"
#endif
+#if HAS_PRUSA_MMU3
+ #include "../feature/mmu3/mmu2.h"
+ #include "../feature/mmu3/SpoolJoin.h"
+ #include "../feature/mmu3/mmu2_reporting.h"
+#endif
+
#pragma pack(push, 1) // No padding between variables
#if HAS_ETHERNET
@@ -653,6 +659,23 @@ typedef struct SettingsDataStruct {
ne_coeff_t stepper_ne; // M592 A B C
#endif
+ //
+ // MMU3
+ //
+ #if HAS_PRUSA_MMU3
+ bool spool_join_enabled; // EEPROM_SPOOL_JOIN
+ uint16_t fail_total_num; // EEPROM_MMU_FAIL_TOT
+ uint8_t fail_num; // EEPROM_MMU_FAIL
+ uint16_t load_fail_total_num; // EEPROM_MMU_LOAD_FAIL_TOT
+ uint8_t load_fail_num; // EEPROM_MMU_LOAD_FAIL
+ uint16_t tool_change_counter;
+ uint32_t tool_change_total_counter; // EEPROM_MMU_MATERIAL_CHANGES
+ uint8_t cutter_mode; // EEPROM_MMU_CUTTER_ENABLED
+ uint8_t stealth_mode; // EEPROM_MMU_STEALTH
+ bool mmu_hw_enabled; // EEPROM_MMU_ENABLED
+ // uint32_t material_changes
+ #endif
+
} SettingsData;
//static_assert(sizeof(SettingsData) <= MARLIN_EEPROM_SIZE, "EEPROM too small to contain SettingsData!");
@@ -1760,6 +1783,23 @@ void MarlinSettings::postprocess() {
EEPROM_WRITE(stepper.ne);
#endif
+ //
+ // MMU3
+ //
+ #if HAS_PRUSA_MMU3
+ EEPROM_WRITE(spooljoin.enabled); // EEPROM_SPOOL_JOIN
+ // for testing purposes fill with default values
+ EEPROM_WRITE(MMU3::operation_statistics.fail_total_num); //EEPROM_MMU_FAIL_TOT
+ EEPROM_WRITE(MMU3::operation_statistics.fail_num); // EEPROM_MMU_FAIL
+ EEPROM_WRITE(MMU3::operation_statistics.load_fail_total_num); // EEPROM_MMU_LOAD_FAIL_TOT
+ EEPROM_WRITE(MMU3::operation_statistics.load_fail_num); // EEPROM_MMU_LOAD_FAIL
+ EEPROM_WRITE(MMU3::operation_statistics.tool_change_counter);
+ EEPROM_WRITE(MMU3::operation_statistics.tool_change_total_counter); // EEPROM_MMU_MATERIAL_CHANGES
+ EEPROM_WRITE(mmu3.cutter_mode); // EEPROM_MMU_CUTTER_ENABLED
+ EEPROM_WRITE(mmu3.stealth_mode); // EEPROM_MMU_STEALTH
+ EEPROM_WRITE(mmu3.mmu_hw_enabled); // EEPROM_MMU_ENABLED
+ #endif
+
//
// Report final CRC and Data Size
//
@@ -2881,6 +2921,41 @@ void MarlinSettings::postprocess() {
EEPROM_READ(stepper.ne);
#endif
+ //
+ // MMU3
+ //
+ #if HAS_PRUSA_MMU3
+ spooljoin.epprom_addr = eeprom_index;
+ EEPROM_READ(spooljoin.enabled); // EEPROM_SPOOL_JOIN
+
+ MMU3::operation_statistics.fail_total_num_addr = eeprom_index;
+ EEPROM_READ(MMU3::operation_statistics.fail_total_num); //EEPROM_MMU_FAIL_TOT
+
+ MMU3::operation_statistics.fail_num_addr = eeprom_index;
+ EEPROM_READ(MMU3::operation_statistics.fail_num); // EEPROM_MMU_FAIL;
+
+ MMU3::operation_statistics.load_fail_total_num_addr = eeprom_index;
+ EEPROM_READ(MMU3::operation_statistics.load_fail_total_num); // EEPROM_MMU_LOAD_FAIL_TOT
+
+ MMU3::operation_statistics.load_fail_num_addr = eeprom_index;
+ EEPROM_READ(MMU3::operation_statistics.load_fail_num); // EEPROM_MMU_LOAD_FAIL
+
+ MMU3::operation_statistics.tool_change_counter_addr = eeprom_index;
+ EEPROM_READ(MMU3::operation_statistics.tool_change_counter);
+
+ MMU3::operation_statistics.tool_change_total_counter_addr = eeprom_index;
+ EEPROM_READ(MMU3::operation_statistics.tool_change_total_counter); // EEPROM_MMU_MATERIAL_CHANGES
+
+ mmu3.cutter_mode_addr = eeprom_index;
+ EEPROM_READ(mmu3.cutter_mode); // EEPROM_MMU_CUTTER_ENABLED
+
+ mmu3.stealth_mode_addr = eeprom_index;
+ EEPROM_READ(mmu3.stealth_mode); // EEPROM_MMU_STEALTH
+
+ mmu3.mmu_hw_enabled_addr = eeprom_index;
+ EEPROM_READ(mmu3.mmu_hw_enabled); // EEPROM_MMU_ENABLED
+ #endif
+
//
// Validate Final Size and CRC
//
@@ -3743,6 +3818,17 @@ void MarlinSettings::reset() {
#endif
#endif
+ //
+ // MMU Settings
+ //
+ #if HAS_PRUSA_MMU3
+ spooljoin.enabled = false;
+ MMU3::operation_statistics.reset_stats();
+ mmu3.cutter_mode = 0;
+ mmu3.stealth_mode = 0;
+ mmu3.mmu_hw_enabled = true;
+ #endif
+
//
// Hotend Idle Timeout
//
@@ -4062,6 +4148,11 @@ void MarlinSettings::reset() {
// Model predictive control
//
TERN_(MPCTEMP, gcode.M306_report(forReplay));
+
+ //
+ // MMU3
+ //
+ TERN_(HAS_PRUSA_MMU3, gcode.MMU3_report(forReplay));
}
#endif // !DISABLE_M503
diff --git a/Marlin/src/module/stepper.cpp b/Marlin/src/module/stepper.cpp
index 2b5a75eff840..400be38181da 100644
--- a/Marlin/src/module/stepper.cpp
+++ b/Marlin/src/module/stepper.cpp
@@ -1531,6 +1531,7 @@ void Stepper::isr() {
uint8_t max_loops = 10;
#if ENABLED(FT_MOTION)
+ static uint32_t ftMotion_nextAuxISR = 0U; // Storage for the next ISR of the auxilliary tasks.
const bool using_ftMotion = ftMotion.cfg.active;
#else
constexpr bool using_ftMotion = false;
@@ -1550,21 +1551,15 @@ void Stepper::isr() {
if (!nextMainISR) { // Main ISR is ready to fire during this iteration?
nextMainISR = FTM_MIN_TICKS; // Set to minimum interval (a limit on the top speed)
ftMotion_stepper(); // Run FTM Stepping
- }
-
- #if ENABLED(BABYSTEPPING)
- if (nextBabystepISR == 0) { // Avoid ANY stepping too soon after baby-stepping
- nextBabystepISR = babystepping_isr();
- NOLESS(nextMainISR, (BABYSTEP_TICKS) / 8); // FULL STOP for 125µs after a baby-step
+ // Define 2.5 msec task for auxilliary functions.
+ if (!ftMotion_nextAuxISR) {
+ TERN_(BABYSTEPPING, if (babystep.has_steps()) babystepping_isr());
+ ftMotion_nextAuxISR = (STEPPER_TIMER_RATE) / 400;
}
- if (nextBabystepISR != BABYSTEP_NEVER) // Avoid baby-stepping too close to axis Stepping
- NOLESS(nextBabystepISR, nextMainISR / 2); // TODO: Only look at axes enabled for baby-stepping
- #endif
-
- interval = nextMainISR; // Interval is either some old nextMainISR or FTM_MIN_TICKS
- TERN_(BABYSTEPPING, NOMORE(interval, nextBabystepISR)); // Come back early for Babystepping?
-
- nextMainISR = 0; // For FT Motion fire again ASAP
+ }
+ interval = _MIN(nextMainISR, ftMotion_nextAuxISR);
+ nextMainISR -= interval;
+ ftMotion_nextAuxISR -= interval;
}
#endif
@@ -3537,15 +3532,6 @@ void Stepper::report_positions() {
#define _FTM_STEP(AXIS) TEST(command, FT_BIT_STEP_##AXIS)
#define _FTM_DIR(AXIS) TEST(command, FT_BIT_DIR_##AXIS)
- /**
- * Set bits in axis_did_move for any axes moving in this block,
- * clearing the bits at the start of each new segment.
- */
- if (TEST(command, FT_BIT_START)) axis_did_move.reset();
-
- #define _FTM_AXIS_DID_MOVE(AXIS) axis_did_move.bset(_AXIS(AXIS), _FTM_STEP(AXIS));
- LOGICAL_AXIS_MAP(_FTM_AXIS_DID_MOVE);
-
/**
* Update direction bits for steppers that were stepped by this command.
* HX, HY, HZ direction bits were set for Core kinematics
@@ -3607,11 +3593,6 @@ void Stepper::report_positions() {
#define _FTM_STEP_STOP(AXIS) AXIS##_APPLY_STEP(!STEP_STATE_##AXIS, false);
LOGICAL_AXIS_MAP(_FTM_STEP_STOP);
- // Check endstops on every step using axis_did_move as set by every step
- // TODO: Update endstop states less frequently to save processing.
- // NOTE: endstops.poll is still called at 1KHz by Temperature ISR.
- IF_DISABLED(ENDSTOP_INTERRUPTS_FEATURE, if ((bool)axis_did_move) endstops.update());
-
} // Stepper::ftMotion_stepper
#endif // FT_MOTION
diff --git a/Marlin/src/module/stepper.h b/Marlin/src/module/stepper.h
index e5a4249dbd16..208303f47fbe 100644
--- a/Marlin/src/module/stepper.h
+++ b/Marlin/src/module/stepper.h
@@ -59,7 +59,7 @@
#define E_STATES EXTRUDERS // All steppers are set together for each mixer. (Currently limited to 1.)
#elif HAS_SWITCHING_EXTRUDER
#define E_STATES E_STEPPERS // One stepper for every two EXTRUDERS. The last extruder can be non-switching.
-#elif HAS_PRUSA_MMU2
+#elif HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3
#define E_STATES E_STEPPERS // One E stepper shared with all EXTRUDERS, so setting any only sets one.
#else
#define E_STATES E_STEPPERS // One stepper for each extruder, so each can be disabled individually.
diff --git a/Marlin/src/module/stepper/indirection.h b/Marlin/src/module/stepper/indirection.h
index 1ffeb8b907aa..4c83cbd6a66b 100644
--- a/Marlin/src/module/stepper/indirection.h
+++ b/Marlin/src/module/stepper/indirection.h
@@ -571,7 +571,7 @@ void reset_stepper_drivers(); // Called by settings.load / settings.reset
#define TOOL_ESTEPPER(T) ((T) >> 1)
-#elif HAS_PRUSA_MMU2 // One multiplexed stepper driver
+#elif HAS_PRUSA_MMU2 || HAS_PRUSA_MMU3 // One multiplexed stepper driver
#define E_STEP_WRITE(E,V) E0_STEP_WRITE(V)
#define FWD_E_DIR(E) E0_DIR_WRITE(HIGH)
diff --git a/Marlin/src/module/tool_change.cpp b/Marlin/src/module/tool_change.cpp
index b5cacc2f3bb9..80ebe71b2af4 100644
--- a/Marlin/src/module/tool_change.cpp
+++ b/Marlin/src/module/tool_change.cpp
@@ -76,10 +76,12 @@
#include "../feature/fanmux.h"
#endif
-#if HAS_PRUSA_MMU1
- #include "../feature/mmu/mmu.h"
+#if HAS_PRUSA_MMU3
+ #include "../feature/mmu3/mmu2.h"
#elif HAS_PRUSA_MMU2
#include "../feature/mmu/mmu2.h"
+#elif HAS_PRUSA_MMU1
+ #include "../feature/mmu/mmu.h"
#endif
#if HAS_MARLINUI_MENU
@@ -1122,6 +1124,12 @@ void tool_change(const uint8_t new_tool, bool no_move/*=false*/) {
mixer.T(new_tool);
#endif
+ #elif HAS_PRUSA_MMU3
+
+ UNUSED(no_move);
+
+ mmu3.tool_change(new_tool);
+
#elif HAS_PRUSA_MMU2
UNUSED(no_move);
diff --git a/buildroot/bin/build_all_examples b/buildroot/bin/build_all_examples
index a77259bb360f..13f976c787a1 100755
--- a/buildroot/bin/build_all_examples
+++ b/buildroot/bin/build_all_examples
@@ -2,16 +2,17 @@
#
# Usage:
#
-# build_all_examples [-b|--branch=] - Branch to fetch from Configurations repo
+# build_all_examples [-b|--branch=] - Branch to fetch from Configurations repo (import-2.1.x)
# [-c|--continue] - Continue the paused build
-# [-d|--debug] - Print extra debug output
-# [-i|--ini] - Archive ini/json/yml files in the temp config folder
-# [-l|--limit=#] - Limit the number of builds in this run
-# [-n|--nobuild] - Don't actually build anything.
+# [-p|--purge] - Purge the status file and start over
+# [-s|--skip] - Continue the paused build, skipping one
# [-r|--resume=] - Start at some config in the filesystem order
-# [-s|--skip] - Do the thing
-#
-# build_all_examples [...] branch [resume-from]
+# [-e|--export=N] - Set CONFIG_EXPORT and export into each config folder
+# [-d|--debug] - Print extra debug output (after)
+# [-l|--limit=#] - Limit the number of builds in this run
+# [-n|--nobuild] - Don't actually build anything
+# [-f|--nofail] - Don't stop on a failed build
+# [-h|--help] - Print usage and exit
#
HERE=`dirname $0`
@@ -21,12 +22,19 @@ HERE=`dirname $0`
GITREPO=https://github.com/MarlinFirmware/Configurations.git
STAT_FILE=./.pio/.buildall
-usage() { echo "
-Usage: $SELF [-b|--branch=] [-d|--debug] [-i|--ini] [-r|--resume=]
- $SELF [-b|--branch=] [-d|--debug] [-i|--ini] [-c|--continue]
- $SELF [-b|--branch=] [-d|--debug] [-i|--ini] [-s|--skip]
- $SELF [-b|--branch=] [-d|--debug] [-n|--nobuild]
- $SELF [...] branch [resume-point]
+usage() { echo "Usage:
+
+build_all_examples [-b|--branch=] - Branch to fetch from Configurations repo (import-2.1.x)
+ [-c|--continue] - Continue the paused build
+ [-p|--purge] - Purge the status file and start over
+ [-s|--skip] - Continue the paused build, skipping one
+ [-r|--resume=] - Start at some config in the filesystem order
+ [-e|--export=N] - Set CONFIG_EXPORT and export into each config folder
+ [-d|--debug] - Print extra debug output (after)
+ [-l|--limit=#] - Limit the number of builds in this run
+ [-n|--nobuild] - Don't actually build anything
+ [-f|--nofail] - Don't stop on a failed build
+ [-h|--help] - Print usage and exit
"
}
@@ -36,28 +44,32 @@ unset FIRST_CONF
EXIT_USAGE=
LIMIT=1000
-while getopts 'b:cdhil:nqr:sv-:' OFLAG; do
+while getopts 'b:ce:fdhl:npr:sv-:' OFLAG; do
case "${OFLAG}" in
b) BRANCH=$OPTARG ; bugout "Branch: $BRANCH" ;;
- r) FIRST_CONF="$OPTARG" ; bugout "Resume: $FIRST_CONF" ;;
+ f) NOFAIL=1 ; bugout "Continue on Fail" ;;
+ r) ISRES=1 ; FIRST_CONF="$OPTARG" ; bugout "Resume: $FIRST_CONF" ;;
c) CONTINUE=1 ; bugout "Continue" ;;
s) CONTSKIP=1 ; bugout "Continue, skipping" ;;
- i) COPY_INI=1 ; bugout "Archive INI/JSON/YML files" ;;
+ e) CEXPORT="$OPTARG" ; bugout "Export $CEXPORT" ;;
h) EXIT_USAGE=1 ; break ;;
l) LIMIT=$OPTARG ; bugout "Limit to $LIMIT build(s)" ;;
d|v) DEBUG=1 ; bugout "Debug ON" ;;
n) DRYRUN=1 ; bugout "Dry Run" ;;
+ p) PURGE=1 ; bugout "Purge stat file" ;;
-) IFS="=" read -r ONAM OVAL <<< "$OPTARG"
case "$ONAM" in
- branch) BRANCH=$OVAL ; bugout "Branch: $BRANCH" ;;
- resume) FIRST_CONF="$OVAL" ; bugout "Resume: $FIRST_CONF" ;;
+ branch) BRANCH=$OVAL ; bugout "Branch: $BRANCH" ;;
+ nofail) NOFAIL=1 ; bugout "Continue on Fail" ;;
+ resume) ISRES=1 ; FIRST_CONF="$OVAL" ; bugout "Resume: $FIRST_CONF" ;;
continue) CONTINUE=1 ; bugout "Continue" ;;
- skip) CONTSKIP=2 ; bugout "Continue, skipping" ;;
+ skip) CONTSKIP=1 ; bugout "Continue, skipping" ;;
+ export) CEXPORT="$OVAL" ; bugout "Export $EXPORT" ;;
limit) LIMIT=$OVAL ; bugout "Limit to $LIMIT build(s)" ;;
- ini) COPY_INI=1 ; bugout "Archive INI/JSON/YML files" ;;
help) [[ -z "$OVAL" ]] || perror "option can't take value $OVAL" $ONAM ; EXIT_USAGE=1 ;;
debug) DEBUG=1 ; bugout "Debug ON" ;;
nobuild) DRYRUN=1 ; bugout "Dry Run" ;;
+ purge) PURGE=1 ; bugout "Purge stat file" ;;
*) EXIT_USAGE=2 ; echo "$SELF: unrecognized option \`--$ONAM'" ; break ;;
esac
;;
@@ -65,21 +77,20 @@ while getopts 'b:cdhil:nqr:sv-:' OFLAG; do
esac
done
-# Extra arguments count as BRANCH, FIRST_CONF
-shift $((OPTIND - 1))
-[[ $# > 0 ]] && { BRANCH=$1 ; shift 1 ; bugout "BRANCH=$BRANCH" ; }
-[[ $# > 0 ]] && { FIRST_CONF=$1 ; shift 1 ; bugout "FIRST_CONF=$FIRST_CONF" ; }
-[[ $# > 0 ]] && { EXIT_USAGE=2 ; echo "too many arguments" ; }
+# Check for mixed continue, skip, resume arguments. Only one should be used.
+((CONTINUE + CONTSKIP + ISRES + PURGE > 1)) && { echo "Don't mix -c, -p, -s, and -r options" ; echo ; EXIT_USAGE=2 ; }
+# Exit with helpful usage information
((EXIT_USAGE)) && { usage ; let EXIT_USAGE-- ; exit $EXIT_USAGE ; }
-echo "This script downloads each Configuration and attempts to build it."
-echo "On failure the last-built configs will be left in your working copy."
+echo
+echo "This script downloads all example configs and attempts to build them."
+echo "On failure the last-built configs are left in your working copy."
echo "Restore your configs with 'git checkout -f' or 'git reset --hard HEAD'."
+echo
-if [[ -f "$STAT_FILE" ]]; then
- IFS='*' read BRANCH FIRST_CONF <"$STAT_FILE"
-fi
+[[ -n $PURGE ]] && rm -f "$STAT_FILE"
+[[ -z $FIRST_CONF && -f $STAT_FILE ]] && IFS='*' read BRANCH FIRST_CONF <"$STAT_FILE"
# If -c is given start from the last attempted build
if ((CONTINUE)); then
@@ -97,9 +108,9 @@ elif ((CONTSKIP)); then
fi
# Check if the current repository has unmerged changes
-if [[ $SKIP_CONF ]]; then
+if ((SKIP_CONF)); then
echo "Skipping $FIRST_CONF"
-elif [[ $FIRST_CONF ]]; then
+elif [[ -n $FIRST_CONF ]]; then
echo "Resuming from $FIRST_CONF"
else
git diff --quiet || { echo "The working copy is modified. Commit or stash changes before proceeding."; exit ; }
@@ -138,36 +149,30 @@ for CONF in $CONF_TREE ; do
# ...if skipping, don't build this one
compgen -G "${CONF}Con*.h" > /dev/null || continue
+ # Command arguments for 'build_example'
+ CARGS=("-b" "$TMP" "-c" "$DIR")
+
+ # Exporting? Add -e argument
+ ((CEXPORT)) && CARGS+=("-e" "$CEXPORT")
+
+ # Continue on fail? Add -n argument
+ ((NOFAIL)) && CARGS+=("-n")
+
# Build or print build command for --nobuild
if [[ $DRYRUN ]]; then
- echo -e "\033[0;32m[DRYRUN] build_example internal \"$TMP\" \"$DIR\"\033[0m"
+ echo -e "\033[0;32m[DRYRUN] build_example ${CARGS[@]}\033[0m"
else
# Remember where we are in case of failure
echo "${BRANCH}*${DIR}" >"$STAT_FILE"
- # Build folder is unknown so delete all report files
- if [[ $COPY_INI ]]; then
- IFIND='find ./.pio/build/ -name "config.ini" -o -name "schema.json" -o -name "schema.yml"'
- $IFIND -exec rm "{}" \;
- fi
- ((DEBUG)) && echo "\"$HERE/build_example\" internal \"$TMP\" \"$DIR\""
- "$HERE/build_example" internal "$TMP" "$DIR" || { echo "Failed to build $DIR"; exit ; }
- # Build folder is unknown so copy all report files
- [[ $COPY_INI ]] && $IFIND -exec cp "{}" "$CONF" \;
+ ((DEBUG)) && echo "\"$HERE/build_example\" ${CARGS[@]}"
+ "$HERE"/build_example "${CARGS[@]}" || { echo "Failed to build $DIR" ; exit ; }
fi
((--LIMIT)) || { echo "Limit reached" ; PAUSE=1 ; break ; }
+ echo ; echo
+
done
# Delete the build state if not paused early
[[ $PAUSE ]] || rm "$STAT_FILE"
-
-# Delete the temp folder if not preserving generated INI files
-if [[ -e "$TMP/config/examples" ]]; then
- if [[ $COPY_INI ]]; then
- OPEN=$( which gnome-open xdg-open open | head -n1 )
- $OPEN "$TMP"
- elif [[ ! $PAUSE ]]; then
- rm -rf "$TMP"
- fi
-fi
diff --git a/buildroot/bin/build_example b/buildroot/bin/build_example
index dabc4c572cdf..c32ee7360a6d 100755
--- a/buildroot/bin/build_example
+++ b/buildroot/bin/build_example
@@ -1,26 +1,69 @@
#!/usr/bin/env bash
#
-# build_example
+# Usage:
#
-# Usage: build_example internal config-home config-folder
+# build_example -b|--base= - Configurations root folder (e.g., ./.pio/build-BRANCH)
+# -c|--config= - Path of the configs to build (within config/examples)
+# [-e|--export=N] - Set CONFIG_EXPORT before build and export into the config folder
+# [-n|--nofail] - Don't stop on a failed build
+# [-r|--reveal] - Reveal the config folder after the build
+# [-h|--help] - Print usage and exit
+# [--allow] - Allow this script to run standalone
#
HERE=`dirname $0`
. "$HERE/mfutil"
-# Require 'internal' as the first argument
-[[ "$1" == "internal" ]] || { echo "Don't call this script directly, use build_all_examples instead." ; exit 1 ; }
+# Get arguments
+CLEANER=1
+ALLOW=""
+BASE=""
+CONFIG=""
+REVEAL=""
+EXPNUM=""
+NOFAIL=""
+while getopts 'b:c:e:hinr-:' OFLAG; do
+ case "${OFLAG}" in
+ b) BASE="$OPTARG" ;;
+ c) CONFIG="$OPTARG" ;;
+ e) EXPNUM="$OPTARG" ;;
+ h) EXIT_USAGE=1 ; break ;;
+ n) NOFAIL=1 ;;
+ r) REVEAL=1 ;;
+ -) IFS="=" read -r ONAM OVAL <<< "$OPTARG"
+ case "$ONAM" in
+ allow) ALLOW=1 ;;
+ base) BASE="$OVAL" ;;
+ config) CONFIG="$OVAL" ;;
+ export) EXPNUM="$OVAL" ;;
+ help) EXIT_USAGE=1 ; break ;;
+ nofail) NOFAIL=1 ;;
+ reveal) REVEAL=1 ;;
+ *) EXIT_USAGE=2 ; echo "$SELF: unrecognized option \`--$ONAM'" ; break ;;
+ esac
+ ;;
+ esac
+done
-echo "Testing $3:"
+[[ $ALLOW || $SHLVL -gt 2 ]] || { echo "Don't call this script directly, use build_all_examples instead." ; exit 1 ; }
-SUB=$2/config/examples/$3
-[[ -d "$SUB" ]] || { echo "$SUB is not a good path" ; exit 1 ; }
+# Make sure the examples exist
+SUB1="$BASE/config/examples"
+[[ -d "$SUB1" ]] || { echo "--base $BASE doesn't contain config/examples" ; exit 1 ; }
+
+# Make sure the specific config folder exists
+SUB=$SUB1/$CONFIG
+[[ -d "$SUB" ]] || { echo "--config $CONFIG doesn't exist" ; exit 1 ; }
compgen -G "${SUB}Con*.h" > /dev/null || { echo "No configuration files found in $SUB" ; exit 1 ; }
+# Delete any previous exported configs
+rm -f Marlin/Config.h Marlin/Config-export.h
+
echo "Getting configuration files from $SUB"
-cp "$2/config/default"/*.h Marlin/
+cp "$BASE/config/default"/*.h Marlin/
+cp "$SUB"/Config.h Marlin/ 2>/dev/null
cp "$SUB"/Configuration.h Marlin/ 2>/dev/null
cp "$SUB"/Configuration_adv.h Marlin/ 2>/dev/null
cp "$SUB"/_Bootscreen.h Marlin/ 2>/dev/null
@@ -35,9 +78,52 @@ rm Marlin/Configuration.h~
unset IFS; set +f
# Suppress fatal warnings
-echo -e "\n#define NO_CONTROLLER_CUSTOM_WIRING_WARNING" >> Marlin/Configuration.h
+if ((CLEANER)); then
+ opt_add NO_CONTROLLER_CUSTOM_WIRING_WARNING
+ opt_add NO_AUTO_ASSIGN_WARNING
+ opt_add NO_CREALITY_DRIVER_WARNING
+ opt_add DIAG_JUMPERS_REMOVED
+ opt_add DIAG_PINS_REMOVED
+ opt_add NO_MK3_FAN_PINS_WARNING
+ opt_add NO_USER_FEEDBACK_WARNING
+ opt_add NO_Z_SAFE_HOMING_WARNING
+ opt_add NO_LCD_CONTRAST_WARNING
+ opt_add NO_MICROPROBE_WARNING
+ opt_add NO_CONFIGURATION_EMBEDDING_WARNING
+fi
+
+FNAME=("-name" "marlin_config.json" \
+ "-o" "-name" "config.ini" \
+ "-o" "-name" "schema.json" \
+ "-o" "-name" "schema.yml")
+
+# If EXPNUM is set then apply to the config before build
+if [[ $EXPNUM ]]; then
+ opt_set CONFIG_EXPORT $EXPNUM
+ # Clean up old exports
+ find ./.pio/build \( "${FNAME[@]}" \) -exec rm "{}" \;
+fi
+
+set +e
echo "Building the firmware now..."
-"$HERE/mftest" -s -a -n1 || { echo "Failed"; exit 1; }
+"$HERE/mftest" -s -a -n1 ; ERR=$?
+
+[[ $ERR -eq 0 ]] && echo "Success" || echo "Failed"
+
+set -e
+
+# Copy exports back to the configs
+if [[ $EXPNUM ]]; then
+ echo "Exporting $EXPNUM"
+ [[ -f Marlin/Config-export.h ]] && { cp Marlin/Config-export.h "$SUB"/Config.h ; }
+ find ./.pio/build/ "${FNAME[@]}" -exec cp "{}" "$SUB" \;
+fi
+
+# Exit with error unless --nofail is set
+[[ $ERR -gt 0 && -z $NOFAIL ]] && exit $ERR
+
+# Reveal the configs after the build, if requested
+((REVEAL)) && { echo "Revealing $SUB" ; open "$SUB" ; }
-echo "Success"
+exit 0
diff --git a/buildroot/bin/mftest b/buildroot/bin/mftest
index 7806d37cfa1c..1fffaf5031f7 100755
--- a/buildroot/bin/mftest
+++ b/buildroot/bin/mftest
@@ -8,7 +8,7 @@
[[ -d Marlin/src ]] || { echo "Please 'cd' to the Marlin repo root." ; exit 1 ; }
-which pio || { echo "Make sure 'pio' is in your execution PATH." ; exit 1 ; }
+which pio >/dev/null || { echo "Make sure 'pio' is in your execution PATH." ; exit 1 ; }
perror() { echo -e "$0: \033[0;31m$1 -- $2\033[0m" ; }
errout() { echo -e "\033[0;31m$1\033[0m" ; }
diff --git a/buildroot/share/PlatformIO/scripts/common-dependencies.h b/buildroot/share/PlatformIO/scripts/common-dependencies.h
index 2def0d88b683..5d959b603b5e 100644
--- a/buildroot/share/PlatformIO/scripts/common-dependencies.h
+++ b/buildroot/share/PlatformIO/scripts/common-dependencies.h
@@ -86,8 +86,8 @@
#if HAS_TEMPERATURE
#define HAS_MENU_TEMPERATURE
#endif
- #if ENABLED(MMU2_MENUS)
- #define HAS_MENU_MMU2
+ #if ENABLED(MMU_MENUS)
+ #define HAS_MENU_MMU
#endif
#if ENABLED(PASSWORD_FEATURE)
#define HAS_MENU_PASSWORD
diff --git a/buildroot/tests/LPC1768 b/buildroot/tests/LPC1768
index 403e2d134511..f932625fa738 100755
--- a/buildroot/tests/LPC1768
+++ b/buildroot/tests/LPC1768
@@ -59,5 +59,40 @@ opt_set MOTHERBOARD BOARD_BTT_SKR_V1_3 EXTRUDERS 2 \
opt_enable PIDTEMPBED PIDTEMPCHAMBER PID_EXTRUSION_SCALING PID_FAN_SCALING
exec_test $1 $2 "SKR v1.3 with 2*Extr, bed, chamber all PID." "$3"
+#
+# SKR 1.4 with MMU2
+#
+restore_configs
+opt_set MOTHERBOARD BOARD_BTT_SKR_V1_4 SERIAL_PORT -1 \
+ BAUDRATE 115200 X_DRIVER_TYPE TMC2209 Y_DRIVER_TYPE TMC2209 \
+ Z_DRIVER_TYPE TMC2209 Z2_DRIVER_TYPE TMC2209 E0_DRIVER_TYPE TMC2209 \
+ EXTRUDERS 5 MMU_MODEL PRUSA_MMU2 HEATER_0_MAXTEMP 305 \
+ BED_MAXTEMP 125 HOTEND_OVERSHOOT 5 INVERT_X_DIR true \
+ INVERT_E0_DIR true X_BED_SIZE 235 Y_BED_SIZE 225 Z_MAX_POS 240 \
+ GRID_MAX_POINTS_X 5 E0_AUTO_FAN_PIN FAN1_PIN \
+ BLTOUCH_HS_MODE true BLTOUCH_HS_EXTRA_CLEARANCE 0 \
+ Z_STEPPER_ALIGN_XY '{{10,110},{200,110}}' \
+ Z_STEPPER_ALIGN_ITERATIONS 10 DEFAULT_STEPPER_TIMEOUT_SEC 0 \
+ SLOWDOWN_DIVISOR 16 SDCARD_CONNECTION ONBOARD BLOCK_BUFFER_SIZE 64 \
+ CHOPPER_TIMING CHOPPER_DEFAULT_24V MMU2_SERIAL_PORT 0
+opt_enable PIDTEMPBED S_CURVE_ACCELERATION \
+ USE_PROBE_FOR_Z_HOMING BLTOUCH FILAMENT_RUNOUT_SENSOR \
+ AUTO_BED_LEVELING_BILINEAR RESTORE_LEVELING_AFTER_G28 \
+ EXTRAPOLATE_BEYOND_GRID LCD_BED_LEVELING MESH_EDIT_MENU Z_SAFE_HOMING \
+ EEPROM_SETTINGS EEPROM_AUTO_INIT NOZZLE_PARK_FEATURE SDSUPPORT \
+ SPEAKER CR10_STOCKDISPLAY QUICK_HOME BLTOUCH_FORCE_SW_MODE \
+ Z_STEPPER_AUTO_ALIGN INPUT_SHAPING_X INPUT_SHAPING_Y SHAPING_MENU \
+ ADAPTIVE_STEP_SMOOTHING LCD_INFO_MENU STATUS_MESSAGE_SCROLLING \
+ SET_PROGRESS_MANUALLY M73_REPORT SHOW_REMAINING_TIME \
+ PRINT_PROGRESS_SHOW_DECIMALS AUTO_REPORT_SD_STATUS USE_BIG_EDIT_FONT \
+ BABYSTEPPING BABYSTEP_WITHOUT_HOMING BABYSTEP_ALWAYS_AVAILABLE \
+ DOUBLECLICK_FOR_Z_BABYSTEPPING BABYSTEP_DISPLAY_TOTAL LIN_ADVANCE \
+ BEZIER_CURVE_SUPPORT EMERGENCY_PARSER ADVANCED_PAUSE_FEATURE \
+ TMC_DEBUG HOST_ACTION_COMMANDS HOST_PAUSE_M76 HOST_PROMPT_SUPPORT \
+ HOST_STATUS_NOTIFICATIONS MMU2_DEBUG
+opt_disable Z_MIN_PROBE_USES_Z_MIN_ENDSTOP_PIN FILAMENT_LOAD_UNLOAD_GCODES \
+ PARK_HEAD_ON_PAUSE
+exec_test $1 $2 "BigTreeTech SKR 1.4 | MMU2" "$3"
+
# clean up
restore_configs
diff --git a/buildroot/tests/LPC1769 b/buildroot/tests/LPC1769
index 54a023a46cec..6cdeb43582ec 100755
--- a/buildroot/tests/LPC1769
+++ b/buildroot/tests/LPC1769
@@ -64,5 +64,40 @@ opt_enable EEPROM_SETTINGS EEPROM_CHITCHAT MECHANICAL_GANTRY_CALIBRATION \
opt_disable PSU_CONTROL Z_MIN_PROBE_USES_Z_MIN_ENDSTOP_PIN
exec_test $1 $2 "Cohesion3D Remix DELTA | ABL Bilinear | EEPROM | Sensorless Homing/Probing | I Axis" "$3"
+#
+# SKR 1.4 Turbo with MMU3
+#
+restore_configs
+opt_set MOTHERBOARD BOARD_BTT_SKR_V1_4_TURBO SERIAL_PORT -1 \
+ BAUDRATE 115200 X_DRIVER_TYPE TMC2209 Y_DRIVER_TYPE TMC2209 \
+ Z_DRIVER_TYPE TMC2209 Z2_DRIVER_TYPE TMC2209 E0_DRIVER_TYPE TMC2209 \
+ EXTRUDERS 5 MMU_MODEL PRUSA_MMU3 HEATER_0_MAXTEMP 305 \
+ BED_MAXTEMP 125 HOTEND_OVERSHOOT 5 INVERT_X_DIR true \
+ INVERT_E0_DIR true X_BED_SIZE 235 Y_BED_SIZE 225 Z_MAX_POS 240 \
+ GRID_MAX_POINTS_X 5 E0_AUTO_FAN_PIN FAN1_PIN \
+ BLTOUCH_HS_MODE true BLTOUCH_HS_EXTRA_CLEARANCE 0 \
+ Z_STEPPER_ALIGN_XY '{{10,110},{200,110}}' \
+ Z_STEPPER_ALIGN_ITERATIONS 10 DEFAULT_STEPPER_TIMEOUT_SEC 0 \
+ SLOWDOWN_DIVISOR 16 SDCARD_CONNECTION ONBOARD BLOCK_BUFFER_SIZE 64 \
+ CHOPPER_TIMING CHOPPER_DEFAULT_24V MMU2_SERIAL_PORT 0 \
+ Z_MIN_ENDSTOP_HIT_STATE HIGH
+opt_enable PIDTEMPBED S_CURVE_ACCELERATION \
+ USE_PROBE_FOR_Z_HOMING BLTOUCH FILAMENT_RUNOUT_SENSOR \
+ AUTO_BED_LEVELING_BILINEAR RESTORE_LEVELING_AFTER_G28 \
+ EXTRAPOLATE_BEYOND_GRID LCD_BED_LEVELING MESH_EDIT_MENU Z_SAFE_HOMING \
+ EEPROM_SETTINGS EEPROM_AUTO_INIT NOZZLE_PARK_FEATURE SDSUPPORT \
+ SPEAKER CR10_STOCKDISPLAY QUICK_HOME BLTOUCH_FORCE_SW_MODE \
+ Z_STEPPER_AUTO_ALIGN INPUT_SHAPING_X INPUT_SHAPING_Y SHAPING_MENU \
+ ADAPTIVE_STEP_SMOOTHING LCD_INFO_MENU STATUS_MESSAGE_SCROLLING \
+ SET_PROGRESS_MANUALLY M73_REPORT SHOW_REMAINING_TIME \
+ PRINT_PROGRESS_SHOW_DECIMALS AUTO_REPORT_SD_STATUS USE_BIG_EDIT_FONT \
+ BABYSTEPPING BABYSTEP_WITHOUT_HOMING BABYSTEP_ALWAYS_AVAILABLE \
+ DOUBLECLICK_FOR_Z_BABYSTEPPING BABYSTEP_DISPLAY_TOTAL LIN_ADVANCE \
+ BEZIER_CURVE_SUPPORT EMERGENCY_PARSER ADVANCED_PAUSE_FEATURE \
+ TMC_DEBUG HOST_ACTION_COMMANDS HOST_PAUSE_M76 HOST_PROMPT_SUPPORT HOST_STATUS_NOTIFICATIONS \
+ MMU_SPOOL_JOIN_CONSUMES_ALL_FILAMENT MMU_MENUS MMU2_DEBUG
+opt_disable Z_MIN_PROBE_USES_Z_MIN_ENDSTOP_PIN FILAMENT_LOAD_UNLOAD_GCODES PARK_HEAD_ON_PAUSE
+exec_test $1 $2 "BigTreeTech SKR 1.4 Turbo | MMU3" "$3"
+
# clean up
restore_configs
diff --git a/buildroot/tests/STM32H743VI_btt b/buildroot/tests/STM32H743VI_btt
index 48933e8c4642..b77cdb09c1e8 100755
--- a/buildroot/tests/STM32H743VI_btt
+++ b/buildroot/tests/STM32H743VI_btt
@@ -10,8 +10,85 @@ set -e
#
# Build with the default configurations
#
+restore_configs
use_example_configs "Creality/Ender-5 Plus/BigTreeTech SKR 3"
exec_test $1 $2 "Creality Ender-5 Plus with BigTreeTech SKR 3" "$3"
+
+#
+# SKR 3 EZ default
+#
+restore_configs
+opt_set MOTHERBOARD BOARD_BTT_SKR_V3_0_EZ SERIAL_PORT -1
+exec_test $1 $2 "BigTreeTech SKR 3 EZ | Default Configuration" "$3"
+
+#
+# SKR 3 EZ with MMU2
+#
+restore_configs
+opt_set MOTHERBOARD BOARD_BTT_SKR_V3_0_EZ SERIAL_PORT -1 \
+ BAUDRATE 115200 X_DRIVER_TYPE TMC2209 Y_DRIVER_TYPE TMC2209 \
+ Z_DRIVER_TYPE TMC2209 Z2_DRIVER_TYPE TMC2209 E0_DRIVER_TYPE TMC2209 \
+ EXTRUDERS 5 MMU_MODEL PRUSA_MMU2 HEATER_0_MAXTEMP 305 \
+ BED_MAXTEMP 125 HOTEND_OVERSHOOT 5 INVERT_X_DIR true \
+ INVERT_E0_DIR true X_BED_SIZE 235 Y_BED_SIZE 225 Z_MAX_POS 240 \
+ GRID_MAX_POINTS_X 5 E0_AUTO_FAN_PIN FAN1_PIN \
+ BLTOUCH_HS_MODE true BLTOUCH_HS_EXTRA_CLEARANCE 0 \
+ Z_STEPPER_ALIGN_XY '{{10,110},{200,110}}' \
+ Z_STEPPER_ALIGN_ITERATIONS 10 DEFAULT_STEPPER_TIMEOUT_SEC 0 \
+ SLOWDOWN_DIVISOR 16 SDCARD_CONNECTION ONBOARD BLOCK_BUFFER_SIZE 64 \
+ CHOPPER_TIMING CHOPPER_DEFAULT_24V MMU2_SERIAL_PORT 2
+opt_enable PIDTEMPBED ENDSTOP_INTERRUPTS_FEATURE S_CURVE_ACCELERATION \
+ USE_PROBE_FOR_Z_HOMING BLTOUCH FILAMENT_RUNOUT_SENSOR \
+ AUTO_BED_LEVELING_BILINEAR RESTORE_LEVELING_AFTER_G28 \
+ EXTRAPOLATE_BEYOND_GRID LCD_BED_LEVELING MESH_EDIT_MENU Z_SAFE_HOMING \
+ EEPROM_SETTINGS EEPROM_AUTO_INIT NOZZLE_PARK_FEATURE SDSUPPORT \
+ SPEAKER CR10_STOCKDISPLAY QUICK_HOME BLTOUCH_FORCE_SW_MODE \
+ Z_STEPPER_AUTO_ALIGN INPUT_SHAPING_X INPUT_SHAPING_Y SHAPING_MENU \
+ ADAPTIVE_STEP_SMOOTHING LCD_INFO_MENU STATUS_MESSAGE_SCROLLING \
+ SET_PROGRESS_MANUALLY M73_REPORT SHOW_REMAINING_TIME \
+ PRINT_PROGRESS_SHOW_DECIMALS AUTO_REPORT_SD_STATUS USE_BIG_EDIT_FONT \
+ BABYSTEPPING BABYSTEP_WITHOUT_HOMING BABYSTEP_ALWAYS_AVAILABLE \
+ DOUBLECLICK_FOR_Z_BABYSTEPPING BABYSTEP_DISPLAY_TOTAL LIN_ADVANCE \
+ BEZIER_CURVE_SUPPORT EMERGENCY_PARSER ADVANCED_PAUSE_FEATURE \
+ TMC_DEBUG HOST_ACTION_COMMANDS HOST_PAUSE_M76 HOST_PROMPT_SUPPORT HOST_STATUS_NOTIFICATIONS \
+ MMU2_DEBUG
+opt_disable Z_MIN_PROBE_USES_Z_MIN_ENDSTOP_PIN FILAMENT_LOAD_UNLOAD_GCODES PARK_HEAD_ON_PAUSE
+exec_test $1 $2 "BigTreeTech SKR 3 EZ | MMU2" "$3"
+
+#
+# SKR 3 EZ with MMU3
+#
+restore_configs
+opt_set MOTHERBOARD BOARD_BTT_SKR_V3_0_EZ SERIAL_PORT -1 \
+ BAUDRATE 115200 X_DRIVER_TYPE TMC2209 Y_DRIVER_TYPE TMC2209 \
+ Z_DRIVER_TYPE TMC2209 Z2_DRIVER_TYPE TMC2209 E0_DRIVER_TYPE TMC2209 \
+ EXTRUDERS 5 MMU_MODEL PRUSA_MMU3 HEATER_0_MAXTEMP 305 \
+ BED_MAXTEMP 125 HOTEND_OVERSHOOT 5 INVERT_X_DIR true \
+ INVERT_E0_DIR true X_BED_SIZE 235 Y_BED_SIZE 225 Z_MAX_POS 240 \
+ GRID_MAX_POINTS_X 5 E0_AUTO_FAN_PIN FAN1_PIN \
+ BLTOUCH_HS_MODE true BLTOUCH_HS_EXTRA_CLEARANCE 0 \
+ Z_STEPPER_ALIGN_XY '{{10,110},{200,110}}' \
+ Z_STEPPER_ALIGN_ITERATIONS 10 DEFAULT_STEPPER_TIMEOUT_SEC 0 \
+ SLOWDOWN_DIVISOR 16 SDCARD_CONNECTION ONBOARD BLOCK_BUFFER_SIZE 64 \
+ CHOPPER_TIMING CHOPPER_DEFAULT_24V MMU2_SERIAL_PORT 2
+opt_enable PIDTEMPBED ENDSTOP_INTERRUPTS_FEATURE S_CURVE_ACCELERATION \
+ USE_PROBE_FOR_Z_HOMING BLTOUCH FILAMENT_RUNOUT_SENSOR \
+ AUTO_BED_LEVELING_BILINEAR RESTORE_LEVELING_AFTER_G28 \
+ EXTRAPOLATE_BEYOND_GRID LCD_BED_LEVELING MESH_EDIT_MENU Z_SAFE_HOMING \
+ EEPROM_SETTINGS EEPROM_AUTO_INIT NOZZLE_PARK_FEATURE SDSUPPORT \
+ SPEAKER CR10_STOCKDISPLAY QUICK_HOME BLTOUCH_FORCE_SW_MODE \
+ Z_STEPPER_AUTO_ALIGN INPUT_SHAPING_X INPUT_SHAPING_Y SHAPING_MENU \
+ ADAPTIVE_STEP_SMOOTHING LCD_INFO_MENU STATUS_MESSAGE_SCROLLING \
+ SET_PROGRESS_MANUALLY M73_REPORT SHOW_REMAINING_TIME \
+ PRINT_PROGRESS_SHOW_DECIMALS AUTO_REPORT_SD_STATUS USE_BIG_EDIT_FONT \
+ BABYSTEPPING BABYSTEP_WITHOUT_HOMING BABYSTEP_ALWAYS_AVAILABLE \
+ DOUBLECLICK_FOR_Z_BABYSTEPPING BABYSTEP_DISPLAY_TOTAL LIN_ADVANCE \
+ BEZIER_CURVE_SUPPORT EMERGENCY_PARSER ADVANCED_PAUSE_FEATURE \
+ TMC_DEBUG HOST_ACTION_COMMANDS HOST_PAUSE_M76 HOST_PROMPT_SUPPORT HOST_STATUS_NOTIFICATIONS \
+ MMU_MENUS MMU_SPOOL_JOIN_CONSUMES_ALL_FILAMENT MMU2_DEBUG
+opt_disable Z_MIN_PROBE_USES_Z_MIN_ENDSTOP_PIN FILAMENT_LOAD_UNLOAD_GCODES PARK_HEAD_ON_PAUSE
+exec_test $1 $2 "BigTreeTech SKR 3 EZ | MMU3" "$3"
+
# clean up
restore_configs
diff --git a/buildroot/tests/rambo b/buildroot/tests/rambo
index 74a1db25164f..16b88dda2cd5 100755
--- a/buildroot/tests/rambo
+++ b/buildroot/tests/rambo
@@ -81,6 +81,14 @@ opt_set MOTHERBOARD BOARD_RAMBO EXTRUDERS 5 MMU_MODEL PRUSA_MMU2
opt_enable REPRAP_DISCOUNT_FULL_GRAPHIC_SMART_CONTROLLER NOZZLE_PARK_FEATURE ADVANCED_PAUSE_FEATURE EMERGENCY_PARSER MMU2_DEBUG
exec_test $1 $2 "Rambo with PRUSA_MMU2 " "$3"
+#
+# Rambo with MMU3
+#
+restore_configs
+opt_set MOTHERBOARD BOARD_RAMBO EXTRUDERS 5 MMU_MODEL PRUSA_MMU3
+opt_enable REPRAP_DISCOUNT_FULL_GRAPHIC_SMART_CONTROLLER FILAMENT_RUNOUT_SENSOR NOZZLE_PARK_FEATURE ADVANCED_PAUSE_FEATURE EMERGENCY_PARSER MMU_MENUS MMU2_DEBUG EEPROM_SETTINGS
+exec_test $1 $2 "Rambo with PRUSA_MMU3 " "$3"
+
#
# Build with the default configurations
#
diff --git a/ini/features.ini b/ini/features.ini
index bfbf9ae7b60b..a502808f095e 100644
--- a/ini/features.ini
+++ b/ini/features.ini
@@ -181,7 +181,7 @@ HAS_MENU_MULTI_LANGUAGE = build_src_filter=+
HAS_MENU_MEDIA = build_src_filter=+
HAS_MENU_MIXER = build_src_filter=+
-HAS_MENU_MMU2 = build_src_filter=+
+HAS_MENU_MMU = build_src_filter=+
HAS_MENU_ONE_CLICK_PRINT = build_src_filter=+ +
HAS_MENU_PASSWORD = build_src_filter=+
HAS_MENU_POWER_MONITOR = build_src_filter=+
@@ -255,6 +255,7 @@ HAS_MEATPACK = build_src_filter=+ +
HAS_PRUSA_MMU1 = build_src_filter=+
HAS_PRUSA_MMU2 = build_src_filter=+ +
+HAS_PRUSA_MMU3 = build_src_filter=+ +
PASSWORD_FEATURE = build_src_filter=+ +
ADVANCED_PAUSE_FEATURE = build_src_filter=+ +
CONFIGURE_FILAMENT_CHANGE = build_src_filter=+