MMU2 interface overhaul

First port of the new MMU2-printer interface into 8bit FW.
This commit is contained in:
D.R.racer 2022-04-20 11:47:58 +02:00
parent c27e4623c5
commit 2e293e90a0
30 changed files with 2907 additions and 458 deletions

View File

@ -461,7 +461,7 @@ void gcode_M114();
#if (defined(FANCHECK) && (((defined(TACH_0) && (TACH_0 >-1)) || (defined(TACH_1) && (TACH_1 > -1))))) #if (defined(FANCHECK) && (((defined(TACH_0) && (TACH_0 >-1)) || (defined(TACH_1) && (TACH_1 > -1)))))
void gcode_M123(); void gcode_M123();
#endif //FANCHECK and TACH_0 and TACH_1 #endif //FANCHECK and TACH_0 and TACH_1
void gcode_M701(); void gcode_M701(uint8_t mmuSlotIndex);
#define UVLO !(PINE & (1<<4)) #define UVLO !(PINE & (1<<4))

View File

@ -86,6 +86,7 @@
#include <avr/wdt.h> #include <avr/wdt.h>
#include <avr/pgmspace.h> #include <avr/pgmspace.h>
#include "Tcodes.h"
#include "Dcodes.h" #include "Dcodes.h"
#include "AutoDeplete.h" #include "AutoDeplete.h"
@ -125,7 +126,7 @@
#include <SPI.h> #include <SPI.h>
#endif #endif
#include "mmu.h" #include "mmu2.h"
#define VERSION_STRING "1.0.2" #define VERSION_STRING "1.0.2"
@ -1047,7 +1048,7 @@ void setup()
{ {
timer2_init(); // enables functional millis timer2_init(); // enables functional millis
mmu_init(); MMU2::mmu2.Start();
ultralcd_init(); ultralcd_init();
@ -1623,7 +1624,7 @@ void setup()
#endif //UVLO_SUPPORT #endif //UVLO_SUPPORT
fCheckModeInit(); fCheckModeInit();
fSetMmuMode(mmu_enabled); fSetMmuMode(MMU2::mmu2.Enabled());
KEEPALIVE_STATE(NOT_BUSY); KEEPALIVE_STATE(NOT_BUSY);
#ifdef WATCHDOG #ifdef WATCHDOG
wdt_enable(WDTO_4S); wdt_enable(WDTO_4S);
@ -1856,7 +1857,7 @@ void loop()
} }
} }
#endif //TMC2130 #endif //TMC2130
mmu_loop(); MMU2::mmu2.mmu_loop();
} }
#define DEFINE_PGM_READ_ANY(type, reader) \ #define DEFINE_PGM_READ_ANY(type, reader) \
@ -3469,15 +3470,14 @@ static T gcode_M600_filament_change_z_shift()
#else #else
return T(0); return T(0);
#endif #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(); st_synchronize();
float lastpos[4]; float lastpos[4];
prusa_statistics(22); prusa_statistics(22);
//First backup current position and settings //First backup current position and settings
int feedmultiplyBckp = feedmultiply; int feedmultiplyBckp = feedmultiply;
float HotendTempBckp = degTargetHotend(active_extruder); 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[Z_AXIS] = current_position[Z_AXIS];
lastpos[E_AXIS] = current_position[E_AXIS]; lastpos[E_AXIS] = current_position[E_AXIS];
//Retract E // Retract E
current_position[E_AXIS] += e_shift; current_position[E_AXIS] += e_shift;
plan_buffer_line_curposXYZE(FILAMENTCHANGE_RFEED); plan_buffer_line_curposXYZE(FILAMENTCHANGE_RFEED);
st_synchronize(); st_synchronize();
//Lift Z // Lift Z
current_position[Z_AXIS] += z_shift; current_position[Z_AXIS] += z_shift;
clamp_to_software_endstops(current_position); clamp_to_software_endstops(current_position);
plan_buffer_line_curposXYZE(FILAMENTCHANGE_ZFEED); plan_buffer_line_curposXYZE(FILAMENTCHANGE_ZFEED);
st_synchronize(); st_synchronize();
//Move XY to side // Move XY to side
current_position[X_AXIS] = x_position; current_position[X_AXIS] = x_position;
current_position[Y_AXIS] = y_position; current_position[Y_AXIS] = y_position;
plan_buffer_line_curposXYZE(FILAMENTCHANGE_XYFEED); plan_buffer_line_curposXYZE(FILAMENTCHANGE_XYFEED);
st_synchronize(); st_synchronize();
//Beep, manage nozzle heater and wait for user to start unload filament // Beep, manage nozzle heater and wait for user to start unload filament
if(!mmu_enabled) M600_wait_for_user(HotendTempBckp); if (!MMU2::mmu2.Enabled())
M600_wait_for_user(HotendTempBckp);
lcd_change_fil_state = 0; lcd_change_fil_state = 0;
// Unload filament // Unload filament
if (mmu_enabled) extr_unload(); //unload just current filament for multimaterial printers (used also in M702) if (MMU2::mmu2.Enabled())
else unload_filament(true); //unload filament for single material (used also in M702) MMU2::mmu2.unload(); // unload just current filament for multimaterial printers (used also in M702)
//finish moves else
st_synchronize(); unload_filament(true); // unload filament for single material (used also in M702)
st_synchronize(); // finish moves
#ifdef FILAMENT_SENSOR #ifdef FILAMENT_SENSOR
fsensor.setRunoutEnabled(false); //suppress filament runouts while loading filament. 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 //(FILAMENT_SENSOR_TYPE == FSENSOR_PAT9125)
#endif #endif
if (!mmu_enabled) if (!MMU2::mmu2.Enabled()) {
{
KEEPALIVE_STATE(PAUSED_FOR_USER); KEEPALIVE_STATE(PAUSED_FOR_USER);
lcd_change_fil_state = lcd_show_fullscreen_message_yes_no_and_wait_P( lcd_change_fil_state =
_i("Was filament unload successful?"), ////MSG_UNLOAD_SUCCESSFUL c=20 r=2 lcd_show_fullscreen_message_yes_no_and_wait_P(_i("Was filament unload successful?"), false, true); ////MSG_UNLOAD_SUCCESSFUL c=20 r=2
false, true); if (lcd_change_fil_state == 0) {
if (lcd_change_fil_state == 0)
{
lcd_clear(); lcd_clear();
lcd_puts_at_P(0, 2, _T(MSG_PLEASE_WAIT)); lcd_puts_at_P(0, 2, _T(MSG_PLEASE_WAIT));
current_position[X_AXIS] -= 100; 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 (!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 if (saved_printing)
mmu_M600_wait_and_beep(); 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) { if (saved_printing) {
lcd_clear(); lcd_clear();
lcd_puts_at_P(0, 2, _T(MSG_PLEASE_WAIT)); lcd_puts_at_P(0, 2, _T(MSG_PLEASE_WAIT));
mmu_command(MmuCmd::R0); //@@TODO mmu_command(MmuCmd::R0);
manage_response(false, false); // manage_response(false, false);
} }
} }
mmu_M600_load_filament(automatic, HotendTempBckp); //@@TODO mmu_M600_load_filament(automatic, HotendTempBckp);
} } else
else
M600_load_filament(); 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; fanSpeed = fanSpeedBckp;
//Feed a little of filament to stabilize pressure // Feed a little of filament to stabilize pressure
if (!automatic) if (!automatic) {
{
current_position[E_AXIS] += FILAMENTCHANGE_RECFEED; current_position[E_AXIS] += FILAMENTCHANGE_RECFEED;
plan_buffer_line_curposXYZE(FILAMENTCHANGE_EXFEED); plan_buffer_line_curposXYZE(FILAMENTCHANGE_EXFEED);
} }
//Move XY back // Move XY back
plan_buffer_line(lastpos[X_AXIS], lastpos[Y_AXIS], current_position[Z_AXIS], current_position[E_AXIS], plan_buffer_line(lastpos[X_AXIS], lastpos[Y_AXIS], current_position[Z_AXIS], current_position[E_AXIS], FILAMENTCHANGE_XYFEED, active_extruder);
FILAMENTCHANGE_XYFEED, active_extruder);
st_synchronize(); st_synchronize();
//Move Z back // Move Z back
plan_buffer_line(lastpos[X_AXIS], lastpos[Y_AXIS], lastpos[Z_AXIS], current_position[E_AXIS], plan_buffer_line(lastpos[X_AXIS], lastpos[Y_AXIS], lastpos[Z_AXIS], current_position[E_AXIS], FILAMENTCHANGE_ZFEED, active_extruder);
FILAMENTCHANGE_ZFEED, active_extruder);
st_synchronize(); st_synchronize();
//Set E position to original // Set E position to original
plan_set_e_position(lastpos[E_AXIS]); plan_set_e_position(lastpos[E_AXIS]);
memcpy(current_position, lastpos, sizeof(lastpos)); memcpy(current_position, lastpos, sizeof(lastpos));
set_destination_to_current(); set_destination_to_current();
//Recover feed rate // Recover feed rate
feedmultiply = feedmultiplyBckp; feedmultiply = feedmultiplyBckp;
char cmd[9]; char cmd[9];
sprintf_P(cmd, PSTR("M220 S%i"), feedmultiplyBckp); 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; custom_message_type = CustomMsg::Status;
} }
void gcode_M701() void gcode_M701(uint8_t mmuSlotIndex){
{ printf_P(PSTR("gcode_M701 begin\n"));
printf_P(PSTR("gcode_M701 begin\n"));
#ifdef FILAMENT_SENSOR #ifdef FILAMENT_SENSOR
fsensor.setRunoutEnabled(false); //suppress filament runouts while loading filament. fsensor.setRunoutEnabled(false); // suppress filament runouts while loading filament.
fsensor.setAutoLoadEnabled(false); //suppress filament autoloads while loading filament. fsensor.setAutoLoadEnabled(false); // suppress filament autoloads while loading filament.
#if (FILAMENT_SENSOR_TYPE == FSENSOR_PAT9125) #if (FILAMENT_SENSOR_TYPE == FSENSOR_PAT9125)
fsensor.setJamDetectionEnabled(false); //suppress filament jam detection while loading filament. fsensor.setJamDetectionEnabled(false); // suppress filament jam detection while loading filament.
#endif //(FILAMENT_SENSOR_TYPE == FSENSOR_PAT9125) #endif //(FILAMENT_SENSOR_TYPE == FSENSOR_PAT9125)
#endif #endif
prusa_statistics(22); prusa_statistics(22);
if (mmu_enabled)
{
extr_adj(tmp_extruder);//loads current extruder
mmu_extruder = tmp_extruder;
} }
else
{
enable_z();
custom_message_type = CustomMsg::FilamentLoading;
#ifdef FSENSOR_QUALITY if (MMU2::mmu2.Enabled() && mmuSlotIndex < MMU_FILAMENT_COUNT) {
fsensor_oq_meassure_start(40); MMU2::mmu2.load_filament(mmuSlotIndex); // loads current extruder
#endif //FSENSOR_QUALITY // 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; const int feed_mm_before_raising = 30;
static_assert(feed_mm_before_raising <= FILAMENTCHANGE_FIRSTFEED); static_assert(feed_mm_before_raising <= FILAMENTCHANGE_FIRSTFEED);
@ -3639,41 +3629,30 @@ void gcode_M701()
plan_buffer_line_curposXYZE(FILAMENTCHANGE_EFEED_FIRST); //fast sequence plan_buffer_line_curposXYZE(FILAMENTCHANGE_EFEED_FIRST); //fast sequence
st_synchronize(); 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; current_position[E_AXIS] += feed_mm_before_raising;
plan_buffer_line_curposXYZE(FILAMENTCHANGE_EFEED_FIRST); //fast sequence 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) { Sound_MakeCustom(50, 500, false);
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;
#ifdef FSENSOR_QUALITY if (!farm_mode && loading_flag) {
fsensor_oq_meassure_stop(); 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 #ifdef FILAMENT_SENSOR
fsensor.settings_init(); //restore filament runout state. fsensor.settings_init(); // restore filament runout state.
#endif #endif
} }
/** /**
@ -4263,7 +4242,7 @@ void process_commands()
} }
else if (code_seen_P(PSTR("MMURES"))) // PRUSA MMURES 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 else if (code_seen_P(PSTR("RESET"))) { // PRUSA RESET
#ifdef WATCHDOG #ifdef WATCHDOG
@ -7614,14 +7593,13 @@ Sigma_Exit:
{ {
// currently three different materials are needed (default, flex and PVA) // currently three different materials are needed (default, flex and PVA)
// add storing this information for different load/unload profiles etc. in the future // add storing this information for different load/unload profiles etc. in the future
// firmware does not wait for "ok" from mmu if (MMU2::mmu2.Enabled())
if (mmu_enabled)
{ {
uint8_t extruder = 255; uint8_t extruder = 255;
uint8_t filament = FILAMENT_UNDEFINED; uint8_t filament = FILAMENT_UNDEFINED;
if(code_seen('E')) extruder = code_value_uint8(); if(code_seen('E')) extruder = code_value_uint8();
if(code_seen('F')) filament = 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; break;
@ -7859,7 +7837,7 @@ Sigma_Exit:
#endif #endif
} }
if (mmu_enabled && code_seen_P(PSTR("AUTO"))) if (MMU2::mmu2.Enabled() && code_seen_P(PSTR("AUTO")))
automatic = true; automatic = true;
gcode_M600(automatic, x_position, y_position, z_shift, e_shift_init, e_shift_late); gcode_M600(automatic, x_position, y_position, z_shift, e_shift_init, e_shift_late);
@ -8509,9 +8487,10 @@ Sigma_Exit:
*/ */
case 701: case 701:
{ {
if (mmu_enabled && (code_seen('E') || code_seen('T'))) uint8_t mmuSlotIndex = 0xffU;
tmp_extruder = code_value_uint8(); if (MMU2::mmu2.Enabled() && code_seen('E'))
gcode_M701(); mmuSlotIndex = code_value_uint8();
gcode_M701(mmuSlotIndex);
} }
break; break;
@ -8528,10 +8507,10 @@ Sigma_Exit:
case 702: case 702:
{ {
if (code_seen('C')) { 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 { else {
if(mmu_enabled) extr_unload(); //! unload current filament if(MMU2::mmu2.Enabled()) MMU2::mmu2.unload(); //! unload current filament
else unload_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 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. @n Tc Load to nozzle after filament was prepared by Tc and extruder nozzle is already heated.
*/ */
else if(code_seen('T')) else if(code_seen('T')){
{ TCodes(strchr_pointer, code_value());
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);
}
}
}
} // end if(code_seen('T')) (end of T codes) } // end if(code_seen('T')) (end of T codes)
/*! /*!
#### End of T-Codes #### End of T-Codes
@ -9491,7 +9339,7 @@ void manage_inactivity(bool ignore_stepper_queue/*=false*/) //default argument s
} }
#endif #endif
check_axes_activity(); check_axes_activity();
mmu_loop(); MMU2::mmu2.mmu_loop();
// handle longpress // handle longpress
if(lcd_longpress_trigger) if(lcd_longpress_trigger)
@ -11422,10 +11270,12 @@ void M600_check_state(float nozzle_temp)
{ {
// Filament failed to load so load it again // Filament failed to load so load it again
case 2: case 2:
if (mmu_enabled) if (MMU2::mmu2.Enabled()){
mmu_M600_load_filament(false, nozzle_temp); //nonautomatic load; change to "wrong filament loaded" option? //@@TODO mmu_M600_load_filament(false, nozzle_temp); //nonautomatic load; change to "wrong filament loaded" option?
else
} else {
M600_load_filament_movements(); M600_load_filament_movements();
}
break; break;
// Filament loaded properly but color is not clear // Filament loaded properly but color is not clear

124
Firmware/Tcodes.cpp Normal file
View File

@ -0,0 +1,124 @@
#include "Tcodes.h"
#include "Marlin.h"
#include "mmu2.h"
#include "stepper.h"
#include <avr/pgmspace.h>
#include <ctype.h>
#include <stdint.h>
#include "language.h"
#include "messages.h"
#include "ultralcd.h"
#include <stdio.h>
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
}
}
}
}

