mmu2.cpp 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185
  1. /**
  2. * Marlin 3D Printer Firmware
  3. * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
  4. *
  5. * Based on Sprinter and grbl.
  6. * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
  7. *
  8. * This program is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation, either version 3 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. *
  21. */
  22. /**
  23. * mmu2.cpp
  24. */
  25. #include "../../inc/MarlinConfigPre.h"
  26. #if HAS_PRUSA_MMU3
  27. #include "mmu2.h"
  28. #include "mmu2_error_converter.h"
  29. #include "mmu2_fsensor.h"
  30. #include "mmu2_log.h"
  31. #include "mmu2_marlin.h"
  32. #include "mmu2_marlin_macros.h"
  33. #include "mmu2_power.h"
  34. #include "mmu2_progress_converter.h"
  35. #include "mmu2_reporting.h"
  36. #include "strlen_cx.h"
  37. #include "SpoolJoin.h"
  38. #include "../../inc/MarlinConfig.h"
  39. #include "../../lcd/marlinui.h"
  40. #include "../../module/planner.h"
  41. #include "../../module/motion.h"
  42. #include "../../gcode/parser.h"
  43. #include "../../gcode/queue.h"
  44. #include "../runout.h"
  45. #if HAS_LEVELING
  46. #include "../bedlevel/bedlevel.h"
  47. #endif
  48. #include "../pause.h"
  49. #include "../../libs/stopwatch.h"
  50. // As of FW 3.12 we only support building the FW with only one extruder, all the multi-extruder infrastructure will be removed.
  51. // Saves at least 800B of code size
  52. //#ifdef __AVR__
  53. //static_assert(EXTRUDERS == 1);
  54. //#endif
  55. #define MMU2_NO_TOOL 99
  56. MMU3::MMU3 mmu3;
  57. namespace MMU3 {
  58. template <typename F>
  59. void waitForHotendTargetTemp(uint16_t delay, F f) {
  60. while (((thermal_degTargetHotend() - thermal_degHotend()) > 5)) {
  61. f();
  62. safe_delay_keep_alive(delay);
  63. }
  64. }
  65. void WaitForHotendTargetTempBeep() {
  66. waitForHotendTargetTemp(3000, []{});
  67. //MakeSound(Prompt);
  68. }
  69. uint8_t MMU3::cutter_mode; // Initialized by settings.load
  70. int MMU3::cutter_mode_addr; // Initialized by settings.load
  71. uint8_t MMU3::stealth_mode; // Initialized by settings.load
  72. int MMU3::stealth_mode_addr; // Initialized by settings.load
  73. // TODO: Currently, by logic, the value stored in the EEPROM for is ignored and
  74. // mmu_hw_enabled is always overwritten by the MMU State. Thus restarting
  75. // printer will always set the MMU as senabled.
  76. bool MMU3::mmu_hw_enabled; // Initialized by settings.load
  77. int MMU3::mmu_hw_enabled_addr; // Initialized by settings.load
  78. MMU3::MMU3()
  79. : logic(MMU2_TOOL_CHANGE_LOAD_LENGTH, MMU2_LOAD_TO_NOZZLE_FEED_RATE)
  80. , extruder(MMU2_NO_TOOL)
  81. , tool_change_extruder(MMU2_NO_TOOL)
  82. , resume_position()
  83. , resume_hotend_temp(0)
  84. , logicStepLastStatus(StepStatus::Finished)
  85. , _state(xState::Stopped)
  86. , mmu_print_saved(SavedState::None)
  87. , loadFilamentStarted(false)
  88. , unloadFilamentStarted(false)
  89. , toolchange_counter(0)
  90. , _tmcFailures(0) { }
  91. void MMU3::status() {
  92. // Useful information to see during bootup and change state
  93. SERIAL_ECHOLN(F("MMU is "), mmu_hw_enabled ? GET_TEXT_F(MSG_ON) : GET_TEXT_F(MSG_OFF));
  94. }
  95. void MMU3::start() {
  96. mmu_hw_enabled = true;
  97. #if ENABLED(EEPROM_SETTINGS)
  98. // Save mmu_hw_enabled to EEPROM
  99. // TODO: Move to settings.cpp (for now)
  100. persistentStore.access_start();
  101. persistentStore.write_data(mmu_hw_enabled_addr, mmu_hw_enabled);
  102. persistentStore.access_finish();
  103. settings.save();
  104. #endif
  105. MMU2_SERIAL.begin(MMU_BAUD);
  106. powerOn();
  107. MMU2_SERIAL.flush(); // Make sure the UART buffer is clear before starting communication
  108. setCurrentTool(MMU2_NO_TOOL);
  109. _state = xState::Connecting;
  110. // Start communication
  111. logic.start();
  112. logic.ResetRetryAttempts();
  113. logic.ResetCommunicationTimeoutAttempts();
  114. }
  115. void MMU3::stop() {
  116. stopKeepPowered();
  117. powerOff();
  118. }
  119. void MMU3::stopKeepPowered() {
  120. mmu_hw_enabled = false;
  121. #if ENABLED(EEPROM_SETTINGS)
  122. // Save mmu_hw_enabled to EEPROM
  123. persistentStore.access_start();
  124. persistentStore.write_data(mmu_hw_enabled_addr, mmu_hw_enabled);
  125. persistentStore.access_finish();
  126. settings.save();
  127. #endif
  128. _state = xState::Stopped;
  129. logic.stop();
  130. MMU2_SERIAL.end();
  131. }
  132. void MMU3::tune() {
  133. switch (lastErrorCode) {
  134. case ErrorCode::HOMING_SELECTOR_FAILED:
  135. case ErrorCode::HOMING_IDLER_FAILED: {
  136. // Prompt a menu for different values
  137. tuneIdlerStallguardThreshold();
  138. break;
  139. }
  140. default: break;
  141. }
  142. }
  143. void MMU3::reset(ResetForm level) {
  144. switch (level) {
  145. case Software: resetX0(); break;
  146. case ResetPin: triggerResetPin(); break;
  147. case CutThePower: powerCycle(); break;
  148. case EraseEEPROM: resetX42(); break;
  149. default: break;
  150. }
  151. }
  152. void MMU3::resetX0() { logic.ResetMMU(); } // Send soft reset
  153. void MMU3::resetX42() { logic.ResetMMU(42); }
  154. void MMU3::triggerResetPin() { power_reset(); }
  155. void MMU3::powerCycle() {
  156. // cut the power to the MMU and after a while restore it
  157. // Sadly, MK3/S/+ cannot do this
  158. stop();
  159. safe_delay_keep_alive(1000);
  160. start();
  161. }
  162. void MMU3::powerOff() { power_off(); }
  163. void MMU3::powerOn() { power_on(); }
  164. bool MMU3::readRegister(uint8_t address) {
  165. if (!waitForMMUReady()) return false;
  166. do {
  167. logic.readRegister(address); // we may signal the accepted/rejected status of the response as return value of this function
  168. } while (!manage_response(false, false));
  169. // Update cached value
  170. lastReadRegisterValue = logic.rsp.paramValue;
  171. return true;
  172. }
  173. bool __attribute__((noinline)) MMU3::writeRegister(uint8_t address, uint16_t data) {
  174. if (!waitForMMUReady()) return false;
  175. // special cases - intercept requests of registers which influence the printer's behaviour too + perform the change even on the printer's side
  176. switch (address) {
  177. case (uint8_t)Register::Extra_Load_Distance: logic.PlanExtraLoadDistance(data); break;
  178. case (uint8_t)Register::Pulley_Slow_Feedrate: logic.PlanPulleySlowFeedRate(data); break;
  179. default: break; // Don't intercept any other register writes
  180. }
  181. do {
  182. logic.writeRegister(address, data); // we may signal the accepted/rejected status of the response as return value of this function
  183. } while (!manage_response(false, false));
  184. return true;
  185. }
  186. void MMU3::mmu_loop() {
  187. // We only leave this method if the current command was successfully
  188. // completed - that's the Marlin's way of blocking operation
  189. // Atomic compare_exchange would have been the most appropriate solution
  190. // here, but this gets called only in Marlin's task, so thread safety
  191. // should be kept
  192. static bool avoidRecursion = false;
  193. if (avoidRecursion) return;
  194. avoidRecursion = true;
  195. mmu_loop_inner(true);
  196. avoidRecursion = false;
  197. }
  198. void __attribute__((noinline)) MMU3::mmu_loop_inner(bool reportErrors) {
  199. logicStepLastStatus = logicStep(reportErrors); // it looks like the mmu_loop doesn't need to be a blocking call
  200. CheckErrorScreenUserInput();
  201. }
  202. /**
  203. * Check if there are extruder moves planned ahead.
  204. *
  205. * TODO: This should go to the planner, but for now keep it here!
  206. */
  207. bool MMU3::e_active() {
  208. unsigned char e_active = 0;
  209. block_t *block;
  210. if (planner.block_buffer_tail != planner.block_buffer_head) {
  211. uint8_t block_index = planner.block_buffer_tail;
  212. while (block_index != planner.block_buffer_head) {
  213. block = &planner.block_buffer[block_index];
  214. if (block->steps[E_AXIS] != 0) e_active++;
  215. block_index = (block_index + 1) & (BLOCK_BUFFER_SIZE - 1);
  216. }
  217. }
  218. return (e_active > 0);
  219. }
  220. /**
  221. * Trigger an M600 or the SpoolJoin feature if the FINDA cannot detect any
  222. * filament during the print.
  223. *
  224. * In case of SpoolJoin feature is triggered, Marlin's implementation is a
  225. * little different than Prusa's, as we are completely consuming the filament
  226. * before switching to the next slot. There will be a little bit of filament
  227. * left when the new filament is extruded SpoolJoin is not intended to be used with
  228. * multi color/material prints so this should be fine.
  229. */
  230. void MMU3::checkFINDARunout() {
  231. if (!findaDetectsFilament()
  232. //&& printJobOngoing()
  233. && parser.codenum != 600
  234. && TERN1(HAS_LEVELING, planner.leveling_active)
  235. && xy_are_trusted()
  236. && e_active()
  237. #if ENABLED(MMU_SPOOL_JOIN_CONSUMES_ALL_FILAMENT)
  238. && runout.enabled // to prevent M600 to be triggered during M600 AUTO
  239. && !FILAMENT_PRESENT() // so the filament is totally consumed
  240. #endif
  241. ) {
  242. SERIAL_ECHOLN_P("FINDA filament runout!");
  243. if (spooljoin.isEnabled() && get_current_tool() != (uint8_t)FILAMENT_UNKNOWN) { // Can't auto if F=?
  244. #if ENABLED(MMU_SPOOL_JOIN_CONSUMES_ALL_FILAMENT)
  245. // set the current tool to FILAMENT_UNKNOWN so that we don't try to unload it
  246. extruder = MMU2_NO_TOOL;
  247. // disable the filament runout sensor (this is going to be re-enabled after the filament is loaded)
  248. runout.reset();
  249. runout.filament_ran_out = false; // trying to disable the purge more / continue message
  250. runout.enabled = false;
  251. #endif
  252. queue.enqueue_now(F("M600A")); // Save print and run M600 command
  253. }
  254. else {
  255. marlin_stop_and_save_print_to_ram();
  256. resume_print();
  257. queue.enqueue_now(F("M600")); // Save print and run M600 command
  258. }
  259. }
  260. }
  261. struct ReportingRAII {
  262. CommandInProgress cip;
  263. explicit inline __attribute__((always_inline)) ReportingRAII(CommandInProgress cip)
  264. : cip(cip) {
  265. BeginReport(cip, ProgressCode::EngagingIdler);
  266. }
  267. inline __attribute__((always_inline)) ~ReportingRAII() {
  268. EndReport(cip, ProgressCode::OK);
  269. }
  270. };
  271. bool MMU3::waitForMMUReady() {
  272. switch (state()) {
  273. case xState::Stopped: return false;
  274. case xState::Connecting:
  275. // Should we wait until the MMU reconnects?
  276. // Fire up a fsm_dlg and show "MMU not responding"?
  277. default: return true;
  278. }
  279. }
  280. bool MMU3::retryIfPossible(const ErrorCode ec) {
  281. if (logic.RetryAttempts()) {
  282. SetButtonResponse(ButtonOperations::Retry);
  283. // check, that Retry is actually allowed on that operation
  284. if (ButtonAvailable(ec) != Buttons::NoButton) {
  285. logic.SetInAutoRetry(true);
  286. SERIAL_ECHOLN_P("RetryButtonPressed");
  287. // We don't decrement until the button is acknowledged by the MMU.
  288. // --retryAttempts; // "used" one retry attempt
  289. return true;
  290. }
  291. }
  292. logic.SetInAutoRetry(false);
  293. return false;
  294. }
  295. bool MMU3::verifyFilamentEnteredPTFE() {
  296. planner_synchronize();
  297. if (WhereIsFilament() != FilamentState::AT_FSENSOR)
  298. return false;
  299. // MMU has finished its load, push the filament further by some defined constant length
  300. // If the filament sensor reads 0 at any moment, then report FAILURE
  301. const float tryload_length = MMU2_CHECK_FILAMENT_PRESENCE_EXTRUSION_LENGTH - logic.ExtraLoadDistance();
  302. TryLoadUnloadReporter tlur(tryload_length);
  303. /**
  304. * The position is a triangle wave.
  305. * Current position is not zero, it is an offset
  306. *
  307. * Keep in mind that the relationship between machine position
  308. * and pixel index is not linear. The area around the amplitude
  309. * needs to be taken care of carefully. The current implementation
  310. * handles each move separately so there is no need to watch for the change
  311. * in the slope's sign or check the last machine position.
  312. * y(x)
  313. * ▲
  314. * │ ^◄────────── tryload_length + current_position
  315. * machine │ / \
  316. * position │ / \◄────────── stepper_position_mm + current_position
  317. * (mm) │ / \
  318. * │ / \
  319. * │/ \◄───────current_position
  320. * └──────────────► x
  321. * 0 19
  322. * pixel #
  323. */
  324. bool filament_inserted = true; // Expect success
  325. // Pixel index will go from 0 to 10, then back from 10 to 0.
  326. // A change in this value indicates a new pixel should be drawn on the display.
  327. for (uint8_t move = 0; move < 2; move++) {
  328. extruder_move(move == 0 ? tryload_length : -tryload_length, MMU2_VERIFY_LOAD_TO_NOZZLE_FEED_RATE);
  329. while (planner_any_moves()) {
  330. filament_inserted = filament_inserted && (WhereIsFilament() == FilamentState::AT_FSENSOR);
  331. tlur.Progress(filament_inserted);
  332. safe_delay_keep_alive(0);
  333. }
  334. }
  335. Disable_E0();
  336. if (!filament_inserted) IncrementLoadFails();
  337. tlur.DumpToSerial();
  338. return filament_inserted;
  339. }
  340. bool MMU3::toolChangeCommonOnce(uint8_t slot) {
  341. static_assert(MMU2_MAX_RETRIES > 1); // Need >1 retries to do the cut in the last attempt
  342. uint8_t retries = 0;
  343. for (;;) {
  344. for (;;) {
  345. 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
  346. tool_change_extruder = slot;
  347. logic.ToolChange(slot); // Let the MMU pull the filament out and push a new one in
  348. if (manage_response(true, true)) break;
  349. // Otherwise: failed to perform the command - unload first and then let it run again
  350. IncrementMMUFails();
  351. // Just in case we stood in an error screen for too long and the hotend got cold
  352. resumeHotendTemp();
  353. // If the extruder has been parked, it will get unparked once the ToolChange command finishes OK
  354. // - so no resumeUnpark() at this spot
  355. unloadInner();
  356. // If we run out of retries, we must do something ... maybe raise an error screen and allow the user to do something.
  357. // But honestly - if the MMU restarts during every toolchange something else is seriously broken
  358. // and stopping a print is probably our best option.
  359. }
  360. if (verifyFilamentEnteredPTFE()) return true; // success
  361. // Prepare a retry attempt
  362. unloadInner();
  363. if (retries == (MMU2_MAX_RETRIES) - 1 && cutter_enabled()) {
  364. cutFilamentInner(slot); // try cutting filament tip at the last attempt
  365. retries = 0; // reset retries every MMU2_MAX_RETRIES
  366. }
  367. ++retries;
  368. }
  369. return false; // Couldn't accomplish the task
  370. }
  371. void MMU3::toolChangeCommon(uint8_t slot) {
  372. while (!toolChangeCommonOnce(slot)) { // While not successfully fed into extruder's PTFE tube...
  373. // Failed autoretry, report an error by forcing a "printer" error into the MMU infrastructure - it is a hack to leverage existing code
  374. // @@TODO theoretically logic layer may not need to be spoiled with the printer error - maybe just the manage_response needs it...
  375. logic.SetPrinterError(ErrorCode::LOAD_TO_EXTRUDER_FAILED);
  376. // We only have to wait for the user to fix the issue and press "Retry".
  377. // Please see checkUserInput() for details how we "leave" manage_response.
  378. // If manage_response returns false at this spot (MMU operation interrupted aka MMU reset)
  379. // we can safely continue because the MMU is not doing an operation now.
  380. static_cast<void>(manage_response(true, true)); // yes, I'd like to silence [[nodiscard]] warning at this spot by casting to void
  381. }
  382. setCurrentTool(slot); // filament change is finished
  383. spooljoin.setSlot(slot);
  384. ++toolchange_counter;
  385. // Also increment the total number of tool changes
  386. operation_statistics.increment_tool_change_counter();
  387. }
  388. bool MMU3::tool_change(uint8_t slot) {
  389. if (!waitForMMUReady()) return false;
  390. if (slot != extruder) {
  391. if (
  392. //findaDetectsFilament()
  393. //!IS_SD_PRINTING() && !usb_timer.running()
  394. !marlin_printingIsActive()
  395. ) {
  396. // If Tcodes are used manually through the serial
  397. // we need to unload manually as well -- but only if FINDA detects filament
  398. unload();
  399. }
  400. ReportingRAII rep(CommandInProgress::ToolChange);
  401. FSensorBlockRunout blockRunout;
  402. planner_synchronize();
  403. toolChangeCommon(slot);
  404. }
  405. return true;
  406. }
  407. /**
  408. * Handle special T?/Tx/Tc commands
  409. *
  410. * - T? Gcode to extrude shouldn't have to follow, load to extruder wheels is done automatically
  411. * - 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.
  412. * - Tc Load to nozzle after filament was prepared by Tx and extruder nozzle is already heated.
  413. */
  414. bool MMU3::tool_change(char code, uint8_t slot) {
  415. if (!waitForMMUReady()) return false;
  416. FSensorBlockRunout blockRunout;
  417. switch (code) {
  418. case '?': {
  419. waitForHotendTargetTemp(100, []{});
  420. load_to_nozzle(slot);
  421. }
  422. break;
  423. case 'x': {
  424. thermal_setExtrudeMintemp(0); // Allow cold extrusion since Tx only loads to the gears not nozzle
  425. tool_change(slot);
  426. thermal_setExtrudeMintemp(EXTRUDE_MINTEMP);
  427. }
  428. break;
  429. case 'c': {
  430. waitForHotendTargetTemp(100, []{});
  431. execute_load_to_nozzle_sequence();
  432. }
  433. break;
  434. }
  435. return true;
  436. }
  437. void MMU3::get_statistics() {
  438. logic.Statistics();
  439. }
  440. uint8_t __attribute__((noinline)) MMU3::get_current_tool() const {
  441. return extruder == MMU2_NO_TOOL ? (uint8_t)FILAMENT_UNKNOWN : extruder;
  442. }
  443. uint8_t MMU3::get_tool_change_tool() const {
  444. return tool_change_extruder == MMU2_NO_TOOL ? (uint8_t)FILAMENT_UNKNOWN : tool_change_extruder;
  445. }
  446. void MMU3::setCurrentTool(uint8_t ex) {
  447. extruder = ex;
  448. MMU2_ECHO_MSGRPGM(PSTR("MMU2tool="));
  449. SERIAL_ECHOLN((int)ex);
  450. }
  451. bool MMU3::set_filament_type(uint8_t /*slot*/, uint8_t /*type*/) {
  452. if (!waitForMMUReady()) return false;
  453. // @@TODO - this is not supported in the new MMU yet
  454. // slot = slot; // @@TODO
  455. // type = type; // @@TODO
  456. // cmd_arg = filamentType;
  457. // command(MMU_CMD_F0 + index);
  458. if (!manage_response(false, false)) {
  459. // @@TODO failed to perform the command - retry
  460. // Comment: how is it possible for a filament type set to fail? manage_response(true, true)
  461. }
  462. return true;
  463. }
  464. void MMU3::unloadInner() {
  465. FSensorBlockRunout blockRunout;
  466. filament_ramming();
  467. // we assume the printer managed to relieve filament tip from the gears,
  468. // so repeating that part in case of an MMU restart is not necessary
  469. for (;;) {
  470. Disable_E0();
  471. logic.UnloadFilament();
  472. if (manage_response(false, true)) break;
  473. IncrementMMUFails();
  474. }
  475. //MakeSound(Confirm);
  476. // no active tool
  477. setCurrentTool(MMU2_NO_TOOL);
  478. tool_change_extruder = MMU2_NO_TOOL;
  479. }
  480. bool MMU3::unload() {
  481. if (!waitForMMUReady()) return false;
  482. WaitForHotendTargetTempBeep();
  483. // Scope for ReportingRAII
  484. {
  485. ReportingRAII rep(CommandInProgress::UnloadFilament);
  486. unloadInner();
  487. }
  488. ScreenUpdateEnable();
  489. return true;
  490. }
  491. void MMU3::cutFilamentInner(uint8_t slot) {
  492. for (;;) {
  493. Disable_E0();
  494. logic.CutFilament(slot);
  495. if (manage_response(false, true)) break;
  496. IncrementMMUFails();
  497. }
  498. }
  499. bool MMU3::cut_filament(uint8_t slot, bool enableFullScreenMsg /*= true*/) {
  500. if (!waitForMMUReady()) return false;
  501. if (enableFullScreenMsg) fullScreenMsgCut(slot);
  502. // Scope for ReportingRAII
  503. {
  504. if (findaDetectsFilament()) unload();
  505. ReportingRAII rep(CommandInProgress::CutFilament);
  506. cutFilamentInner(slot);
  507. setCurrentTool(MMU2_NO_TOOL);
  508. tool_change_extruder = MMU2_NO_TOOL;
  509. //MakeSound(SoundType::Confirm);
  510. }
  511. ScreenUpdateEnable();
  512. return true;
  513. }
  514. bool MMU3::loading_test(uint8_t slot) {
  515. fullScreenMsgTest(slot);
  516. tool_change(slot);
  517. planner_synchronize();
  518. unload();
  519. ScreenUpdateEnable();
  520. return true;
  521. }
  522. bool MMU3::load_to_feeder(uint8_t slot) {
  523. if (!waitForMMUReady()) return false;
  524. fullScreenMsgLoad(slot);
  525. // Scope for ReportingRAII
  526. {
  527. ReportingRAII rep(CommandInProgress::LoadFilament);
  528. for (;;) {
  529. Disable_E0();
  530. logic.LoadFilament(slot);
  531. if (manage_response(false, false)) break;
  532. IncrementMMUFails();
  533. }
  534. //MakeSound(SoundType::Confirm);
  535. }
  536. ScreenUpdateEnable();
  537. return true;
  538. }
  539. bool MMU3::load_to_nozzle(uint8_t slot) {
  540. if (!waitForMMUReady()) return false;
  541. WaitForHotendTargetTempBeep();
  542. fullScreenMsgLoad(slot);
  543. // Scope for ReportingRAII
  544. {
  545. // Used for MMU-menu operation "Load to Nozzle"
  546. ReportingRAII rep(CommandInProgress::ToolChange);
  547. FSensorBlockRunout blockRunout;
  548. // Filament already loaded? Free it and shape its tip properly.
  549. if (extruder != MMU2_NO_TOOL) filament_ramming();
  550. toolChangeCommon(slot);
  551. // Finish loading to the nozzle with finely tuned steps.
  552. execute_load_to_nozzle_sequence();
  553. //MakeSound(Confirm);
  554. }
  555. ScreenUpdateEnable();
  556. return true;
  557. }
  558. bool MMU3::eject_filament(uint8_t slot, bool enableFullScreenMsg /* = true */) {
  559. if (!waitForMMUReady()) return false;
  560. if (enableFullScreenMsg) fullScreenMsgEject(slot);
  561. // Scope for ReportingRAII
  562. {
  563. if (findaDetectsFilament())
  564. unload();
  565. ReportingRAII rep(CommandInProgress::EjectFilament);
  566. for (;;) {
  567. Disable_E0();
  568. logic.EjectFilament(slot);
  569. if (manage_response(false, true))
  570. break;
  571. IncrementMMUFails();
  572. }
  573. setCurrentTool(MMU2_NO_TOOL);
  574. tool_change_extruder = MMU2_NO_TOOL;
  575. //MakeSound(Confirm);
  576. }
  577. ScreenUpdateEnable();
  578. return true;
  579. }
  580. void MMU3::button(uint8_t index) {
  581. LogEchoEvent(F("button"));
  582. logic.button(index);
  583. }
  584. void MMU3::home(uint8_t mode) {
  585. logic.home(mode);
  586. }
  587. void MMU3::saveHotendTemp(bool turn_off_nozzle) {
  588. if (mmu_print_saved & SavedState::Cooldown) return;
  589. if (turn_off_nozzle && !(mmu_print_saved & SavedState::CooldownPending)) {
  590. Disable_E0();
  591. resume_hotend_temp = thermal_degTargetHotend();
  592. mmu_print_saved |= SavedState::CooldownPending;
  593. LogEchoEvent(F("Heater cooldown pending"));
  594. }
  595. }
  596. void MMU3::saveAndPark(bool move_axes) {
  597. if (mmu_print_saved == SavedState::None) { // First occurrence. Save current position, park print head, disable nozzle heater.
  598. LogEchoEvent(F("Saving and parking"));
  599. Disable_E0();
  600. planner_synchronize();
  601. // In case a power panic happens while waiting for the user
  602. // take a partial back up of print state into RAM (current position, etc.)
  603. marlin_refresh_print_state_in_ram();
  604. if (move_axes) {
  605. mmu_print_saved |= SavedState::ParkExtruder;
  606. resume_position = planner_current_position(); // save current pos
  607. // Do not lift Z, as it will double lift if there is another error
  608. // right after the current one is solved.
  609. // Move XY aside
  610. if (xy_are_trusted()) nozzle_park();
  611. }
  612. }
  613. }
  614. void MMU3::resumeHotendTemp() {
  615. if ((mmu_print_saved & SavedState::CooldownPending)) {
  616. // Clear the "pending" flag if we haven't cooled yet.
  617. mmu_print_saved &= ~(SavedState::CooldownPending);
  618. LogEchoEvent(F("Cooldown flag cleared"));
  619. }
  620. if ((mmu_print_saved & SavedState::Cooldown) && resume_hotend_temp) {
  621. LogEchoEvent(F("Resuming Temp"));
  622. // @@TODO MMU2_ECHO_MSGRPGM(PSTR("Restoring hotend temperature "));
  623. SERIAL_ECHOLN(resume_hotend_temp);
  624. mmu_print_saved &= ~(SavedState::Cooldown);
  625. thermal_setTargetHotend(resume_hotend_temp);
  626. fullScreenMsgRestoringTemperature();
  627. // @todo better report the event and let the GUI do its work somewhere else
  628. ReportErrorHookSensorLineRender();
  629. waitForHotendTargetTemp(100, [] {
  630. marlin_manage_inactivity(true);
  631. mmu3.mmu_loop_inner(false);
  632. ReportErrorHookDynamicRender();
  633. });
  634. ScreenUpdateEnable(); // temporary hack to stop this locking the printer...
  635. LogEchoEvent(F("Hotend temperature reached"));
  636. ScreenClear();
  637. }
  638. }
  639. void MMU3::resumeUnpark() {
  640. if (mmu_print_saved & SavedState::ParkExtruder) {
  641. LogEchoEvent(F("Resuming XYZ"));
  642. // Move XY to starting position, then Z
  643. motion_do_blocking_move_to_xy(resume_position.x, resume_position.x, feedRate_t(NOZZLE_PARK_XY_FEEDRATE));
  644. // Move Z_AXIS to saved position
  645. motion_do_blocking_move_to_z(resume_position.z, feedRate_t(NOZZLE_PARK_Z_FEEDRATE));
  646. // From this point forward, power panic should not use
  647. // the partial backup in RAM since the extruder is no
  648. // longer in parking position
  649. marlin_clear_print_state_in_ram();
  650. mmu_print_saved &= ~(SavedState::ParkExtruder);
  651. }
  652. }
  653. void MMU3::checkUserInput() {
  654. auto btn = ButtonPressed(lastErrorCode);
  655. // Was a button pressed on the MMU itself instead of the LCD?
  656. if (btn == Buttons::NoButton && lastButton != Buttons::NoButton) {
  657. btn = lastButton;
  658. lastButton = Buttons::NoButton; // Clear it.
  659. }
  660. if (mmuLastErrorSource() == MMU3::ErrorSourcePrinter && btn != Buttons::NoButton) {
  661. // When the printer has raised an error screen, and a button was selected
  662. // the error screen should always be dismissed.
  663. clearPrinterError();
  664. // A horrible hack - clear the explicit printer error allowing manage_response to recover on MMU's Finished state
  665. // Moreover - if the MMU is currently doing something (like the LoadFilament - see comment above)
  666. // we'll actually wait for it automagically in manage_response and after it finishes correctly,
  667. // we'll issue another command (like toolchange)
  668. }
  669. switch (btn) {
  670. case Buttons::Left:
  671. case Buttons::Middle:
  672. case Buttons::Right:
  673. SERIAL_ECHOPGM("checkUserInput-btnLMR ");
  674. SERIAL_ECHOLN((int)buttons_to_uint8t(btn));
  675. resumeHotendTemp(); // Recover the hotend temp before we attempt to do anything else...
  676. if (mmuLastErrorSource() == MMU3::ErrorSourceMMU)
  677. // Do not send a button to the MMU unless the MMU is in error state
  678. button(buttons_to_uint8t(btn));
  679. // A quick hack: for specific error codes move the E-motor every time.
  680. // Not sure if we can rely on the fsensor.
  681. // Just plan the move, let the MMU take over when it is ready
  682. switch (lastErrorCode) {
  683. case ErrorCode::FSENSOR_DIDNT_SWITCH_OFF:
  684. case ErrorCode::FSENSOR_TOO_EARLY: helpUnloadToFinda(); break;
  685. default: break;
  686. }
  687. break;
  688. case Buttons::TuneMMU:
  689. tune();
  690. break;
  691. case Buttons::Load:
  692. case Buttons::Eject:
  693. // High level operation
  694. setPrinterButtonOperation(btn);
  695. break;
  696. case Buttons::ResetMMU:
  697. reset(ResetPin); // Cannot do power cycle on the MK3
  698. // ... but mmu2_power.cpp knows this and triggers a soft-reset instead.
  699. break;
  700. case Buttons::DisableMMU:
  701. stop();
  702. //DisableMMUInSettings(); // stop() already does this...
  703. status();
  704. break;
  705. case Buttons::StopPrint:
  706. // @@TODO Unsure if we should handle this high level operation at this spot
  707. break;
  708. default: break;
  709. }
  710. }
  711. /**
  712. * Originally, this was used to wait for response and deal with timeout if necessary.
  713. * The new protocol implementation enables much nicer and intense reporting, so this method will boil down
  714. * just to verify the result of an issued command (which was basically the original idea)
  715. *
  716. * 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.
  717. * 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.
  718. * That's what's being done here...
  719. */
  720. bool MMU3::manage_response(const bool move_axes, const bool turn_off_nozzle) {
  721. mmu_print_saved = SavedState::None;
  722. MARLIN_KEEPALIVE_STATE_IN_PROCESS;
  723. Stopwatch nozzle_timer;
  724. for (;;) {
  725. // in our new implementation, we know the exact state of the MMU at any moment, we do not have to wait for a timeout
  726. // So in this case we should decide if the operation is:
  727. // - still running -> wait normally in idle()
  728. // - failed -> then do the safety moves on the printer like before
  729. // - finished ok -> proceed with reading other commands
  730. safe_delay_keep_alive(0); // calls logicStep() and remembers its return status
  731. if (mmu_print_saved & SavedState::CooldownPending) {
  732. if (!nozzle_timer.isRunning()) {
  733. nozzle_timer.start();
  734. LogEchoEvent(F("Cooling Timeout started"));
  735. }
  736. else if (nozzle_timer.duration() > (PAUSE_PARK_NOZZLE_TIMEOUT * 1000ul)) { // mins->msec.
  737. mmu_print_saved &= ~(SavedState::CooldownPending);
  738. mmu_print_saved |= SavedState::Cooldown;
  739. thermal_setTargetHotend(0);
  740. LogEchoEvent(F("Heater cooldown"));
  741. }
  742. }
  743. else if (nozzle_timer.isRunning()) {
  744. nozzle_timer.stop();
  745. LogEchoEvent(F("Cooling timer stopped"));
  746. }
  747. switch (logicStepLastStatus) {
  748. case Finished:
  749. // command/operation completed, let Marlin continue its work
  750. // the E may have some more moves to finish - wait for them
  751. resumeHotendTemp();
  752. resumeUnpark(); // We can now travel back to the tower or wherever we were when we saved.
  753. if (!TuneMenuEntered()) {
  754. // If the error screen is sleeping (running 'Tune' menu)
  755. // then don't reset retry attempts because we this will trigger
  756. // an automatic retry attempt when 'Tune' button is selected. We want the
  757. // error screen to appear once more so the user can hit 'Retry' button manually.
  758. logic.ResetRetryAttempts(); // Reset the retry counter.
  759. }
  760. planner_synchronize();
  761. return true;
  762. case Interrupted:
  763. // now what :D ... big bad ... ramming, unload, retry the whole command originally issued
  764. return false;
  765. case VersionMismatch: // this basically means the MMU will be disabled until reconnected
  766. checkUserInput();
  767. return true;
  768. case PrinterError:
  769. saveAndPark(move_axes);
  770. saveHotendTemp(turn_off_nozzle);
  771. checkUserInput();
  772. // if button pressed "Done", return true, otherwise stay within manage_response
  773. // Please see checkUserInput() for details how we "leave" manage_response
  774. break;
  775. case CommandError:
  776. case CommunicationTimeout:
  777. case ProtocolError:
  778. case ButtonPushed:
  779. if (!logic.InAutoRetry()) {
  780. // Don't proceed to the park/save if we are doing an autoretry.
  781. saveAndPark(move_axes);
  782. saveHotendTemp(turn_off_nozzle);
  783. checkUserInput();
  784. }
  785. break;
  786. case CommunicationRecovered: // @@TODO communication recovered and maybe an error recovered as well
  787. // Maybe the logic layer can detect the change of state a respond with one "Recovered" to be handled here
  788. resumeHotendTemp();
  789. resumeUnpark();
  790. break;
  791. case Processing: // Wait for the MMU to respond
  792. default: break;
  793. }
  794. }
  795. }
  796. StepStatus MMU3::logicStep(bool reportErrors) {
  797. // Process any buttons before proceeding with another MMU Query
  798. checkUserInput();
  799. const StepStatus ss = logic.Step();
  800. switch (ss) {
  801. case Finished:
  802. // At this point it is safe to trigger a runout and not interrupt the MMU protocol
  803. checkFINDARunout();
  804. break;
  805. case Processing:
  806. onMMUProgressMsg(logic.Progress());
  807. break;
  808. case ButtonPushed:
  809. lastButton = logic.button();
  810. LogEchoEvent(F("MMU button pushed"));
  811. checkUserInput(); // Process the button immediately
  812. break;
  813. case Interrupted:
  814. // can be silently handed over to a higher layer, no processing necessary at this spot
  815. break;
  816. default:
  817. if (reportErrors) {
  818. switch (ss) {
  819. case CommandError:
  820. reportError(logic.Error(), ErrorSourceMMU);
  821. break;
  822. case CommunicationTimeout:
  823. _state = xState::Connecting;
  824. reportError(ErrorCode::MMU_NOT_RESPONDING, ErrorSourcePrinter);
  825. break;
  826. case ProtocolError:
  827. _state = xState::Connecting;
  828. reportError(ErrorCode::PROTOCOL_ERROR, ErrorSourcePrinter);
  829. break;
  830. case VersionMismatch:
  831. stopKeepPowered();
  832. reportError(ErrorCode::VERSION_MISMATCH, ErrorSourcePrinter);
  833. break;
  834. case PrinterError:
  835. reportError(logic.PrinterError(), ErrorSourcePrinter);
  836. break;
  837. default:
  838. break;
  839. }
  840. }
  841. }
  842. if (logic.Running()) _state = xState::Active;
  843. return ss;
  844. }
  845. void MMU3::filament_ramming() {
  846. execute_extruder_sequence(ramming_sequence, sizeof(ramming_sequence) / sizeof(E_Step));
  847. }
  848. void MMU3::execute_extruder_sequence(const E_Step *sequence, uint8_t steps) {
  849. planner_synchronize();
  850. const E_Step *step = sequence;
  851. for (uint8_t i = steps; i > 0; --i) {
  852. extruder_move(pgm_read_float(&(step->extrude)), pgm_read_float(&(step->feedRate)));
  853. step++;
  854. }
  855. planner_synchronize(); // it looks like it's better to sync the moves at the end - smoother move (if the sequence is not too long).
  856. Disable_E0();
  857. }
  858. void MMU3::execute_load_to_nozzle_sequence() {
  859. planner_synchronize();
  860. // Compensate for configurable Extra Loading Distance
  861. planner_set_current_position_E(planner_get_current_position_E() - (logic.ExtraLoadDistance() - MMU2_FILAMENT_SENSOR_POSITION));
  862. execute_extruder_sequence(load_to_nozzle_sequence, sizeof(load_to_nozzle_sequence) / sizeof(load_to_nozzle_sequence[0]));
  863. }
  864. void MMU3::reportError(ErrorCode ec, ErrorSource res) {
  865. // Due to a potential lossy error reporting layers linked to this hook
  866. // we'd better report everything to make sure especially the error states
  867. // do not get lost.
  868. // - The good news here is the fact, that the MMU reports the errors repeatedly until resolved.
  869. // - The bad news is, that MMU not responding may repeatedly occur on printers not having the MMU at all.
  870. //
  871. // Not sure how to properly handle this situation, options:
  872. // - skip reporting "MMU not responding" (at least for now)
  873. // - report only changes of states (we can miss an error message)
  874. // - maybe some combination of MMUAvailable + UseMMU flags and decide based on their state
  875. // Right now the filtering of MMU_NOT_RESPONDING is done in ReportErrorHook() as it is not a problem if mmu2.cpp
  876. // Depending on the Progress code, we may want to do some action when an error occurs
  877. switch (logic.Progress()) {
  878. case ProgressCode::UnloadingToFinda:
  879. unloadFilamentStarted = false;
  880. planner_abort_queued_moves(); // Abort excess E-moves to be safe
  881. break;
  882. case ProgressCode::FeedingToFSensor:
  883. // FSENSOR error during load. Make sure E-motor stops moving.
  884. loadFilamentStarted = false;
  885. planner_abort_queued_moves(); // Abort excess E-moves to be safe
  886. break;
  887. default: break;
  888. }
  889. if (ec != lastErrorCode) { // deduplicate: only report changes in error codes into the log
  890. lastErrorCode = ec;
  891. lastErrorSource = res;
  892. LogErrorEvent(PrusaErrorTitle(PrusaErrorCodeIndex(ec)));
  893. if (ec != ErrorCode::OK && ec != ErrorCode::FILAMENT_EJECTED && ec != ErrorCode::FILAMENT_CHANGE) {
  894. IncrementMMUFails();
  895. // Check if it is a "power" failure. TMC-related errors are considered power failures.
  896. static constexpr uint16_t tmcMask =
  897. ( (uint16_t)ErrorCode::TMC_IOIN_MISMATCH
  898. | (uint16_t)ErrorCode::TMC_RESET
  899. | (uint16_t)ErrorCode::TMC_UNDERVOLTAGE_ON_CHARGE_PUMP
  900. | (uint16_t)ErrorCode::TMC_SHORT_TO_GROUND
  901. | (uint16_t)ErrorCode::TMC_OVER_TEMPERATURE_WARN
  902. | (uint16_t)ErrorCode::TMC_OVER_TEMPERATURE_ERROR
  903. | (uint16_t)ErrorCode::MMU_SOLDERING_NEEDS_ATTENTION ) & 0x7FFFU; // skip the top bit
  904. static_assert(tmcMask == 0x7E00); // Just make sure we fail compilation if any of the TMC error codes change
  905. if ((uint16_t)ec & tmcMask) { // @@TODO can be optimized to uint8_t operation
  906. // TMC-related errors are from 0x8200 higher
  907. incrementTMCFailures();
  908. }
  909. }
  910. }
  911. if (!retryIfPossible(ec))
  912. // If retry attempts are all used up
  913. // or if 'Retry' operation is not available
  914. // raise the MMU error screen and wait for user input
  915. ReportErrorHook((CommandInProgress)logic.CommandInProgress(), ec, uint8_t(lastErrorSource));
  916. }
  917. void MMU3::reportProgress(ProgressCode pc) {
  918. ReportProgressHook((CommandInProgress)logic.CommandInProgress(), pc);
  919. LogEchoEvent(ProgressCodeToText(pc));
  920. }
  921. void MMU3::onMMUProgressMsg(ProgressCode pc) {
  922. if (pc != lastProgressCode)
  923. onMMUProgressMsgChanged(pc);
  924. else
  925. onMMUProgressMsgSame(pc);
  926. }
  927. void MMU3::onMMUProgressMsgChanged(ProgressCode pc) {
  928. reportProgress(pc);
  929. lastProgressCode = pc;
  930. switch (pc) {
  931. case ProgressCode::UnloadingToFinda:
  932. if ( (CommandInProgress)logic.CommandInProgress() == CommandInProgress::UnloadFilament
  933. || ((CommandInProgress)logic.CommandInProgress() == CommandInProgress::ToolChange)
  934. ) {
  935. // If MK3S sent U0 command, ramming sequence takes care of releasing the filament.
  936. // If Toolchange is done while printing, PrusaSlicer takes care of releasing the filament
  937. // If printing is not in progress, ToolChange will issue a U0 command.
  938. break;
  939. }
  940. else {
  941. // We're likely recovering from an MMU error
  942. planner_synchronize();
  943. unloadFilamentStarted = true;
  944. helpUnloadToFinda();
  945. }
  946. break;
  947. case ProgressCode::FeedingToFSensor:
  948. // prepare for the movement of the E-motor
  949. planner_synchronize();
  950. loadFilamentStarted = true;
  951. break;
  952. default: break; // do nothing yet
  953. }
  954. }
  955. void __attribute__((noinline)) MMU3::helpUnloadToFinda() {
  956. extruder_move(-MMU2_RETRY_UNLOAD_TO_FINDA_LENGTH, MMU2_RETRY_UNLOAD_TO_FINDA_FEED_RATE);
  957. }
  958. void MMU3::onMMUProgressMsgSame(ProgressCode pc) {
  959. const uint8_t pulley_slow_feedrate = logic.PulleySlowFeedRate();
  960. const float extrude_distance = _MIN(_MAX(EXTRUDE_MAXLENGTH - 1, 1), pulley_slow_feedrate);
  961. switch (pc) {
  962. case ProgressCode::UnloadingToFinda:
  963. if (unloadFilamentStarted && !planner_any_moves()) { // Only plan a move if there is no move ongoing
  964. switch (WhereIsFilament()) {
  965. case FilamentState::AT_FSENSOR:
  966. case FilamentState::IN_NOZZLE:
  967. case FilamentState::UNAVAILABLE: // actually Unavailable makes sense as well to start the E-move to release the filament from the gears
  968. helpUnloadToFinda();
  969. break;
  970. default:
  971. unloadFilamentStarted = false;
  972. }
  973. }
  974. break;
  975. case ProgressCode::FeedingToFSensor:
  976. if (loadFilamentStarted) {
  977. switch (WhereIsFilament()) {
  978. case FilamentState::AT_FSENSOR:
  979. // fsensor triggered, finish FeedingToExtruder state
  980. loadFilamentStarted = false;
  981. // Abort any excess E-move from the planner queue
  982. planner_abort_queued_moves();
  983. // After the MMU knows the FSENSOR is triggered it will:
  984. // 1. Push the filament by additional 30mm (see fsensorToNozzle)
  985. // 2. Disengage the idler and push another 2mm.
  986. extruder_move(logic.ExtraLoadDistance() + 2, logic.PulleySlowFeedRate());
  987. break;
  988. case FilamentState::NOT_PRESENT:
  989. // fsensor not triggered, continue moving extruder
  990. //
  991. // Instead of doing a very long extrude as in PrusaFirmware,
  992. // Marlin's own MMU2s code has a better approach to this by spinning
  993. // the extruder indefinitelly...
  994. //
  995. // this ensures that while the MMU is pushing the filament,
  996. // the extruder will keep rotating, preventing the filament to hit
  997. // the extruder gears...
  998. while (planner.movesplanned() < 3) {
  999. extruder_move(extrude_distance, pulley_slow_feedrate, false);
  1000. }
  1001. break;
  1002. default: break; // Abort here?
  1003. }
  1004. }
  1005. break;
  1006. default: break; // do nothing yet
  1007. }
  1008. }
  1009. } // MMU3
  1010. #endif // HAS_PRUSA_MMU3