1
0
mirror of https://github.com/MarlinFirmware/Marlin.git synced 2024-11-30 07:17:59 +00:00

Support dual x-carriage printers

Dual x-carriage designs offer some substantial improvements for dual
extruder printing.
This commit is contained in:
Robert F-C 2013-07-17 22:44:45 +10:00
parent 5ab872de1f
commit d7390e13d9
5 changed files with 242 additions and 25 deletions

View File

@ -146,6 +146,36 @@
#define EXTRUDERS 1 #define EXTRUDERS 1
#endif #endif
// Enable this for dual x-carriage printers.
// A dual x-carriage design has the advantage that the inactive extruder can be parked which
// prevents hot-end ooze contaminating the print. It also reduces the weight of each x-carriage
// allowing faster printing speeds.
#define DUAL_X_CARRIAGE
#ifdef DUAL_X_CARRIAGE
// Configuration for second X-carriage
// Note: the first x-carriage is defined as the x-carriage which homes to the minimum endstop;
// the second x-carriage always homes to the maximum endstop.
#define X2_MIN_POS 88 // set minimum to ensure second x-carriage doesn't hit the parked first X-carriage
#define X2_MAX_POS 350.45 // set maximum to the distance between toolheads when both heads are homed
#define X2_HOME_DIR 1 // the second X-carriage always homes to the maximum endstop position
#define X2_HOME_POS X2_MAX_POS // default home position is the maximum carriage position
// However: In this mode the EXTRUDER_OFFSET_X value for the second extruder provides a software
// override for X2_HOME_POS. This also allow recalibration of the distance between the two endstops
// without modifying the firmware (through the "M218 T1 X???" command).
// Remember: you should set the second extruder x-offset to 0 in your slicer.
// Pins for second x-carriage stepper driver (defined here to avoid further complicating pins.h)
#define X2_ENABLE_PIN 29
#define X2_STEP_PIN 25
#define X2_DIR_PIN 23
// The following settings control the behaviour of the automatic parking and unparking of inactive extruder
#define TOOLCHANGE_PARK_ZLIFT 0.1 // the distance to raise Z axis when parking an extruder
#define TOOLCHANGE_UNPARK_ZLIFT 1 // the distance to raise Z axis when unparking an extruder
#define TOOLCHANGE_UNPARK_SKIP_TRAVEL_MOVES // disable if slicer natively suports dual x-carriage mode.
// When enabled this avoids unnecessary & inadvertant moves from the last position of old extruder.
#endif // DUAL_X_CARRIAGE
//homing hits the endstop, then retracts by this distance, before it tries to slowly bump again: //homing hits the endstop, then retracts by this distance, before it tries to slowly bump again:
#define X_HOME_RETRACT_MM 5 #define X_HOME_RETRACT_MM 5
#define Y_HOME_RETRACT_MM 5 #define Y_HOME_RETRACT_MM 5

View File

@ -96,7 +96,11 @@ void process_commands();
void manage_inactivity(); void manage_inactivity();
#if defined(X_ENABLE_PIN) && X_ENABLE_PIN > -1 #if defined(DUAL_X_CARRIAGE) && defined(X_ENABLE_PIN) && X_ENABLE_PIN > -1 \
&& defined(X2_ENABLE_PIN) && X2_ENABLE_PIN > -1
#define enable_x() do { WRITE(X_ENABLE_PIN, X_ENABLE_ON); WRITE(X2_ENABLE_PIN, X_ENABLE_ON); } while (0)
#define disable_x() do { WRITE(X_ENABLE_PIN,!X_ENABLE_ON); WRITE(X2_ENABLE_PIN,!X_ENABLE_ON); } while (0)
#elif defined(X_ENABLE_PIN) && X_ENABLE_PIN > -1
#define enable_x() WRITE(X_ENABLE_PIN, X_ENABLE_ON) #define enable_x() WRITE(X_ENABLE_PIN, X_ENABLE_ON)
#define disable_x() WRITE(X_ENABLE_PIN,!X_ENABLE_ON) #define disable_x() WRITE(X_ENABLE_PIN,!X_ENABLE_ON)
#else #else

View File

