From 2e293e90a0bc8dc1ffb93aa0be31bdbf6e2eac96 Mon Sep 17 00:00:00 2001 From: "D.R.racer" Date: Wed, 20 Apr 2022 11:47:58 +0200 Subject: [PATCH] MMU2 interface overhaul First port of the new MMU2-printer interface into 8bit FW. --- Firmware/Marlin.h | 2 +- Firmware/Marlin_main.cpp | 334 ++++--------- Firmware/Tcodes.cpp | 124 +++++ Firmware/Tcodes.h | 5 + Firmware/first_lay_cal.cpp | 6 +- Firmware/mmu.h | 120 ----- Firmware/mmu2.cpp | 711 +++++++++++++++++++++++++++ Firmware/mmu2.h | 204 ++++++++ Firmware/mmu2/error_codes.h | 97 ++++ Firmware/mmu2/progress_codes.h | 42 ++ Firmware/mmu2_error_converter.cpp | 6 + Firmware/mmu2_error_converter.h | 7 + Firmware/mmu2_fsensor.cpp | 14 + Firmware/mmu2_fsensor.h | 24 + Firmware/mmu2_log.h | 23 + Firmware/mmu2_power.cpp | 12 + Firmware/mmu2_power.h | 11 + Firmware/mmu2_progress_converter.cpp | 6 + Firmware/mmu2_progress_converter.h | 7 + Firmware/mmu2_protocol.cpp | 247 ++++++++++ Firmware/mmu2_protocol.h | 184 +++++++ Firmware/mmu2_protocol_logic.cpp | 564 +++++++++++++++++++++ Firmware/mmu2_protocol_logic.h | 314 ++++++++++++ Firmware/mmu2_reporting.cpp | 21 + Firmware/mmu2_reporting.h | 53 ++ Firmware/mmu2_serial.cpp | 15 + Firmware/mmu2_serial.h | 21 + Firmware/stepper.cpp | 2 +- Firmware/strlen_cx.h | 5 + Firmware/ultralcd.cpp | 184 +++---- 30 files changed, 2907 insertions(+), 458 deletions(-) create mode 100644 Firmware/Tcodes.cpp create mode 100644 Firmware/Tcodes.h delete mode 100644 Firmware/mmu.h create mode 100644 Firmware/mmu2.cpp create mode 100644 Firmware/mmu2.h create mode 100644 Firmware/mmu2/error_codes.h create mode 100644 Firmware/mmu2/progress_codes.h create mode 100644 Firmware/mmu2_error_converter.cpp create mode 100644 Firmware/mmu2_error_converter.h create mode 100644 Firmware/mmu2_fsensor.cpp create mode 100644 Firmware/mmu2_fsensor.h create mode 100644 Firmware/mmu2_log.h create mode 100644 Firmware/mmu2_power.cpp create mode 100644 Firmware/mmu2_power.h create mode 100644 Firmware/mmu2_progress_converter.cpp create mode 100644 Firmware/mmu2_progress_converter.h create mode 100644 Firmware/mmu2_protocol.cpp create mode 100644 Firmware/mmu2_protocol.h create mode 100644 Firmware/mmu2_protocol_logic.cpp create mode 100644 Firmware/mmu2_protocol_logic.h create mode 100644 Firmware/mmu2_reporting.cpp create mode 100644 Firmware/mmu2_reporting.h create mode 100644 Firmware/mmu2_serial.cpp create mode 100644 Firmware/mmu2_serial.h create mode 100644 Firmware/strlen_cx.h diff --git a/Firmware/Marlin.h b/Firmware/Marlin.h index 12b48782d..da63894db 100755 --- a/Firmware/Marlin.h +++ b/Firmware/Marlin.h @@ -461,7 +461,7 @@ void gcode_M114(); #if (defined(FANCHECK) && (((defined(TACH_0) && (TACH_0 >-1)) || (defined(TACH_1) && (TACH_1 > -1))))) void gcode_M123(); #endif //FANCHECK and TACH_0 and TACH_1 -void gcode_M701(); +void gcode_M701(uint8_t mmuSlotIndex); #define UVLO !(PINE & (1<<4)) diff --git a/Firmware/Marlin_main.cpp b/Firmware/Marlin_main.cpp index ac5cb8984..d7e2e50d2 100644 --- a/Firmware/Marlin_main.cpp +++ b/Firmware/Marlin_main.cpp @@ -86,6 +86,7 @@ #include #include +#include "Tcodes.h" #include "Dcodes.h" #include "AutoDeplete.h" @@ -125,7 +126,7 @@ #include #endif -#include "mmu.h" +#include "mmu2.h" #define VERSION_STRING "1.0.2" @@ -1047,7 +1048,7 @@ void setup() { timer2_init(); // enables functional millis - mmu_init(); + MMU2::mmu2.Start(); ultralcd_init(); @@ -1623,7 +1624,7 @@ void setup() #endif //UVLO_SUPPORT fCheckModeInit(); - fSetMmuMode(mmu_enabled); + fSetMmuMode(MMU2::mmu2.Enabled()); KEEPALIVE_STATE(NOT_BUSY); #ifdef WATCHDOG wdt_enable(WDTO_4S); @@ -1856,7 +1857,7 @@ void loop() } } #endif //TMC2130 - mmu_loop(); + MMU2::mmu2.mmu_loop(); } #define DEFINE_PGM_READ_ANY(type, reader) \ @@ -3469,15 +3470,14 @@ static T gcode_M600_filament_change_z_shift() #else return T(0); #endif -} +} -static void gcode_M600(bool automatic, float x_position, float y_position, float z_shift, float e_shift, float /*e_shift_late*/) -{ +static void gcode_M600(bool automatic, float x_position, float y_position, float z_shift, float e_shift, float /*e_shift_late*/) { st_synchronize(); float lastpos[4]; prusa_statistics(22); - + //First backup current position and settings int feedmultiplyBckp = feedmultiply; float HotendTempBckp = degTargetHotend(active_extruder); @@ -3488,33 +3488,35 @@ static void gcode_M600(bool automatic, float x_position, float y_position, float lastpos[Z_AXIS] = current_position[Z_AXIS]; lastpos[E_AXIS] = current_position[E_AXIS]; - //Retract E + // Retract E current_position[E_AXIS] += e_shift; plan_buffer_line_curposXYZE(FILAMENTCHANGE_RFEED); st_synchronize(); - //Lift Z + // Lift Z current_position[Z_AXIS] += z_shift; clamp_to_software_endstops(current_position); plan_buffer_line_curposXYZE(FILAMENTCHANGE_ZFEED); st_synchronize(); - //Move XY to side + // Move XY to side current_position[X_AXIS] = x_position; current_position[Y_AXIS] = y_position; plan_buffer_line_curposXYZE(FILAMENTCHANGE_XYFEED); st_synchronize(); - //Beep, manage nozzle heater and wait for user to start unload filament - if(!mmu_enabled) M600_wait_for_user(HotendTempBckp); + // Beep, manage nozzle heater and wait for user to start unload filament + if (!MMU2::mmu2.Enabled()) + M600_wait_for_user(HotendTempBckp); lcd_change_fil_state = 0; // Unload filament - if (mmu_enabled) extr_unload(); //unload just current filament for multimaterial printers (used also in M702) - else unload_filament(true); //unload filament for single material (used also in M702) - //finish moves - st_synchronize(); + if (MMU2::mmu2.Enabled()) + MMU2::mmu2.unload(); // unload just current filament for multimaterial printers (used also in M702) + else + unload_filament(true); // unload filament for single material (used also in M702) + st_synchronize(); // finish moves #ifdef FILAMENT_SENSOR fsensor.setRunoutEnabled(false); //suppress filament runouts while loading filament. @@ -3524,14 +3526,11 @@ static void gcode_M600(bool automatic, float x_position, float y_position, float #endif //(FILAMENT_SENSOR_TYPE == FSENSOR_PAT9125) #endif - if (!mmu_enabled) - { + if (!MMU2::mmu2.Enabled()) { KEEPALIVE_STATE(PAUSED_FOR_USER); - lcd_change_fil_state = lcd_show_fullscreen_message_yes_no_and_wait_P( - _i("Was filament unload successful?"), ////MSG_UNLOAD_SUCCESSFUL c=20 r=2 - false, true); - if (lcd_change_fil_state == 0) - { + lcd_change_fil_state = + lcd_show_fullscreen_message_yes_no_and_wait_P(_i("Was filament unload successful?"), false, true); ////MSG_UNLOAD_SUCCESSFUL c=20 r=2 + if (lcd_change_fil_state == 0) { lcd_clear(); lcd_puts_at_P(0, 2, _T(MSG_PLEASE_WAIT)); current_position[X_AXIS] -= 100; @@ -3541,55 +3540,53 @@ static void gcode_M600(bool automatic, float x_position, float y_position, float } } - if (mmu_enabled) - { + if (MMU2::mmu2.Enabled()) { if (!automatic) { - if (saved_printing) mmu_eject_filament(mmu_extruder, false); //if M600 was invoked by filament senzor (FINDA) eject filament so user can easily remove it - mmu_M600_wait_and_beep(); + if (saved_printing) + MMU2::mmu2.eject_filament(MMU2::mmu2.get_current_tool(), + false); // if M600 was invoked by filament senzor (FINDA) eject filament so user can easily remove it +//@@TODO mmu_M600_wait_and_beep(); if (saved_printing) { lcd_clear(); lcd_puts_at_P(0, 2, _T(MSG_PLEASE_WAIT)); - mmu_command(MmuCmd::R0); - manage_response(false, false); +//@@TODO mmu_command(MmuCmd::R0); +// manage_response(false, false); } } - mmu_M600_load_filament(automatic, HotendTempBckp); - } - else +//@@TODO mmu_M600_load_filament(automatic, HotendTempBckp); + } else M600_load_filament(); - if (!automatic) M600_check_state(HotendTempBckp); + if (!automatic) + M600_check_state(HotendTempBckp); - lcd_update_enable(true); + lcd_update_enable(true); - //Not let's go back to print + // Not let's go back to print fanSpeed = fanSpeedBckp; - //Feed a little of filament to stabilize pressure - if (!automatic) - { + // Feed a little of filament to stabilize pressure + if (!automatic) { current_position[E_AXIS] += FILAMENTCHANGE_RECFEED; plan_buffer_line_curposXYZE(FILAMENTCHANGE_EXFEED); } - //Move XY back - plan_buffer_line(lastpos[X_AXIS], lastpos[Y_AXIS], current_position[Z_AXIS], current_position[E_AXIS], - FILAMENTCHANGE_XYFEED, active_extruder); + // Move XY back + plan_buffer_line(lastpos[X_AXIS], lastpos[Y_AXIS], current_position[Z_AXIS], current_position[E_AXIS], FILAMENTCHANGE_XYFEED, active_extruder); st_synchronize(); - //Move Z back - plan_buffer_line(lastpos[X_AXIS], lastpos[Y_AXIS], lastpos[Z_AXIS], current_position[E_AXIS], - FILAMENTCHANGE_ZFEED, active_extruder); + // Move Z back + plan_buffer_line(lastpos[X_AXIS], lastpos[Y_AXIS], lastpos[Z_AXIS], current_position[E_AXIS], FILAMENTCHANGE_ZFEED, active_extruder); st_synchronize(); - //Set E position to original + // Set E position to original plan_set_e_position(lastpos[E_AXIS]); memcpy(current_position, lastpos, sizeof(lastpos)); set_destination_to_current(); - //Recover feed rate + // Recover feed rate feedmultiply = feedmultiplyBckp; char cmd[9]; sprintf_P(cmd, PSTR("M220 S%i"), feedmultiplyBckp); @@ -3603,33 +3600,26 @@ static void gcode_M600(bool automatic, float x_position, float y_position, float custom_message_type = CustomMsg::Status; } -void gcode_M701() -{ - printf_P(PSTR("gcode_M701 begin\n")); +void gcode_M701(uint8_t mmuSlotIndex){ + printf_P(PSTR("gcode_M701 begin\n")); #ifdef FILAMENT_SENSOR - fsensor.setRunoutEnabled(false); //suppress filament runouts while loading filament. - fsensor.setAutoLoadEnabled(false); //suppress filament autoloads while loading filament. + fsensor.setRunoutEnabled(false); // suppress filament runouts while loading filament. + fsensor.setAutoLoadEnabled(false); // suppress filament autoloads while loading filament. #if (FILAMENT_SENSOR_TYPE == FSENSOR_PAT9125) - fsensor.setJamDetectionEnabled(false); //suppress filament jam detection while loading filament. -#endif //(FILAMENT_SENSOR_TYPE == FSENSOR_PAT9125) + fsensor.setJamDetectionEnabled(false); // suppress filament jam detection while loading filament. +#endif //(FILAMENT_SENSOR_TYPE == FSENSOR_PAT9125) #endif - prusa_statistics(22); - - if (mmu_enabled) - { - extr_adj(tmp_extruder);//loads current extruder - mmu_extruder = tmp_extruder; + prusa_statistics(22); } - else - { - enable_z(); - custom_message_type = CustomMsg::FilamentLoading; -#ifdef FSENSOR_QUALITY - fsensor_oq_meassure_start(40); -#endif //FSENSOR_QUALITY + if (MMU2::mmu2.Enabled() && mmuSlotIndex < MMU_FILAMENT_COUNT) { + MMU2::mmu2.load_filament(mmuSlotIndex); // loads current extruder + // mmu_extruder = mmuSlotIndex; // @@TODO shall load filament set current tool to some specific index? We don't do that anymore. + } else { + enable_z(); + custom_message_type = CustomMsg::FilamentLoading; const int feed_mm_before_raising = 30; static_assert(feed_mm_before_raising <= FILAMENTCHANGE_FIRSTFEED); @@ -3639,41 +3629,30 @@ void gcode_M701() plan_buffer_line_curposXYZE(FILAMENTCHANGE_EFEED_FIRST); //fast sequence st_synchronize(); - raise_z_above(MIN_Z_FOR_LOAD, false); + raise_z_above(MIN_Z_FOR_LOAD, false); current_position[E_AXIS] += feed_mm_before_raising; plan_buffer_line_curposXYZE(FILAMENTCHANGE_EFEED_FIRST); //fast sequence - - load_filament_final_feed(); //slow sequence - st_synchronize(); - Sound_MakeCustom(50,500,false); + load_filament_final_feed(); // slow sequence + st_synchronize(); - if (!farm_mode && loading_flag) { - lcd_load_filament_color_check(); - } - lcd_update_enable(true); - lcd_update(2); - lcd_setstatuspgm(MSG_WELCOME); - disable_z(); - loading_flag = false; - custom_message_type = CustomMsg::Status; + Sound_MakeCustom(50, 500, false); -#ifdef FSENSOR_QUALITY - fsensor_oq_meassure_stop(); + if (!farm_mode && loading_flag) { + lcd_load_filament_color_check(); + } + lcd_update_enable(true); + lcd_update(2); + lcd_setstatuspgm(MSG_WELCOME); + disable_z(); + loading_flag = false; + custom_message_type = CustomMsg::Status; + } + + eFilamentAction = FilamentAction::None; - if (!fsensor_oq_result()) - { - bool disable = lcd_show_fullscreen_message_yes_no_and_wait_P(_n("Fil. sensor response is poor, disable it?"), false, true); - lcd_update_enable(true); - lcd_update(2); - if (disable) - fsensor_disable(); - } - - eFilamentAction = FilamentAction::None; - #ifdef FILAMENT_SENSOR - fsensor.settings_init(); //restore filament runout state. + fsensor.settings_init(); // restore filament runout state. #endif } /** @@ -4263,7 +4242,7 @@ void process_commands() } else if (code_seen_P(PSTR("MMURES"))) // PRUSA MMURES { - mmu_reset(); + MMU2::mmu2.Reset(MMU2::MMU2::Software); } else if (code_seen_P(PSTR("RESET"))) { // PRUSA RESET #ifdef WATCHDOG @@ -7614,14 +7593,13 @@ Sigma_Exit: { // currently three different materials are needed (default, flex and PVA) // add storing this information for different load/unload profiles etc. in the future - // firmware does not wait for "ok" from mmu - if (mmu_enabled) + if (MMU2::mmu2.Enabled()) { uint8_t extruder = 255; uint8_t filament = FILAMENT_UNDEFINED; if(code_seen('E')) extruder = code_value_uint8(); if(code_seen('F')) filament = code_value_uint8(); - mmu_set_filament_type(extruder, filament); + MMU2::mmu2.set_filament_type(extruder, filament); } } break; @@ -7859,7 +7837,7 @@ Sigma_Exit: #endif } - if (mmu_enabled && code_seen_P(PSTR("AUTO"))) + if (MMU2::mmu2.Enabled() && code_seen_P(PSTR("AUTO"))) automatic = true; gcode_M600(automatic, x_position, y_position, z_shift, e_shift_init, e_shift_late); @@ -8509,9 +8487,10 @@ Sigma_Exit: */ case 701: { - if (mmu_enabled && (code_seen('E') || code_seen('T'))) - tmp_extruder = code_value_uint8(); - gcode_M701(); + uint8_t mmuSlotIndex = 0xffU; + if (MMU2::mmu2.Enabled() && code_seen('E')) + mmuSlotIndex = code_value_uint8(); + gcode_M701(mmuSlotIndex); } break; @@ -8528,10 +8507,10 @@ Sigma_Exit: case 702: { if (code_seen('C')) { - if(mmu_enabled) extr_unload(); //! if "C" unload current filament; if mmu is not present no action is performed + if(MMU2::mmu2.Enabled()) MMU2::mmu2.unload(); //! if "C" unload current filament; if mmu is not present no action is performed } else { - if(mmu_enabled) extr_unload(); //! unload current filament + if(MMU2::mmu2.Enabled()) MMU2::mmu2.unload(); //! unload current filament else unload_filament(); } } @@ -8558,139 +8537,8 @@ Sigma_Exit: @n Tx Same as T?, except nozzle doesn't have to be preheated. Tc must be placed after extruder nozzle is preheated to finish filament load. @n Tc Load to nozzle after filament was prepared by Tc and extruder nozzle is already heated. */ - else if(code_seen('T')) - { - static const char duplicate_Tcode_ignored[] PROGMEM = "Duplicate T-code ignored."; - - int index; - bool load_to_nozzle = false; - for (index = 1; *(strchr_pointer + index) == ' ' || *(strchr_pointer + index) == '\t'; index++); - - *(strchr_pointer + index) = tolower(*(strchr_pointer + index)); - - if ((*(strchr_pointer + index) < '0' || *(strchr_pointer + index) > '4') && *(strchr_pointer + index) != '?' && *(strchr_pointer + index) != 'x' && *(strchr_pointer + index) != 'c') { - SERIAL_ECHOLNPGM("Invalid T code."); - } - else if (*(strchr_pointer + index) == 'x'){ //load to bondtech gears; if mmu is not present do nothing - if (mmu_enabled) - { - tmp_extruder = choose_menu_P(_T(MSG_SELECT_FILAMENT), _T(MSG_FILAMENT)); - if ((tmp_extruder == mmu_extruder) && mmu_fil_loaded) //dont execute the same T-code twice in a row - { - puts_P(duplicate_Tcode_ignored); - } - else - { - st_synchronize(); - mmu_command(MmuCmd::T0 + tmp_extruder); - manage_response(true, true, MMU_TCODE_MOVE); - } - } - } - else if (*(strchr_pointer + index) == 'c') { //load to from bondtech gears to nozzle (nozzle should be preheated) - if (mmu_enabled) - { - st_synchronize(); - mmu_continue_loading(usb_timer.running() || (lcd_commands_type == LcdCommands::Layer1Cal)); - mmu_extruder = tmp_extruder; //filament change is finished - mmu_load_to_nozzle(); - } - } - else { - if (*(strchr_pointer + index) == '?') - { - if(mmu_enabled) - { - tmp_extruder = choose_menu_P(_T(MSG_SELECT_FILAMENT), _T(MSG_FILAMENT)); - load_to_nozzle = true; - } else - { - tmp_extruder = choose_menu_P(_T(MSG_SELECT_EXTRUDER), _T(MSG_EXTRUDER)); - } - } - else { - tmp_extruder = code_value(); - if (mmu_enabled && lcd_autoDepleteEnabled()) - { - tmp_extruder = ad_getAlternative(tmp_extruder); - } - } - st_synchronize(); - - if (mmu_enabled) - { - if ((tmp_extruder == mmu_extruder) && mmu_fil_loaded) //dont execute the same T-code twice in a row - { - puts_P(duplicate_Tcode_ignored); - } - else - { -#if defined(MMU_HAS_CUTTER) && defined(MMU_ALWAYS_CUT) - if (EEPROM_MMU_CUTTER_ENABLED_always == eeprom_read_byte((uint8_t*)EEPROM_MMU_CUTTER_ENABLED)) - { - mmu_command(MmuCmd::K0 + tmp_extruder); - manage_response(true, true, MMU_UNLOAD_MOVE); - } -#endif //defined(MMU_HAS_CUTTER) && defined(MMU_ALWAYS_CUT) - mmu_command(MmuCmd::T0 + tmp_extruder); - manage_response(true, true, MMU_TCODE_MOVE); - mmu_continue_loading(usb_timer.running() || (lcd_commands_type == LcdCommands::Layer1Cal)); - - mmu_extruder = tmp_extruder; //filament change is finished - - if (load_to_nozzle)// for single material usage with mmu - { - mmu_load_to_nozzle(); - } - } - } - else - { - if (tmp_extruder >= EXTRUDERS) { - SERIAL_ECHO_START; - SERIAL_ECHO('T'); - SERIAL_PROTOCOLLN((int)tmp_extruder); - SERIAL_ECHOLNRPGM(_n("Invalid extruder"));////MSG_INVALID_EXTRUDER - } - else { -#if EXTRUDERS > 1 - bool make_move = false; -#endif - if (code_seen('F')) { -#if EXTRUDERS > 1 - make_move = true; -#endif - next_feedrate = code_value(); - if (next_feedrate > 0.0) { - feedrate = next_feedrate; - } - } -#if EXTRUDERS > 1 - if (tmp_extruder != active_extruder) { - // Save current position to return to after applying extruder offset - set_destination_to_current(); - // Offset extruder (only by XY) - int i; - for (i = 0; i < 2; i++) { - current_position[i] = current_position[i] - - extruder_offset[i][active_extruder] + - extruder_offset[i][tmp_extruder]; - } - // Set the new active extruder and position - active_extruder = tmp_extruder; - plan_set_position_curposXYZE(); - // Move to the old position if 'F' was in the parameters - if (make_move) { - prepare_move(); - } - } -#endif - SERIAL_ECHO_START; - SERIAL_ECHORPGM(_n("Active Extruder: "));////MSG_ACTIVE_EXTRUDER - SERIAL_PROTOCOLLN((int)active_extruder); - } - } - } + else if(code_seen('T')){ + TCodes(strchr_pointer, code_value()); } // end if(code_seen('T')) (end of T codes) /*! #### End of T-Codes @@ -9491,7 +9339,7 @@ void manage_inactivity(bool ignore_stepper_queue/*=false*/) //default argument s } #endif check_axes_activity(); - mmu_loop(); + MMU2::mmu2.mmu_loop(); // handle longpress if(lcd_longpress_trigger) @@ -11422,10 +11270,12 @@ void M600_check_state(float nozzle_temp) { // Filament failed to load so load it again case 2: - if (mmu_enabled) - mmu_M600_load_filament(false, nozzle_temp); //nonautomatic load; change to "wrong filament loaded" option? - else + if (MMU2::mmu2.Enabled()){ +//@@TODO mmu_M600_load_filament(false, nozzle_temp); //nonautomatic load; change to "wrong filament loaded" option? + + } else { M600_load_filament_movements(); + } break; // Filament loaded properly but color is not clear diff --git a/Firmware/Tcodes.cpp b/Firmware/Tcodes.cpp new file mode 100644 index 000000000..e94dfe8b1 --- /dev/null +++ b/Firmware/Tcodes.cpp @@ -0,0 +1,124 @@ +#include "Tcodes.h" +#include "Marlin.h" +#include "mmu2.h" +#include "stepper.h" +#include +#include +#include +#include "language.h" +#include "messages.h" +#include "ultralcd.h" +#include + +static const char duplicate_Tcode_ignored[] PROGMEM = "Duplicate T-code ignored."; + +inline bool IsInvalidTCode(char *const s, uint8_t i) { + return ((s[i] < '0' || s[i] > '4') && s[i] != '?' && s[i] != 'x' && s[i] != 'c'); +} + +inline void TCodeInvalid() { + SERIAL_ECHOLNPGM("Invalid T code."); +} + +// load to bondtech gears; if mmu is not present do nothing +void TCodeX() { + if (MMU2::mmu2.Enabled()) { + uint8_t selectedSlot = choose_menu_P(_T(MSG_CHOOSE_FILAMENT), _T(MSG_FILAMENT)); + if ((selectedSlot == MMU2::mmu2.get_current_tool()) /*&& mmu_fil_loaded @@TODO */){ + // dont execute the same T-code twice in a row + puts_P(duplicate_Tcode_ignored); + } else { + st_synchronize(); + MMU2::mmu2.tool_change(selectedSlot); + } + } +} + +// load to from bondtech gears to nozzle (nozzle should be preheated) +void TCodeC() { + if (MMU2::mmu2.Enabled()) { + st_synchronize(); +// @@TODO mmu_continue_loading(usb_timer.running() || (lcd_commands_type == LcdCommands::Layer1Cal)); +// mmu_extruder = selectedSlot; // filament change is finished +// MMU2::mmu2.load_filament_to_nozzle(); + } +} + +struct SChooseFromMenu { + uint8_t slot:7; + uint8_t loadToNozzle:1; + inline constexpr SChooseFromMenu(uint8_t slot, bool loadToNozzle):slot(slot), loadToNozzle(loadToNozzle){} + inline constexpr SChooseFromMenu():slot(0), loadToNozzle(false) { } +}; + +SChooseFromMenu TCodeChooseFromMenu() { + if (MMU2::mmu2.Enabled()) { + return SChooseFromMenu( choose_menu_P(_T(MSG_CHOOSE_FILAMENT), _T(MSG_FILAMENT)), true ); + } else { + return SChooseFromMenu( choose_menu_P(_T(MSG_CHOOSE_EXTRUDER), _T(MSG_EXTRUDER)), false ); + } +} + +void TCodes(char *const strchr_pointer, uint8_t codeValue) { + uint8_t index; + for (index = 1; strchr_pointer[index] == ' ' || strchr_pointer[index] == '\t'; index++) + ; + + strchr_pointer[index] = tolower(strchr_pointer[index]); + + if (IsInvalidTCode(strchr_pointer, index)) + TCodeInvalid(); + else if (strchr_pointer[index] == 'x') + TCodeX(); + else if (strchr_pointer[index] == 'c') + TCodeC(); + else { + SChooseFromMenu selectedSlot; + if (strchr_pointer[index] == '?') + selectedSlot = TCodeChooseFromMenu(); + else { + selectedSlot.slot = codeValue; + if (MMU2::mmu2.Enabled() && lcd_autoDepleteEnabled()) { +// @@TODO selectedSlot.slot = ad_getAlternative(selectedSlot); + } + } + st_synchronize(); + + if (MMU2::mmu2.Enabled()) { + if ((selectedSlot.slot == MMU2::mmu2.get_current_tool()) /*&& mmu_fil_loaded @@TODO*/){ + // don't execute the same T-code twice in a row + puts_P(duplicate_Tcode_ignored); + } else { +#if defined(MMU_HAS_CUTTER) && defined(MMU_ALWAYS_CUT) + if (EEPROM_MMU_CUTTER_ENABLED_always == eeprom_read_byte((uint8_t *)EEPROM_MMU_CUTTER_ENABLED)) { + mmu_command(MmuCmd::K0 + selectedSlot); + manage_response(true, true, MMU_UNLOAD_MOVE); + } +#endif // defined(MMU_HAS_CUTTER) && defined(MMU_ALWAYS_CUT) + MMU2::mmu2.tool_change(selectedSlot.slot); +// @@TODO mmu_continue_loading(usb_timer.running() || (lcd_commands_type == LcdCommands::Layer1Cal)); + + if (selectedSlot.loadToNozzle){ // for single material usage with mmu + MMU2::mmu2.load_filament_to_nozzle(selectedSlot.slot); + } + } + } else { + if (selectedSlot.slot >= EXTRUDERS) { + SERIAL_ECHO_START; + SERIAL_ECHO('T'); + SERIAL_ECHOLN(selectedSlot.slot + '0'); + SERIAL_ECHOLNRPGM(_n("Invalid extruder")); ////MSG_INVALID_EXTRUDER + } else { +// @@TODO if (code_seen('F')) { +// next_feedrate = code_value(); +// if (next_feedrate > 0.0) { +// feedrate = next_feedrate; +// } +// } + SERIAL_ECHO_START; + SERIAL_ECHORPGM(_n("Active Extruder: ")); ////MSG_ACTIVE_EXTRUDER + SERIAL_ECHOLN(active_extruder + '0'); // this is not changed in our FW at all, can be optimized away + } + } + } +} diff --git a/Firmware/Tcodes.h b/Firmware/Tcodes.h new file mode 100644 index 000000000..684475780 --- /dev/null +++ b/Firmware/Tcodes.h @@ -0,0 +1,5 @@ +/// @file +#pragma once +#include + +void TCodes(char * const strchr_pointer, uint8_t codeValue); diff --git a/Firmware/first_lay_cal.cpp b/Firmware/first_lay_cal.cpp index 0e01794bd..5d7f0aa9b 100644 --- a/Firmware/first_lay_cal.cpp +++ b/Firmware/first_lay_cal.cpp @@ -8,7 +8,7 @@ #include "language.h" #include "Marlin.h" #include "cmdqueue.h" -#include "mmu.h" +#include "mmu2.h" #include //! @brief Wait for preheat @@ -34,7 +34,7 @@ void lay1cal_wait_preheat() //! @param filament filament to use (applies for MMU only) void lay1cal_load_filament(char *cmd_buffer, uint8_t filament) { - if (mmu_enabled) + if (MMU2::mmu2.Enabled()) { enquecommand_P(PSTR("M83")); enquecommand_P(PSTR("G1 Y-3.0 F1000.0")); @@ -73,7 +73,7 @@ void lay1cal_intro_line() cmd_intro_mmu_12, }; - if (mmu_enabled) + if (MMU2::mmu2.Enabled()) { for (uint8_t i = 0; i < (sizeof(intro_mmu_cmd)/sizeof(intro_mmu_cmd[0])); ++i) { diff --git a/Firmware/mmu.h b/Firmware/mmu.h deleted file mode 100644 index 12dad9d41..000000000 --- a/Firmware/mmu.h +++ /dev/null @@ -1,120 +0,0 @@ -//! @file - -#ifndef MMU_H -#define MMU_H - -#include -#include "Timer.h" - - -extern bool mmu_enabled; -extern bool mmu_fil_loaded; - -extern uint8_t mmu_extruder; - -extern uint8_t tmp_extruder; - -extern int8_t mmu_finda; -extern LongTimer mmu_last_finda_response; -extern bool ir_sensor_detected; - -extern int16_t mmu_version; -extern int16_t mmu_buildnr; - -extern uint16_t mmu_power_failures; - -#define MMU_FILAMENT_UNKNOWN 255 - -#define MMU_NO_MOVE 0 -#define MMU_UNLOAD_MOVE 1 -#define MMU_LOAD_MOVE 2 -#define MMU_TCODE_MOVE 3 - -#define MMU_LOAD_FEEDRATE 19.02f //mm/s -#define MMU_LOAD_TIME_MS 2000 //should be fine tuned to load time for shortest allowed PTFE tubing and maximum loading speed - -enum class MmuCmd : uint_least8_t -{ - None, - T0, - T1, - T2, - T3, - T4, - L0, - L1, - L2, - L3, - L4, - C0, - U0, - E0, - E1, - E2, - E3, - E4, - K0, - K1, - K2, - K3, - K4, - R0, - S3, - W0, //!< Wait and signal load error -}; - -inline MmuCmd operator+ (MmuCmd cmd, uint8_t filament) -{ - return static_cast(static_cast(cmd) + filament ); -} - -inline uint8_t operator- (MmuCmd cmda, MmuCmd cmdb) -{ - return (static_cast(cmda) - static_cast(cmdb)); -} - -extern int mmu_puts_P(const char* str); - -extern int mmu_printf_P(const char* format, ...); - -extern int8_t mmu_rx_ok(void); - -extern bool check_for_ir_sensor(); - -extern void mmu_init(void); - -extern void mmu_loop(void); - - -extern void mmu_reset(void); - -extern int8_t mmu_set_filament_type(uint8_t extruder, uint8_t filament); - -extern void mmu_command(MmuCmd cmd); - -extern bool mmu_get_response(uint8_t move = 0); - -extern void manage_response(bool move_axes, bool turn_off_nozzle, uint8_t move = MMU_NO_MOVE); - -extern void mmu_load_to_nozzle(); - -extern void mmu_M600_load_filament(bool automatic, float nozzle_temp); -extern void mmu_M600_wait_and_beep(); - -extern void extr_adj(uint8_t extruder); -extern void extr_unload(); -extern void load_all(); - -extern bool mmu_check_version(); -extern void mmu_show_warning(); -extern void lcd_mmu_load_to_nozzle(uint8_t filament_nr); -extern void mmu_eject_filament(uint8_t filament, bool recover); -#ifdef MMU_HAS_CUTTER -extern void mmu_cut_filament(uint8_t filament_nr); -#endif //MMU_HAS_CUTTER -extern void mmu_continue_loading(bool blocking); -extern void mmu_filament_ramming(); -extern void mmu_wait_for_heater_blocking(); -extern void mmu_load_step(bool synchronize = true); - -#endif //MMU_H diff --git a/Firmware/mmu2.cpp b/Firmware/mmu2.cpp new file mode 100644 index 000000000..8ec794914 --- /dev/null +++ b/Firmware/mmu2.cpp @@ -0,0 +1,711 @@ +#include "mmu2.h" +#include "mmu2_fsensor.h" +#include "mmu2_log.h" +#include "mmu2_power.h" +#include "mmu2_reporting.h" + +#include "Marlin.h" +#include "stepper.h" +#include "mmu2_error_converter.h" +#include "mmu2_progress_converter.h" + +// @@TODO remove this and enable it in the configuration files +// Settings for filament load / unload from the LCD menu. +// This is for Prusa MK3-style extruders. Customize for your hardware. +#define MMU2_FILAMENTCHANGE_EJECT_FEED 80.0 +#define MMU2_LOAD_TO_NOZZLE_SEQUENCE \ + { 7.2, 562 }, \ + { 14.4, 871 }, \ + { 36.0, 1393 }, \ + { 14.4, 871 }, \ + { 50.0, 198 } + +// @@TODO +#define FILAMENT_MMU2_RAMMING_SEQUENCE { 7.2, 562 } + +//@@TODO extract into configuration if it makes sense + +// Nominal distance from the extruder gear to the nozzle tip is 87mm +// However, some slipping may occur and we need separate distances for +// LoadToNozzle and ToolChange. +// - +5mm seemed good for LoadToNozzle, +// - but too much (made blobs) for a ToolChange +static constexpr float MMU2_LOAD_TO_NOZZLE_LENGTH = 87.0F + 5.0F; + +// As discussed with our PrusaSlicer profile specialist +// - ToolChange shall not try to push filament into the very tip of the nozzle +// to have some space for additional G-code to tune the extruded filament length +// in the profile +static constexpr float MMU2_TOOL_CHANGE_LOAD_LENGTH = 30.0F; + +static constexpr float MMU2_LOAD_TO_NOZZLE_FEED_RATE = 20.0F; +static constexpr uint8_t MMU2_NO_TOOL = 99; +static constexpr uint32_t MMU_BAUD = 115200; + +typedef uint16_t feedRate_t; + +struct E_Step { + float extrude; ///< extrude distance in mm + feedRate_t feedRate; ///< feed rate in mm/s +}; + +static constexpr E_Step ramming_sequence[] PROGMEM = FILAMENT_MMU2_RAMMING_SEQUENCE; +static constexpr E_Step load_to_nozzle_sequence[] PROGMEM = { MMU2_LOAD_TO_NOZZLE_SEQUENCE }; + +namespace MMU2 { + +void execute_extruder_sequence(const E_Step *sequence, int steps); + +MMU2 mmu2; + +MMU2::MMU2() + : logic(&mmu2Serial) + , extruder(MMU2_NO_TOOL) + , resume_position() + , resume_hotend_temp(0) + , logicStepLastStatus(StepStatus::Finished) + , state(xState::Stopped) + , mmu_print_saved(false) + , loadFilamentStarted(false) + , loadingToNozzle(false) +{ +} + +void MMU2::Start() { + mmu2Serial.begin(MMU_BAUD); + + PowerOn(); + mmu2Serial.flush(); // make sure the UART buffer is clear before starting communication + + extruder = MMU2_NO_TOOL; + state = xState::Connecting; + + // start the communication + logic.Start(); +} + +void MMU2::Stop() { + StopKeepPowered(); + PowerOff(); +} + +void MMU2::StopKeepPowered(){ + state = xState::Stopped; + logic.Stop(); + mmu2Serial.close(); +} + +void MMU2::Reset(ResetForm level){ + switch (level) { + case Software: ResetX0(); break; + case ResetPin: TriggerResetPin(); break; + case CutThePower: PowerCycle(); break; + default: break; + } +} + +void MMU2::ResetX0() { + logic.ResetMMU(); // Send soft reset +} + +void MMU2::TriggerResetPin(){ + reset(); +} + +void MMU2::PowerCycle(){ + // cut the power to the MMU and after a while restore it + PowerOff(); + _delay(1000); //@@TODO + PowerOn(); +} + +void MMU2::PowerOff(){ + power_off(); +} + +void MMU2::PowerOn(){ + power_on(); +} + +void MMU2::mmu_loop() { + // We only leave this method if the current command was successfully completed - that's the Marlin's way of blocking operation + // Atomic compare_exchange would have been the most appropriate solution here, but this gets called only in Marlin's task, + // so thread safety should be kept + static bool avoidRecursion = false; + if (avoidRecursion) + return; + avoidRecursion = true; + + logicStepLastStatus = LogicStep(); // it looks like the mmu_loop doesn't need to be a blocking call + + avoidRecursion = false; +} + +struct ReportingRAII { + CommandInProgress cip; + inline ReportingRAII(CommandInProgress cip):cip(cip){ + BeginReport(cip, (uint16_t)ProgressCode::EngagingIdler); + } + inline ~ReportingRAII(){ + EndReport(cip, (uint16_t)ProgressCode::OK); + } +}; + +bool MMU2::WaitForMMUReady(){ + switch(State()){ + case xState::Stopped: + return false; + case xState::Connecting: + // shall we wait until the MMU reconnects? + // fire-up a fsm_dlg and show "MMU not responding"? + default: + return true; + } +} + +bool MMU2::tool_change(uint8_t index) { + if( ! WaitForMMUReady()) + return false; + + if (index != extruder) { + ReportingRAII rep(CommandInProgress::ToolChange); + BlockRunoutRAII blockRunout; + + st_synchronize(); + + logic.ToolChange(index); // let the MMU pull the filament out and push a new one in + manage_response(false, false); // true, true); + + // reset current position to whatever the planner thinks it is + // @@TODO is there some "standard" way of doing this? +//@@TODO current_position[E_AXIS] = Planner::get_machine_position_mm()[3]; + + extruder = index; //filament change is finished + SetActiveExtruder(0); + + // @@TODO really report onto the serial? May be for the Octoprint? Not important now + // SERIAL_ECHO_START(); + // SERIAL_ECHOLNPAIR(MSG_ACTIVE_EXTRUDER, int(extruder)); + } + return true; +} + +/// Handle special T?/Tx/Tc commands +/// +///- T? Gcode to extrude shouldn't have to follow, load to extruder wheels is done automatically +///- Tx Same as T?, except nozzle doesn't have to be preheated. Tc must be placed after extruder nozzle is preheated to finish filament load. +///- Tc Load to nozzle after filament was prepared by Tx and extruder nozzle is already heated. +bool MMU2::tool_change(const char *special) { + if( ! WaitForMMUReady()) + return false; + +#if 0 //@@TODO + BlockRunoutRAII blockRunout; + + switch (*special) { + case '?': { + uint8_t index = 0; // mmu2_choose_filament(); //@@TODO GUI - user selects + while (!thermalManager.wait_for_hotend(active_extruder, false)) + safe_delay(100); + load_filament_to_nozzle(index); + } break; + + case 'x': { + planner.synchronize(); + uint8_t index = 0; //mmu2_choose_filament(); //@@TODO GUI - user selects + disable_E0(); + logic.ToolChange(index); + manage_response(false, false); // true, true); + + enable_E0(); + extruder = index; + SetActiveExtruder(0); + } break; + + case 'c': { + while (!thermalManager.wait_for_hotend(active_extruder, false)) + safe_delay(100); + execute_extruder_sequence((const E_Step *)load_to_nozzle_sequence, COUNT(load_to_nozzle_sequence)); + } break; + } + +#endif + return true; +} + +uint8_t MMU2::get_current_tool() { + return extruder == MMU2_NO_TOOL ? -1 : extruder; +} + +bool MMU2::set_filament_type(uint8_t index, uint8_t type) { + if( ! WaitForMMUReady()) + return false; + + // @@TODO - this is not supported in the new MMU yet + // cmd_arg = filamentType; + // command(MMU_CMD_F0 + index); + + manage_response(false, false); // true, true); + + return true; +} + +bool MMU2::unload() { + if( ! WaitForMMUReady()) + return false; + + // @@TODO +// if (thermalManager.tooColdToExtrude(active_extruder)) { +// BUZZ(200, 404); +// LCD_ALERTMESSAGEPGM(MSG_HOTEND_TOO_COLD); +// return false; +// } + + { + ReportingRAII rep(CommandInProgress::UnloadFilament); + filament_ramming(); + + logic.UnloadFilament(); + manage_response(false, false); // false, true); + +// BUZZ(200, 404); + + // no active tool + extruder = MMU2_NO_TOOL; + } + return true; +} + +bool MMU2::cut_filament(uint8_t index){ + if( ! WaitForMMUReady()) + return false; + + ReportingRAII rep(CommandInProgress::CutFilament); + logic.CutFilament(index); + manage_response(false, false); // false, true); + + return true; +} + +bool MMU2::load_filament(uint8_t index) { + if( ! WaitForMMUReady()) + return false; + + ReportingRAII rep(CommandInProgress::LoadFilament); + logic.LoadFilament(index); + manage_response(false, false); +// BUZZ(200, 404); + + return true; +} + +struct LoadingToNozzleRAII { + MMU2 &mmu2; + inline LoadingToNozzleRAII(MMU2 &mmu2):mmu2(mmu2){ + mmu2.loadingToNozzle = true; + } + inline ~LoadingToNozzleRAII(){ + mmu2.loadingToNozzle = false; + } +}; + +bool MMU2::load_filament_to_nozzle(uint8_t index) { + if( ! WaitForMMUReady()) + return false; + + LoadingToNozzleRAII ln(*this); + + // if (0){ // @@TODO DEBUG + + // @@TODO how is this supposed to be done in 8bit FW? +/* if (thermalManager.tooColdToExtrude(active_extruder)) { + BUZZ(200, 404); + LCD_ALERTMESSAGEPGM(MSG_HOTEND_TOO_COLD); + return false; + } else*/ { + // used for MMU-menu operation "Load to Nozzle" + ReportingRAII rep(CommandInProgress::ToolChange); + BlockRunoutRAII blockRunout; + + if( extruder != MMU2_NO_TOOL ){ // we already have some filament loaded - free it + shape its tip properly + filament_ramming(); + } + + logic.ToolChange(index); + manage_response(false, false); // true, true); + + // reset current position to whatever the planner thinks it is + // @@TODO is there some "standard" way of doing this? +//@@TODO current_position[E_AXIS] = Planner::get_machine_position_mm()[3]; + + extruder = index; + SetActiveExtruder(0); + +// BUZZ(200, 404); + return true; + } +} + +bool MMU2::eject_filament(uint8_t index, bool recover) { + if( ! WaitForMMUReady()) + return false; + + //@@TODO +// if (thermalManager.tooColdToExtrude(active_extruder)) { +// BUZZ(200, 404); +// LCD_ALERTMESSAGEPGM(MSG_HOTEND_TOO_COLD); +// return false; +// } + + ReportingRAII rep(CommandInProgress::EjectFilament); + current_position[E_AXIS] -= MMU2_FILAMENTCHANGE_EJECT_FEED; +//@@TODO line_to_current_position(2500 / 60); + st_synchronize(); + logic.EjectFilament(index); + manage_response(false, false); + + if (recover) { + // LCD_MESSAGEPGM(MSG_MMU2_EJECT_RECOVER); +// BUZZ(200, 404); + +//@@TODO wait_for_user = true; + + //#if ENABLED(HOST_PROMPT_SUPPORT) + // host_prompt_do(PROMPT_USER_CONTINUE, PSTR("MMU2 Eject Recover"), PSTR("Continue")); + //#endif + //#if ENABLED(EXTENSIBLE_UI) + // ExtUI::onUserConfirmRequired_P(PSTR("MMU2 Eject Recover")); + //#endif + +//@@TODO while (wait_for_user) idle(true); + +// BUZZ(200, 404); +// BUZZ(200, 404); + + // logic.Command(); //@@TODO command(MMU_CMD_R0); + manage_response(false, false); + } + + //@@TODO ui.reset_status(); + + // no active tool + extruder = MMU2_NO_TOOL; + +// BUZZ(200, 404); + +// disable_E0(); + + return true; +} + +void MMU2::Button(uint8_t index){ + logic.Button(index); +} + +void MMU2::Home(uint8_t mode){ + logic.Home(mode); +} + +void MMU2::SaveAndPark(bool move_axes, bool turn_off_nozzle) { +//@@TODO static constexpr xyz_pos_t park_point = NOZZLE_PARK_POINT_M600; +// if (!mmu_print_saved) { // First occurrence. Save current position, park print head, disable nozzle heater. +// LogEchoEvent("Saving and parking"); +// st_synchronize(); + +// mmu_print_saved = true; + +// resume_hotend_temp = thermalManager.degTargetHotend(active_extruder); +// resume_position = current_position; + +// if (move_axes && all_axes_homed()) +// nozzle.park(2, park_point); + +// if (turn_off_nozzle){ +// LogEchoEvent("Heater off"); +// thermalManager.setTargetHotend(0, active_extruder); +// } +// } +// // keep the motors powered forever (until some other strategy is chosen) +// gcode.reset_stepper_timeout(); +} + +void MMU2::ResumeAndUnPark(bool move_axes, bool turn_off_nozzle) { + if (mmu_print_saved) { + LogEchoEvent("Resuming print"); + + if (turn_off_nozzle && resume_hotend_temp) { + MMU2_ECHO_MSG("Restoring hotend temperature "); + SERIAL_ECHOLN(resume_hotend_temp); +//@@TODO thermalManager.setTargetHotend(resume_hotend_temp, active_extruder); + +// while (!thermalManager.wait_for_hotend(active_extruder, false)){ +// safe_delay(1000); +// } + LogEchoEvent("Hotend temperature reached"); + } + +//@@TODO if (move_axes && all_axes_homed()) { +// LogEchoEvent("Resuming XYZ"); + +// // Move XY to starting position, then Z +// do_blocking_move_to_xy(resume_position, feedRate_t(NOZZLE_PARK_XY_FEEDRATE)); + +// // Move Z_AXIS to saved position +// do_blocking_move_to_z(resume_position.z, feedRate_t(NOZZLE_PARK_Z_FEEDRATE)); +// } else { +// LogEchoEvent("NOT resuming XYZ"); +// } + } +} + +void MMU2::CheckUserInput(){ + auto btn = ButtonPressed((uint16_t)lastErrorCode); + switch (btn) { + case Left: + case Middle: + case Right: + Button(btn); + break; + case RestartMMU: + Reset(CutThePower); + break; + case StopPrint: + // @@TODO not sure if we shall handle this high level operation at this spot + break; + default: + break; + } +} + +/// Originally, this was used to wait for response and deal with timeout if necessary. +/// The new protocol implementation enables much nicer and intense reporting, so this method will boil down +/// just to verify the result of an issued command (which was basically the original idea) +/// +/// It is closely related to mmu_loop() (which corresponds to our ProtocolLogic::Step()), which does NOT perform any blocking wait for a command to finish. +/// But - in case of an error, the command is not yet finished, but we must react accordingly - move the printhead elsewhere, stop heating, eat a cat or so. +/// That's what's being done here... +void MMU2::manage_response(const bool move_axes, const bool turn_off_nozzle) { + mmu_print_saved = false; + + KEEPALIVE_STATE(PAUSED_FOR_USER); + + for (;;) { + // in our new implementation, we know the exact state of the MMU at any moment, we do not have to wait for a timeout + // So in this case we shall decide if the operation is: + // - still running -> wait normally in idle() + // - failed -> then do the safety moves on the printer like before + // - finished ok -> proceed with reading other commands + + // @@TODO this needs verification - we need something which matches Marlin2's idle() + manage_inactivity(true); // calls LogicStep() and remembers its return status + + switch (logicStepLastStatus) { + case Finished: + // command/operation completed, let Marlin continue its work + // the E may have some more moves to finish - wait for them + st_synchronize(); + return; + case VersionMismatch: // this basically means the MMU will be disabled until reconnected + return; + case CommunicationTimeout: + case CommandError: + case ProtocolError: + SaveAndPark(move_axes, turn_off_nozzle); // and wait for the user to resolve the problem + CheckUserInput(); + break; + case CommunicationRecovered: // @@TODO communication recovered and may be an error recovered as well + // may be the logic layer can detect the change of state a respond with one "Recovered" to be handled here + ResumeAndUnPark(move_axes, turn_off_nozzle); + break; + case Processing: // wait for the MMU to respond + default: + break; + } + } +} + +StepStatus MMU2::LogicStep() { + StepStatus ss = logic.Step(); + switch (ss) { + case Finished: + case Processing: + OnMMUProgressMsg(logic.Progress()); + break; + case CommandError: + ReportError(logic.Error()); + break; + case CommunicationTimeout: + state = xState::Connecting; + ReportError(ErrorCode::MMU_NOT_RESPONDING); + break; + case ProtocolError: + state = xState::Connecting; + ReportError(ErrorCode::PROTOCOL_ERROR); + break; + case VersionMismatch: + StopKeepPowered(); + ReportError(ErrorCode::VERSION_MISMATCH); + break; + default: + break; + } + + if( logic.Running() ){ + state = xState::Active; + } + return ss; +} + +void MMU2::filament_ramming() { + execute_extruder_sequence((const E_Step *)ramming_sequence, sizeof(ramming_sequence) / sizeof(E_Step)); +} + +void MMU2::execute_extruder_sequence(const E_Step *sequence, int steps) { + + st_synchronize(); + + const E_Step *step = sequence; + + for (uint8_t i = 0; i < steps; i++) { + const float es = pgm_read_float(&(step->extrude)); + const feedRate_t fr_mm_m = pgm_read_float(&(step->feedRate)); + + // DEBUG_ECHO_START(); + // DEBUG_ECHOLNPAIR("E step ", es, "/", fr_mm_m); + + current_position[E_AXIS] += es; +// line_to_current_position(MMM_TO_MMS(fr_mm_m)); + st_synchronize(); + + step++; + } + +// disable_E0(); +} + +void MMU2::SetActiveExtruder(uint8_t ex){ + active_extruder = ex; +} + +constexpr int strlen_constexpr(const char* str){ + return *str ? 1 + strlen_constexpr(str + 1) : 0; +} + +void MMU2::ReportError(ErrorCode ec) { + // Due to a potential lossy error reporting layers linked to this hook + // we'd better report everything to make sure especially the error states + // do not get lost. + // - The good news here is the fact, that the MMU reports the errors repeatedly until resolved. + // - The bad news is, that MMU not responding may repeatedly occur on printers not having the MMU at all. + // + // Not sure how to properly handle this situation, options: + // - skip reporting "MMU not responding" (at least for now) + // - report only changes of states (we can miss an error message) + // - may be some combination of MMUAvailable + UseMMU flags and decide based on their state + // Right now the filtering of MMU_NOT_RESPONDING is done in ReportErrorHook() as it is not a problem if mmu2.cpp + ReportErrorHook((CommandInProgress)logic.CommandInProgress(), (uint16_t)ec); + + if( ec != lastErrorCode ){ // deduplicate: only report changes in error codes into the log + lastErrorCode = ec; + + // Log error format: MMU2:E=32766 TextDescription + char msg[64]; + snprintf(msg, sizeof(msg), "MMU2:E=%hu", (uint16_t)ec); + // Append a human readable form of the error code(s) + TranslateErr((uint16_t)ec, msg, sizeof(msg)); + + // beware - the prefix in the message ("MMU2") will get stripped by the logging subsystem + // and a correct MMU2 component will be assigned accordingly - see appmain.cpp + // Therefore I'm not calling MMU2_ERROR_MSG or MMU2_ECHO_MSG here + SERIAL_ECHO_START; + SERIAL_ECHOLN(msg); + } + + static_assert(mmu2Magic[0] == 'M' + && mmu2Magic[1] == 'M' + && mmu2Magic[2] == 'U' + && mmu2Magic[3] == '2' + && mmu2Magic[4] == ':' + && strlen_constexpr(mmu2Magic) == 5, + "MMU2 logging prefix mismatch, must be updated at various spots" + ); +} + +void MMU2::ReportProgress(ProgressCode pc) { + ReportProgressHook((CommandInProgress)logic.CommandInProgress(), (uint16_t)pc); + + // Log progress - example: MMU2:P=123 EngageIdler + char msg[64]; + snprintf(msg, sizeof(msg), "MMU2:P=%hu", (uint16_t)pc); + // Append a human readable form of the progress code + TranslateProgress((uint16_t)pc, msg, sizeof(msg)); + + SERIAL_ECHO_START; + SERIAL_ECHOLN(msg); +} + +void MMU2::OnMMUProgressMsg(ProgressCode pc){ + if( pc != lastProgressCode){ + ReportProgress(pc); + lastProgressCode = pc; + + // Act accordingly - one-time handling + switch(pc){ + case ProgressCode::FeedingToBondtech: + // prepare for the movement of the E-motor + st_synchronize(); +//@@TODO sync_plan_position(); +// enable_E0(); + loadFilamentStarted = true; + break; + default: + // do nothing yet + break; + } + } else { + // Act accordingly - every status change (even the same state) + switch(pc){ + case ProgressCode::FeedingToBondtech: + if( WhereIsFilament() == FilamentState::AT_FSENSOR && loadFilamentStarted){// fsensor triggered, move the extruder to help loading + // rotate the extruder motor - no planner sync, just add more moves - as long as they are roughly at the same speed as the MMU is pushing, + // it really doesn't matter + // char tmp[64]; // @@TODO this shouldn't be needed anymore, but kept here in case of something strange + // // happens in Marlin again + // snprintf(tmp,sizeof (tmp), "E moveTo=%4.1f f=%4.0f s=%hu\n", current_position.e, feedrate_mm_s, feedrate_percentage); + // MMU2_ECHO_MSG(tmp); + + // Ideally we'd use: + // line_to_current_position(MMU2_LOAD_TO_NOZZLE_FEED_RATE); + // However, as it ignores MBL completely (which I don't care about in case of E-movement), + // we need to take the raw Z coordinates and only add some movement to E + // otherwise we risk planning a very short Z move with an extremely long E-move, + // which obviously ends up in a disaster (over/underflow of E/Z steps). + // The problem becomes obvious in Planner::_populate_block when computing da, db, dc like this: + // const int32_t da = target.a - position.a, db = target.b - position.b, dc = target.c - position.c; + // And since current_position[3] != position_float[3], we get a tiny move in Z, which is something I really want to avoid here + // @@TODO is there a "standard" way of doing this? +//@@TODO xyze_pos_t tgt = Planner::get_machine_position_mm(); + const float e = loadingToNozzle ? MMU2_LOAD_TO_NOZZLE_LENGTH : MMU2_TOOL_CHANGE_LOAD_LENGTH; +//@@TODO tgt[3] += e / planner.e_factor[active_extruder]; +// plan_buffer_line(tgt, MMU2_LOAD_TO_NOZZLE_FEED_RATE, active_extruder); // @@TODO magic constant - must match the feedrate of the MMU + loadFilamentStarted = false; + } + break; + default: + // do nothing yet + break; + } + } +} + +void MMU2::LogErrorEvent(const char *msg){ + MMU2_ERROR_MSG(msg); + SERIAL_ECHOLN(); +} + +void MMU2::LogEchoEvent(const char *msg){ + MMU2_ECHO_MSG(msg); + SERIAL_ECHOLN(); +} + +} // namespace MMU2 diff --git a/Firmware/mmu2.h b/Firmware/mmu2.h new file mode 100644 index 000000000..d3809b727 --- /dev/null +++ b/Firmware/mmu2.h @@ -0,0 +1,204 @@ +/// @file +#pragma once +#include "mmu2_protocol_logic.h" + +struct E_Step; + +namespace MMU2 { + +struct xyz_pos_t { + uint16_t xyz[3]; // @@TODO + xyz_pos_t()=default; +}; + +// general MMU setup for MK3 +enum : uint8_t { + FILAMENT_UNKNOWN = 0xffU +}; + +/// Top-level interface between Logic and Marlin. +/// Intentionally named MMU2 to be (almost) a drop-in replacement for the previous implementation. +/// Most of the public methods share the original naming convention as well. +class MMU2 { +public: + MMU2(); + + /// Powers ON the MMU, then initializes the UART and protocol logic + void Start(); + + /// Stops the protocol logic, closes the UART, powers OFF the MMU + void Stop(); + + /// States of a printer with the MMU: + /// - Active + /// - Connecting + /// - Stopped + /// + /// When the printer's FW starts, the MMU2 mode is either Stopped or NotResponding (based on user's preference). + /// When the MMU successfully establishes communication, the state changes to Active. + enum class xState : uint_fast8_t { + Active, ///< MMU has been detected, connected, communicates and is ready to be worked with. + Connecting, ///< MMU is connected but it doesn't communicate (yet). The user wants the MMU, but it is not ready to be worked with. + Stopped ///< The user doesn't want the printer to work with the MMU. The MMU itself is not powered and does not work at all. + }; + + inline xState State() const { return state; } + + // @@TODO temporary wrappers to make old gcc survive the code + inline bool Enabled()const { return State() == xState::Active; } + + /// Different levels of resetting the MMU + enum ResetForm : uint8_t { + Software = 0, ///< sends a X0 command into the MMU, the MMU will watchdog-reset itself + ResetPin = 1, ///< trigger the reset pin of the MMU + CutThePower = 2 ///< power off and power on (that includes +5V and +24V power lines) + }; + + /// Perform a reset of the MMU + /// @param level physical form of the reset + void Reset(ResetForm level); + + /// Power off the MMU (cut the power) + void PowerOff(); + + /// Power on the MMU + void PowerOn(); + + + /// The main loop of MMU processing. + /// Doesn't loop (block) inside, performs just one step of logic state machines. + /// Also, internally it prevents recursive entries. + void mmu_loop(); + + /// The main MMU command - select a different slot + /// @param index of the slot to be selected + /// @returns false if the operation cannot be performed (Stopped) + bool tool_change(uint8_t index); + + /// Handling of special Tx, Tc, T? commands + bool tool_change(const char *special); + + /// Unload of filament in collaboration with the MMU. + /// That includes rotating the printer's extruder in order to release filament. + /// @returns false if the operation cannot be performed (Stopped or cold extruder) + bool unload(); + + /// Load (insert) filament just into the MMU (not into printer's nozzle) + /// @returns false if the operation cannot be performed (Stopped) + bool load_filament(uint8_t index); + + /// Load (push) filament from the MMU into the printer's nozzle + /// @returns false if the operation cannot be performed (Stopped or cold extruder) + bool load_filament_to_nozzle(uint8_t index); + + /// Move MMU's selector aside and push the selected filament forward. + /// Usable for improving filament's tip or pulling the remaining piece of filament out completely. + bool eject_filament(uint8_t index, bool recover); + + /// Issue a Cut command into the MMU + /// Requires unloaded filament from the printer (obviously) + /// @returns false if the operation cannot be performed (Stopped) + bool cut_filament(uint8_t index); + + /// @returns the active filament slot index (0-4) or 0xff in case of no active tool + uint8_t get_current_tool(); + + bool set_filament_type(uint8_t index, uint8_t type); + + /// Issue a "button" click into the MMU - to be used from Error screens of the MMU + /// to select one of the 3 possible options to resolve the issue + void Button(uint8_t index); + + /// Issue an explicit "homing" command into the MMU + void Home(uint8_t mode); + + /// @returns current state of FINDA (true=filament present, false=filament not present) + inline bool FindaDetectsFilament()const { return logic.FindaPressed(); } + +private: + /// Perform software self-reset of the MMU (sends an X0 command) + void ResetX0(); + + /// Trigger reset pin of the MMU + void TriggerResetPin(); + + /// Perform power cycle of the MMU (cold boot) + /// Please note this is a blocking operation (sleeps for some time inside while doing the power cycle) + void PowerCycle(); + + /// Stop the communication, but keep the MMU powered on (for scenarios with incorrect FW version) + void StopKeepPowered(); + + /// Along with the mmu_loop method, this loops until a response from the MMU is received and acts upon. + /// In case of an error, it parks the print head and turns off nozzle heating + void manage_response(const bool move_axes, const bool turn_off_nozzle); + + /// Performs one step of the protocol logic state machine + /// and reports progress and errors if needed to attached ExtUIs. + /// Updates the global state of MMU (Active/Connecting/Stopped) at runtime, see @ref State + StepStatus LogicStep(); + + void filament_ramming(); + void execute_extruder_sequence(const E_Step *sequence, int steps); + void SetActiveExtruder(uint8_t ex); + + /// Reports an error into attached ExtUIs + /// @param ec error code, see ErrorCode + void ReportError(ErrorCode ec); + + /// Reports progress of operations into attached ExtUIs + /// @param pc progress code, see ProgressCode + void ReportProgress(ProgressCode pc); + + /// Responds to a change of MMU's progress + /// - plans additional steps, e.g. starts the E-motor after fsensor trigger + void OnMMUProgressMsg(ProgressCode pc); + + /// Report the msg into the general logging subsystem (through Marlin's SERIAL_ECHO stuff) + void LogErrorEvent(const char *msg); + + /// Report the msg into the general logging subsystem (through Marlin's SERIAL_ECHO stuff) + void LogEchoEvent(const char *msg); + + /// Save print and park the print head + void SaveAndPark(bool move_axes, bool turn_off_nozzle); + + /// Resume print (unpark, turn on heating etc.) + void ResumeAndUnPark(bool move_axes, bool turn_off_nozzle); + + /// Check for any button/user input coming from the printer's UI + void CheckUserInput(); + + /// Entry check of all external commands. + /// It can wait until the MMU becomes ready. + /// Optionally, it can also emit/display an error screen and the user can decide what to do next. + /// @returns false if the MMU is not ready to perform the command (for whatever reason) + bool WaitForMMUReady(); + + ProtocolLogic logic; ///< implementation of the protocol logic layer + int extruder; ///< currently active slot in the MMU ... somewhat... not sure where to get it from yet + + xyz_pos_t resume_position; + int16_t resume_hotend_temp; + + ProgressCode lastProgressCode = ProgressCode::OK; + ErrorCode lastErrorCode = ErrorCode::MMU_NOT_RESPONDING; + + StepStatus logicStepLastStatus; + + enum xState state; + + bool mmu_print_saved; + bool loadFilamentStarted; + + friend struct LoadingToNozzleRAII; + /// true in case we are doing the LoadToNozzle operation - that means the filament shall be loaded all the way down to the nozzle + /// unlike the mid-print ToolChange commands, which only load the first ~30mm and then the G-code takes over. + bool loadingToNozzle; +}; + +/// following Marlin's way of doing stuff - one and only instance of MMU implementation in the code base +/// + avoiding buggy singletons on the AVR platform +extern MMU2 mmu2; + +} // namespace MMU2 diff --git a/Firmware/mmu2/error_codes.h b/Firmware/mmu2/error_codes.h new file mode 100644 index 000000000..1b495898c --- /dev/null +++ b/Firmware/mmu2/error_codes.h @@ -0,0 +1,97 @@ +/// @file error_codes.h +#pragma once +#include + +/// A complete set of error codes which may be a result of a high-level command/operation. +/// This header file shall be included in the printer's firmware as well as a reference, +/// therefore the error codes have been extracted to one place. +/// +/// Please note the errors are intentionally coded as "negative" values (highest bit set), +/// becase they are a complement to reporting the state of the high-level state machines - +/// positive values are considered as normal progress, negative values are errors. +/// +/// Please note, that multiple TMC errors can occur at once, thus they are defined as a bitmask of the higher byte. +/// Also, as there are 3 TMC drivers on the board, each error is added a bit for the corresponding TMC - +/// TMC_PULLEY_BIT, TMC_SELECTOR_BIT, TMC_IDLER_BIT, +/// The resulting error is a bitwise OR over 3 TMC drivers and their status, which should cover most of the situations correctly. +enum class ErrorCode : uint_fast16_t { + RUNNING = 0x0000, ///< the operation is still running - keep this value as ZERO as it is used for initialization of error codes as well + OK = 0x0001, ///< the operation finished OK + + // TMC bit masks + TMC_PULLEY_BIT = 0x0040, ///< +64 TMC Pulley bit + TMC_SELECTOR_BIT = 0x0080, ///< +128 TMC Pulley bit + TMC_IDLER_BIT = 0x0100, ///< +256 TMC Pulley bit + + /// Unload Filament related error codes + FINDA_DIDNT_SWITCH_ON = 0x8001, ///< E32769 FINDA didn't switch on while loading filament - either there is something blocking the metal ball or a cable is broken/disconnected + FINDA_DIDNT_SWITCH_OFF = 0x8002, ///< E32770 FINDA didn't switch off while unloading filament + + FSENSOR_DIDNT_SWITCH_ON = 0x8003, ///< E32771 Filament sensor didn't switch on while performing LoadFilament + FSENSOR_DIDNT_SWITCH_OFF = 0x8004, ///< E32772 Filament sensor didn't switch off while performing UnloadFilament + + FILAMENT_ALREADY_LOADED = 0x8005, ///< E32773 cannot perform operation LoadFilament or move the selector as the filament is already loaded + + INVALID_TOOL = 0x8006, ///< E32774 tool/slot index out of range (typically issuing T5 into an MMU with just 5 slots - valid range 0-4) + + HOMING_FAILED = 0x8007, ///< generic homing failed error - always reported with the corresponding axis bit set (Idler or Selector) as follows: + HOMING_SELECTOR_FAILED = HOMING_FAILED | TMC_SELECTOR_BIT, ///< E32903 the Selector was unable to home properly - that means something is blocking its movement + HOMING_IDLER_FAILED = HOMING_FAILED | TMC_IDLER_BIT, ///< E33031 the Idler was unable to home properly - that means something is blocking its movement + STALLED_PULLEY = HOMING_FAILED | TMC_PULLEY_BIT, ///< E32839 for the Pulley "homing" means just stallguard detected during Pulley's operation (Pulley doesn't home) + + QUEUE_FULL = 0x802b, ///< E32811 internal logic error - attempt to move with a full queue + + VERSION_MISMATCH = 0x802c, ///< E32812 internal error of the printer - incompatible version of the MMU FW + PROTOCOL_ERROR = 0x802d, ///< E32813 internal error of the printer - communication with the MMU got garbled - protocol decoder couldn't decode the incoming messages + MMU_NOT_RESPONDING = 0x802e, ///< E32814 internal error of the printer - communication with the MMU is not working + INTERNAL = 0x802f, ///< E32815 internal runtime error (software) + + /// TMC driver init error - TMC dead or bad communication + /// - E33344 Pulley TMC driver + /// - E33404 Selector TMC driver + /// - E33536 Idler TMC driver + /// - E33728 All 3 TMC driver + TMC_IOIN_MISMATCH = 0x8200, + + /// TMC driver reset - recoverable, we just need to rehome the axis + /// Idler: can be rehomed any time + /// Selector: if there is a filament, remove it and rehome, if there is no filament, just rehome + /// Pulley: do nothing - for the loading sequence - just restart and move slowly, for the unload sequence just restart + /// - E33856 Pulley TMC driver + /// - E33920 Selector TMC driver + /// - E34048 Idler TMC driver + /// - E34240 All 3 TMC driver + TMC_RESET = 0x8400, + + /// not enough current for the TMC, NOT RECOVERABLE + /// - E34880 Pulley TMC driver + /// - E34944 Selector TMC driver + /// - E35072 Idler TMC driver + /// - E35264 All 3 TMC driver + TMC_UNDERVOLTAGE_ON_CHARGE_PUMP = 0x8800, + + /// TMC driver serious error - short to ground on coil A or coil B - dangerous to recover + /// - E36928 Pulley TMC driver + /// - E36992 Selector TMC driver + /// - E37120 Idler TMC driver + /// - E37312 All 3 TMC driver + TMC_SHORT_TO_GROUND = 0x9000, + + /// TMC driver over temperature warning - can be recovered by restarting the driver. + /// If this error happens, we should probably go into the error state as soon as the current command is finished. + /// The driver technically still works at this point. + /// - E41024 Pulley TMC driver + /// - E41088 Selector TMC driver + /// - E41216 Idler TMC driver + /// - E41408 All 3 TMC driver + TMC_OVER_TEMPERATURE_WARN = 0xA000, + + /// TMC driver over temperature error - we really shouldn't ever reach this error. + /// It can still be recovered if the driver cools down below 120C. + /// The driver needs to be disabled and enabled again for operation to resume after this error is cleared. + /// - E49216 Pulley TMC driver + /// - E49280 Selector TMC driver + /// - E49408 Idler TMC driver + /// - E49600 All 3 TMC driver + TMC_OVER_TEMPERATURE_ERROR = 0xC000 +}; diff --git a/Firmware/mmu2/progress_codes.h b/Firmware/mmu2/progress_codes.h new file mode 100644 index 000000000..bdb17c647 --- /dev/null +++ b/Firmware/mmu2/progress_codes.h @@ -0,0 +1,42 @@ +/// @file progress_codes.h +#pragma once +#include + +/// A complete set of progress codes which may be reported while running a high-level command/operation +/// This header file shall be included in the printer's firmware as well as a reference, +/// therefore the progress codes have been extracted to one place +enum class ProgressCode : uint_fast8_t { + OK = 0, ///< finished ok + + EngagingIdler, // P1 + DisengagingIdler, // P2 + UnloadingToFinda, // P3 + UnloadingToPulley, //P4 + FeedingToFinda, // P5 + FeedingToBondtech, // P6 + FeedingToNozzle, // P7 + AvoidingGrind, // P8 + FinishingMoves, // P9 + + ERRDisengagingIdler, // P10 + ERREngagingIdler, // P11 + ERRWaitingForUser, // P12 + ERRInternal, // P13 + ERRHelpingFilament, // P14 + ERRTMCFailed, // P15 + + UnloadingFilament, // P16 + LoadingFilament, // P17 + SelectingFilamentSlot, // P18 + PreparingBlade, // P19 + PushingFilament, // P20 + PerformingCut, // P21 + ReturningSelector, // P22 + ParkingSelector, // P23 + EjectingFilament, // P24 + RetractingFromFinda, // P25 + + Homing, + + Empty = 0xff // dummy empty state +}; diff --git a/Firmware/mmu2_error_converter.cpp b/Firmware/mmu2_error_converter.cpp new file mode 100644 index 000000000..2165bfb10 --- /dev/null +++ b/Firmware/mmu2_error_converter.cpp @@ -0,0 +1,6 @@ +#include "mmu2_error_converter.h" + +namespace MMU2 { +// @@TODO +void TranslateErr(uint16_t ec, char *dst, size_t dstSize) { } +} diff --git a/Firmware/mmu2_error_converter.h b/Firmware/mmu2_error_converter.h new file mode 100644 index 000000000..5166863ea --- /dev/null +++ b/Firmware/mmu2_error_converter.h @@ -0,0 +1,7 @@ +#pragma once +#include +#include + +namespace MMU2 { +void TranslateErr(uint16_t ec, char *dst, size_t dstSize); +} diff --git a/Firmware/mmu2_fsensor.cpp b/Firmware/mmu2_fsensor.cpp new file mode 100644 index 000000000..ee9a70573 --- /dev/null +++ b/Firmware/mmu2_fsensor.cpp @@ -0,0 +1,14 @@ +#include "mmu2_fsensor.h" + +namespace MMU2 { + +FilamentState WhereIsFilament(){ + // @@TODO + return FilamentState::IN_NOZZLE; +} + +// on AVR this does nothing +BlockRunoutRAII::BlockRunoutRAII() { } +BlockRunoutRAII::~BlockRunoutRAII() { } + +} // namespace MMU2 diff --git a/Firmware/mmu2_fsensor.h b/Firmware/mmu2_fsensor.h new file mode 100644 index 000000000..9450c206c --- /dev/null +++ b/Firmware/mmu2_fsensor.h @@ -0,0 +1,24 @@ +#pragma once +#include + +namespace MMU2 { + +/// Possible states of filament from the perspective of presence in various parts of the printer +/// Beware, the numeric codes are important and sent into the MMU +enum class FilamentState : uint_fast8_t { + NOT_PRESENT = 0, ///< filament sensor doesn't see the filament + AT_FSENSOR = 1, ///< filament detected by the filament sensor, but the nozzle has not detected the filament yet + IN_NOZZLE = 2 ///< filament detected by the filament sensor and also loaded in the nozzle +}; + +FilamentState WhereIsFilament(); + +/// Can be used to block printer's filament sensor handling - to avoid errorneous injecting of M600 +/// while doing a toolchange with the MMU +class BlockRunoutRAII { +public: + BlockRunoutRAII(); + ~BlockRunoutRAII(); +}; + +} // namespace MMU2 diff --git a/Firmware/mmu2_log.h b/Firmware/mmu2_log.h new file mode 100644 index 000000000..0194534d9 --- /dev/null +++ b/Firmware/mmu2_log.h @@ -0,0 +1,23 @@ +#pragma once + +#ifndef UNITTEST +#include "Marlin.h" + +// Beware - before changing this prefix, think twice +// you'd need to change appmain.cpp app_marlin_serial_output_write_hook +// and MMU2::ReportError + MMU2::ReportProgress +static constexpr char mmu2Magic[] PROGMEM = "MMU2:"; + +#define SERIAL_MMU2() { serialprintPGM(mmu2Magic); } + +#define MMU2_ECHO_MSG(S) do{ SERIAL_ECHO_START; SERIAL_MMU2(); SERIAL_ECHO(S); }while(0) +#define MMU2_ERROR_MSG(S) do{ SERIAL_ERROR_START; SERIAL_MMU2(); SERIAL_ECHO(S); }while(0) + +#else // #ifndef UNITTEST + +#define MMU2_ECHO_MSG(S) /* */ +#define MMU2_ERROR_MSG(S) /* */ +#define SERIAL_ECHO(S) /* */ +#define SERIAL_ECHOLN(S) /* */ + +#endif // #ifndef UNITTEST diff --git a/Firmware/mmu2_power.cpp b/Firmware/mmu2_power.cpp new file mode 100644 index 000000000..32f93df26 --- /dev/null +++ b/Firmware/mmu2_power.cpp @@ -0,0 +1,12 @@ +#include "mmu2_power.h" + +namespace MMU2 { + +// sadly, on MK3 we cannot do this on HW +void power_on() { } + +void power_off() { } + +void reset() { } + +} // namespace MMU2 diff --git a/Firmware/mmu2_power.h b/Firmware/mmu2_power.h new file mode 100644 index 000000000..d4ee1b129 --- /dev/null +++ b/Firmware/mmu2_power.h @@ -0,0 +1,11 @@ +#pragma once + +namespace MMU2 { + +void power_on(); + +void power_off(); + +void reset(); + +} // namespace MMU2 diff --git a/Firmware/mmu2_progress_converter.cpp b/Firmware/mmu2_progress_converter.cpp new file mode 100644 index 000000000..aefe3f731 --- /dev/null +++ b/Firmware/mmu2_progress_converter.cpp @@ -0,0 +1,6 @@ +#include "mmu2_progress_converter.h" + +namespace MMU2 { +//@@TODO +void TranslateProgress(uint16_t pc, char *dst, size_t dstSize) { } +} diff --git a/Firmware/mmu2_progress_converter.h b/Firmware/mmu2_progress_converter.h new file mode 100644 index 000000000..97fd344b8 --- /dev/null +++ b/Firmware/mmu2_progress_converter.h @@ -0,0 +1,7 @@ +#pragma once +#include +#include + +namespace MMU2 { +void TranslateProgress(uint16_t pc, char *dst, size_t dstSize); +} diff --git a/Firmware/mmu2_protocol.cpp b/Firmware/mmu2_protocol.cpp new file mode 100644 index 000000000..6fdf9c1b7 --- /dev/null +++ b/Firmware/mmu2_protocol.cpp @@ -0,0 +1,247 @@ +/// @file +#include "mmu2_protocol.h" + +// protocol definition +// command: Q0 +// meaning: query operation status +// Query/command: query +// Expected reply from the MMU: +// any of the running operation statuses: OID: [T|L|U|E|C|W|K][0-4] +// 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 : operation finished - will be repeated to "Q" messages until a new command is issued + +namespace modules { +namespace protocol { + +// decoding automaton +// states: input -> transition into state +// Code QTLMUXPSBEWK -> msgcode +// \n ->start +// * ->error +// error \n ->start +// * ->error +// msgcode 0-9 ->msgvalue +// * ->error +// msgvalue 0-9 ->msgvalue +// \n ->start successfully accepted command + +DecodeStatus Protocol::DecodeRequest(uint8_t c) { + switch (rqState) { + case RequestStates::Code: + switch (c) { + case 'Q': + case 'T': + case 'L': + case 'M': + case 'U': + case 'X': + case 'P': + case 'S': + case 'B': + case 'E': + case 'W': + case 'K': + case 'F': + case 'f': + case 'H': + requestMsg.code = (RequestMsgCodes)c; + requestMsg.value = 0; + rqState = RequestStates::Value; + return DecodeStatus::NeedMoreData; + default: + requestMsg.code = RequestMsgCodes::unknown; + rqState = RequestStates::Error; + return DecodeStatus::Error; + } + case RequestStates::Value: + if (IsDigit(c)) { + requestMsg.value *= 10; + requestMsg.value += c - '0'; + return DecodeStatus::NeedMoreData; + } else if (IsNewLine(c)) { + rqState = RequestStates::Code; + return DecodeStatus::MessageCompleted; + } else { + requestMsg.code = RequestMsgCodes::unknown; + rqState = RequestStates::Error; + return DecodeStatus::Error; + } + default: //case error: + if (IsNewLine(c)) { + rqState = RequestStates::Code; + return DecodeStatus::MessageCompleted; + } else { + requestMsg.code = RequestMsgCodes::unknown; + rqState = RequestStates::Error; + return DecodeStatus::Error; + } + } +} + +uint8_t Protocol::EncodeRequest(const RequestMsg &msg, uint8_t *txbuff) { + constexpr uint8_t reqSize = 3; + txbuff[0] = (uint8_t)msg.code; + txbuff[1] = msg.value + '0'; + txbuff[2] = '\n'; + return reqSize; + static_assert(reqSize <= MaxRequestSize(), "Request message length exceeded the maximum size, increase the magic constant in MaxRequestSize()"); +} + +DecodeStatus Protocol::DecodeResponse(uint8_t c) { + switch (rspState) { + case ResponseStates::RequestCode: + switch (c) { + case 'Q': + case 'T': + case 'L': + case 'M': + case 'U': + case 'X': + case 'P': + case 'S': + case 'B': + case 'E': + case 'W': + case 'K': + case 'F': + case 'f': + case 'H': + responseMsg.request.code = (RequestMsgCodes)c; + responseMsg.request.value = 0; + rspState = ResponseStates::RequestValue; + return DecodeStatus::NeedMoreData; + case 0x0a: + case 0x0d: + // skip leading whitespace if any (makes integration with other SW easier/tolerant) + return DecodeStatus::NeedMoreData; + default: + rspState = ResponseStates::Error; + return DecodeStatus::Error; + } + case ResponseStates::RequestValue: + if (IsDigit(c)) { + responseMsg.request.value *= 10; + responseMsg.request.value += c - '0'; + return DecodeStatus::NeedMoreData; + } else if (c == ' ') { + rspState = ResponseStates::ParamCode; + return DecodeStatus::NeedMoreData; + } else { + rspState = ResponseStates::Error; + return DecodeStatus::Error; + } + case ResponseStates::ParamCode: + switch (c) { + case 'P': + case 'E': + case 'F': + case 'A': + case 'R': + rspState = ResponseStates::ParamValue; + responseMsg.paramCode = (ResponseMsgParamCodes)c; + responseMsg.paramValue = 0; + return DecodeStatus::NeedMoreData; + default: + responseMsg.paramCode = ResponseMsgParamCodes::unknown; + rspState = ResponseStates::Error; + return DecodeStatus::Error; + } + case ResponseStates::ParamValue: + if (IsDigit(c)) { + responseMsg.paramValue *= 10; + responseMsg.paramValue += c - '0'; + return DecodeStatus::NeedMoreData; + } else if (IsNewLine(c)) { + rspState = ResponseStates::RequestCode; + return DecodeStatus::MessageCompleted; + } else { + responseMsg.paramCode = ResponseMsgParamCodes::unknown; + rspState = ResponseStates::Error; + return DecodeStatus::Error; + } + default: //case error: + if (IsNewLine(c)) { + rspState = ResponseStates::RequestCode; + return DecodeStatus::MessageCompleted; + } else { + responseMsg.paramCode = ResponseMsgParamCodes::unknown; + return DecodeStatus::Error; + } + } +} + +uint8_t Protocol::EncodeResponseCmdAR(const RequestMsg &msg, ResponseMsgParamCodes ar, uint8_t *txbuff) { + txbuff[0] = (uint8_t)msg.code; + txbuff[1] = msg.value + '0'; + txbuff[2] = ' '; + txbuff[3] = (uint8_t)ar; + txbuff[4] = '\n'; + return 5; +} + +uint8_t Protocol::EncodeResponseReadFINDA(const RequestMsg &msg, uint8_t findaValue, uint8_t *txbuff) { + txbuff[0] = (uint8_t)msg.code; + txbuff[1] = msg.value + '0'; + txbuff[2] = ' '; + txbuff[3] = (uint8_t)ResponseMsgParamCodes::Accepted; + txbuff[4] = findaValue + '0'; + txbuff[5] = '\n'; + return 6; +} + +uint8_t Protocol::EncodeResponseVersion(const RequestMsg &msg, uint8_t value, uint8_t *txbuff) { + txbuff[0] = (uint8_t)msg.code; + txbuff[1] = msg.value + '0'; + txbuff[2] = ' '; + txbuff[3] = (uint8_t)ResponseMsgParamCodes::Accepted; + uint8_t *dst = txbuff + 4; + if (value < 10) { + *dst++ = value + '0'; + } else if (value < 100) { + *dst++ = value / 10 + '0'; + *dst++ = value % 10 + '0'; + } else { + *dst++ = value / 100 + '0'; + *dst++ = (value / 10) % 10 + '0'; + *dst++ = value % 10 + '0'; + } + *dst = '\n'; + return dst - txbuff + 1; +} + +uint8_t Protocol::EncodeResponseQueryOperation(const RequestMsg &msg, ResponseMsgParamCodes code, uint16_t value, uint8_t *txbuff) { + txbuff[0] = (uint8_t)msg.code; + txbuff[1] = msg.value + '0'; + txbuff[2] = ' '; + txbuff[3] = (uint8_t)code; + uint8_t *dst = txbuff + 4; + if (code != ResponseMsgParamCodes::Finished) { + if (value < 10) { + *dst++ = value + '0'; + } else if (value < 100) { + *dst++ = value / 10 + '0'; + *dst++ = value % 10 + '0'; + } else if (value < 1000) { + *dst++ = value / 100 + '0'; + *dst++ = (value / 10) % 10 + '0'; + *dst++ = value % 10 + '0'; + } else if (value < 10000) { + *dst++ = value / 1000 + '0'; + *dst++ = (value / 100) % 10 + '0'; + *dst++ = (value / 10) % 10 + '0'; + *dst++ = value % 10 + '0'; + } else { + *dst++ = value / 10000 + '0'; + *dst++ = (value / 1000) % 10 + '0'; + *dst++ = (value / 100) % 10 + '0'; + *dst++ = (value / 10) % 10 + '0'; + *dst++ = value % 10 + '0'; + } + } + *dst = '\n'; + return dst - txbuff + 1; +} + +} // namespace protocol +} // namespace modules diff --git a/Firmware/mmu2_protocol.h b/Firmware/mmu2_protocol.h new file mode 100644 index 000000000..68fde9a1a --- /dev/null +++ b/Firmware/mmu2_protocol.h @@ -0,0 +1,184 @@ +/// @file protocol.h +#pragma once +#include + +namespace modules { + +/// @brief The MMU communication protocol implementation and related stuff. +/// +/// See description of the new protocol in the MMU 2021 doc +/// @@TODO possibly add some checksum to verify the correctness of messages +namespace protocol { + +/// Definition of request message codes +enum class RequestMsgCodes : uint8_t { + unknown = 0, + Query = 'Q', + Tool = 'T', + Load = 'L', + Mode = 'M', + Unload = 'U', + Reset = 'X', + Finda = 'P', + Version = 'S', + Button = 'B', + Eject = 'E', + Wait = 'W', + Cut = 'K', + FilamentType = 'F', + FilamentSensor = 'f', + Home = 'H' +}; + +/// Definition of response message parameter codes +enum class ResponseMsgParamCodes : uint8_t { + unknown = 0, + Processing = 'P', + Error = 'E', + Finished = 'F', + Accepted = 'A', + Rejected = 'R' +}; + +/// A request message - requests are being sent by the printer into the MMU. +struct RequestMsg { + RequestMsgCodes code; ///< code of the request message + uint8_t value; ///< value of the request message + + /// @param code of the request message + /// @param value of the request message + inline RequestMsg(RequestMsgCodes code, uint8_t value) + : code(code) + , value(value) {} +}; + +/// A response message - responses are being sent from the MMU into the printer as a response to a request message. +struct ResponseMsg { + RequestMsg request; ///< response is always preceeded by the request message + ResponseMsgParamCodes paramCode; ///< code of the parameter + uint16_t paramValue; ///< value of the parameter + + /// @param request the source request message this response is a reply to + /// @param paramCode code of the parameter + /// @param paramValue value of the parameter + inline ResponseMsg(RequestMsg request, ResponseMsgParamCodes paramCode, uint16_t paramValue) + : request(request) + , paramCode(paramCode) + , paramValue(paramValue) {} +}; + +/// Message decoding return values +enum class DecodeStatus : uint_fast8_t { + MessageCompleted, ///< message completed and successfully lexed + NeedMoreData, ///< message incomplete yet, waiting for another byte to come + Error, ///< input character broke message decoding +}; + +/// Protocol class is responsible for creating/decoding messages in Rx/Tx buffer +/// +/// Beware - in the decoding more, it is meant to be a statefull instance which works through public methods +/// processing one input byte per call. +class Protocol { +public: + inline Protocol() + : rqState(RequestStates::Code) + , requestMsg(RequestMsgCodes::unknown, 0) + , rspState(ResponseStates::RequestCode) + , responseMsg(RequestMsg(RequestMsgCodes::unknown, 0), ResponseMsgParamCodes::unknown, 0) { + } + + /// Takes the input byte c and steps one step through the state machine + /// @returns state of the message being decoded + DecodeStatus DecodeRequest(uint8_t c); + + /// Decodes response message in rxbuff + /// @returns decoded response message structure + DecodeStatus DecodeResponse(uint8_t c); + + /// Encodes request message msg into txbuff memory + /// It is expected the txbuff is large enough to fit the message + /// @returns number of bytes written into txbuff + static uint8_t EncodeRequest(const RequestMsg &msg, uint8_t *txbuff); + + /// @returns the maximum byte length necessary to encode a request message + /// Beneficial in case of pre-allocating a buffer for enconding a RequestMsg. + static constexpr uint8_t MaxRequestSize() { return 3; } + + /// Encode generic response Command Accepted or Rejected + /// @param msg source request message for this response + /// @param ar code of response parameter + /// @param txbuff where to format the message + /// @returns number of bytes written into txbuff + static uint8_t EncodeResponseCmdAR(const RequestMsg &msg, ResponseMsgParamCodes ar, uint8_t *txbuff); + + /// Encode response to Read FINDA query + /// @param msg source request message for this response + /// @param findaValue 1/0 (on/off) status of FINDA + /// @param txbuff where to format the message + /// @returns number of bytes written into txbuff + static uint8_t EncodeResponseReadFINDA(const RequestMsg &msg, uint8_t findaValue, uint8_t *txbuff); + + /// Encode response to Version query + /// @param msg source request message for this response + /// @param value version number (0-255) + /// @param txbuff where to format the message + /// @returns number of bytes written into txbuff + static uint8_t EncodeResponseVersion(const RequestMsg &msg, uint8_t value, uint8_t *txbuff); + + /// Encode response to Query operation status + /// @param msg source request message for this response + /// @param code status of operation (Processing, Error, Finished) + /// @param value related to status of operation(e.g. error code or progress) + /// @param txbuff where to format the message + /// @returns number of bytes written into txbuff + static uint8_t EncodeResponseQueryOperation(const RequestMsg &msg, ResponseMsgParamCodes code, uint16_t value, uint8_t *txbuff); + + /// @returns the most recently lexed request message + inline const RequestMsg GetRequestMsg() const { return requestMsg; } + + /// @returns the most recently lexed response message + inline const ResponseMsg GetResponseMsg() const { return responseMsg; } + + /// resets the internal request decoding state (typically after an error) + void ResetRequestDecoder() { + rqState = RequestStates::Code; + } + + /// resets the internal response decoding state (typically after an error) + void ResetResponseDecoder() { + rspState = ResponseStates::RequestCode; + } + +private: + enum class RequestStates : uint8_t { + Code, ///< starting state - expects message code + Value, ///< expecting code value + Error ///< automaton in error state + }; + + RequestStates rqState; + RequestMsg requestMsg; + + enum class ResponseStates : uint8_t { + RequestCode, ///< starting state - expects message code + RequestValue, ///< expecting code value + ParamCode, ///< expecting param code + ParamValue, ///< expecting param value + Error ///< automaton in error state + }; + + ResponseStates rspState; + ResponseMsg responseMsg; + + static bool IsNewLine(uint8_t c) { + return c == '\n' || c == '\r'; + } + static bool IsDigit(uint8_t c) { + return c >= '0' && c <= '9'; + } +}; + +} // namespace protocol +} // namespace modules + +namespace mp = modules::protocol; diff --git a/Firmware/mmu2_protocol_logic.cpp b/Firmware/mmu2_protocol_logic.cpp new file mode 100644 index 000000000..3dde6fae7 --- /dev/null +++ b/Firmware/mmu2_protocol_logic.cpp @@ -0,0 +1,564 @@ +#include "mmu2_protocol_logic.h" +#include "mmu2_log.h" +#include "mmu2_fsensor.h" +#include "system_timer.h" +#include + +namespace MMU2 { + +StepStatus ProtocolLogicPartBase::ProcessFINDAReqSent(StepStatus finishedRV, State nextState){ + auto expmsg = logic->ExpectingMessage(linkLayerTimeout); + if (expmsg != MessageReady) + return expmsg; + logic->findaPressed = logic->rsp.paramValue; + state = nextState; + return finishedRV; +} + +void ProtocolLogicPartBase::CheckAndReportAsyncEvents(){ + // even when waiting for a query period, we need to report a change in filament sensor's state + // - it is vital for a precise synchronization of moves of the printer and the MMU + uint8_t fs = (uint8_t)WhereIsFilament(); + if( fs != logic->lastFSensor ){ + SendAndUpdateFilamentSensor(); + } +} + +void ProtocolLogicPartBase::SendQuery(){ + logic->SendMsg(RequestMsg(RequestMsgCodes::Query, 0)); + state = State::QuerySent; +} + +void ProtocolLogicPartBase::SendFINDAQuery(){ + logic->SendMsg(RequestMsg(RequestMsgCodes::Finda, 0 ) ); + state = State::FINDAReqSent; +} + +void ProtocolLogicPartBase::SendAndUpdateFilamentSensor(){ + logic->SendMsg(RequestMsg(RequestMsgCodes::FilamentSensor, logic->lastFSensor = (uint8_t)WhereIsFilament() ) ); + state = State::FilamentSensorStateSent; +} + +void ProtocolLogicPartBase::SendButton(uint8_t btn){ + logic->SendMsg(RequestMsg(RequestMsgCodes::Button, btn)); + state = State::ButtonSent; +} + +StepStatus ProtocolLogic::ProcessUARTByte(uint8_t c) { + switch (protocol.DecodeResponse(c)) { + case DecodeStatus::MessageCompleted: + // @@TODO reset direction of communication + return MessageReady; + case DecodeStatus::NeedMoreData: + return Processing; + case DecodeStatus::Error: + default: + return ProtocolError; + } +} + +StepStatus ProtocolLogic::ExpectingMessage(uint32_t timeout) { + int bytesConsumed = 0; + int c = -1; + + // try to consume as many rx bytes as possible (until a message has been completed) + while((c = uart->read()) >= 0){ + ++bytesConsumed; + RecordReceivedByte(c); + switch (protocol.DecodeResponse(c)) { + case DecodeStatus::MessageCompleted: + rsp = protocol.GetResponseMsg(); + LogResponse(); + // @@TODO reset direction of communication + RecordUARTActivity(); // something has happened on the UART, update the timeout record + return MessageReady; + case DecodeStatus::NeedMoreData: + break; + case DecodeStatus::Error: + default: + RecordUARTActivity(); // something has happened on the UART, update the timeout record + return ProtocolError; + } + } + if( bytesConsumed != 0 ){ + RecordUARTActivity(); // something has happened on the UART, update the timeout record + return Processing; // consumed some bytes, but message still not ready + } else if (Elapsed(timeout)) { + return CommunicationTimeout; + } + return Processing; +} + +void ProtocolLogic::SendMsg(RequestMsg rq) { + uint8_t txbuff[Protocol::MaxRequestSize()]; + uint8_t len = Protocol::EncodeRequest(rq, txbuff); + uart->write(txbuff, len); + LogRequestMsg(txbuff, len); + RecordUARTActivity(); +} + +void StartSeq::Restart() { + state = State::S0Sent; + logic->SendMsg(RequestMsg(RequestMsgCodes::Version, 0)); +} + +StepStatus StartSeq::Step() { + auto expmsg = logic->ExpectingMessage(linkLayerTimeout); + if (expmsg != MessageReady) + return expmsg; + + // solve initial handshake + switch (state) { + case State::S0Sent: // received response to S0 - major + if (logic->rsp.paramValue != 2) { + return VersionMismatch; + } + logic->dataTO.Reset(); // got meaningful response from the MMU, stop data layer timeout tracking + logic->SendMsg(RequestMsg(RequestMsgCodes::Version, 1)); + state = State::S1Sent; + break; + case State::S1Sent: // received response to S1 - minor + if (logic->rsp.paramValue != 0) { + return VersionMismatch; + } + logic->SendMsg(RequestMsg(RequestMsgCodes::Version, 2)); + state = State::S2Sent; + break; + case State::S2Sent: // received response to S2 - revision + if (logic->rsp.paramValue != 0) { + return VersionMismatch; + } + // Start General Interrogation after line up. + // For now we just send the state of the filament sensor, but we may request + // data point states from the MMU as well. TBD in the future, especially with another protocol + SendAndUpdateFilamentSensor(); + break; + case State::FilamentSensorStateSent: + state = State::Ready; + return Finished; + break; + default: + return VersionMismatch; + } + return Processing; +} + +void Command::Restart() { + state = State::CommandSent; + logic->SendMsg(logic->command.rq); +} + +StepStatus Command::Step() { + switch (state) { + case State::CommandSent: { + auto expmsg = logic->ExpectingMessage(linkLayerTimeout); + if (expmsg != MessageReady) + return expmsg; + + switch (logic->rsp.paramCode) { // the response should be either accepted or rejected + case ResponseMsgParamCodes::Accepted: + logic->progressCode = ProgressCode::OK; + logic->errorCode = ErrorCode::RUNNING; + state = State::Wait; + break; + case ResponseMsgParamCodes::Rejected: + // rejected - should normally not happen, but report the error up + logic->progressCode = ProgressCode::OK; + logic->errorCode = ErrorCode::PROTOCOL_ERROR; + return CommandRejected; + default: + return ProtocolError; + } + } break; + case State::Wait: + if (logic->Elapsed(heartBeatPeriod)) { + SendQuery(); + } else { + // even when waiting for a query period, we need to report a change in filament sensor's state + // - it is vital for a precise synchronization of moves of the printer and the MMU + CheckAndReportAsyncEvents(); + } + break; + case State::QuerySent: { + auto expmsg = logic->ExpectingMessage(linkLayerTimeout); + if (expmsg != MessageReady) + return expmsg; + } + // [[fallthrough]]; + case State::ContinueFromIdle: + switch (logic->rsp.paramCode) { + case ResponseMsgParamCodes::Processing: + logic->progressCode = static_cast(logic->rsp.paramValue); + logic->errorCode = ErrorCode::OK; + SendAndUpdateFilamentSensor(); // keep on reporting the state of fsensor regularly + break; + case ResponseMsgParamCodes::Error: + // in case of an error the progress code remains as it has been before + logic->errorCode = static_cast(logic->rsp.paramValue); + // keep on reporting the state of fsensor regularly even in command error state + // - the MMU checks FINDA and fsensor even while recovering from errors + SendAndUpdateFilamentSensor(); + return CommandError; + case ResponseMsgParamCodes::Finished: + logic->progressCode = ProgressCode::OK; + state = State::Ready; + return Finished; + default: + return ProtocolError; + } + break; + case State::FilamentSensorStateSent:{ + auto expmsg = logic->ExpectingMessage(linkLayerTimeout); + if (expmsg != MessageReady) + return expmsg; + SendFINDAQuery(); + } break; + case State::FINDAReqSent: + return ProcessFINDAReqSent(Processing, State::Wait); + case State::ButtonSent:{ + // button is never confirmed ... may be it should be + // auto expmsg = logic->ExpectingMessage(linkLayerTimeout); + // if (expmsg != MessageReady) + // return expmsg; + SendQuery(); + } break; + default: + return ProtocolError; + } + return Processing; +} + +void Idle::Restart() { + state = State::Ready; +} + +StepStatus Idle::Step() { + switch (state) { + case State::Ready: // check timeout + if (logic->Elapsed(heartBeatPeriod)) { + logic->SendMsg(RequestMsg(RequestMsgCodes::Query, 0)); + state = State::QuerySent; + return Processing; + } + break; + case State::QuerySent: { // check UART + auto expmsg = logic->ExpectingMessage(linkLayerTimeout); + if (expmsg != MessageReady) + return expmsg; + // If we are accidentally in Idle and we receive something like "T0 P1" - that means the communication dropped out while a command was in progress. + // That causes no issues here, we just need to switch to Command processing and continue there from now on. + // The usual response in this case should be some command and "F" - finished - that confirms we are in an Idle state even on the MMU side. + switch( logic->rsp.request.code ){ + case RequestMsgCodes::Cut: + case RequestMsgCodes::Eject: + case RequestMsgCodes::Load: + case RequestMsgCodes::Mode: + case RequestMsgCodes::Tool: + case RequestMsgCodes::Unload: + if( logic->rsp.paramCode != ResponseMsgParamCodes::Finished ){ + logic->SwitchFromIdleToCommand(); + return Processing; + } + default: + break; + } + SendFINDAQuery(); + return Processing; + } break; + case State::FINDAReqSent: + return ProcessFINDAReqSent(Finished, State::Ready); + default: + return ProtocolError; + } + + // The "return Finished" in this state machine requires a bit of explanation: + // The Idle state either did nothing (still waiting for the heartbeat timeout) + // or just successfully received the answer to Q0, whatever that was. + // In both cases, it is ready to hand over work to a command or something else, + // therefore we are returning Finished (also to exit mmu_loop() and unblock Marlin's loop!). + // If there is no work, we'll end up in the Idle state again + // and we'll send the heartbeat message after the specified timeout. + return Finished; +} + +ProtocolLogic::ProtocolLogic(MMU2Serial *uart) + : stopped(this) + , startSeq(this) + , idle(this) + , command(this) + , currentState(&stopped) + , plannedRq(RequestMsgCodes::unknown, 0) + , lastUARTActivityMs(0) + , rsp(RequestMsg(RequestMsgCodes::unknown, 0), ResponseMsgParamCodes::unknown, 0) + , state(State::Stopped) + , lrb(0) + , uart(uart) + , lastFSensor((uint8_t)WhereIsFilament()) +{} + +void ProtocolLogic::Start() { + state = State::InitSequence; + currentState = &startSeq; + startSeq.Restart(); +} + +void ProtocolLogic::Stop() { + state = State::Stopped; + currentState = &stopped; +} + +void ProtocolLogic::ToolChange(uint8_t slot) { + PlanGenericRequest(RequestMsg(RequestMsgCodes::Tool, slot)); +} + +void ProtocolLogic::UnloadFilament() { + PlanGenericRequest(RequestMsg(RequestMsgCodes::Unload, 0)); +} + +void ProtocolLogic::LoadFilament(uint8_t slot) { + PlanGenericRequest(RequestMsg(RequestMsgCodes::Load, slot)); +} + +void ProtocolLogic::EjectFilament(uint8_t slot) { + PlanGenericRequest(RequestMsg(RequestMsgCodes::Eject, slot)); +} + +void ProtocolLogic::CutFilament(uint8_t slot){ + PlanGenericRequest(RequestMsg(RequestMsgCodes::Cut, slot)); +} + +void ProtocolLogic::ResetMMU() { + PlanGenericRequest(RequestMsg(RequestMsgCodes::Reset, 0)); +} + +void ProtocolLogic::Button(uint8_t index){ + PlanGenericRequest(RequestMsg(RequestMsgCodes::Button, index)); +} + +void ProtocolLogic::Home(uint8_t mode){ + PlanGenericRequest(RequestMsg(RequestMsgCodes::Home, mode)); +} + +void ProtocolLogic::PlanGenericRequest(RequestMsg rq) { + plannedRq = rq; + if( ! currentState->ExpectsResponse() ){ + ActivatePlannedRequest(); + } // otherwise wait for an empty window to activate the request +} + +bool MMU2::ProtocolLogic::ActivatePlannedRequest(){ + if( plannedRq.code == RequestMsgCodes::Button ){ + // only issue the button to the MMU and do not restart the state machines + command.SendButton(plannedRq.value); + plannedRq = RequestMsg(RequestMsgCodes::unknown, 0); + return true; + } else if( plannedRq.code != RequestMsgCodes::unknown ){ + currentState = &command; + command.SetRequestMsg(plannedRq); + plannedRq = RequestMsg(RequestMsgCodes::unknown, 0); + command.Restart(); + return true; + } + return false; +} + +void ProtocolLogic::SwitchFromIdleToCommand(){ + currentState = &command; + command.SetRequestMsg(rsp.request); + // we are recovering from a communication drop out, the command is already running + // and we have just received a response to a Q0 message about a command progress + command.ContinueFromIdle(); +} + +void ProtocolLogic::SwitchToIdle() { + state = State::Running; + currentState = &idle; + idle.Restart(); +} + +void ProtocolLogic::HandleCommunicationTimeout() { + uart->flush(); // clear the output buffer + currentState = &startSeq; + state = State::InitSequence; + startSeq.Restart(); +} + +bool ProtocolLogic::Elapsed(uint32_t timeout) const { + return _millis() >= (lastUARTActivityMs + timeout); +} + +void ProtocolLogic::RecordUARTActivity() { + lastUARTActivityMs = _millis(); +} + +void ProtocolLogic::RecordReceivedByte(uint8_t c){ + lastReceivedBytes[lrb] = c; + lrb = (lrb+1) % lastReceivedBytes.size(); +} + +char NibbleToChar(uint8_t c){ + switch (c) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + return c + '0'; + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + return (c - 10) + 'a'; + default: + return 0; + } +} + +void ProtocolLogic::FormatLastReceivedBytes(char *dst){ + for(uint8_t i = 0; i < lastReceivedBytes.size(); ++i){ + uint8_t b = lastReceivedBytes[ (lrb-i-1) % lastReceivedBytes.size() ]; + dst[i*3] = NibbleToChar(b >> 4); + dst[i*3+1] = NibbleToChar(b & 0xf); + dst[i*3+2] = ' '; + } + dst[ (lastReceivedBytes.size() - 1) * 3 + 2] = 0; // terminate properly +} + +void ProtocolLogic::FormatLastResponseMsgAndClearLRB(char *dst){ + *dst++ = '<'; + for(uint8_t i = 0; i < lrb; ++i){ + uint8_t b = lastReceivedBytes[ i ]; + if( b < 32 )b = '.'; + if( b > 127 )b = '.'; + *dst++ = b; + } + *dst = 0; // terminate properly + lrb = 0; // reset the input buffer index in case of a clean message +} + +void ProtocolLogic::LogRequestMsg(const uint8_t *txbuff, uint8_t size){ + constexpr uint_fast8_t rqs = modules::protocol::Protocol::MaxRequestSize() + 2; + char tmp[rqs] = ">"; + static char lastMsg[rqs] = ""; + for(uint8_t i = 0; i < size; ++i){ + uint8_t b = txbuff[i]; + if( b < 32 )b = '.'; + if( b > 127 )b = '.'; + tmp[i+1] = b; + } + tmp[size+1] = '\n'; + tmp[size+2] = 0; + if( !strncmp(tmp, ">S0.\n", rqs) && !strncmp(lastMsg, tmp, rqs) ){ + // @@TODO we skip the repeated request msgs for now + // to avoid spoiling the whole log just with ">S0" messages + // especially when the MMU is not connected. + // We'll lose the ability to see if the printer is actually + // trying to find the MMU, but since it has been reliable in the past + // we can live without it for now. + } else { + MMU2_ECHO_MSG(tmp); + } + memcpy(lastMsg, tmp, rqs); +} + +void MMU2::ProtocolLogic::LogError(const char *reason){ + char lrb[lastReceivedBytes.size() * 3]; + FormatLastReceivedBytes(lrb); + + MMU2_ERROR_MSG(reason); + SERIAL_ECHO(", last bytes: "); + SERIAL_ECHOLN(lrb); +} + +void ProtocolLogic::LogResponse(){ + char lrb[lastReceivedBytes.size()]; + FormatLastResponseMsgAndClearLRB(lrb); + MMU2_ECHO_MSG(lrb); + SERIAL_ECHOLN(); +} + +StepStatus MMU2::ProtocolLogic::HandleCommError(const char *msg, StepStatus ss){ + protocol.ResetResponseDecoder(); + HandleCommunicationTimeout(); + if( dataTO.Record(ss) ){ + LogError(msg); + return dataTO.InitialCause(); + } else { + return Processing; // suppress short drop outs of communication + } +} + +StepStatus ProtocolLogic::Step() { + if( ! currentState->ExpectsResponse() ){ // if not waiting for a response, activate a planned request immediately + ActivatePlannedRequest(); + } + auto currentStatus = currentState->Step(); + switch (currentStatus) { + case Processing: + // we are ok, the state machine continues correctly + break; + case Finished: { + // We are ok, switching to Idle if there is no potential next request planned. + // But the trouble is we must report a finished command if the previous command has just been finished + // i.e. only try to find some planned command if we just finished the Idle cycle + bool previousCommandFinished = currentState == &command; // @@TODO this is a nasty hack :( + if( ! ActivatePlannedRequest() ){ // if nothing is planned, switch to Idle + SwitchToIdle(); + } else { + // if the previous cycle was Idle and now we have planned a new command -> avoid returning Finished + if( ! previousCommandFinished && currentState == &command){ + currentStatus = Processing; + } + } + } + break; + case CommandRejected: + // we have to repeat it - that's the only thing we can do + // no change in state + // @@TODO wait until Q0 returns command in progress finished, then we can send this one + LogError("Command rejected"); + command.Restart(); + break; + case CommandError: + LogError("Command Error"); + // we shall probably transfer into the Idle state and await further instructions from the upper layer + // Idle state may solve the problem of keeping up the heart beat running + break; + case VersionMismatch: + LogError("Version mismatch"); + Stop(); // cannot continue + break; + case ProtocolError: + currentStatus = HandleCommError("Protocol error", ProtocolError); + break; + case CommunicationTimeout: + currentStatus = HandleCommError("Communication timeout", CommunicationTimeout); + break; + default: + break; + } + return currentStatus; +} + +uint8_t ProtocolLogic::CommandInProgress() const { + if( currentState != &command ) + return 0; + return (uint8_t)command.ReqMsg().code; +} + +bool DropOutFilter::Record(StepStatus ss){ + if( occurrences == maxOccurrences ){ + cause = ss; + } + --occurrences; + return occurrences == 0; +} + +} // namespace MMU2 diff --git a/Firmware/mmu2_protocol_logic.h b/Firmware/mmu2_protocol_logic.h new file mode 100644 index 000000000..e96f93cd2 --- /dev/null +++ b/Firmware/mmu2_protocol_logic.h @@ -0,0 +1,314 @@ +#pragma once +#include +// #include //@@TODO Don't we have STL for AVR somewhere? +template +class array { + T data[N]; +public: + array() = default; + inline constexpr T* begin()const { return data; } + inline constexpr T* end()const { return data + N; } + constexpr uint8_t size()const { return N; } + inline T &operator[](uint8_t i){ + return data[i]; + } +}; + +#include "mmu2/error_codes.h" +#include "mmu2/progress_codes.h" +#include "mmu2_protocol.h" + +#include "mmu2_serial.h" + +/// New MMU2 protocol logic +namespace MMU2 { + +using namespace modules::protocol; + +class ProtocolLogic; + +/// ProtocolLogic stepping statuses +enum StepStatus : uint_fast8_t { + Processing = 0, + MessageReady, ///< a message has been successfully decoded from the received bytes + Finished, + CommunicationTimeout, ///< the MMU failed to respond to a request within a specified time frame + ProtocolError, ///< bytes read from the MMU didn't form a valid response + CommandRejected, ///< the MMU rejected the command due to some other command in progress, may be the user is operating the MMU locally (button commands) + CommandError, ///< the command in progress stopped due to unrecoverable error, user interaction required + VersionMismatch, ///< the MMU reports its firmware version incompatible with our implementation + CommunicationRecovered, +}; + + +static constexpr uint32_t linkLayerTimeout = 2000; ///< default link layer communication timeout +static constexpr uint32_t dataLayerTimeout = linkLayerTimeout * 3; ///< data layer communication timeout +static constexpr uint32_t heartBeatPeriod = linkLayerTimeout / 2; ///< period of heart beat messages (Q0) + +static_assert( heartBeatPeriod < linkLayerTimeout && linkLayerTimeout < dataLayerTimeout, "Incorrect ordering of timeouts"); + +/// Base class for sub-automata of the ProtocolLogic class. +/// Their operation should never block (wait inside). +class ProtocolLogicPartBase { +public: + inline ProtocolLogicPartBase(ProtocolLogic *logic) + : logic(logic) + , state(State::Ready) {} + + /// Restarts the sub-automaton + virtual void Restart() = 0; + + /// Makes one step in the sub-automaton + /// @returns StepStatus + virtual StepStatus Step() = 0; + + /// @returns true if the state machine is waiting for a response from the MMU + bool ExpectsResponse()const { return state != State::Ready && state != State::Wait; } + +protected: + ProtocolLogic *logic; ///< pointer to parent ProtocolLogic layer + friend class ProtocolLogic; + + /// Common internal states of the derived sub-automata + /// General rule of thumb: *Sent states are waiting for a response from the MMU + enum class State : uint_fast8_t { + Ready, + Wait, + + S0Sent, + S1Sent, + S2Sent, + QuerySent, + CommandSent, + FilamentSensorStateSent, + FINDAReqSent, + ButtonSent, + + ContinueFromIdle + }; + + State state; ///< internal state of the sub-automaton + + /// @returns the status of processing of the FINDA query response + /// @param finishedRV returned value in case the message was successfully received and processed + /// @param nextState is a state where the state machine should transfer to after the message was successfully received and processed + StepStatus ProcessFINDAReqSent(StepStatus finishedRV, State nextState); + + /// Called repeatedly while waiting for a query (Q0) period. + /// All event checks to report immediately from the printer to the MMU shall be done in this method. + /// So far, the only such a case is the filament sensor, but there can be more like this in the future. + void CheckAndReportAsyncEvents(); + + void SendQuery(); + + void SendFINDAQuery(); + + void SendAndUpdateFilamentSensor(); + + void SendButton(uint8_t btn); +}; + +/// Starting sequence of the communication with the MMU. +/// The printer shall ask for MMU's version numbers. +/// If everything goes well and the MMU's version is good enough, +/// the ProtocolLogic layer may continue talking to the MMU +class StartSeq : public ProtocolLogicPartBase { +public: + inline StartSeq(ProtocolLogic *logic) + : ProtocolLogicPartBase(logic) {} + void Restart() override; + StepStatus Step() override; +}; + +/// A command and its lifecycle. +/// CommandSent: +/// - the command was placed into the UART TX buffer, awaiting response from the MMU +/// - if the MMU confirms the command, we'll wait for it to finish +/// - if the MMU refuses the command, we report an error (should normally not happen unless someone is hacking the communication without waiting for the previous command to finish) +/// Wait: +/// - waiting for the MMU to process the command - may take several seconds, for example Tool change operation +/// - meawhile, every 300ms we send a Q0 query to obtain the current state of the command being processed +/// - as soon as we receive a response to Q0 from the MMU, we process it in the next state +/// QuerySent - check the reply from the MMU - can be any of the following: +/// - Processing: the MMU is still working +/// - Error: the command failed on the MMU, we'll have the exact error report in the response message +/// - Finished: the MMU finished the command successfully, another command may be issued now +class Command : public ProtocolLogicPartBase { +public: + inline Command(ProtocolLogic *logic) + : ProtocolLogicPartBase(logic) + , rq(RequestMsgCodes::unknown, 0) {} + void Restart() override; + StepStatus Step() override; + inline void SetRequestMsg(RequestMsg msg) { + rq = msg; + } + void ContinueFromIdle(){ + state = State::ContinueFromIdle; + } + inline const RequestMsg &ReqMsg()const { return rq; } + +private: + RequestMsg rq; +}; + +/// Idle state - we have no command for the MMU, so we are only regularly querying its state with Q0 messages. +/// The idle state can be interrupted any time to issue a command into the MMU +class Idle : public ProtocolLogicPartBase { +public: + inline Idle(ProtocolLogic *logic) + : ProtocolLogicPartBase(logic) {} + void Restart() override; + StepStatus Step() override; +}; + +/// The communication with the MMU is stopped/disabled (for whatever reason). +/// Nothing is being put onto the UART. +class Stopped : public ProtocolLogicPartBase { +public: + inline Stopped(ProtocolLogic *logic) + : ProtocolLogicPartBase(logic) {} + void Restart() override {} + StepStatus Step() override { return Processing; } +}; + +///< Filter of short consecutive drop outs which are recovered instantly +class DropOutFilter { + StepStatus cause; + uint8_t occurrences; +public: + static constexpr uint8_t maxOccurrences = 3; + static_assert (maxOccurrences > 1, "we should really silently ignore at least 1 comm drop out if recovered immediately afterwards"); + DropOutFilter() = default; + + /// @returns true if the error should be reported to higher levels (max. number of consecutive occurrences reached) + bool Record(StepStatus ss); + + /// @returns the initial cause which started this drop out event + inline StepStatus InitialCause()const { return cause; } + + /// Rearms the object for further processing - basically call this once the MMU responds with something meaningful (e.g. S0 A2) + inline void Reset(){ occurrences = maxOccurrences; } +}; + +/// Logic layer of the MMU vs. printer communication protocol +class ProtocolLogic { +public: + ProtocolLogic(MMU2Serial *uart); + + /// Start/Enable communication with the MMU + void Start(); + + /// Stop/Disable communication with the MMU + void Stop(); + + // Issue commands to the MMU + void ToolChange(uint8_t slot); + void UnloadFilament(); + void LoadFilament(uint8_t slot); + void EjectFilament(uint8_t slot); + void CutFilament(uint8_t slot); + void ResetMMU(); + void Button(uint8_t index); + void Home(uint8_t mode); + + /// Step the state machine + StepStatus Step(); + + /// @returns the current/latest error code as reported by the MMU + ErrorCode Error() const { return errorCode; } + + /// @returns the current/latest process code as reported by the MMU + ProgressCode Progress() const { return progressCode; } + + uint8_t CommandInProgress()const; + + inline bool Running()const { + return state == State::Running; + } + + inline bool FindaPressed() const { + return findaPressed; + } + +#ifndef UNITTEST +private: +#endif + + StepStatus ProcessUARTByte(uint8_t c); + StepStatus ExpectingMessage(uint32_t timeout); + void SendMsg(RequestMsg rq); + void SwitchToIdle(); + void HandleCommunicationTimeout(); + StepStatus HandleCommError(const char *msg, StepStatus ss); + bool Elapsed(uint32_t timeout) const; + void RecordUARTActivity(); + void RecordReceivedByte(uint8_t c); + void FormatLastReceivedBytes(char *dst); + void FormatLastResponseMsgAndClearLRB(char *dst); + void LogRequestMsg(const uint8_t *txbuff, uint8_t size); + void LogError(const char *reason); + void LogResponse(); + void SwitchFromIdleToCommand(); + + enum class State : uint_fast8_t { + Stopped, ///< stopped for whatever reason + InitSequence, ///< initial sequence running + Running ///< normal operation - Idle + Command processing + }; + + // individual sub-state machines - may be they can be combined into a union since only one is active at once + Stopped stopped; + StartSeq startSeq; + Idle idle; + Command command; + ProtocolLogicPartBase *currentState; ///< command currently being processed + + /// Records the next planned state, "unknown" msg code if no command is planned. + /// This is not intended to be a queue of commands to process, protocol_logic must not queue commands. + /// It exists solely to prevent breaking the Request-Response protocol handshake - + /// - during tests it turned out, that the commands from Marlin are coming in such an asynchronnous way, that + /// we could accidentally send T2 immediately after Q0 without waiting for reception of response to Q0. + /// + /// Beware, if Marlin manages to call PlanGenericCommand multiple times before a response comes, + /// these variables will get overwritten by the last call. + /// However, that should not happen under normal circumstances as Marlin should wait for the Command to finish, + /// which includes all responses (and error recovery if any). + RequestMsg plannedRq; + + /// Plan a command to be processed once the immediate response to a sent request arrives + void PlanGenericRequest(RequestMsg rq); + /// Activate the planned state once the immediate response to a sent request arrived + bool ActivatePlannedRequest(); + + uint32_t lastUARTActivityMs; ///< timestamp - last ms when something occurred on the UART + DropOutFilter dataTO; ///< Filter of short consecutive drop outs which are recovered instantly + + ResponseMsg rsp; ///< decoded response message from the MMU protocol + + State state; ///< internal state of ProtocolLogic + + Protocol protocol; ///< protocol codec + + array lastReceivedBytes; ///< remembers the last few bytes of incoming communication for diagnostic purposes + uint8_t lrb; + + MMU2Serial *uart; ///< UART interface + + ErrorCode errorCode; ///< last received error code from the MMU + ProgressCode progressCode; ///< last received progress code from the MMU + + uint8_t lastFSensor; ///< last state of filament sensor + + bool findaPressed; + + friend class ProtocolLogicPartBase; + friend class Stopped; + friend class Command; + friend class Idle; + friend class StartSeq; + + friend class MMU2; +}; + +} // namespace MMU2 diff --git a/Firmware/mmu2_reporting.cpp b/Firmware/mmu2_reporting.cpp new file mode 100644 index 000000000..18549ac35 --- /dev/null +++ b/Firmware/mmu2_reporting.cpp @@ -0,0 +1,21 @@ +#include "mmu2_reporting.h" + +// @@TODO implement the interface for MK3 + +namespace MMU2 { + +void BeginReport(CommandInProgress cip, uint16_t ec) { } + +void EndReport(CommandInProgress cip, uint16_t ec) { } + +void ReportErrorHook(CommandInProgress cip, uint16_t ec) { } + +void ReportProgressHook(CommandInProgress cip, uint16_t ec) { } + +Buttons ButtonPressed(uint16_t ec) { } + +bool MMUAvailable() { } + +bool UseMMU() { } + +} // namespace MMU2 diff --git a/Firmware/mmu2_reporting.h b/Firmware/mmu2_reporting.h new file mode 100644 index 000000000..66fd5100c --- /dev/null +++ b/Firmware/mmu2_reporting.h @@ -0,0 +1,53 @@ +/// @file mmu2_reporting.h + +#pragma once +#include + +namespace MMU2 { + +enum CommandInProgress : uint8_t { + NoCommand = 0, + CutFilament = 'C', + EjectFilament = 'E', + Homing = 'H', + LoadFilament = 'L', + Reset = 'X', + ToolChange = 'T', + UnloadFilament = 'U', +}; + +/// Called at the begin of every MMU operation +void BeginReport(CommandInProgress cip, uint16_t ec); + +/// Called at the end of every MMU operation +void EndReport(CommandInProgress cip, uint16_t ec); + +/// Called when the MMU sends operation error (even repeatedly) +void ReportErrorHook(CommandInProgress cip, uint16_t ec); + +/// Called when the MMU sends operation progress update +void ReportProgressHook(CommandInProgress cip, uint16_t ec); + +/// Button codes + extended actions performed on the printer's side +enum Buttons : uint8_t { + Left = 0, + Middle, + Right, + + // performed on the printer's side + RestartMMU, + StopPrint, + + NoButton = 0xff // shall be kept last +}; + +Buttons ButtonPressed(uint16_t ec); + +/// @returns true if the MMU is communicating and available +/// can change at runtime +bool MMUAvailable(); + +/// Global Enable/Disable use MMU (to be stored in EEPROM) +bool UseMMU(); + +} // namespace diff --git a/Firmware/mmu2_serial.cpp b/Firmware/mmu2_serial.cpp new file mode 100644 index 000000000..2b9388624 --- /dev/null +++ b/Firmware/mmu2_serial.cpp @@ -0,0 +1,15 @@ +#include "mmu2_serial.h" + +//@@TODO implement for MK3 + +namespace MMU2 { + +void MMU2Serial::begin(uint32_t baud){ } +void MMU2Serial::close() { } +int MMU2Serial::read() { } +void MMU2Serial::flush() { } +size_t MMU2Serial::write(const uint8_t *buffer, size_t size) { } + +MMU2Serial mmu2Serial; + +} // namespace MMU2 diff --git a/Firmware/mmu2_serial.h b/Firmware/mmu2_serial.h new file mode 100644 index 000000000..61949284c --- /dev/null +++ b/Firmware/mmu2_serial.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include + +namespace MMU2 { + +/// A minimal serial interface for the MMU +class MMU2Serial { +public: + MMU2Serial() = default; +// bool available()const; + void begin(uint32_t baud); + void close(); + int read(); + void flush(); + size_t write(const uint8_t *buffer, size_t size); +}; + +extern MMU2Serial mmu2Serial; + +} // namespace MMU2 diff --git a/Firmware/stepper.cpp b/Firmware/stepper.cpp index 108f6216a..45f2141f8 100644 --- a/Firmware/stepper.cpp +++ b/Firmware/stepper.cpp @@ -38,7 +38,7 @@ #include "Filament_sensor.h" -#include "mmu.h" +#include "mmu2.h" #include "ConfigurationStore.h" #include "Prusa_farm.h" diff --git a/Firmware/strlen_cx.h b/Firmware/strlen_cx.h new file mode 100644 index 000000000..1dfc0c7b2 --- /dev/null +++ b/Firmware/strlen_cx.h @@ -0,0 +1,5 @@ +#pragma once + +constexpr inline int strlen_constexpr(const char* str){ + return *str ? 1 + strlen_constexpr(str + 1) : 0; +} diff --git a/Firmware/ultralcd.cpp b/Firmware/ultralcd.cpp index c8ea0c113..dd43adb76 100755 --- a/Firmware/ultralcd.cpp +++ b/Firmware/ultralcd.cpp @@ -36,7 +36,7 @@ #include "sound.h" -#include "mmu.h" +#include "mmu2.h" #include "static_assert.h" #include "first_lay_cal.h" @@ -450,19 +450,20 @@ void lcdui_print_percent_done(void) } // Print extruder status (5 chars total) -void lcdui_print_extruder(void) -{ - int chars = 0; - if (mmu_extruder == tmp_extruder) { - if (mmu_extruder == MMU_FILAMENT_UNKNOWN) chars = lcd_printf_P(_N(" F?")); - else chars = lcd_printf_P(_N(" F%u"), mmu_extruder + 1); - } - else - { - if (mmu_extruder == MMU_FILAMENT_UNKNOWN) chars = lcd_printf_P(_N(" ?>%u"), tmp_extruder + 1); - else chars = lcd_printf_P(_N(" %u>%u"), mmu_extruder + 1, tmp_extruder + 1); - } - lcd_space(5 - chars); +void lcdui_print_extruder(void) { + uint8_t chars = 0; +// @@TODO if (MMU2::mmu2.get_current_tool() == tmp_extruder) { +// if (MMU2::mmu2.get_current_tool() == MMU2::FILAMENT_UNKNOWN) +// chars = lcd_printf_P(_N(" F?")); +// else +// chars = lcd_printf_P(_N(" F%u"), MMU2::mmu2.get_current_tool() + 1); +// } else { +// if (MMU2::mmu2.get_current_tool() == MMU2::FILAMENT_UNKNOWN) +// chars = lcd_printf_P(_N(" ?>%u"), tmp_extruder + 1); +// else +// chars = lcd_printf_P(_N(" %u>%u"), MMU2::mmu2.get_current_tool() + 1, tmp_extruder + 1); +// } + lcd_space(5 - chars); } // Print farm number (5 chars total) @@ -719,7 +720,7 @@ void lcdui_print_status_screen(void) //Print SD status (7 chars) lcdui_print_percent_done(); - if (mmu_enabled) + if (MMU2::mmu2.Enabled()) //Print extruder status (5 chars) lcdui_print_extruder(); else if (farm_mode) @@ -945,7 +946,7 @@ void lcd_commands() enquecommand_P(PSTR("M140 S0")); // turn off heatbed enquecommand_P(PSTR("G1 Z10 F1300.000")); //lift Z enquecommand_P(PSTR("G1 X10 Y180 F4000")); //Go to parking position - if (mmu_enabled) enquecommand_P(PSTR("M702 C")); //unload from nozzle + if (MMU2::mmu2.Enabled()) enquecommand_P(PSTR("M702 C")); //unload from nozzle enquecommand_P(PSTR("M84"));// disable motors forceMenuExpire = true; //if user dont confirm live adjust Z value by pressing the knob, we are saving last value by timeout to status screen lcd_commands_step = 1; @@ -1188,14 +1189,14 @@ static void lcd_menu_fails_stats_mmu_print() //! @todo Positioning of the messages and values on LCD aren't fixed to their exact place. This causes issues with translations. static void lcd_menu_fails_stats_mmu_total() { - mmu_command(MmuCmd::S3); +// @@TODO mmu_command(MmuCmd::S3); lcd_timeoutToStatus.stop(); //infinite timeout lcd_home(); - lcd_printf_P(PSTR("%S\n" " %-16.16S%-3d\n" " %-16.16S%-3d\n" " %-16.16S%-3d"), - _T(MSG_TOTAL_FAILURES), - _T(MSG_MMU_FAILS), clamp999( eeprom_read_word((uint16_t*)EEPROM_MMU_FAIL_TOT) ), - _T(MSG_MMU_LOAD_FAILS), clamp999( eeprom_read_word((uint16_t*)EEPROM_MMU_LOAD_FAIL_TOT) ), - _i("MMU power fails"), clamp999( mmu_power_failures )); ////MSG_MMU_POWER_FAILS c=15 +// lcd_printf_P(PSTR("%S\n" " %-16.16S%-3d\n" " %-16.16S%-3d\n" " %-16.16S%-3d"), +// _T(MSG_TOTAL_FAILURES), +// _T(MSG_MMU_FAILS), clamp999( eeprom_read_word((uint16_t*)EEPROM_MMU_FAIL_TOT) ), +// _T(MSG_MMU_LOAD_FAILS), clamp999( eeprom_read_word((uint16_t*)EEPROM_MMU_LOAD_FAIL_TOT) ), +// _i("MMU power fails"), clamp999( mmu_power_failures )); ////MSG_MMU_POWER_FAILS c=15 menu_back_if_clicked_fb(); } @@ -1680,13 +1681,15 @@ static void lcd_support_menu() #endif // IR_SENSOR_ANALOG MENU_ITEM_BACK_P(STR_SEPARATOR); - if (mmu_enabled) + if (MMU2::mmu2.Enabled()) { MENU_ITEM_BACK_P(_i("MMU2 connected")); ////MSG_MMU_CONNECTED c=18 MENU_ITEM_BACK_P(PSTR(" FW:")); ////c=17 if (((menu_item - 1) == menu_line) && lcd_draw_update) { lcd_set_cursor(6, menu_row); + uint8_t mmu_version = 200; // @@TODO + uint8_t mmu_buildnr = 0; if ((mmu_version > 0) && (mmu_buildnr > 0)) lcd_printf_P(PSTR("%d.%d.%d-%d"), mmu_version/100, mmu_version%100/10, mmu_version%10, mmu_buildnr); else @@ -1919,7 +1922,7 @@ void mFilamentItem(uint16_t nTemp, uint16_t nTempBed) nLevel = bFilamentPreheatState ? 1 : 2; bFilamentAction = true; menu_back(nLevel); - extr_unload(); + MMU2::mmu2.unload(); break; case FilamentAction::MmuEject: nLevel = bFilamentPreheatState ? 1 : 2; @@ -3430,15 +3433,15 @@ static void lcd_show_sensors_state() uint8_t idler_state = STATE_NA; pinda_state = READ(Z_MIN_PIN); - if (mmu_enabled && !mmu_last_finda_response.expired(1000)) + if (MMU2::mmu2.Enabled()) { - finda_state = mmu_finda; + finda_state = MMU2::mmu2.FindaDetectsFilament(); } lcd_puts_at_P(0, 0, MSG_PINDA); lcd_set_cursor(LCD_WIDTH - 14, 0); lcd_print_state(pinda_state); - if (mmu_enabled == true) + if (MMU2::mmu2.Enabled()) { lcd_puts_at_P(10, 0, _n("FINDA"));////MSG_FINDA c=5 lcd_set_cursor(LCD_WIDTH - 3, 0); @@ -3775,7 +3778,7 @@ void lcd_first_layer_calibration_reset() void lcd_v2_calibration() { - if (mmu_enabled) + if (MMU2::mmu2.Enabled()) { const uint8_t filament = choose_menu_P( _T(MSG_SELECT_FILAMENT), @@ -3882,22 +3885,19 @@ static void wait_preheat() } -static void lcd_wizard_load() -{ - if (mmu_enabled) - { - lcd_show_fullscreen_message_and_wait_P(_i("Please insert filament into the first tube of the MMU, then press the knob to load it."));////MSG_MMU_INSERT_FILAMENT_FIRST_TUBE c=20 r=6 - tmp_extruder = 0; - } - else - { - lcd_show_fullscreen_message_and_wait_P(_i("Please insert filament into the extruder, then press the knob to load it."));////MSG_WIZARD_LOAD_FILAMENT c=20 r=6 - } - lcd_update_enable(false); - lcd_clear(); - lcd_puts_at_P(0, 2, _T(MSG_LOADING_FILAMENT)); - loading_flag = true; - gcode_M701(); +static void lcd_wizard_load() { + if (MMU2::mmu2.Enabled()) { + lcd_show_fullscreen_message_and_wait_P( + _i("Please insert filament into the first tube of the MMU, then press the knob to load it.")); ////MSG_MMU_INSERT_FILAMENT_FIRST_TUBE c=20 r=6 + } else { + lcd_show_fullscreen_message_and_wait_P( + _i("Please insert filament into the extruder, then press the knob to load it.")); ////MSG_WIZARD_LOAD_FILAMENT c=20 r=6 + } + lcd_update_enable(false); + lcd_clear(); + lcd_puts_at_P(0, 2, _T(MSG_LOADING_FILAMENT)); + loading_flag = true; + gcode_M701(0); } bool lcd_autoDepleteEnabled() @@ -3913,7 +3913,7 @@ static void wizard_lay1cal_message(bool cold) { lcd_show_fullscreen_message_and_wait_P( _i("Now I will calibrate distance between tip of the nozzle and heatbed surface.")); ////MSG_WIZARD_V2_CAL c=20 r=8 - if (mmu_enabled) + if (MMU2::mmu2.Enabled()) { lcd_show_fullscreen_message_and_wait_P( _i("Select a filament for the First Layer Calibration and select it in the on-screen menu."));////MSG_SELECT_FIL_1ST_LAYERCAL c=20 r=7 @@ -4056,7 +4056,7 @@ void lcd_wizard(WizState state) //start to preheat nozzle and bed to save some time later setTargetHotend(PLA_PREHEAT_HOTEND_TEMP, 0); setTargetBed(PLA_PREHEAT_HPB_TEMP); - if (mmu_enabled) + if (MMU2::mmu2.Enabled()) { wizard_event = lcd_show_fullscreen_message_yes_no_and_wait_P(_T(MSG_FILAMENT_LOADED), true); } else @@ -4066,7 +4066,7 @@ void lcd_wizard(WizState state) if (wizard_event) state = S::Lay1CalCold; else { - if(mmu_enabled) state = S::LoadFilCold; + if(MMU2::mmu2.Enabled()) state = S::LoadFilCold; else state = S::Preheat; } break; @@ -4256,7 +4256,7 @@ static void auto_deplete_switch() static void settingsAutoDeplete() { - if (mmu_enabled) + if (MMU2::mmu2.Enabled()) { #ifdef FILAMENT_SENSOR if (fsensor.isError()) { @@ -4280,7 +4280,7 @@ while(0)\ #ifdef MMU_HAS_CUTTER static void settingsCutter() { - if (mmu_enabled) + if (MMU2::mmu2.Enabled()) { if (EEPROM_MMU_CUTTER_ENABLED_enabled == eeprom_read_byte((uint8_t*)EEPROM_MMU_CUTTER_ENABLED)) { @@ -4360,7 +4360,7 @@ while (0) #define SETTINGS_MMU_MODE \ do\ {\ - if (mmu_enabled)\ + if (MMU2::mmu2.Enabled())\ {\ if (SilentModeMenu_MMU == 0) MENU_ITEM_TOGGLE_P(_T(MSG_MMU_MODE), _T(MSG_NORMAL), lcd_silent_mode_mmu_set);\ else MENU_ITEM_TOGGLE_P(_T(MSG_MMU_MODE), _T(MSG_STEALTH), lcd_silent_mode_mmu_set);\ @@ -4716,7 +4716,7 @@ void lcd_hw_setup_menu(void) // can not be "static" #if defined(FILAMENT_SENSOR) && (FILAMENT_SENSOR_TYPE == FSENSOR_IR_ANALOG) //! Fsensor Detection isn't ready for mmu yet it is temporarily disabled. //! @todo Don't forget to remove this as soon Fsensor Detection works with mmu - if(!mmu_enabled) MENU_ITEM_FUNCTION_P(PSTR("Fsensor Detection"), lcd_detect_IRsensor); + if(!MMU2::mmu2.Enabled()) MENU_ITEM_FUNCTION_P(PSTR("Fsensor Detection"), lcd_detect_IRsensor); #endif //IR_SENSOR_ANALOG if (_md->experimental_menu_visibility) @@ -4878,7 +4878,7 @@ static void lcd_calibration_menu() //! //! Create list of items with header. Header can not be selected. //! Each item has text description passed by function parameter and -//! number. There are 5 numbered items, if mmu_enabled, 4 otherwise. +//! number. There are 5 numbered items, if MMU2::mmu2.Enabled(), 4 otherwise. //! Items are numbered from 1 to 4 or 5. But index returned starts at 0. //! There can be last item with different text and no number. //! @@ -4889,7 +4889,7 @@ static void lcd_calibration_menu() uint8_t choose_menu_P(const char *header, const char *item, const char *last_item) { //following code should handle 3 to 127 number of items well - const int8_t items_no = last_item?(mmu_enabled?6:5):(mmu_enabled?5:4); + const int8_t items_no = last_item?(MMU2::mmu2.Enabled()?6:5):(MMU2::mmu2.Enabled()?5:4); const uint8_t item_len = item?strlen_P(item):0; int8_t first = 0; int8_t enc_dif = lcd_encoder_diff; @@ -5061,68 +5061,71 @@ static void lcd_disable_farm_mode() } +static inline void load_all_wrapper(){ + for(uint8_t i = 0; i < 5; ++i){ + MMU2::mmu2.load_filament(i); + } +} +static inline void load_filament_wrapper(uint8_t i){ + MMU2::mmu2.load_filament(i); +} -static void mmu_load_filament_menu() -{ +static void mmu_load_filament_menu() { MENU_BEGIN(); MENU_ITEM_BACK_P(_T(MSG_MAIN)); - MENU_ITEM_FUNCTION_P(_i("Load all"), load_all); ////MSG_LOAD_ALL c=18 + MENU_ITEM_FUNCTION_P(_i("Load all"), load_all_wrapper); ////MSG_LOAD_ALL c=18 for (uint8_t i = 0; i < MMU_FILAMENT_COUNT; i++) - MENU_ITEM_FUNCTION_NR_P(_T(MSG_LOAD_FILAMENT), i + '1', extr_adj, i); ////MSG_LOAD_FILAMENT c=16 + MENU_ITEM_FUNCTION_NR_P(_T(MSG_LOAD_FILAMENT), i + '1', load_filament_wrapper, i); ////MSG_LOAD_FILAMENT c=16 MENU_END(); } -static void mmu_load_to_nozzle_menu() -{ - if (bFilamentAction) - { +static inline void lcd_mmu_load_to_nozzle_wrapper(uint8_t index){ + MMU2::mmu2.load_filament_to_nozzle(index); +} + +static void mmu_load_to_nozzle_menu() { + if (bFilamentAction) { MENU_BEGIN(); MENU_ITEM_BACK_P(_T(MSG_MAIN)); for (uint8_t i = 0; i < MMU_FILAMENT_COUNT; i++) - MENU_ITEM_FUNCTION_NR_P(_T(MSG_LOAD_FILAMENT), i + '1', lcd_mmu_load_to_nozzle, i); ////MSG_LOAD_FILAMENT c=16 + MENU_ITEM_FUNCTION_NR_P(_T(MSG_LOAD_FILAMENT), i + '1', lcd_mmu_load_to_nozzle_wrapper, i); ////MSG_LOAD_FILAMENT c=16 MENU_END(); - } - else - { + } else { eFilamentAction = FilamentAction::MmuLoad; preheat_or_continue(); } } -static void mmu_eject_filament(uint8_t filament) -{ +static void mmu_eject_filament(uint8_t filament) { menu_back(); - mmu_eject_filament(filament, true); + MMU2::mmu2.eject_filament(filament, true); } -static void mmu_fil_eject_menu() -{ - if (bFilamentAction) - { +static void mmu_fil_eject_menu() { + if (bFilamentAction) { MENU_BEGIN(); MENU_ITEM_BACK_P(_T(MSG_MAIN)); for (uint8_t i = 0; i < MMU_FILAMENT_COUNT; i++) MENU_ITEM_FUNCTION_NR_P(_T(MSG_EJECT_FILAMENT), i + '1', mmu_eject_filament, i); ////MSG_EJECT_FILAMENT c=16 MENU_END(); - } - else - { + } else { eFilamentAction = FilamentAction::MmuEject; preheat_or_continue(); } } #ifdef MMU_HAS_CUTTER +static inline void mmu_cut_filament_wrapper(uint8_t index){ + MMU2::mmu2.cut_filament(index); +} -static void mmu_cut_filament_menu() -{ - if(bFilamentAction) - { +static void mmu_cut_filament_menu() { + if (bFilamentAction) { MENU_BEGIN(); MENU_ITEM_BACK_P(_T(MSG_MAIN)); for (uint8_t i = 0; i < MMU_FILAMENT_COUNT; i++) - MENU_ITEM_FUNCTION_NR_P(_T(MSG_CUT_FILAMENT), i + '1', mmu_cut_filament, i); ////MSG_CUT_FILAMENT c=16 + MENU_ITEM_FUNCTION_NR_P(_T(MSG_CUT_FILAMENT), i + '1', mmu_cut_filament_wrapper, i); ////MSG_CUT_FILAMENT c=16 MENU_END(); } else @@ -5522,7 +5525,7 @@ static void lcd_main_menu() } if ( ! ( IS_SD_PRINTING || usb_timer.running() || (lcd_commands_type == LcdCommands::Layer1Cal) ) ) { - if (mmu_enabled) { + if (MMU2::mmu2.Enabled()) { MENU_ITEM_SUBMENU_P(_T(MSG_LOAD_FILAMENT), mmu_load_filament_menu); MENU_ITEM_SUBMENU_P(_i("Load to nozzle"), mmu_load_to_nozzle_menu);////MSG_LOAD_TO_NOZZLE c=18 MENU_ITEM_SUBMENU_P(_T(MSG_UNLOAD_FILAMENT), mmu_unload_filament); @@ -5532,7 +5535,7 @@ static void lcd_main_menu() #endif //MMU_HAS_CUTTER } else { #ifdef FILAMENT_SENSOR - if (fsensor.getAutoLoadEnabled() && (mmu_enabled == false)) { + if (fsensor.getAutoLoadEnabled() && (MMU2::mmu2.Enabled() == false)) { MENU_ITEM_SUBMENU_P(_i("AutoLoad filament"), lcd_menu_AutoLoadFilament);////MSG_AUTOLOAD_FILAMENT c=18 } else @@ -5553,7 +5556,7 @@ static void lcd_main_menu() #if defined(TMC2130) || defined(FILAMENT_SENSOR) MENU_ITEM_SUBMENU_P(_i("Fail stats"), lcd_menu_fails_stats);////MSG_FAIL_STATS c=18 #endif - if (mmu_enabled) { + if (MMU2::mmu2.Enabled()) { MENU_ITEM_SUBMENU_P(_i("Fail stats MMU"), lcd_menu_fails_stats_mmu);////MSG_MMU_FAIL_STATS c=18 } MENU_ITEM_SUBMENU_P(_i("Support"), lcd_support_menu);////MSG_SUPPORT c=18 @@ -5902,7 +5905,7 @@ void print_stop() fanSpeed = 0; } - if (mmu_enabled) extr_unload(); //M702 C + if (MMU2::mmu2.Enabled()) MMU2::mmu2.unload(); //M702 C finishAndDisableSteppers(); //M84 axis_relative_modes = E_AXIS_MASK; //XYZ absolute, E relative } @@ -6228,7 +6231,7 @@ bool lcd_selftest() //! As the Fsensor Detection isn't yet ready for the mmu2s we set temporarily the IR sensor 0.3 or older for mmu2s //! @todo Don't forget to remove this as soon Fsensor Detection works with mmu if(fsensor.getSensorRevision() == IR_sensor_analog::SensorRevision::_Undef) { - if (!mmu_enabled) { + if (!MMU2::mmu2.Enabled()) { lcd_detect_IRsensor(); } else { @@ -6424,7 +6427,7 @@ bool lcd_selftest() if (_result) { #if (FILAMENT_SENSOR_TYPE == FSENSOR_IR) || (FILAMENT_SENSOR_TYPE == FSENSOR_IR_ANALOG) - if (mmu_enabled) + if (MMU2::mmu2.Enabled()) { _progress = lcd_selftest_screen(TestScreen::Fsensor, _progress, 3, true, 2000); //check filaments sensor _result = selftest_irsensor(); @@ -7048,19 +7051,18 @@ static bool selftest_irsensor() { TempBackup tempBackup; setTargetHotend(ABS_PREHEAT_HOTEND_TEMP,active_extruder); - mmu_wait_for_heater_blocking(); +//@@TODO mmu_wait_for_heater_blocking(); progress = lcd_selftest_screen(TestScreen::Fsensor, 0, 1, true, 0); - mmu_filament_ramming(); +//@@TODO mmu_filament_ramming(); } progress = lcd_selftest_screen(TestScreen::Fsensor, progress, 1, true, 0); - mmu_command(MmuCmd::U0); - manage_response(false, false); + MMU2::mmu2.unload(); // mmu_command(MmuCmd::U0); manage_response(false, false); for(uint_least8_t i = 0; i < 200; ++i) { if (0 == (i % 32)) progress = lcd_selftest_screen(TestScreen::Fsensor, progress, 1, true, 0); - mmu_load_step(false); +//@@TODO mmu_load_step(false); while (blocks_queued()) { if (fsensor.getFilamentPresent())