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=+