@ -677,7 +677,46 @@ XYZ_CONSTS_FROM_CONFIG(float, max_length, MAX_LENGTH);
XYZ_CONSTS_FROM_CONFIG(float, home_retract_mm, HOME_RETRACT_MM); XYZ_CONSTS_FROM_CONFIG(float, home_retract_mm, HOME_RETRACT_MM);
XYZ_CONSTS_FROM_CONFIG(signed char, home_dir, HOME_DIR); XYZ_CONSTS_FROM_CONFIG(signed char, home_dir, HOME_DIR);
#ifdef DUAL_X_CARRIAGE
#if EXTRUDERS == 1 || defined(COREXY) \
|| !defined(X2_ENABLE_PIN) || !defined(X2_STEP_PIN) || !defined(X2_DIR_PIN) \
|| !defined(X2_HOME_POS) || !defined(X2_MIN_POS) || !defined(X2_MAX_POS) \
|| !defined(X_MAX_PIN) || X_MAX_PIN < 0
#error "Missing or invalid definitions for DUAL_X_CARRIAGE mode."
#endif
#if X_HOME_DIR != -1 || X2_HOME_DIR != 1
#error "Please use canonical x-carriage assignment" // the x-carriages are defined by their homing directions
#endif
static float x_home_pos(int extruder) {
if (extruder == 0)
return base_home_pos(X_AXIS) + add_homeing[X_AXIS];
else
// In dual carriage mode the extruder offset provides an override of the
// second X-carriage offset when homed - otherwise X2_HOME_POS is used.
// This allow soft recalibration of the second extruder offset position without firmware reflash
// (through the M218 command).
return (extruder_offset[X_AXIS][1] != 0) ? extruder_offset[X_AXIS][1] : X2_HOME_POS;
}
static int x_home_dir(int extruder) {
return (extruder == 0) ? X_HOME_DIR : X2_HOME_DIR;
}
static bool active_extruder_parked = false;
static float raised_parked_position[NUM_AXIS];
static unsigned long delayed_move_time = 0;
#endif
static void axis_is_at_home(int axis) { static void axis_is_at_home(int axis) {
#ifdef DUAL_X_CARRIAGE
if (axis == X_AXIS && active_extruder != 0) {
current_position[X_AXIS] = x_home_pos(active_extruder);
min_pos[X_AXIS] = X2_MIN_POS;
max_pos[X_AXIS] = X2_MAX_POS;
return;
}
#endif
current_position[axis] = base_home_pos(axis) + add_homeing[axis]; current_position[axis] = base_home_pos(axis) + add_homeing[axis];
min_pos[axis] = base_min_pos(axis) + add_homeing[axis]; min_pos[axis] = base_min_pos(axis) + add_homeing[axis];
max_pos[axis] = base_max_pos(axis) + add_homeing[axis]; max_pos[axis] = base_max_pos(axis) + add_homeing[axis];
@ -686,10 +725,16 @@ static void axis_is_at_home(int axis) {
static void homeaxis(int axis) { static void homeaxis(int axis) {
#define HOMEAXIS_DO(LETTER) \ #define HOMEAXIS_DO(LETTER) \
((LETTER##_MIN_PIN > -1 && LETTER##_HOME_DIR==-1) || (LETTER##_MAX_PIN > -1 && LETTER##_HOME_DIR==1)) ((LETTER##_MIN_PIN > -1 && LETTER##_HOME_DIR==-1) || (LETTER##_MAX_PIN > -1 && LETTER##_HOME_DIR==1))
if (axis==X_AXIS ? HOMEAXIS_DO(X) : if (axis==X_AXIS ? HOMEAXIS_DO(X) :
axis==Y_AXIS ? HOMEAXIS_DO(Y) : axis==Y_AXIS ? HOMEAXIS_DO(Y) :
axis==Z_AXIS ? HOMEAXIS_DO(Z) : axis==Z_AXIS ? HOMEAXIS_DO(Z) :
0) { 0) {
int axis_home_dir = home_dir(axis);
#ifdef DUAL_X_CARRIAGE
if (axis == X_AXIS)
axis_home_dir = x_home_dir(active_extruder);
#endif
// Engage Servo endstop if enabled // Engage Servo endstop if enabled
#ifdef SERVO_ENDSTOPS #ifdef SERVO_ENDSTOPS
@ -864,8 +909,14 @@ void process_commands()
{ {
current_position[X_AXIS] = 0;current_position[Y_AXIS] = 0; current_position[X_AXIS] = 0;current_position[Y_AXIS] = 0;
#ifdef DUAL_X_CARRIAGE
int x_axis_home_dir = home_dir(X_AXIS);
#else
int x_axis_home_dir = x_home_dir(active_extruder);
#endif
plan_set_position(current_position[X_AXIS], current_position[Y_AXIS], current_position[Z_AXIS], current_position[E_AXIS]); plan_set_position(current_position[X_AXIS], current_position[Y_AXIS], current_position[Z_AXIS], current_position[E_AXIS]);
destination[X_AXIS] = 1.5 * X_MAX_LENGTH * X_HOME_DIR;destination[Y_AXIS] = 1.5 * Y_MAX_LENGTH * Y_HOME_DIR; destination[X_AXIS] = 1.5 * max_length(X_AXIS) * x_axis_home_dir;destination[Y_AXIS] = 1.5 * max_length(Y_AXIS) * home_dir(Y_AXIS);
feedrate = homing_feedrate[X_AXIS]; feedrate = homing_feedrate[X_AXIS];
if(homing_feedrate[Y_AXIS]<feedrate) if(homing_feedrate[Y_AXIS]<feedrate)
feedrate =homing_feedrate[Y_AXIS]; feedrate =homing_feedrate[Y_AXIS];
@ -890,6 +941,14 @@ void process_commands()
if((home_all_axis) || (code_seen(axis_codes[X_AXIS]))) if((home_all_axis) || (code_seen(axis_codes[X_AXIS])))
{ {
#ifdef DUAL_X_CARRIAGE
int tmp_extruder = active_extruder;
active_extruder = !active_extruder;
HOMEAXIS(X);
active_extruder = tmp_extruder;
active_extruder_parked = false;
delayed_move_time = 0;
#endif
HOMEAXIS(X); HOMEAXIS(X);
} }
@ -922,7 +981,7 @@ void process_commands()
} }
} }
plan_set_position(current_position[X_AXIS], current_position[Y_AXIS], current_position[Z_AXIS], current_position[E_AXIS]); plan_set_position(current_position[X_AXIS], current_position[Y_AXIS], current_position[Z_AXIS], current_position[E_AXIS]);
#endif // DELTA #endif // else DELTA
#ifdef ENDSTOPS_ONLY_FOR_HOMING #ifdef ENDSTOPS_ONLY_FOR_HOMING
enable_endstops(false); enable_endstops(false);
@ -2001,6 +2060,36 @@ void process_commands()
if(tmp_extruder != active_extruder) { if(tmp_extruder != active_extruder) {
// Save current position to return to after applying extruder offset // Save current position to return to after applying extruder offset
memcpy(destination, current_position, sizeof(destination)); memcpy(destination, current_position, sizeof(destination));
#ifdef DUAL_X_CARRIAGE
if (Stopped == false && delayed_move_time == 0 && current_position[X_AXIS] != x_home_pos(active_extruder))
{
// Park old head: 1) raise 2) move to park position 3) lower
plan_buffer_line(current_position[X_AXIS], current_position[Y_AXIS], current_position[Z_AXIS] + TOOLCHANGE_PARK_ZLIFT,
current_position[E_AXIS], max_feedrate[Z_AXIS], active_extruder);
plan_buffer_line(x_home_pos(active_extruder), current_position[Y_AXIS], current_position[Z_AXIS] + TOOLCHANGE_PARK_ZLIFT,
current_position[E_AXIS], max_feedrate[X_AXIS], active_extruder);
plan_buffer_line(x_home_pos(active_extruder), current_position[Y_AXIS], current_position[Z_AXIS],
current_position[E_AXIS], max_feedrate[Z_AXIS], active_extruder);
st_synchronize();
}
// only apply Y extruder offset in dual x carriage mode (x offset is already used in determining home pos)
current_position[Y_AXIS] = current_position[Y_AXIS] -
extruder_offset[Y_AXIS][active_extruder] +
extruder_offset[Y_AXIS][tmp_extruder];
active_extruder = tmp_extruder;
// Inactive head always starts at its parked position.
axis_is_at_home(X_AXIS);
// record raised toolhead position for use by unpark
memcpy(raised_parked_position, current_position, sizeof(raised_parked_position));
raised_parked_position[Z_AXIS] += TOOLCHANGE_UNPARK_ZLIFT;
active_extruder_parked = true;
delayed_move_time = 0;
#else
// Offset extruder (only by XY) // Offset extruder (only by XY)
int i; int i;
for(i = 0; i < 2; i++) { for(i = 0; i < 2; i++) {
@ -2010,6 +2099,7 @@ void process_commands()
} }
// Set the new active extruder and position // Set the new active extruder and position
active_extruder = tmp_extruder; active_extruder = tmp_extruder;
#endif //else DUAL_X_CARRIAGE
plan_set_position(current_position[X_AXIS], current_position[Y_AXIS], current_position[Z_AXIS], current_position[E_AXIS]); plan_set_position(current_position[X_AXIS], current_position[Y_AXIS], current_position[Z_AXIS], current_position[E_AXIS]);
// Move to the old position if 'F' was in the parameters // Move to the old position if 'F' was in the parameters
if(make_move && Stopped == false) { if(make_move && Stopped == false) {
@ -2204,6 +2294,40 @@ void prepare_move()
active_extruder); active_extruder);
} }
#else #else
#if defined(DUAL_X_CARRIAGE)
if (active_extruder_parked)
{
if (current_position[E_AXIS] == destination[E_AXIS])
{
// this is a travel move
#ifdef TOOLCHANGE_UNPARK_SKIP_TRAVEL_MOVES
if (delayed_move_time != 0xFFFFFFFFUL)
{
// skip this move but still update current_position in main so that it can
// be used as starting position before extrusion (but not in planner)
memcpy(current_position, destination, sizeof(current_position));
if (destination[Z_AXIS] > raised_parked_position[Z_AXIS])
raised_parked_position[Z_AXIS] = destination[Z_AXIS];
delayed_move_time = millis();
return;
}
delayed_move_time = 0;
#else
// this will cause the unpark code below to execute the specified lift in moving to the initial (travel move) position.
memcpy(current_position, destination, sizeof(current_position));
#endif
}
// unpark extruder: 1) raise, 2) move into starting XY position, 3) lower
plan_buffer_line(raised_parked_position[X_AXIS], raised_parked_position[Y_AXIS], raised_parked_position[Z_AXIS], current_position[E_AXIS], max_feedrate[Z_AXIS], active_extruder);
plan_buffer_line(current_position[X_AXIS], current_position[Y_AXIS], raised_parked_position[Z_AXIS],
current_position[E_AXIS], min(max_feedrate[X_AXIS],max_feedrate[Y_AXIS]), active_extruder);
plan_buffer_line(current_position[X_AXIS], current_position[Y_AXIS], current_position[Z_AXIS],
current_position[E_AXIS], max_feedrate[Z_AXIS], active_extruder);
active_extruder_parked = false;
}
#endif //DUAL_X_CARRIAGE
// Do not use feedmultiply for E or Z only moves // Do not use feedmultiply for E or Z only moves
if( (current_position[X_AXIS] == destination [X_AXIS]) && (current_position[Y_AXIS] == destination [Y_AXIS])) { if( (current_position[X_AXIS] == destination [X_AXIS]) && (current_position[Y_AXIS] == destination [Y_AXIS])) {
plan_buffer_line(destination[X_AXIS], destination[Y_AXIS], destination[Z_AXIS], destination[E_AXIS], feedrate/60, active_extruder); plan_buffer_line(destination[X_AXIS], destination[Y_AXIS], destination[Z_AXIS], destination[E_AXIS], feedrate/60, active_extruder);
@ -2254,6 +2378,9 @@ void controllerFan()
|| !READ(E2_ENABLE_PIN) || !READ(E2_ENABLE_PIN)
#endif #endif
#if EXTRUDER > 1 #if EXTRUDER > 1
#if defined(X2_ENABLE_PIN) && X2_ENABLE_PIN > -1
|| !READ(X2_ENABLE_PIN)
#endif
|| !READ(E1_ENABLE_PIN) || !READ(E1_ENABLE_PIN)
#endif #endif
|| !READ(E0_ENABLE_PIN)) //If any of the drivers are enabled... || !READ(E0_ENABLE_PIN)) //If any of the drivers are enabled...
@ -2320,6 +2447,16 @@ void manage_inactivity()
WRITE(E0_ENABLE_PIN,oldstatus); WRITE(E0_ENABLE_PIN,oldstatus);
} }
#endif #endif
#if defined(DUAL_X_CARRIAGE) && defined(TOOLCHANGE_UNPARK_SKIP_TRAVEL_MOVES)
// handle delayed move timeout
if (delayed_move_time != 0 && (millis() - delayed_move_time) > 1000)
{
// travel moves have been received so enact them
delayed_move_time = 0xFFFFFFFFUL; // force moves to be done
memcpy(destination,current_position,sizeof(destination));
prepare_move();
}
#endif
check_axes_activity(); check_axes_activity();
} }

View File

@ -348,10 +348,20 @@ ISR(TIMER1_COMPA_vect)
// Set the direction bits (X_AXIS=A_AXIS and Y_AXIS=B_AXIS for COREXY) // Set the direction bits (X_AXIS=A_AXIS and Y_AXIS=B_AXIS for COREXY)
if((out_bits & (1<<X_AXIS))!=0){ if((out_bits & (1<<X_AXIS))!=0){
#ifdef DUAL_X_CARRIAGE
if (active_extruder != 0)
WRITE(X2_DIR_PIN,INVERT_X_DIR);
else
#endif
WRITE(X_DIR_PIN, INVERT_X_DIR); WRITE(X_DIR_PIN, INVERT_X_DIR);
count_direction[X_AXIS]=-1; count_direction[X_AXIS]=-1;
} }
else{ else{
#ifdef DUAL_X_CARRIAGE
if (active_extruder != 0)
WRITE(X2_DIR_PIN,!INVERT_X_DIR);
else
#endif
WRITE(X_DIR_PIN, !INVERT_X_DIR); WRITE(X_DIR_PIN, !INVERT_X_DIR);
count_direction[X_AXIS]=1; count_direction[X_AXIS]=1;
} }
@ -371,6 +381,11 @@ ISR(TIMER1_COMPA_vect)
if ((((out_bits & (1<<X_AXIS)) != 0)&&(out_bits & (1<<Y_AXIS)) != 0)) { //-X occurs for -A and -B if ((((out_bits & (1<<X_AXIS)) != 0)&&(out_bits & (1<<Y_AXIS)) != 0)) { //-X occurs for -A and -B
#endif #endif
CHECK_ENDSTOPS CHECK_ENDSTOPS
{
#ifdef DUAL_X_CARRIAGE
// with 2 x-carriages, endstops are only checked in the homing direction for the active extruder
if ((active_extruder == 0 && X_HOME_DIR == -1) || (active_extruder != 0 && X2_HOME_DIR == -1))
#endif
{ {
#if defined(X_MIN_PIN) && X_MIN_PIN > -1 #if defined(X_MIN_PIN) && X_MIN_PIN > -1
bool x_min_endstop=(READ(X_MIN_PIN) != X_ENDSTOPS_INVERTING); bool x_min_endstop=(READ(X_MIN_PIN) != X_ENDSTOPS_INVERTING);
@ -383,8 +398,14 @@ ISR(TIMER1_COMPA_vect)
#endif #endif
} }
} }
}
else { // +direction else { // +direction
CHECK_ENDSTOPS CHECK_ENDSTOPS
{
#ifdef DUAL_X_CARRIAGE
// with 2 x-carriages, endstops are only checked in the homing direction for the active extruder
if ((active_extruder == 0 && X_HOME_DIR == 1) || (active_extruder != 0 && X2_HOME_DIR == 1))
#endif
{ {
#if defined(X_MAX_PIN) && X_MAX_PIN > -1 #if defined(X_MAX_PIN) && X_MAX_PIN > -1
bool x_max_endstop=(READ(X_MAX_PIN) != X_ENDSTOPS_INVERTING); bool x_max_endstop=(READ(X_MAX_PIN) != X_ENDSTOPS_INVERTING);
@ -397,6 +418,7 @@ ISR(TIMER1_COMPA_vect)
#endif #endif
} }
} }
}
#ifndef COREXY #ifndef COREXY
if ((out_bits & (1<<Y_AXIS)) != 0) { // -direction if ((out_bits & (1<<Y_AXIS)) != 0) { // -direction
@ -507,9 +529,19 @@ ISR(TIMER1_COMPA_vect)
counter_x += current_block->steps_x; counter_x += current_block->steps_x;
if (counter_x > 0) { if (counter_x > 0) {
#ifdef DUAL_X_CARRIAGE
if (active_extruder != 0)
WRITE(X2_STEP_PIN,!INVERT_X_STEP_PIN);
else
#endif
WRITE(X_STEP_PIN, !INVERT_X_STEP_PIN); WRITE(X_STEP_PIN, !INVERT_X_STEP_PIN);
counter_x -= current_block->step_event_count; counter_x -= current_block->step_event_count;
count_position[X_AXIS]+=count_direction[X_AXIS]; count_position[X_AXIS]+=count_direction[X_AXIS];
#ifdef DUAL_X_CARRIAGE
if (active_extruder != 0)
WRITE(X2_STEP_PIN,INVERT_X_STEP_PIN);
else
#endif
WRITE(X_STEP_PIN, INVERT_X_STEP_PIN); WRITE(X_STEP_PIN, INVERT_X_STEP_PIN);
} }
@ -685,6 +717,9 @@ void st_init()
#if defined(X_DIR_PIN) && X_DIR_PIN > -1 #if defined(X_DIR_PIN) && X_DIR_PIN > -1
SET_OUTPUT(X_DIR_PIN); SET_OUTPUT(X_DIR_PIN);
#endif #endif
#if defined(X2_DIR_PIN) && X2_DIR_PIN > -1
SET_OUTPUT(X2_DIR_PIN);
#endif
#if defined(Y_DIR_PIN) && Y_DIR_PIN > -1 #if defined(Y_DIR_PIN) && Y_DIR_PIN > -1
SET_OUTPUT(Y_DIR_PIN); SET_OUTPUT(Y_DIR_PIN);
#endif #endif
@ -711,6 +746,10 @@ void st_init()
SET_OUTPUT(X_ENABLE_PIN); SET_OUTPUT(X_ENABLE_PIN);
if(!X_ENABLE_ON) WRITE(X_ENABLE_PIN,HIGH); if(!X_ENABLE_ON) WRITE(X_ENABLE_PIN,HIGH);
#endif #endif
#if defined(X2_ENABLE_PIN) && X2_ENABLE_PIN > -1
SET_OUTPUT(X2_ENABLE_PIN);
if(!X_ENABLE_ON) WRITE(X2_ENABLE_PIN,HIGH);
#endif
#if defined(Y_ENABLE_PIN) && Y_ENABLE_PIN > -1 #if defined(Y_ENABLE_PIN) && Y_ENABLE_PIN > -1
SET_OUTPUT(Y_ENABLE_PIN); SET_OUTPUT(Y_ENABLE_PIN);
if(!Y_ENABLE_ON) WRITE(Y_ENABLE_PIN,HIGH); if(!Y_ENABLE_ON) WRITE(Y_ENABLE_PIN,HIGH);
@ -788,6 +827,11 @@ void st_init()
WRITE(X_STEP_PIN,INVERT_X_STEP_PIN); WRITE(X_STEP_PIN,INVERT_X_STEP_PIN);
disable_x(); disable_x();
#endif #endif
#if defined(X2_STEP_PIN) && (X2_STEP_PIN > -1)
SET_OUTPUT(X2_STEP_PIN);
WRITE(X2_STEP_PIN,INVERT_X_STEP_PIN);
disable_x();
#endif
#if defined(Y_STEP_PIN) && (Y_STEP_PIN > -1) #if defined(Y_STEP_PIN) && (Y_STEP_PIN > -1)
SET_OUTPUT(Y_STEP_PIN); SET_OUTPUT(Y_STEP_PIN);
WRITE(Y_STEP_PIN,INVERT_Y_STEP_PIN); WRITE(Y_STEP_PIN,INVERT_Y_STEP_PIN);

View File

@ -41,6 +41,8 @@ Features:
* Heater power reporting. Useful for PID monitoring. * Heater power reporting. Useful for PID monitoring.
* PID tuning * PID tuning
* CoreXY kinematics (www.corexy.com/theory.html) * CoreXY kinematics (www.corexy.com/theory.html)
* Delta kinematics
* Dual X-carriage support for multiple extruder systems
* Configurable serial port to support connection of wireless adaptors. * Configurable serial port to support connection of wireless adaptors.
* Automatic operation of extruder/cold-end cooling fans based on nozzle temperature * Automatic operation of extruder/cold-end cooling fans based on nozzle temperature
* RC Servo Support, specify angle or duration for continuous rotation servos. * RC Servo Support, specify angle or duration for continuous rotation servos.