5
Firmware/Tcodes.h Normal file
View File

@ -0,0 +1,5 @@
/// @file
#pragma once
#include <stdint.h>
void TCodes(char * const strchr_pointer, uint8_t codeValue);

View File

@ -8,7 +8,7 @@
#include "language.h" #include "language.h"
#include "Marlin.h" #include "Marlin.h"
#include "cmdqueue.h" #include "cmdqueue.h"
#include "mmu.h" #include "mmu2.h"
#include <avr/pgmspace.h> #include <avr/pgmspace.h>
//! @brief Wait for preheat //! @brief Wait for preheat
@ -34,7 +34,7 @@ void lay1cal_wait_preheat()
//! @param filament filament to use (applies for MMU only) //! @param filament filament to use (applies for MMU only)
void lay1cal_load_filament(char *cmd_buffer, uint8_t filament) void lay1cal_load_filament(char *cmd_buffer, uint8_t filament)
{ {
if (mmu_enabled) if (MMU2::mmu2.Enabled())
{ {
enquecommand_P(PSTR("M83")); enquecommand_P(PSTR("M83"));
enquecommand_P(PSTR("G1 Y-3.0 F1000.0")); enquecommand_P(PSTR("G1 Y-3.0 F1000.0"));
@ -73,7 +73,7 @@ void lay1cal_intro_line()
cmd_intro_mmu_12, 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) for (uint8_t i = 0; i < (sizeof(intro_mmu_cmd)/sizeof(intro_mmu_cmd[0])); ++i)
{ {

View File

@ -1,120 +0,0 @@
//! @file
#ifndef MMU_H
#define MMU_H
#include <inttypes.h>
#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<MmuCmd>(static_cast<uint8_t>(cmd) + filament );
}
inline uint8_t operator- (MmuCmd cmda, MmuCmd cmdb)
{
return (static_cast<uint8_t>(cmda) - static_cast<uint8_t>(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

711
Firmware/mmu2.cpp Normal file
View File

@ -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

204
Firmware/mmu2.h Normal file
View File

@ -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

View File

@ -0,0 +1,97 @@
/// @file error_codes.h
#pragma once
#include <stdint.h>
/// 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
};

View File

@ -0,0 +1,42 @@
/// @file progress_codes.h
#pragma once
#include <stdint.h>
/// 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
};

View File

@ -0,0 +1,6 @@
#include "mmu2_error_converter.h"
namespace MMU2 {
// @@TODO
void TranslateErr(uint16_t ec, char *dst, size_t dstSize) { }
}

View File

@ -0,0 +1,7 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
namespace MMU2 {
void TranslateErr(uint16_t ec, char *dst, size_t dstSize);
}

14
Firmware/mmu2_fsensor.cpp Normal file
View File

@ -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

24
Firmware/mmu2_fsensor.h Normal file
View File

@ -0,0 +1,24 @@
#pragma once
#include <stdint.h>
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

23
Firmware/mmu2_log.h Normal file
View File

@ -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

12
Firmware/mmu2_power.cpp Normal file
View File

@ -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

11
Firmware/mmu2_power.h Normal file
View File

@ -0,0 +1,11 @@
#pragma once
namespace MMU2 {
void power_on();
void power_off();
void reset();
} // namespace MMU2

View File

@ -0,0 +1,6 @@
#include "mmu2_progress_converter.h"
namespace MMU2 {
//@@TODO
void TranslateProgress(uint16_t pc, char *dst, size_t dstSize) { }
}

View File

@ -0,0 +1,7 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
namespace MMU2 {
void TranslateProgress(uint16_t pc, char *dst, size_t dstSize);
}

247
Firmware/mmu2_protocol.cpp Normal file
View File

@ -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]
// <OID> P[0-9] : command being processed i.e. operation running, may contain a state number
// <OID> E[0-9][0-9] : error 1-9 while doing a tool change
// <OID> 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

184
Firmware/mmu2_protocol.h Normal file
View File

@ -0,0 +1,184 @@
/// @file protocol.h
#pragma once
#include <stdint.h>
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;

View File

@ -0,0 +1,564 @@
#include "mmu2_protocol_logic.h"
#include "mmu2_log.h"
#include "mmu2_fsensor.h"
#include "system_timer.h"
#include <string.h>
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<ProgressCode>(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<ErrorCode>(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

View File

@ -0,0 +1,314 @@
#pragma once
#include <stdint.h>
// #include <array> //@@TODO Don't we have STL for AVR somewhere?
template<typename T, uint8_t N>
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<uint8_t, 16> 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

View File

@ -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

53
Firmware/mmu2_reporting.h Normal file
View File

@ -0,0 +1,53 @@
/// @file mmu2_reporting.h
#pragma once
#include <stdint.h>
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

15
Firmware/mmu2_serial.cpp Normal file
View File

@ -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

21
Firmware/mmu2_serial.h Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
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

View File

@ -38,7 +38,7 @@
#include "Filament_sensor.h" #include "Filament_sensor.h"
#include "mmu.h" #include "mmu2.h"
#include "ConfigurationStore.h" #include "ConfigurationStore.h"
#include "Prusa_farm.h" #include "Prusa_farm.h"

5
Firmware/strlen_cx.h Normal file
View File

@ -0,0 +1,5 @@
#pragma once
constexpr inline int strlen_constexpr(const char* str){
return *str ? 1 + strlen_constexpr(str + 1) : 0;
}

View File

@ -36,7 +36,7 @@
#include "sound.h" #include "sound.h"
#include "mmu.h" #include "mmu2.h"
#include "static_assert.h" #include "static_assert.h"
#include "first_lay_cal.h" #include "first_lay_cal.h"
@ -450,19 +450,20 @@ void lcdui_print_percent_done(void)
} }
// Print extruder status (5 chars total) // Print extruder status (5 chars total)
void lcdui_print_extruder(void) void lcdui_print_extruder(void) {
{ uint8_t chars = 0;
int chars = 0; // @@TODO if (MMU2::mmu2.get_current_tool() == tmp_extruder) {
if (mmu_extruder == tmp_extruder) { // if (MMU2::mmu2.get_current_tool() == MMU2::FILAMENT_UNKNOWN)
if (mmu_extruder == MMU_FILAMENT_UNKNOWN) chars = lcd_printf_P(_N(" F?")); // chars = lcd_printf_P(_N(" F?"));
else chars = lcd_printf_P(_N(" F%u"), mmu_extruder + 1); // else
} // chars = lcd_printf_P(_N(" F%u"), MMU2::mmu2.get_current_tool() + 1);
else // } else {
{ // if (MMU2::mmu2.get_current_tool() == MMU2::FILAMENT_UNKNOWN)
if (mmu_extruder == MMU_FILAMENT_UNKNOWN) chars = lcd_printf_P(_N(" ?>%u"), tmp_extruder + 1); // chars = lcd_printf_P(_N(" ?>%u"), tmp_extruder + 1);
else chars = lcd_printf_P(_N(" %u>%u"), mmu_extruder + 1, tmp_extruder + 1); // else
} // chars = lcd_printf_P(_N(" %u>%u"), MMU2::mmu2.get_current_tool() + 1, tmp_extruder + 1);
lcd_space(5 - chars); // }
lcd_space(5 - chars);
} }
// Print farm number (5 chars total) // Print farm number (5 chars total)
@ -719,7 +720,7 @@ void lcdui_print_status_screen(void)
//Print SD status (7 chars) //Print SD status (7 chars)
lcdui_print_percent_done(); lcdui_print_percent_done();
if (mmu_enabled) if (MMU2::mmu2.Enabled())
//Print extruder status (5 chars) //Print extruder status (5 chars)
lcdui_print_extruder(); lcdui_print_extruder();
else if (farm_mode) else if (farm_mode)
@ -945,7 +946,7 @@ void lcd_commands()
enquecommand_P(PSTR("M140 S0")); // turn off heatbed enquecommand_P(PSTR("M140 S0")); // turn off heatbed
enquecommand_P(PSTR("G1 Z10 F1300.000")); //lift Z enquecommand_P(PSTR("G1 Z10 F1300.000")); //lift Z
enquecommand_P(PSTR("G1 X10 Y180 F4000")); //Go to parking position 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 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 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; 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. //! @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() static void lcd_menu_fails_stats_mmu_total()
{ {
mmu_command(MmuCmd::S3); // @@TODO mmu_command(MmuCmd::S3);
lcd_timeoutToStatus.stop(); //infinite timeout lcd_timeoutToStatus.stop(); //infinite timeout
lcd_home(); lcd_home();
lcd_printf_P(PSTR("%S\n" " %-16.16S%-3d\n" " %-16.16S%-3d\n" " %-16.16S%-3d"), // lcd_printf_P(PSTR("%S\n" " %-16.16S%-3d\n" " %-16.16S%-3d\n" " %-16.16S%-3d"),
_T(MSG_TOTAL_FAILURES), // _T(MSG_TOTAL_FAILURES),
_T(MSG_MMU_FAILS), clamp999( eeprom_read_word((uint16_t*)EEPROM_MMU_FAIL_TOT) ), // _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) ), // _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 // _i("MMU power fails"), clamp999( mmu_power_failures )); ////MSG_MMU_POWER_FAILS c=15
menu_back_if_clicked_fb(); menu_back_if_clicked_fb();
} }
@ -1680,13 +1681,15 @@ static void lcd_support_menu()
#endif // IR_SENSOR_ANALOG #endif // IR_SENSOR_ANALOG
MENU_ITEM_BACK_P(STR_SEPARATOR); 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(_i("MMU2 connected")); ////MSG_MMU_CONNECTED c=18
MENU_ITEM_BACK_P(PSTR(" FW:")); ////c=17 MENU_ITEM_BACK_P(PSTR(" FW:")); ////c=17
if (((menu_item - 1) == menu_line) && lcd_draw_update) if (((menu_item - 1) == menu_line) && lcd_draw_update)
{ {
lcd_set_cursor(6, menu_row); lcd_set_cursor(6, menu_row);
uint8_t mmu_version = 200; // @@TODO
uint8_t mmu_buildnr = 0;
if ((mmu_version > 0) && (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); lcd_printf_P(PSTR("%d.%d.%d-%d"), mmu_version/100, mmu_version%100/10, mmu_version%10, mmu_buildnr);
else else
@ -1919,7 +1922,7 @@ void mFilamentItem(uint16_t nTemp, uint16_t nTempBed)
nLevel = bFilamentPreheatState ? 1 : 2; nLevel = bFilamentPreheatState ? 1 : 2;
bFilamentAction = true; bFilamentAction = true;
menu_back(nLevel); menu_back(nLevel);
extr_unload(); MMU2::mmu2.unload();
break; break;
case FilamentAction::MmuEject: case FilamentAction::MmuEject:
nLevel = bFilamentPreheatState ? 1 : 2; nLevel = bFilamentPreheatState ? 1 : 2;
@ -3430,15 +3433,15 @@ static void lcd_show_sensors_state()
uint8_t idler_state = STATE_NA; uint8_t idler_state = STATE_NA;
pinda_state = READ(Z_MIN_PIN); 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_puts_at_P(0, 0, MSG_PINDA);
lcd_set_cursor(LCD_WIDTH - 14, 0); lcd_set_cursor(LCD_WIDTH - 14, 0);
lcd_print_state(pinda_state); 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_puts_at_P(10, 0, _n("FINDA"));////MSG_FINDA c=5
lcd_set_cursor(LCD_WIDTH - 3, 0); lcd_set_cursor(LCD_WIDTH - 3, 0);
@ -3775,7 +3778,7 @@ void lcd_first_layer_calibration_reset()
void lcd_v2_calibration() void lcd_v2_calibration()
{ {
if (mmu_enabled) if (MMU2::mmu2.Enabled())
{ {
const uint8_t filament = choose_menu_P( const uint8_t filament = choose_menu_P(
_T(MSG_SELECT_FILAMENT), _T(MSG_SELECT_FILAMENT),
@ -3882,22 +3885,19 @@ static void wait_preheat()
} }
static void lcd_wizard_load() static void lcd_wizard_load() {
{ if (MMU2::mmu2.Enabled()) {
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
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 {
tmp_extruder = 0; 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
else }
{ lcd_update_enable(false);
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_clear();
} lcd_puts_at_P(0, 2, _T(MSG_LOADING_FILAMENT));
lcd_update_enable(false); loading_flag = true;
lcd_clear(); gcode_M701(0);
lcd_puts_at_P(0, 2, _T(MSG_LOADING_FILAMENT));
loading_flag = true;
gcode_M701();
} }
bool lcd_autoDepleteEnabled() bool lcd_autoDepleteEnabled()
@ -3913,7 +3913,7 @@ static void wizard_lay1cal_message(bool cold)
{ {
lcd_show_fullscreen_message_and_wait_P( 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 _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( 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 _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 //start to preheat nozzle and bed to save some time later
setTargetHotend(PLA_PREHEAT_HOTEND_TEMP, 0); setTargetHotend(PLA_PREHEAT_HOTEND_TEMP, 0);
setTargetBed(PLA_PREHEAT_HPB_TEMP); 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); wizard_event = lcd_show_fullscreen_message_yes_no_and_wait_P(_T(MSG_FILAMENT_LOADED), true);
} else } else
@ -4066,7 +4066,7 @@ void lcd_wizard(WizState state)
if (wizard_event) state = S::Lay1CalCold; if (wizard_event) state = S::Lay1CalCold;
else else
{ {
if(mmu_enabled) state = S::LoadFilCold; if(MMU2::mmu2.Enabled()) state = S::LoadFilCold;
else state = S::Preheat; else state = S::Preheat;
} }
break; break;
@ -4256,7 +4256,7 @@ static void auto_deplete_switch()
static void settingsAutoDeplete() static void settingsAutoDeplete()
{ {
if (mmu_enabled) if (MMU2::mmu2.Enabled())
{ {
#ifdef FILAMENT_SENSOR #ifdef FILAMENT_SENSOR
if (fsensor.isError()) { if (fsensor.isError()) {
@ -4280,7 +4280,7 @@ while(0)\
#ifdef MMU_HAS_CUTTER #ifdef MMU_HAS_CUTTER
static void settingsCutter() 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)) 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 \ #define SETTINGS_MMU_MODE \
do\ 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);\ 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);\ 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) #if defined(FILAMENT_SENSOR) && (FILAMENT_SENSOR_TYPE == FSENSOR_IR_ANALOG)
//! Fsensor Detection isn't ready for mmu yet it is temporarily disabled. //! 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 //! @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 #endif //IR_SENSOR_ANALOG
if (_md->experimental_menu_visibility) 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. //! Create list of items with header. Header can not be selected.
//! Each item has text description passed by function parameter and //! 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. //! 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. //! 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) 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 //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; const uint8_t item_len = item?strlen_P(item):0;
int8_t first = 0; int8_t first = 0;
int8_t enc_dif = lcd_encoder_diff; 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_BEGIN();
MENU_ITEM_BACK_P(_T(MSG_MAIN)); 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++) 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(); MENU_END();
} }
static void mmu_load_to_nozzle_menu() static inline void lcd_mmu_load_to_nozzle_wrapper(uint8_t index){
{ MMU2::mmu2.load_filament_to_nozzle(index);
if (bFilamentAction) }
{
static void mmu_load_to_nozzle_menu() {
if (bFilamentAction) {
MENU_BEGIN(); MENU_BEGIN();
MENU_ITEM_BACK_P(_T(MSG_MAIN)); MENU_ITEM_BACK_P(_T(MSG_MAIN));
for (uint8_t i = 0; i < MMU_FILAMENT_COUNT; i++) 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(); MENU_END();
} } else {
else
{
eFilamentAction = FilamentAction::MmuLoad; eFilamentAction = FilamentAction::MmuLoad;
preheat_or_continue(); preheat_or_continue();
} }
} }
static void mmu_eject_filament(uint8_t filament) static void mmu_eject_filament(uint8_t filament) {
{
menu_back(); menu_back();
mmu_eject_filament(filament, true); MMU2::mmu2.eject_filament(filament, true);
} }
static void mmu_fil_eject_menu() static void mmu_fil_eject_menu() {
{ if (bFilamentAction) {
if (bFilamentAction)
{
MENU_BEGIN(); MENU_BEGIN();
MENU_ITEM_BACK_P(_T(MSG_MAIN)); MENU_ITEM_BACK_P(_T(MSG_MAIN));
for (uint8_t i = 0; i < MMU_FILAMENT_COUNT; i++) 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_ITEM_FUNCTION_NR_P(_T(MSG_EJECT_FILAMENT), i + '1', mmu_eject_filament, i); ////MSG_EJECT_FILAMENT c=16
MENU_END(); MENU_END();
} } else {
else
{
eFilamentAction = FilamentAction::MmuEject; eFilamentAction = FilamentAction::MmuEject;
preheat_or_continue(); preheat_or_continue();
} }
} }
#ifdef MMU_HAS_CUTTER #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() static void mmu_cut_filament_menu() {
{ if (bFilamentAction) {
if(bFilamentAction)
{
MENU_BEGIN(); MENU_BEGIN();
MENU_ITEM_BACK_P(_T(MSG_MAIN)); MENU_ITEM_BACK_P(_T(MSG_MAIN));
for (uint8_t i = 0; i < MMU_FILAMENT_COUNT; i++) 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(); MENU_END();
} }
else else
@ -5522,7 +5525,7 @@ static void lcd_main_menu()
} }
if ( ! ( IS_SD_PRINTING || usb_timer.running() || (lcd_commands_type == LcdCommands::Layer1Cal) ) ) { 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(_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(_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); MENU_ITEM_SUBMENU_P(_T(MSG_UNLOAD_FILAMENT), mmu_unload_filament);
@ -5532,7 +5535,7 @@ static void lcd_main_menu()
#endif //MMU_HAS_CUTTER #endif //MMU_HAS_CUTTER
} else { } else {
#ifdef FILAMENT_SENSOR #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 MENU_ITEM_SUBMENU_P(_i("AutoLoad filament"), lcd_menu_AutoLoadFilament);////MSG_AUTOLOAD_FILAMENT c=18
} }
else else
@ -5553,7 +5556,7 @@ static void lcd_main_menu()
#if defined(TMC2130) || defined(FILAMENT_SENSOR) #if defined(TMC2130) || defined(FILAMENT_SENSOR)
MENU_ITEM_SUBMENU_P(_i("Fail stats"), lcd_menu_fails_stats);////MSG_FAIL_STATS c=18 MENU_ITEM_SUBMENU_P(_i("Fail stats"), lcd_menu_fails_stats);////MSG_FAIL_STATS c=18
#endif #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("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 MENU_ITEM_SUBMENU_P(_i("Support"), lcd_support_menu);////MSG_SUPPORT c=18
@ -5902,7 +5905,7 @@ void print_stop()
fanSpeed = 0; fanSpeed = 0;
} }
if (mmu_enabled) extr_unload(); //M702 C if (MMU2::mmu2.Enabled()) MMU2::mmu2.unload(); //M702 C
finishAndDisableSteppers(); //M84 finishAndDisableSteppers(); //M84
axis_relative_modes = E_AXIS_MASK; //XYZ absolute, E relative 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 //! 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 //! @todo Don't forget to remove this as soon Fsensor Detection works with mmu
if(fsensor.getSensorRevision() == IR_sensor_analog::SensorRevision::_Undef) { if(fsensor.getSensorRevision() == IR_sensor_analog::SensorRevision::_Undef) {
if (!mmu_enabled) { if (!MMU2::mmu2.Enabled()) {
lcd_detect_IRsensor(); lcd_detect_IRsensor();
} }
else { else {
@ -6424,7 +6427,7 @@ bool lcd_selftest()
if (_result) if (_result)
{ {
#if (FILAMENT_SENSOR_TYPE == FSENSOR_IR) || (FILAMENT_SENSOR_TYPE == FSENSOR_IR_ANALOG) #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 _progress = lcd_selftest_screen(TestScreen::Fsensor, _progress, 3, true, 2000); //check filaments sensor
_result = selftest_irsensor(); _result = selftest_irsensor();
@ -7048,19 +7051,18 @@ static bool selftest_irsensor()
{ {
TempBackup tempBackup; TempBackup tempBackup;
setTargetHotend(ABS_PREHEAT_HOTEND_TEMP,active_extruder); 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); 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); progress = lcd_selftest_screen(TestScreen::Fsensor, progress, 1, true, 0);
mmu_command(MmuCmd::U0); MMU2::mmu2.unload(); // mmu_command(MmuCmd::U0); manage_response(false, false);
manage_response(false, false);
for(uint_least8_t i = 0; i < 200; ++i) for(uint_least8_t i = 0; i < 200; ++i)
{ {
if (0 == (i % 32)) progress = lcd_selftest_screen(TestScreen::Fsensor, progress, 1, true, 0); 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()) while (blocks_queued())
{ {
if (fsensor.getFilamentPresent()) if (fsensor.getFilamentPresent())