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