- Probe index (optional - defaults to 0
*/
inline void gcode_M43() {
if (parser.seen('T')) { // must be first or else its "S" and "E" parameters will execute endstop or servo test
toggle_pins();
return;
}
// Enable or disable endstop monitoring
if (parser.seen('E')) {
endstop_monitor_flag = parser.value_bool();
SERIAL_PROTOCOLPGM("endstop monitor ");
serialprintPGM(endstop_monitor_flag ? PSTR("en") : PSTR("dis"));
SERIAL_PROTOCOLLNPGM("abled");
return;
}
if (parser.seen('S')) {
servo_probe_test();
return;
}
// Get the range of pins to test or watch
const uint8_t first_pin = parser.byteval('P'),
last_pin = parser.seenval('P') ? first_pin : NUM_DIGITAL_PINS - 1;
if (first_pin > last_pin) return;
const bool ignore_protection = parser.boolval('I');
// Watch until click, M108, or reset
if (parser.boolval('W')) {
SERIAL_PROTOCOLLNPGM("Watching pins");
byte pin_state[last_pin - first_pin + 1];
for (int8_t pin = first_pin; pin <= last_pin; pin++) {
if (pin_is_protected(pin) && !ignore_protection) continue;
pinMode(pin, INPUT_PULLUP);
delay(1);
/*
if (IS_ANALOG(pin))
pin_state[pin - first_pin] = analogRead(pin - analogInputToDigitalPin(0)); // int16_t pin_state[...]
else
//*/
pin_state[pin - first_pin] = digitalRead(pin);
}
#if HAS_RESUME_CONTINUE
wait_for_user = true;
KEEPALIVE_STATE(PAUSED_FOR_USER);
#endif
for (;;) {
for (int8_t pin = first_pin; pin <= last_pin; pin++) {
if (pin_is_protected(pin) && !ignore_protection) continue;
const byte val =
/*
IS_ANALOG(pin)
? analogRead(pin - analogInputToDigitalPin(0)) : // int16_t val
:
//*/
digitalRead(pin);
if (val != pin_state[pin - first_pin]) {
report_pin_state_extended(pin, ignore_protection, false);
pin_state[pin - first_pin] = val;
}
}
#if HAS_RESUME_CONTINUE
if (!wait_for_user) {
KEEPALIVE_STATE(IN_HANDLER);
break;
}
#endif
safe_delay(200);
}
return;
}
// Report current state of selected pin(s)
for (uint8_t pin = first_pin; pin <= last_pin; pin++)
report_pin_state_extended(pin, ignore_protection, true);
}
#endif // PINS_DEBUGGING
#if ENABLED(Z_MIN_PROBE_REPEATABILITY_TEST)
/**
* M48: Z probe repeatability measurement function.
*
* Usage:
* M48
* P = Number of sampled points (4-50, default 10)
* X = Sample X position
* Y = Sample Y position
* V = Verbose level (0-4, default=1)
* E = Engage Z probe for each reading
* L = Number of legs of movement before probe
* S = Schizoid (Or Star if you prefer)
*
* This function requires the machine to be homed before invocation.
*/
inline void gcode_M48() {
if (axis_unhomed_error()) return;
const int8_t verbose_level = parser.byteval('V', 1);
if (!WITHIN(verbose_level, 0, 4)) {
SERIAL_PROTOCOLLNPGM("?(V)erbose level is implausible (0-4).");
return;
}
if (verbose_level > 0)
SERIAL_PROTOCOLLNPGM("M48 Z-Probe Repeatability Test");
const int8_t n_samples = parser.byteval('P', 10);
if (!WITHIN(n_samples, 4, 50)) {
SERIAL_PROTOCOLLNPGM("?Sample size not plausible (4-50).");
return;
}
const bool stow_probe_after_each = parser.boolval('E');
float X_current = current_position[X_AXIS],
Y_current = current_position[Y_AXIS];
const float X_probe_location = parser.linearval('X', X_current + X_PROBE_OFFSET_FROM_EXTRUDER),
Y_probe_location = parser.linearval('Y', Y_current + Y_PROBE_OFFSET_FROM_EXTRUDER);
#if DISABLED(DELTA)
if (!WITHIN(X_probe_location, MIN_PROBE_X, MAX_PROBE_X)) {
out_of_range_error(PSTR("X"));
return;
}
if (!WITHIN(Y_probe_location, MIN_PROBE_Y, MAX_PROBE_Y)) {
out_of_range_error(PSTR("Y"));
return;
}
#else
if (!position_is_reachable_by_probe(X_probe_location, Y_probe_location)) {
SERIAL_PROTOCOLLNPGM("? (X,Y) location outside of probeable radius.");
return;
}
#endif
bool seen_L = parser.seen('L');
uint8_t n_legs = seen_L ? parser.value_byte() : 0;
if (n_legs > 15) {
SERIAL_PROTOCOLLNPGM("?Number of legs in movement not plausible (0-15).");
return;
}
if (n_legs == 1) n_legs = 2;
const bool schizoid_flag = parser.boolval('S');
if (schizoid_flag && !seen_L) n_legs = 7;
/**
* Now get everything to the specified probe point So we can safely do a
* probe to get us close to the bed. If the Z-Axis is far from the bed,
* we don't want to use that as a starting point for each probe.
*/
if (verbose_level > 2)
SERIAL_PROTOCOLLNPGM("Positioning the probe...");
// Disable bed level correction in M48 because we want the raw data when we probe
#if HAS_LEVELING
const bool was_enabled = planner.leveling_active;
set_bed_leveling_enabled(false);
#endif
setup_for_endstop_or_probe_move();
double mean = 0.0, sigma = 0.0, min = 99999.9, max = -99999.9, sample_set[n_samples];
// Move to the first point, deploy, and probe
const float t = probe_pt(X_probe_location, Y_probe_location, stow_probe_after_each, verbose_level);
bool probing_good = !isnan(t);
if (probing_good) {
randomSeed(millis());
for (uint8_t n = 0; n < n_samples; n++) {
if (n_legs) {
const int dir = (random(0, 10) > 5.0) ? -1 : 1; // clockwise or counter clockwise
float angle = random(0.0, 360.0);
const float radius = random(
#if ENABLED(DELTA)
0.1250000000 * (DELTA_PROBEABLE_RADIUS),
0.3333333333 * (DELTA_PROBEABLE_RADIUS)
#else
5.0, 0.125 * min(X_BED_SIZE, Y_BED_SIZE)
#endif
);
if (verbose_level > 3) {
SERIAL_ECHOPAIR("Starting radius: ", radius);
SERIAL_ECHOPAIR(" angle: ", angle);
SERIAL_ECHOPGM(" Direction: ");
if (dir > 0) SERIAL_ECHOPGM("Counter-");
SERIAL_ECHOLNPGM("Clockwise");
}
for (uint8_t l = 0; l < n_legs - 1; l++) {
double delta_angle;
if (schizoid_flag)
// The points of a 5 point star are 72 degrees apart. We need to
// skip a point and go to the next one on the star.
delta_angle = dir * 2.0 * 72.0;
else
// If we do this line, we are just trying to move further
// around the circle.
delta_angle = dir * (float) random(25, 45);
angle += delta_angle;
while (angle > 360.0) // We probably do not need to keep the angle between 0 and 2*PI, but the
angle -= 360.0; // Arduino documentation says the trig functions should not be given values
while (angle < 0.0) // outside of this range. It looks like they behave correctly with
angle += 360.0; // numbers outside of the range, but just to be safe we clamp them.
X_current = X_probe_location - (X_PROBE_OFFSET_FROM_EXTRUDER) + cos(RADIANS(angle)) * radius;
Y_current = Y_probe_location - (Y_PROBE_OFFSET_FROM_EXTRUDER) + sin(RADIANS(angle)) * radius;
#if DISABLED(DELTA)
X_current = constrain(X_current, X_MIN_POS, X_MAX_POS);
Y_current = constrain(Y_current, Y_MIN_POS, Y_MAX_POS);
#else
// If we have gone out too far, we can do a simple fix and scale the numbers
// back in closer to the origin.
while (!position_is_reachable_by_probe(X_current, Y_current)) {
X_current *= 0.8;
Y_current *= 0.8;
if (verbose_level > 3) {
SERIAL_ECHOPAIR("Pulling point towards center:", X_current);
SERIAL_ECHOLNPAIR(", ", Y_current);
}
}
#endif
if (verbose_level > 3) {
SERIAL_PROTOCOLPGM("Going to:");
SERIAL_ECHOPAIR(" X", X_current);
SERIAL_ECHOPAIR(" Y", Y_current);
SERIAL_ECHOLNPAIR(" Z", current_position[Z_AXIS]);
}
do_blocking_move_to_xy(X_current, Y_current);
} // n_legs loop
} // n_legs
// Probe a single point
sample_set[n] = probe_pt(X_probe_location, Y_probe_location, stow_probe_after_each, 0);
// Break the loop if the probe fails
probing_good = !isnan(sample_set[n]);
if (!probing_good) break;
/**
* Get the current mean for the data points we have so far
*/
double sum = 0.0;
for (uint8_t j = 0; j <= n; j++) sum += sample_set[j];
mean = sum / (n + 1);
NOMORE(min, sample_set[n]);
NOLESS(max, sample_set[n]);
/**
* Now, use that mean to calculate the standard deviation for the
* data points we have so far
*/
sum = 0.0;
for (uint8_t j = 0; j <= n; j++)
sum += sq(sample_set[j] - mean);
sigma = SQRT(sum / (n + 1));
if (verbose_level > 0) {
if (verbose_level > 1) {
SERIAL_PROTOCOL(n + 1);
SERIAL_PROTOCOLPGM(" of ");
SERIAL_PROTOCOL((int)n_samples);
SERIAL_PROTOCOLPGM(": z: ");
SERIAL_PROTOCOL_F(sample_set[n], 3);
if (verbose_level > 2) {
SERIAL_PROTOCOLPGM(" mean: ");
SERIAL_PROTOCOL_F(mean, 4);
SERIAL_PROTOCOLPGM(" sigma: ");
SERIAL_PROTOCOL_F(sigma, 6);
SERIAL_PROTOCOLPGM(" min: ");
SERIAL_PROTOCOL_F(min, 3);
SERIAL_PROTOCOLPGM(" max: ");
SERIAL_PROTOCOL_F(max, 3);
SERIAL_PROTOCOLPGM(" range: ");
SERIAL_PROTOCOL_F(max-min, 3);
}
SERIAL_EOL();
}
}
} // n_samples loop
}
STOW_PROBE();
if (probing_good) {
SERIAL_PROTOCOLLNPGM("Finished!");
if (verbose_level > 0) {
SERIAL_PROTOCOLPGM("Mean: ");
SERIAL_PROTOCOL_F(mean, 6);
SERIAL_PROTOCOLPGM(" Min: ");
SERIAL_PROTOCOL_F(min, 3);
SERIAL_PROTOCOLPGM(" Max: ");
SERIAL_PROTOCOL_F(max, 3);
SERIAL_PROTOCOLPGM(" Range: ");
SERIAL_PROTOCOL_F(max-min, 3);
SERIAL_EOL();
}
SERIAL_PROTOCOLPGM("Standard Deviation: ");
SERIAL_PROTOCOL_F(sigma, 6);
SERIAL_EOL();
SERIAL_EOL();
}
clean_up_after_endstop_or_probe_move();
// Re-enable bed level correction if it had been on
#if HAS_LEVELING
set_bed_leveling_enabled(was_enabled);
#endif
report_current_position();
}
#endif // Z_MIN_PROBE_REPEATABILITY_TEST
#if ENABLED(G26_MESH_VALIDATION)
inline void gcode_M49() {
g26_debug_flag ^= true;
SERIAL_PROTOCOLPGM("G26 Debug ");
serialprintPGM(g26_debug_flag ? PSTR("on.\n") : PSTR("off.\n"));
}
#endif // G26_MESH_VALIDATION
#if ENABLED(ULTRA_LCD) && ENABLED(LCD_SET_PROGRESS_MANUALLY)
/**
* M73: Set percentage complete (for display on LCD)
*
* Example:
* M73 P25 ; Set progress to 25%
*
* Notes:
* This has no effect during an SD print job
*/
inline void gcode_M73() {
if (!IS_SD_PRINTING && parser.seen('P')) {
progress_bar_percent = parser.value_byte();
NOMORE(progress_bar_percent, 100);
}
}
#endif // ULTRA_LCD && LCD_SET_PROGRESS_MANUALLY
/**
* M75: Start print timer
*/
inline void gcode_M75() { print_job_timer.start(); }
/**
* M76: Pause print timer
*/
inline void gcode_M76() { print_job_timer.pause(); }
/**
* M77: Stop print timer
*/
inline void gcode_M77() { print_job_timer.stop(); }
#if ENABLED(PRINTCOUNTER)
/**
* M78: Show print statistics
*/
inline void gcode_M78() {
// "M78 S78" will reset the statistics
if (parser.intval('S') == 78)
print_job_timer.initStats();
else
print_job_timer.showStats();
}
#endif
/**
* M104: Set hot end temperature
*/
inline void gcode_M104() {
if (get_target_extruder_from_command(104)) return;
if (DEBUGGING(DRYRUN)) return;
#if ENABLED(SINGLENOZZLE)
if (target_extruder != active_extruder) return;
#endif
if (parser.seenval('S')) {
const int16_t temp = parser.value_celsius();
thermalManager.setTargetHotend(temp, target_extruder);
#if ENABLED(DUAL_X_CARRIAGE)
if (dual_x_carriage_mode == DXC_DUPLICATION_MODE && target_extruder == 0)
thermalManager.setTargetHotend(temp ? temp + duplicate_extruder_temp_offset : 0, 1);
#endif
#if ENABLED(PRINTJOB_TIMER_AUTOSTART)
/**
* Stop the timer at the end of print. Start is managed by 'heat and wait' M109.
* We use half EXTRUDE_MINTEMP here to allow nozzles to be put into hot
* standby mode, for instance in a dual extruder setup, without affecting
* the running print timer.
*/
if (parser.value_celsius() <= (EXTRUDE_MINTEMP) / 2) {
print_job_timer.stop();
LCD_MESSAGEPGM(WELCOME_MSG);
}
#endif
if (parser.value_celsius() > thermalManager.degHotend(target_extruder))
lcd_status_printf_P(0, PSTR("E%i %s"), target_extruder + 1, MSG_HEATING);
}
#if ENABLED(AUTOTEMP)
planner.autotemp_M104_M109();
#endif
}
/**
* M105: Read hot end and bed temperature
*/
inline void gcode_M105() {
if (get_target_extruder_from_command(105)) return;
#if HAS_TEMP_HOTEND || HAS_TEMP_BED
SERIAL_PROTOCOLPGM(MSG_OK);
thermalManager.print_heaterstates();
#else // !HAS_TEMP_HOTEND && !HAS_TEMP_BED
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_ERR_NO_THERMISTORS);
#endif
SERIAL_EOL();
}
#if ENABLED(AUTO_REPORT_TEMPERATURES) && (HAS_TEMP_HOTEND || HAS_TEMP_BED)
/**
* M155: Set temperature auto-report interval. M155 S
*/
inline void gcode_M155() {
if (parser.seenval('S'))
thermalManager.set_auto_report_interval(parser.value_byte());
}
#endif // AUTO_REPORT_TEMPERATURES
#if FAN_COUNT > 0
/**
* M106: Set Fan Speed
*
* S Speed between 0-255
* P Fan index, if more than one fan
*
* With EXTRA_FAN_SPEED enabled:
*
* T Restore/Use/Set Temporary Speed:
* 1 = Restore previous speed after T2
* 2 = Use temporary speed set with T3-255
* 3-255 = Set the speed for use with T2
*/
inline void gcode_M106() {
const uint8_t p = parser.byteval('P');
if (p < FAN_COUNT) {
#if ENABLED(EXTRA_FAN_SPEED)
const int16_t t = parser.intval('T');
if (t > 0) {
switch (t) {
case 1:
fanSpeeds[p] = old_fanSpeeds[p];
break;
case 2:
old_fanSpeeds[p] = fanSpeeds[p];
fanSpeeds[p] = new_fanSpeeds[p];
break;
default:
new_fanSpeeds[p] = min(t, 255);
break;
}
return;
}
#endif // EXTRA_FAN_SPEED
const uint16_t s = parser.ushortval('S', 255);
fanSpeeds[p] = min(s, 255);
}
}
/**
* M107: Fan Off
*/
inline void gcode_M107() {
const uint16_t p = parser.ushortval('P');
if (p < FAN_COUNT) fanSpeeds[p] = 0;
}
#endif // FAN_COUNT > 0
#if DISABLED(EMERGENCY_PARSER)
/**
* M108: Stop the waiting for heaters in M109, M190, M303. Does not affect the target temperature.
*/
inline void gcode_M108() { wait_for_heatup = false; }
/**
* M112: Emergency Stop
*/
inline void gcode_M112() { kill(PSTR(MSG_KILLED)); }
/**
* M410: Quickstop - Abort all planned moves
*
* This will stop the carriages mid-move, so most likely they
* will be out of sync with the stepper position after this.
*/
inline void gcode_M410() { quickstop_stepper(); }
#endif
/**
* M109: Sxxx Wait for extruder(s) to reach temperature. Waits only when heating.
* Rxxx Wait for extruder(s) to reach temperature. Waits when heating and cooling.
*/
#ifndef MIN_COOLING_SLOPE_DEG
#define MIN_COOLING_SLOPE_DEG 1.50
#endif
#ifndef MIN_COOLING_SLOPE_TIME
#define MIN_COOLING_SLOPE_TIME 60
#endif
inline void gcode_M109() {
if (get_target_extruder_from_command(109)) return;
if (DEBUGGING(DRYRUN)) return;
#if ENABLED(SINGLENOZZLE)
if (target_extruder != active_extruder) return;
#endif
const bool no_wait_for_cooling = parser.seenval('S');
if (no_wait_for_cooling || parser.seenval('R')) {
const int16_t temp = parser.value_celsius();
thermalManager.setTargetHotend(temp, target_extruder);
#if ENABLED(DUAL_X_CARRIAGE)
if (dual_x_carriage_mode == DXC_DUPLICATION_MODE && target_extruder == 0)
thermalManager.setTargetHotend(temp ? temp + duplicate_extruder_temp_offset : 0, 1);
#endif
#if ENABLED(PRINTJOB_TIMER_AUTOSTART)
/**
* Use half EXTRUDE_MINTEMP to allow nozzles to be put into hot
* standby mode, (e.g., in a dual extruder setup) without affecting
* the running print timer.
*/
if (parser.value_celsius() <= (EXTRUDE_MINTEMP) / 2) {
print_job_timer.stop();
LCD_MESSAGEPGM(WELCOME_MSG);
}
else
print_job_timer.start();
#endif
if (thermalManager.isHeatingHotend(target_extruder)) lcd_status_printf_P(0, PSTR("E%i %s"), target_extruder + 1, MSG_HEATING);
}
else return;
#if ENABLED(AUTOTEMP)
planner.autotemp_M104_M109();
#endif
#if TEMP_RESIDENCY_TIME > 0
millis_t residency_start_ms = 0;
// Loop until the temperature has stabilized
#define TEMP_CONDITIONS (!residency_start_ms || PENDING(now, residency_start_ms + (TEMP_RESIDENCY_TIME) * 1000UL))
#else
// Loop until the temperature is very close target
#define TEMP_CONDITIONS (wants_to_cool ? thermalManager.isCoolingHotend(target_extruder) : thermalManager.isHeatingHotend(target_extruder))
#endif
float target_temp = -1.0, old_temp = 9999.0;
bool wants_to_cool = false;
wait_for_heatup = true;
millis_t now, next_temp_ms = 0, next_cool_check_ms = 0;
#if DISABLED(BUSY_WHILE_HEATING)
KEEPALIVE_STATE(NOT_BUSY);
#endif
#if ENABLED(PRINTER_EVENT_LEDS)
const float start_temp = thermalManager.degHotend(target_extruder);
uint8_t old_blue = 0;
#endif
do {
// Target temperature might be changed during the loop
if (target_temp != thermalManager.degTargetHotend(target_extruder)) {
wants_to_cool = thermalManager.isCoolingHotend(target_extruder);
target_temp = thermalManager.degTargetHotend(target_extruder);
// Exit if S, continue if S, R, or R
if (no_wait_for_cooling && wants_to_cool) break;
}
now = millis();
if (ELAPSED(now, next_temp_ms)) { //Print temp & remaining time every 1s while waiting
next_temp_ms = now + 1000UL;
thermalManager.print_heaterstates();
#if TEMP_RESIDENCY_TIME > 0
SERIAL_PROTOCOLPGM(" W:");
if (residency_start_ms)
SERIAL_PROTOCOL(long((((TEMP_RESIDENCY_TIME) * 1000UL) - (now - residency_start_ms)) / 1000UL));
else
SERIAL_PROTOCOLCHAR('?');
#endif
SERIAL_EOL();
}
idle();
refresh_cmd_timeout(); // to prevent stepper_inactive_time from running out
const float temp = thermalManager.degHotend(target_extruder);
#if ENABLED(PRINTER_EVENT_LEDS)
// Gradually change LED strip from violet to red as nozzle heats up
if (!wants_to_cool) {
const uint8_t blue = map(constrain(temp, start_temp, target_temp), start_temp, target_temp, 255, 0);
if (blue != old_blue) {
old_blue = blue;
leds.set_color(
MakeLEDColor(255, 0, blue, 0, pixels.getBrightness())
#if ENABLED(NEOPIXEL_IS_SEQUENTIAL)
, true
#endif
);
}
}
#endif
#if TEMP_RESIDENCY_TIME > 0
const float temp_diff = FABS(target_temp - temp);
if (!residency_start_ms) {
// Start the TEMP_RESIDENCY_TIME timer when we reach target temp for the first time.
if (temp_diff < TEMP_WINDOW) residency_start_ms = now;
}
else if (temp_diff > TEMP_HYSTERESIS) {
// Restart the timer whenever the temperature falls outside the hysteresis.
residency_start_ms = now;
}
#endif
// Prevent a wait-forever situation if R is misused i.e. M109 R0
if (wants_to_cool) {
// break after MIN_COOLING_SLOPE_TIME seconds
// if the temperature did not drop at least MIN_COOLING_SLOPE_DEG
if (!next_cool_check_ms || ELAPSED(now, next_cool_check_ms)) {
if (old_temp - temp < MIN_COOLING_SLOPE_DEG) break;
next_cool_check_ms = now + 1000UL * MIN_COOLING_SLOPE_TIME;
old_temp = temp;
}
}
} while (wait_for_heatup && TEMP_CONDITIONS);
if (wait_for_heatup) {
LCD_MESSAGEPGM(MSG_HEATING_COMPLETE);
#if ENABLED(PRINTER_EVENT_LEDS)
leds.set_white();
#endif
}
#if DISABLED(BUSY_WHILE_HEATING)
KEEPALIVE_STATE(IN_HANDLER);
#endif
}
#if HAS_TEMP_BED
#ifndef MIN_COOLING_SLOPE_DEG_BED
#define MIN_COOLING_SLOPE_DEG_BED 1.50
#endif
#ifndef MIN_COOLING_SLOPE_TIME_BED
#define MIN_COOLING_SLOPE_TIME_BED 60
#endif
/**
* M190: Sxxx Wait for bed current temp to reach target temp. Waits only when heating
* Rxxx Wait for bed current temp to reach target temp. Waits when heating and cooling
*/
inline void gcode_M190() {
if (DEBUGGING(DRYRUN)) return;
LCD_MESSAGEPGM(MSG_BED_HEATING);
const bool no_wait_for_cooling = parser.seenval('S');
if (no_wait_for_cooling || parser.seenval('R')) {
thermalManager.setTargetBed(parser.value_celsius());
#if ENABLED(PRINTJOB_TIMER_AUTOSTART)
if (parser.value_celsius() > BED_MINTEMP)
print_job_timer.start();
#endif
}
else return;
#if TEMP_BED_RESIDENCY_TIME > 0
millis_t residency_start_ms = 0;
// Loop until the temperature has stabilized
#define TEMP_BED_CONDITIONS (!residency_start_ms || PENDING(now, residency_start_ms + (TEMP_BED_RESIDENCY_TIME) * 1000UL))
#else
// Loop until the temperature is very close target
#define TEMP_BED_CONDITIONS (wants_to_cool ? thermalManager.isCoolingBed() : thermalManager.isHeatingBed())
#endif
float target_temp = -1.0, old_temp = 9999.0;
bool wants_to_cool = false;
wait_for_heatup = true;
millis_t now, next_temp_ms = 0, next_cool_check_ms = 0;
#if DISABLED(BUSY_WHILE_HEATING)
KEEPALIVE_STATE(NOT_BUSY);
#endif
target_extruder = active_extruder; // for print_heaterstates
#if ENABLED(PRINTER_EVENT_LEDS)
const float start_temp = thermalManager.degBed();
uint8_t old_red = 255;
#endif
do {
// Target temperature might be changed during the loop
if (target_temp != thermalManager.degTargetBed()) {
wants_to_cool = thermalManager.isCoolingBed();
target_temp = thermalManager.degTargetBed();
// Exit if S, continue if S, R, or R
if (no_wait_for_cooling && wants_to_cool) break;
}
now = millis();
if (ELAPSED(now, next_temp_ms)) { //Print Temp Reading every 1 second while heating up.
next_temp_ms = now + 1000UL;
thermalManager.print_heaterstates();
#if TEMP_BED_RESIDENCY_TIME > 0
SERIAL_PROTOCOLPGM(" W:");
if (residency_start_ms)
SERIAL_PROTOCOL(long((((TEMP_BED_RESIDENCY_TIME) * 1000UL) - (now - residency_start_ms)) / 1000UL));
else
SERIAL_PROTOCOLCHAR('?');
#endif
SERIAL_EOL();
}
idle();
refresh_cmd_timeout(); // to prevent stepper_inactive_time from running out
const float temp = thermalManager.degBed();
#if ENABLED(PRINTER_EVENT_LEDS)
// Gradually change LED strip from blue to violet as bed heats up
if (!wants_to_cool) {
const uint8_t red = map(constrain(temp, start_temp, target_temp), start_temp, target_temp, 0, 255);
if (red != old_red) {
old_red = red;
leds.set_color(
MakeLEDColor(red, 0, 255, 0, pixels.getBrightness())
#if ENABLED(NEOPIXEL_IS_SEQUENTIAL)
, true
#endif
);
}
}
#endif
#if TEMP_BED_RESIDENCY_TIME > 0
const float temp_diff = FABS(target_temp - temp);
if (!residency_start_ms) {
// Start the TEMP_BED_RESIDENCY_TIME timer when we reach target temp for the first time.
if (temp_diff < TEMP_BED_WINDOW) residency_start_ms = now;
}
else if (temp_diff > TEMP_BED_HYSTERESIS) {
// Restart the timer whenever the temperature falls outside the hysteresis.
residency_start_ms = now;
}
#endif // TEMP_BED_RESIDENCY_TIME > 0
// Prevent a wait-forever situation if R is misused i.e. M190 R0
if (wants_to_cool) {
// Break after MIN_COOLING_SLOPE_TIME_BED seconds
// if the temperature did not drop at least MIN_COOLING_SLOPE_DEG_BED
if (!next_cool_check_ms || ELAPSED(now, next_cool_check_ms)) {
if (old_temp - temp < MIN_COOLING_SLOPE_DEG_BED) break;
next_cool_check_ms = now + 1000UL * MIN_COOLING_SLOPE_TIME_BED;
old_temp = temp;
}
}
} while (wait_for_heatup && TEMP_BED_CONDITIONS);
if (wait_for_heatup) LCD_MESSAGEPGM(MSG_BED_DONE);
#if DISABLED(BUSY_WHILE_HEATING)
KEEPALIVE_STATE(IN_HANDLER);
#endif
}
#endif // HAS_TEMP_BED
/**
* M110: Set Current Line Number
*/
inline void gcode_M110() {
if (parser.seenval('N')) gcode_LastN = parser.value_long();
}
/**
* M111: Set the debug level
*/
inline void gcode_M111() {
if (parser.seen('S')) marlin_debug_flags = parser.byteval('S');
const static char str_debug_1[] PROGMEM = MSG_DEBUG_ECHO,
str_debug_2[] PROGMEM = MSG_DEBUG_INFO,
str_debug_4[] PROGMEM = MSG_DEBUG_ERRORS,
str_debug_8[] PROGMEM = MSG_DEBUG_DRYRUN,
str_debug_16[] PROGMEM = MSG_DEBUG_COMMUNICATION
#if ENABLED(DEBUG_LEVELING_FEATURE)
, str_debug_32[] PROGMEM = MSG_DEBUG_LEVELING
#endif
;
const static char* const debug_strings[] PROGMEM = {
str_debug_1, str_debug_2, str_debug_4, str_debug_8, str_debug_16
#if ENABLED(DEBUG_LEVELING_FEATURE)
, str_debug_32
#endif
};
SERIAL_ECHO_START();
SERIAL_ECHOPGM(MSG_DEBUG_PREFIX);
if (marlin_debug_flags) {
uint8_t comma = 0;
for (uint8_t i = 0; i < COUNT(debug_strings); i++) {
if (TEST(marlin_debug_flags, i)) {
if (comma++) SERIAL_CHAR(',');
serialprintPGM((char*)pgm_read_word(&debug_strings[i]));
}
}
}
else {
SERIAL_ECHOPGM(MSG_DEBUG_OFF);
}
SERIAL_EOL();
}
#if ENABLED(HOST_KEEPALIVE_FEATURE)
/**
* M113: Get or set Host Keepalive interval (0 to disable)
*
* S Optional. Set the keepalive interval.
*/
inline void gcode_M113() {
if (parser.seenval('S')) {
host_keepalive_interval = parser.value_byte();
NOMORE(host_keepalive_interval, 60);
}
else {
SERIAL_ECHO_START();
SERIAL_ECHOLNPAIR("M113 S", (unsigned long)host_keepalive_interval);
}
}
#endif
#if ENABLED(BARICUDA)
#if HAS_HEATER_1
/**
* M126: Heater 1 valve open
*/
inline void gcode_M126() { baricuda_valve_pressure = parser.byteval('S', 255); }
/**
* M127: Heater 1 valve close
*/
inline void gcode_M127() { baricuda_valve_pressure = 0; }
#endif
#if HAS_HEATER_2
/**
* M128: Heater 2 valve open
*/
inline void gcode_M128() { baricuda_e_to_p_pressure = parser.byteval('S', 255); }
/**
* M129: Heater 2 valve close
*/
inline void gcode_M129() { baricuda_e_to_p_pressure = 0; }
#endif
#endif // BARICUDA
/**
* M140: Set bed temperature
*/
inline void gcode_M140() {
if (DEBUGGING(DRYRUN)) return;
if (parser.seenval('S')) thermalManager.setTargetBed(parser.value_celsius());
}
#if ENABLED(ULTIPANEL)
/**
* M145: Set the heatup state for a material in the LCD menu
*
* S (0=PLA, 1=ABS)
* H
* B
* F
*/
inline void gcode_M145() {
const uint8_t material = (uint8_t)parser.intval('S');
if (material >= COUNT(lcd_preheat_hotend_temp)) {
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_ERR_MATERIAL_INDEX);
}
else {
int v;
if (parser.seenval('H')) {
v = parser.value_int();
lcd_preheat_hotend_temp[material] = constrain(v, EXTRUDE_MINTEMP, HEATER_0_MAXTEMP - 15);
}
if (parser.seenval('F')) {
v = parser.value_int();
lcd_preheat_fan_speed[material] = constrain(v, 0, 255);
}
#if TEMP_SENSOR_BED != 0
if (parser.seenval('B')) {
v = parser.value_int();
lcd_preheat_bed_temp[material] = constrain(v, BED_MINTEMP, BED_MAXTEMP - 15);
}
#endif
}
}
#endif // ULTIPANEL
#if ENABLED(TEMPERATURE_UNITS_SUPPORT)
/**
* M149: Set temperature units
*/
inline void gcode_M149() {
if (parser.seenval('C')) parser.set_input_temp_units(TEMPUNIT_C);
else if (parser.seenval('K')) parser.set_input_temp_units(TEMPUNIT_K);
else if (parser.seenval('F')) parser.set_input_temp_units(TEMPUNIT_F);
}
#endif
#if HAS_POWER_SWITCH
/**
* M80 : Turn on the Power Supply
* M80 S : Report the current state and exit
*/
inline void gcode_M80() {
// S: Report the current power supply state and exit
if (parser.seen('S')) {
serialprintPGM(powersupply_on ? PSTR("PS:1\n") : PSTR("PS:0\n"));
return;
}
OUT_WRITE(PS_ON_PIN, PS_ON_AWAKE); // GND
/**
* If you have a switch on suicide pin, this is useful
* if you want to start another print with suicide feature after
* a print without suicide...
*/
#if HAS_SUICIDE
OUT_WRITE(SUICIDE_PIN, HIGH);
#endif
#if ENABLED(HAVE_TMC2130)
delay(100);
tmc2130_init(); // Settings only stick when the driver has power
#endif
powersupply_on = true;
#if ENABLED(ULTIPANEL)
LCD_MESSAGEPGM(WELCOME_MSG);
#endif
#if ENABLED(HAVE_TMC2208)
delay(100);
tmc2208_init();
#endif
}
#endif // HAS_POWER_SWITCH
/**
* M81: Turn off Power, including Power Supply, if there is one.
*
* This code should ALWAYS be available for EMERGENCY SHUTDOWN!
*/
inline void gcode_M81() {
thermalManager.disable_all_heaters();
stepper.finish_and_disable();
#if FAN_COUNT > 0
for (uint8_t i = 0; i < FAN_COUNT; i++) fanSpeeds[i] = 0;
#if ENABLED(PROBING_FANS_OFF)
fans_paused = false;
ZERO(paused_fanSpeeds);
#endif
#endif
safe_delay(1000); // Wait 1 second before switching off
#if HAS_SUICIDE
stepper.synchronize();
suicide();
#elif HAS_POWER_SWITCH
OUT_WRITE(PS_ON_PIN, PS_ON_ASLEEP);
powersupply_on = false;
#endif
#if ENABLED(ULTIPANEL)
LCD_MESSAGEPGM(MACHINE_NAME " " MSG_OFF ".");
#endif
}
/**
* M82: Set E codes absolute (default)
*/
inline void gcode_M82() { axis_relative_modes[E_AXIS] = false; }
/**
* M83: Set E codes relative while in Absolute Coordinates (G90) mode
*/
inline void gcode_M83() { axis_relative_modes[E_AXIS] = true; }
/**
* M18, M84: Disable stepper motors
*/
inline void gcode_M18_M84() {
if (parser.seenval('S')) {
stepper_inactive_time = parser.value_millis_from_seconds();
}
else {
bool all_axis = !(parser.seen('X') || parser.seen('Y') || parser.seen('Z') || parser.seen('E'));
if (all_axis) {
stepper.finish_and_disable();
}
else {
stepper.synchronize();
if (parser.seen('X')) disable_X();
if (parser.seen('Y')) disable_Y();
if (parser.seen('Z')) disable_Z();
#if E0_ENABLE_PIN != X_ENABLE_PIN && E1_ENABLE_PIN != Y_ENABLE_PIN // Only disable on boards that have separate ENABLE_PINS
if (parser.seen('E')) disable_e_steppers();
#endif
}
#if ENABLED(AUTO_BED_LEVELING_UBL) && ENABLED(ULTIPANEL) // Only needed with an LCD
ubl.lcd_map_control = defer_return_to_status = false;
#endif
}
}
/**
* M85: Set inactivity shutdown timer with parameter S. To disable set zero (default)
*/
inline void gcode_M85() {
if (parser.seen('S')) max_inactive_time = parser.value_millis_from_seconds();
}
/**
* Multi-stepper support for M92, M201, M203
*/
#if ENABLED(DISTINCT_E_FACTORS)
#define GET_TARGET_EXTRUDER(CMD) if (get_target_extruder_from_command(CMD)) return
#define TARGET_EXTRUDER target_extruder
#else
#define GET_TARGET_EXTRUDER(CMD) NOOP
#define TARGET_EXTRUDER 0
#endif
/**
* M92: Set axis steps-per-unit for one or more axes, X, Y, Z, and E.
* (Follows the same syntax as G92)
*
* With multiple extruders use T to specify which one.
*/
inline void gcode_M92() {
GET_TARGET_EXTRUDER(92);
LOOP_XYZE(i) {
if (parser.seen(axis_codes[i])) {
if (i == E_AXIS) {
const float value = parser.value_per_axis_unit((AxisEnum)(E_AXIS + TARGET_EXTRUDER));
if (value < 20.0) {
float factor = planner.axis_steps_per_mm[E_AXIS + TARGET_EXTRUDER] / value; // increase e constants if M92 E14 is given for netfab.
planner.max_jerk[E_AXIS] *= factor;
planner.max_feedrate_mm_s[E_AXIS + TARGET_EXTRUDER] *= factor;
planner.max_acceleration_steps_per_s2[E_AXIS + TARGET_EXTRUDER] *= factor;
}
planner.axis_steps_per_mm[E_AXIS + TARGET_EXTRUDER] = value;
}
else {
planner.axis_steps_per_mm[i] = parser.value_per_axis_unit((AxisEnum)i);
}
}
}
planner.refresh_positioning();
}
/**
* Output the current position to serial
*/
void report_current_position() {
SERIAL_PROTOCOLPGM("X:");
SERIAL_PROTOCOL(LOGICAL_X_POSITION(current_position[X_AXIS]));
SERIAL_PROTOCOLPGM(" Y:");
SERIAL_PROTOCOL(LOGICAL_Y_POSITION(current_position[Y_AXIS]));
SERIAL_PROTOCOLPGM(" Z:");
SERIAL_PROTOCOL(LOGICAL_Z_POSITION(current_position[Z_AXIS]));
SERIAL_PROTOCOLPGM(" E:");
SERIAL_PROTOCOL(current_position[E_AXIS]);
stepper.report_positions();
#if IS_SCARA
SERIAL_PROTOCOLPAIR("SCARA Theta:", stepper.get_axis_position_degrees(A_AXIS));
SERIAL_PROTOCOLLNPAIR(" Psi+Theta:", stepper.get_axis_position_degrees(B_AXIS));
SERIAL_EOL();
#endif
}
#ifdef M114_DETAIL
void report_xyze(const float pos[], const uint8_t n = 4, const uint8_t precision = 3) {
char str[12];
for (uint8_t i = 0; i < n; i++) {
SERIAL_CHAR(' ');
SERIAL_CHAR(axis_codes[i]);
SERIAL_CHAR(':');
SERIAL_PROTOCOL(dtostrf(pos[i], 8, precision, str));
}
SERIAL_EOL();
}
inline void report_xyz(const float pos[]) { report_xyze(pos, 3); }
void report_current_position_detail() {
stepper.synchronize();
SERIAL_PROTOCOLPGM("\nLogical:");
const float logical[XYZ] = {
LOGICAL_X_POSITION(current_position[X_AXIS]),
LOGICAL_Y_POSITION(current_position[Y_AXIS]),
LOGICAL_Z_POSITION(current_position[Z_AXIS])
};
report_xyze(logical);
SERIAL_PROTOCOLPGM("Raw: ");
report_xyz(current_position);
float leveled[XYZ] = { current_position[X_AXIS], current_position[Y_AXIS], current_position[Z_AXIS] };
#if PLANNER_LEVELING
SERIAL_PROTOCOLPGM("Leveled:");
planner.apply_leveling(leveled);
report_xyz(leveled);
SERIAL_PROTOCOLPGM("UnLevel:");
float unleveled[XYZ] = { leveled[X_AXIS], leveled[Y_AXIS], leveled[Z_AXIS] };
planner.unapply_leveling(unleveled);
report_xyz(unleveled);
#endif
#if IS_KINEMATIC
#if IS_SCARA
SERIAL_PROTOCOLPGM("ScaraK: ");
#else
SERIAL_PROTOCOLPGM("DeltaK: ");
#endif
inverse_kinematics(leveled); // writes delta[]
report_xyz(delta);
#endif
SERIAL_PROTOCOLPGM("Stepper:");
LOOP_XYZE(i) {
SERIAL_CHAR(' ');
SERIAL_CHAR(axis_codes[i]);
SERIAL_CHAR(':');
SERIAL_PROTOCOL(stepper.position((AxisEnum)i));
}
SERIAL_EOL();
#if IS_SCARA
const float deg[XYZ] = {
stepper.get_axis_position_degrees(A_AXIS),
stepper.get_axis_position_degrees(B_AXIS)
};
SERIAL_PROTOCOLPGM("Degrees:");
report_xyze(deg, 2);
#endif
SERIAL_PROTOCOLPGM("FromStp:");
get_cartesian_from_steppers(); // writes cartes[XYZ] (with forward kinematics)
const float from_steppers[XYZE] = { cartes[X_AXIS], cartes[Y_AXIS], cartes[Z_AXIS], stepper.get_axis_position_mm(E_AXIS) };
report_xyze(from_steppers);
const float diff[XYZE] = {
from_steppers[X_AXIS] - leveled[X_AXIS],
from_steppers[Y_AXIS] - leveled[Y_AXIS],
from_steppers[Z_AXIS] - leveled[Z_AXIS],
from_steppers[E_AXIS] - current_position[E_AXIS]
};
SERIAL_PROTOCOLPGM("Differ: ");
report_xyze(diff);
}
#endif // M114_DETAIL
/**
* M114: Report current position to host
*/
inline void gcode_M114() {
#ifdef M114_DETAIL
if (parser.seen('D')) {
report_current_position_detail();
return;
}
#endif
stepper.synchronize();
report_current_position();
}
/**
* M115: Capabilities string
*/
#if ENABLED(EXTENDED_CAPABILITIES_REPORT)
static void cap_line(const char * const name, bool ena=false) {
SERIAL_PROTOCOLPGM("Cap:");
serialprintPGM(name);
SERIAL_PROTOCOLPGM(":");
SERIAL_PROTOCOLLN(int(ena ? 1 : 0));
}
#endif
inline void gcode_M115() {
SERIAL_PROTOCOLLNPGM(MSG_M115_REPORT);
#if ENABLED(EXTENDED_CAPABILITIES_REPORT)
// SERIAL_XON_XOFF
cap_line(PSTR("SERIAL_XON_XOFF")
#if ENABLED(SERIAL_XON_XOFF)
, true
#endif
);
// EEPROM (M500, M501)
cap_line(PSTR("EEPROM")
#if ENABLED(EEPROM_SETTINGS)
, true
#endif
);
// Volumetric Extrusion (M200)
cap_line(PSTR("VOLUMETRIC")
#if DISABLED(NO_VOLUMETRICS)
, true
#endif
);
// AUTOREPORT_TEMP (M155)
cap_line(PSTR("AUTOREPORT_TEMP")
#if ENABLED(AUTO_REPORT_TEMPERATURES)
, true
#endif
);
// PROGRESS (M530 S L, M531 , M532 X L)
cap_line(PSTR("PROGRESS"));
// Print Job timer M75, M76, M77
cap_line(PSTR("PRINT_JOB"), true);
// AUTOLEVEL (G29)
cap_line(PSTR("AUTOLEVEL")
#if HAS_AUTOLEVEL
, true
#endif
);
// Z_PROBE (G30)
cap_line(PSTR("Z_PROBE")
#if HAS_BED_PROBE
, true
#endif
);
// MESH_REPORT (M420 V)
cap_line(PSTR("LEVELING_DATA")
#if HAS_LEVELING
, true
#endif
);
// BUILD_PERCENT (M73)
cap_line(PSTR("BUILD_PERCENT")
#if ENABLED(LCD_SET_PROGRESS_MANUALLY)
, true
#endif
);
// SOFTWARE_POWER (M80, M81)
cap_line(PSTR("SOFTWARE_POWER")
#if HAS_POWER_SWITCH
, true
#endif
);
// CASE LIGHTS (M355)
cap_line(PSTR("TOGGLE_LIGHTS")
#if HAS_CASE_LIGHT
, true
#endif
);
cap_line(PSTR("CASE_LIGHT_BRIGHTNESS")
#if HAS_CASE_LIGHT
, USEABLE_HARDWARE_PWM(CASE_LIGHT_PIN)
#endif
);
// EMERGENCY_PARSER (M108, M112, M410)
cap_line(PSTR("EMERGENCY_PARSER")
#if ENABLED(EMERGENCY_PARSER)
, true
#endif
);
#endif // EXTENDED_CAPABILITIES_REPORT
}
/**
* M117: Set LCD Status Message
*/
inline void gcode_M117() { lcd_setstatus(parser.string_arg); }
/**
* M118: Display a message in the host console.
*
* A1 Append '// ' for an action command, as in OctoPrint
* E1 Have the host 'echo:' the text
*/
inline void gcode_M118() {
if (parser.boolval('E')) SERIAL_ECHO_START();
if (parser.boolval('A')) SERIAL_ECHOPGM("// ");
SERIAL_ECHOLN(parser.string_arg);
}
/**
* M119: Output endstop states to serial output
*/
inline void gcode_M119() { endstops.M119(); }
/**
* M120: Enable endstops and set non-homing endstop state to "enabled"
*/
inline void gcode_M120() { endstops.enable_globally(true); }
/**
* M121: Disable endstops and set non-homing endstop state to "disabled"
*/
inline void gcode_M121() { endstops.enable_globally(false); }
#if ENABLED(PARK_HEAD_ON_PAUSE)
/**
* M125: Store current position and move to filament change position.
* Called on pause (by M25) to prevent material leaking onto the
* object. On resume (M24) the head will be moved back and the
* print will resume.
*
* If Marlin is compiled without SD Card support, M125 can be
* used directly to pause the print and move to park position,
* resuming with a button click or M108.
*
* L = override retract length
* X = override X
* Y = override Y
* Z = override Z raise
*/
inline void gcode_M125() {
// Initial retract before move to filament change position
const float retract = -FABS(parser.seen('L') ? parser.value_axis_units(E_AXIS) : 0
#ifdef PAUSE_PARK_RETRACT_LENGTH
+ (PAUSE_PARK_RETRACT_LENGTH)
#endif
);
point_t park_point = NOZZLE_PARK_POINT;
// Move XY axes to filament change position or given position
if (parser.seenval('X')) park_point.x = parser.linearval('X');
if (parser.seenval('Y')) park_point.y = parser.linearval('Y');
// Lift Z axis
if (parser.seenval('Z')) park_point.z = parser.linearval('Z');
#if HOTENDS > 1 && DISABLED(DUAL_X_CARRIAGE)
park_point.x += (active_extruder ? hotend_offset[X_AXIS][active_extruder] : 0);
park_point.y += (active_extruder ? hotend_offset[Y_AXIS][active_extruder] : 0);
#endif
#if DISABLED(SDSUPPORT)
const bool job_running = print_job_timer.isRunning();
#endif
if (pause_print(retract, park_point)) {
#if DISABLED(SDSUPPORT)
// Wait for lcd click or M108
wait_for_filament_reload();
// Return to print position and continue
resume_print();
if (job_running) print_job_timer.start();
#endif
}
}
#endif // PARK_HEAD_ON_PAUSE
#if HAS_COLOR_LEDS
/**
* M150: Set Status LED Color - Use R-U-B-W for R-G-B-W
* and Brightness - Use P (for NEOPIXEL only)
*
* Always sets all 3 or 4 components. If a component is left out, set to 0.
* If brightness is left out, no value changed
*
* Examples:
*
* M150 R255 ; Turn LED red
* M150 R255 U127 ; Turn LED orange (PWM only)
* M150 ; Turn LED off
* M150 R U B ; Turn LED white
* M150 W ; Turn LED white using a white LED
* M150 P127 ; Set LED 50% brightness
* M150 P ; Set LED full brightness
*/
inline void gcode_M150() {
leds.set_color(MakeLEDColor(
parser.seen('R') ? (parser.has_value() ? parser.value_byte() : 255) : 0,
parser.seen('U') ? (parser.has_value() ? parser.value_byte() : 255) : 0,
parser.seen('B') ? (parser.has_value() ? parser.value_byte() : 255) : 0,
parser.seen('W') ? (parser.has_value() ? parser.value_byte() : 255) : 0,
parser.seen('P') ? (parser.has_value() ? parser.value_byte() : 255) : pixels.getBrightness()
));
}
#endif // HAS_COLOR_LEDS
#if DISABLED(NO_VOLUMETRICS)
/**
* M200: Set filament diameter and set E axis units to cubic units
*
* T - Optional extruder number. Current extruder if omitted.
* D - Diameter of the filament. Use "D0" to switch back to linear units on the E axis.
*/
inline void gcode_M200() {
if (get_target_extruder_from_command(200)) return;
if (parser.seen('D')) {
// setting any extruder filament size disables volumetric on the assumption that
// slicers either generate in extruder values as cubic mm or as as filament feeds
// for all extruders
if ( (parser.volumetric_enabled = (parser.value_linear_units() != 0.0)) )
planner.set_filament_size(target_extruder, parser.value_linear_units());
}
planner.calculate_volumetric_multipliers();
}
#endif // !NO_VOLUMETRICS
/**
* M201: Set max acceleration in units/s^2 for print moves (M201 X1000 Y1000)
*
* With multiple extruders use T to specify which one.
*/
inline void gcode_M201() {
GET_TARGET_EXTRUDER(201);
LOOP_XYZE(i) {
if (parser.seen(axis_codes[i])) {
const uint8_t a = i + (i == E_AXIS ? TARGET_EXTRUDER : 0);
planner.max_acceleration_mm_per_s2[a] = parser.value_axis_units((AxisEnum)a);
}
}
// steps per sq second need to be updated to agree with the units per sq second (as they are what is used in the planner)
planner.reset_acceleration_rates();
}
#if 0 // Not used for Sprinter/grbl gen6
inline void gcode_M202() {
LOOP_XYZE(i) {
if (parser.seen(axis_codes[i])) axis_travel_steps_per_sqr_second[i] = parser.value_axis_units((AxisEnum)i) * planner.axis_steps_per_mm[i];
}
}
#endif
/**
* M203: Set maximum feedrate that your machine can sustain (M203 X200 Y200 Z300 E10000) in units/sec
*
* With multiple extruders use T to specify which one.
*/
inline void gcode_M203() {
GET_TARGET_EXTRUDER(203);
LOOP_XYZE(i)
if (parser.seen(axis_codes[i])) {
const uint8_t a = i + (i == E_AXIS ? TARGET_EXTRUDER : 0);
planner.max_feedrate_mm_s[a] = parser.value_axis_units((AxisEnum)a);
}
}
/**
* M204: Set Accelerations in units/sec^2 (M204 P1200 R3000 T3000)
*
* P = Printing moves
* R = Retract only (no X, Y, Z) moves
* T = Travel (non printing) moves
*
* Also sets minimum segment time in ms (B20000) to prevent buffer under-runs and M20 minimum feedrate
*/
inline void gcode_M204() {
if (parser.seen('S')) { // Kept for legacy compatibility. Should NOT BE USED for new developments.
planner.travel_acceleration = planner.acceleration = parser.value_linear_units();
SERIAL_ECHOLNPAIR("Setting Print and Travel Acceleration: ", planner.acceleration);
}
if (parser.seen('P')) {
planner.acceleration = parser.value_linear_units();
SERIAL_ECHOLNPAIR("Setting Print Acceleration: ", planner.acceleration);
}
if (parser.seen('R')) {
planner.retract_acceleration = parser.value_linear_units();
SERIAL_ECHOLNPAIR("Setting Retract Acceleration: ", planner.retract_acceleration);
}
if (parser.seen('T')) {
planner.travel_acceleration = parser.value_linear_units();
SERIAL_ECHOLNPAIR("Setting Travel Acceleration: ", planner.travel_acceleration);
}
}
/**
* M205: Set Advanced Settings
*
* S = Min Feed Rate (units/s)
* T = Min Travel Feed Rate (units/s)
* B = Min Segment Time (µs)
* X = Max X Jerk (units/sec^2)
* Y = Max Y Jerk (units/sec^2)
* Z = Max Z Jerk (units/sec^2)
* E = Max E Jerk (units/sec^2)
*/
inline void gcode_M205() {
if (parser.seen('S')) planner.min_feedrate_mm_s = parser.value_linear_units();
if (parser.seen('T')) planner.min_travel_feedrate_mm_s = parser.value_linear_units();
if (parser.seen('B')) planner.min_segment_time_us = parser.value_ulong();
if (parser.seen('X')) planner.max_jerk[X_AXIS] = parser.value_linear_units();
if (parser.seen('Y')) planner.max_jerk[Y_AXIS] = parser.value_linear_units();
if (parser.seen('Z')) planner.max_jerk[Z_AXIS] = parser.value_linear_units();
if (parser.seen('E')) planner.max_jerk[E_AXIS] = parser.value_linear_units();
}
#if HAS_M206_COMMAND
/**
* M206: Set Additional Homing Offset (X Y Z). SCARA aliases T=X, P=Y
*
* *** @thinkyhead: I recommend deprecating M206 for SCARA in favor of M665.
* *** M206 for SCARA will remain enabled in 1.1.x for compatibility.
* *** In the next 1.2 release, it will simply be disabled by default.
*/
inline void gcode_M206() {
LOOP_XYZ(i)
if (parser.seen(axis_codes[i]))
set_home_offset((AxisEnum)i, parser.value_linear_units());
#if ENABLED(MORGAN_SCARA)
if (parser.seen('T')) set_home_offset(A_AXIS, parser.value_float()); // Theta
if (parser.seen('P')) set_home_offset(B_AXIS, parser.value_float()); // Psi
#endif
report_current_position();
}
#endif // HAS_M206_COMMAND
#if ENABLED(DELTA)
/**
* M665: Set delta configurations
*
* H = delta height
* L = diagonal rod
* R = delta radius
* S = segments per second
* B = delta calibration radius
* X = Alpha (Tower 1) angle trim
* Y = Beta (Tower 2) angle trim
* Z = Rotate A and B by this angle
*/
inline void gcode_M665() {
if (parser.seen('H')) delta_height = parser.value_linear_units();
if (parser.seen('L')) delta_diagonal_rod = parser.value_linear_units();
if (parser.seen('R')) delta_radius = parser.value_linear_units();
if (parser.seen('S')) delta_segments_per_second = parser.value_float();
if (parser.seen('B')) delta_calibration_radius = parser.value_float();
if (parser.seen('X')) delta_tower_angle_trim[A_AXIS] = parser.value_float();
if (parser.seen('Y')) delta_tower_angle_trim[B_AXIS] = parser.value_float();
if (parser.seen('Z')) delta_tower_angle_trim[C_AXIS] = parser.value_float();
recalc_delta_settings();
}
/**
* M666: Set delta endstop adjustment
*/
inline void gcode_M666() {
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) {
SERIAL_ECHOLNPGM(">>> gcode_M666");
}
#endif
LOOP_XYZ(i) {
if (parser.seen(axis_codes[i])) {
if (parser.value_linear_units() * Z_HOME_DIR <= 0)
delta_endstop_adj[i] = parser.value_linear_units();
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) {
SERIAL_ECHOPAIR("delta_endstop_adj[", axis_codes[i]);
SERIAL_ECHOLNPAIR("] = ", delta_endstop_adj[i]);
}
#endif
}
}
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) {
SERIAL_ECHOLNPGM("<<< gcode_M666");
}
#endif
}
#elif IS_SCARA
/**
* M665: Set SCARA settings
*
* Parameters:
*
* S[segments-per-second] - Segments-per-second
* P[theta-psi-offset] - Theta-Psi offset, added to the shoulder (A/X) angle
* T[theta-offset] - Theta offset, added to the elbow (B/Y) angle
*
* A, P, and X are all aliases for the shoulder angle
* B, T, and Y are all aliases for the elbow angle
*/
inline void gcode_M665() {
if (parser.seen('S')) delta_segments_per_second = parser.value_float();
const bool hasA = parser.seen('A'), hasP = parser.seen('P'), hasX = parser.seen('X');
const uint8_t sumAPX = hasA + hasP + hasX;
if (sumAPX == 1)
home_offset[A_AXIS] = parser.value_float();
else if (sumAPX > 1) {
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM("Only one of A, P, or X is allowed.");
return;
}
const bool hasB = parser.seen('B'), hasT = parser.seen('T'), hasY = parser.seen('Y');
const uint8_t sumBTY = hasB + hasT + hasY;
if (sumBTY == 1)
home_offset[B_AXIS] = parser.value_float();
else if (sumBTY > 1) {
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM("Only one of B, T, or Y is allowed.");
return;
}
}
#elif ENABLED(X_DUAL_ENDSTOPS) || ENABLED(Y_DUAL_ENDSTOPS) || ENABLED(Z_DUAL_ENDSTOPS)
/**
* M666: For Z Dual Endstop setup, set z axis offset to the z2 axis.
*/
inline void gcode_M666() {
SERIAL_ECHOPGM("Dual Endstop Adjustment (mm): ");
#if ENABLED(X_DUAL_ENDSTOPS)
if (parser.seen('X')) x_endstop_adj = parser.value_linear_units();
SERIAL_ECHOPAIR(" X", x_endstop_adj);
#endif
#if ENABLED(Y_DUAL_ENDSTOPS)
if (parser.seen('Y')) y_endstop_adj = parser.value_linear_units();
SERIAL_ECHOPAIR(" Y", y_endstop_adj);
#endif
#if ENABLED(Z_DUAL_ENDSTOPS)
if (parser.seen('Z')) z_endstop_adj = parser.value_linear_units();
SERIAL_ECHOPAIR(" Z", z_endstop_adj);
#endif
SERIAL_EOL();
}
#endif // !DELTA && Z_DUAL_ENDSTOPS
#if ENABLED(FWRETRACT)
/**
* M207: Set firmware retraction values
*
* S[+units] retract_length
* W[+units] swap_retract_length (multi-extruder)
* F[units/min] retract_feedrate_mm_s
* Z[units] retract_zlift
*/
inline void gcode_M207() {
if (parser.seen('S')) fwretract.retract_length = parser.value_axis_units(E_AXIS);
if (parser.seen('F')) fwretract.retract_feedrate_mm_s = MMM_TO_MMS(parser.value_axis_units(E_AXIS));
if (parser.seen('Z')) fwretract.retract_zlift = parser.value_linear_units();
if (parser.seen('W')) fwretract.swap_retract_length = parser.value_axis_units(E_AXIS);
}
/**
* M208: Set firmware un-retraction values
*
* S[+units] retract_recover_length (in addition to M207 S*)
* W[+units] swap_retract_recover_length (multi-extruder)
* F[units/min] retract_recover_feedrate_mm_s
* R[units/min] swap_retract_recover_feedrate_mm_s
*/
inline void gcode_M208() {
if (parser.seen('S')) fwretract.retract_recover_length = parser.value_axis_units(E_AXIS);
if (parser.seen('F')) fwretract.retract_recover_feedrate_mm_s = MMM_TO_MMS(parser.value_axis_units(E_AXIS));
if (parser.seen('R')) fwretract.swap_retract_recover_feedrate_mm_s = MMM_TO_MMS(parser.value_axis_units(E_AXIS));
if (parser.seen('W')) fwretract.swap_retract_recover_length = parser.value_axis_units(E_AXIS);
}
/**
* M209: Enable automatic retract (M209 S1)
* For slicers that don't support G10/11, reversed extrude-only
* moves will be classified as retraction.
*/
inline void gcode_M209() {
if (MIN_AUTORETRACT <= MAX_AUTORETRACT) {
if (parser.seen('S')) {
fwretract.autoretract_enabled = parser.value_bool();
for (uint8_t i = 0; i < EXTRUDERS; i++) fwretract.retracted[i] = false;
}
}
}
#endif // FWRETRACT
/**
* M211: Enable, Disable, and/or Report software endstops
*
* Usage: M211 S1 to enable, M211 S0 to disable, M211 alone for report
*/
inline void gcode_M211() {
SERIAL_ECHO_START();
#if HAS_SOFTWARE_ENDSTOPS
if (parser.seen('S')) soft_endstops_enabled = parser.value_bool();
SERIAL_ECHOPGM(MSG_SOFT_ENDSTOPS);
serialprintPGM(soft_endstops_enabled ? PSTR(MSG_ON) : PSTR(MSG_OFF));
#else
SERIAL_ECHOPGM(MSG_SOFT_ENDSTOPS);
SERIAL_ECHOPGM(MSG_OFF);
#endif
SERIAL_ECHOPGM(MSG_SOFT_MIN);
SERIAL_ECHOPAIR( MSG_X, LOGICAL_X_POSITION(soft_endstop_min[X_AXIS]));
SERIAL_ECHOPAIR(" " MSG_Y, LOGICAL_Y_POSITION(soft_endstop_min[Y_AXIS]));
SERIAL_ECHOPAIR(" " MSG_Z, LOGICAL_Z_POSITION(soft_endstop_min[Z_AXIS]));
SERIAL_ECHOPGM(MSG_SOFT_MAX);
SERIAL_ECHOPAIR( MSG_X, LOGICAL_X_POSITION(soft_endstop_max[X_AXIS]));
SERIAL_ECHOPAIR(" " MSG_Y, LOGICAL_Y_POSITION(soft_endstop_max[Y_AXIS]));
SERIAL_ECHOLNPAIR(" " MSG_Z, LOGICAL_Z_POSITION(soft_endstop_max[Z_AXIS]));
}
#if HOTENDS > 1
/**
* M218 - set hotend offset (in linear units)
*
* T
* X
* Y
* Z - Available with DUAL_X_CARRIAGE and SWITCHING_NOZZLE
*/
inline void gcode_M218() {
if (get_target_extruder_from_command(218) || target_extruder == 0) return;
if (parser.seenval('X')) hotend_offset[X_AXIS][target_extruder] = parser.value_linear_units();
if (parser.seenval('Y')) hotend_offset[Y_AXIS][target_extruder] = parser.value_linear_units();
#if ENABLED(DUAL_X_CARRIAGE) || ENABLED(SWITCHING_NOZZLE) || ENABLED(PARKING_EXTRUDER)
if (parser.seenval('Z')) hotend_offset[Z_AXIS][target_extruder] = parser.value_linear_units();
#endif
SERIAL_ECHO_START();
SERIAL_ECHOPGM(MSG_HOTEND_OFFSET);
HOTEND_LOOP() {
SERIAL_CHAR(' ');
SERIAL_ECHO(hotend_offset[X_AXIS][e]);
SERIAL_CHAR(',');
SERIAL_ECHO(hotend_offset[Y_AXIS][e]);
#if ENABLED(DUAL_X_CARRIAGE) || ENABLED(SWITCHING_NOZZLE) || ENABLED(PARKING_EXTRUDER)
SERIAL_CHAR(',');
SERIAL_ECHO(hotend_offset[Z_AXIS][e]);
#endif
}
SERIAL_EOL();
}
#endif // HOTENDS > 1
/**
* M220: Set speed percentage factor, aka "Feed Rate" (M220 S95)
*/
inline void gcode_M220() {
if (parser.seenval('S')) feedrate_percentage = parser.value_int();
}
/**
* M221: Set extrusion percentage (M221 T0 S95)
*/
inline void gcode_M221() {
if (get_target_extruder_from_command(221)) return;
if (parser.seenval('S')) {
planner.flow_percentage[target_extruder] = parser.value_int();
planner.refresh_e_factor(target_extruder);
}
}
/**
* M226: Wait until the specified pin reaches the state required (M226 P S)
*/
inline void gcode_M226() {
if (parser.seen('P')) {
const int pin_number = parser.value_int(),
pin_state = parser.intval('S', -1); // required pin state - default is inverted
if (WITHIN(pin_state, -1, 1) && pin_number > -1 && !pin_is_protected(pin_number)) {
int target = LOW;
stepper.synchronize();
pinMode(pin_number, INPUT);
switch (pin_state) {
case 1:
target = HIGH;
break;
case 0:
target = LOW;
break;
case -1:
target = !digitalRead(pin_number);
break;
}
while (digitalRead(pin_number) != target) idle();
} // pin_state -1 0 1 && pin_number > -1
} // parser.seen('P')
}
#if ENABLED(EXPERIMENTAL_I2CBUS)
/**
* M260: Send data to a I2C slave device
*
* This is a PoC, the formating and arguments for the GCODE will
* change to be more compatible, the current proposal is:
*
* M260 A ; Sets the I2C slave address the data will be sent to
*
* M260 B
* M260 B
* M260 B
*
* M260 S1 ; Send the buffered data and reset the buffer
* M260 R1 ; Reset the buffer without sending data
*
*/
inline void gcode_M260() {
// Set the target address
if (parser.seen('A')) i2c.address(parser.value_byte());
// Add a new byte to the buffer
if (parser.seen('B')) i2c.addbyte(parser.value_byte());
// Flush the buffer to the bus
if (parser.seen('S')) i2c.send();
// Reset and rewind the buffer
else if (parser.seen('R')) i2c.reset();
}
/**
* M261: Request X bytes from I2C slave device
*
* Usage: M261 A B
*/
inline void gcode_M261() {
if (parser.seen('A')) i2c.address(parser.value_byte());
uint8_t bytes = parser.byteval('B', 1);
if (i2c.addr && bytes && bytes <= TWIBUS_BUFFER_SIZE) {
i2c.relay(bytes);
}
else {
SERIAL_ERROR_START();
SERIAL_ERRORLN("Bad i2c request");
}
}
#endif // EXPERIMENTAL_I2CBUS
#if HAS_SERVOS
/**
* M280: Get or set servo position. P [S]
*/
inline void gcode_M280() {
if (!parser.seen('P')) return;
const int servo_index = parser.value_int();
if (WITHIN(servo_index, 0, NUM_SERVOS - 1)) {
if (parser.seen('S'))
MOVE_SERVO(servo_index, parser.value_int());
else {
SERIAL_ECHO_START();
SERIAL_ECHOPAIR(" Servo ", servo_index);
SERIAL_ECHOLNPAIR(": ", servo[servo_index].read());
}
}
else {
SERIAL_ERROR_START();
SERIAL_ECHOPAIR("Servo ", servo_index);
SERIAL_ECHOLNPGM(" out of range");
}
}
#endif // HAS_SERVOS
#if ENABLED(BABYSTEPPING)
#if ENABLED(BABYSTEP_ZPROBE_OFFSET)
FORCE_INLINE void mod_zprobe_zoffset(const float &offs) {
zprobe_zoffset += offs;
SERIAL_ECHO_START();
SERIAL_ECHOLNPAIR(MSG_PROBE_Z_OFFSET ": ", zprobe_zoffset);
}
#endif
/**
* M290: Babystepping
*/
inline void gcode_M290() {
#if ENABLED(BABYSTEP_XY)
for (uint8_t a = X_AXIS; a <= Z_AXIS; a++)
if (parser.seenval(axis_codes[a]) || (a == Z_AXIS && parser.seenval('S'))) {
const float offs = constrain(parser.value_axis_units((AxisEnum)a), -2, 2);
thermalManager.babystep_axis((AxisEnum)a, offs * planner.axis_steps_per_mm[a]);
#if ENABLED(BABYSTEP_ZPROBE_OFFSET)
if (a == Z_AXIS && (!parser.seen('P') || parser.value_bool())) mod_zprobe_zoffset(offs);
#endif
}
#else
if (parser.seenval('Z') || parser.seenval('S')) {
const float offs = constrain(parser.value_axis_units(Z_AXIS), -2, 2);
thermalManager.babystep_axis(Z_AXIS, offs * planner.axis_steps_per_mm[Z_AXIS]);
#if ENABLED(BABYSTEP_ZPROBE_OFFSET)
if (!parser.seen('P') || parser.value_bool()) mod_zprobe_zoffset(offs);
#endif
}
#endif
}
#endif // BABYSTEPPING
#if HAS_BUZZER
/**
* M300: Play beep sound S P
*/
inline void gcode_M300() {
uint16_t const frequency = parser.ushortval('S', 260);
uint16_t duration = parser.ushortval('P', 1000);
// Limits the tone duration to 0-5 seconds.
NOMORE(duration, 5000);
BUZZ(duration, frequency);
}
#endif // HAS_BUZZER
#if ENABLED(PIDTEMP)
/**
* M301: Set PID parameters P I D (and optionally C, L)
*
* P[float] Kp term
* I[float] Ki term (unscaled)
* D[float] Kd term (unscaled)
*
* With PID_EXTRUSION_SCALING:
*
* C[float] Kc term
* L[float] LPQ length
*/
inline void gcode_M301() {
// multi-extruder PID patch: M301 updates or prints a single extruder's PID values
// default behaviour (omitting E parameter) is to update for extruder 0 only
const uint8_t e = parser.byteval('E'); // extruder being updated
if (e < HOTENDS) { // catch bad input value
if (parser.seen('P')) PID_PARAM(Kp, e) = parser.value_float();
if (parser.seen('I')) PID_PARAM(Ki, e) = scalePID_i(parser.value_float());
if (parser.seen('D')) PID_PARAM(Kd, e) = scalePID_d(parser.value_float());
#if ENABLED(PID_EXTRUSION_SCALING)
if (parser.seen('C')) PID_PARAM(Kc, e) = parser.value_float();
if (parser.seen('L')) lpq_len = parser.value_float();
NOMORE(lpq_len, LPQ_MAX_LEN);
#endif
thermalManager.updatePID();
SERIAL_ECHO_START();
#if ENABLED(PID_PARAMS_PER_HOTEND)
SERIAL_ECHOPAIR(" e:", e); // specify extruder in serial output
#endif // PID_PARAMS_PER_HOTEND
SERIAL_ECHOPAIR(" p:", PID_PARAM(Kp, e));
SERIAL_ECHOPAIR(" i:", unscalePID_i(PID_PARAM(Ki, e)));
SERIAL_ECHOPAIR(" d:", unscalePID_d(PID_PARAM(Kd, e)));
#if ENABLED(PID_EXTRUSION_SCALING)
//Kc does not have scaling applied above, or in resetting defaults
SERIAL_ECHOPAIR(" c:", PID_PARAM(Kc, e));
#endif
SERIAL_EOL();
}
else {
SERIAL_ERROR_START();
SERIAL_ERRORLN(MSG_INVALID_EXTRUDER);
}
}
#endif // PIDTEMP
#if ENABLED(PIDTEMPBED)
inline void gcode_M304() {
if (parser.seen('P')) thermalManager.bedKp = parser.value_float();
if (parser.seen('I')) thermalManager.bedKi = scalePID_i(parser.value_float());
if (parser.seen('D')) thermalManager.bedKd = scalePID_d(parser.value_float());
SERIAL_ECHO_START();
SERIAL_ECHOPAIR(" p:", thermalManager.bedKp);
SERIAL_ECHOPAIR(" i:", unscalePID_i(thermalManager.bedKi));
SERIAL_ECHOLNPAIR(" d:", unscalePID_d(thermalManager.bedKd));
}
#endif // PIDTEMPBED
#if defined(CHDK) || HAS_PHOTOGRAPH
/**
* M240: Trigger a camera by emulating a Canon RC-1
* See http://www.doc-diy.net/photo/rc-1_hacked/
*/
inline void gcode_M240() {
#ifdef CHDK
OUT_WRITE(CHDK, HIGH);
chdkHigh = millis();
chdkActive = true;
#elif HAS_PHOTOGRAPH
const uint8_t NUM_PULSES = 16;
const float PULSE_LENGTH = 0.01524;
for (int i = 0; i < NUM_PULSES; i++) {
WRITE(PHOTOGRAPH_PIN, HIGH);
_delay_ms(PULSE_LENGTH);
WRITE(PHOTOGRAPH_PIN, LOW);
_delay_ms(PULSE_LENGTH);
}
delay(7.33);
for (int i = 0; i < NUM_PULSES; i++) {
WRITE(PHOTOGRAPH_PIN, HIGH);
_delay_ms(PULSE_LENGTH);
WRITE(PHOTOGRAPH_PIN, LOW);
_delay_ms(PULSE_LENGTH);
}
#endif // !CHDK && HAS_PHOTOGRAPH
}
#endif // CHDK || PHOTOGRAPH_PIN
#if HAS_LCD_CONTRAST
/**
* M250: Read and optionally set the LCD contrast
*/
inline void gcode_M250() {
if (parser.seen('C')) set_lcd_contrast(parser.value_int());
SERIAL_PROTOCOLPGM("lcd contrast value: ");
SERIAL_PROTOCOL(lcd_contrast);
SERIAL_EOL();
}
#endif // HAS_LCD_CONTRAST
#if ENABLED(PREVENT_COLD_EXTRUSION)
/**
* M302: Allow cold extrudes, or set the minimum extrude temperature
*
* S sets the minimum extrude temperature
* P enables (1) or disables (0) cold extrusion
*
* Examples:
*
* M302 ; report current cold extrusion state
* M302 P0 ; enable cold extrusion checking
* M302 P1 ; disables cold extrusion checking
* M302 S0 ; always allow extrusion (disables checking)
* M302 S170 ; only allow extrusion above 170
* M302 S170 P1 ; set min extrude temp to 170 but leave disabled
*/
inline void gcode_M302() {
const bool seen_S = parser.seen('S');
if (seen_S) {
thermalManager.extrude_min_temp = parser.value_celsius();
thermalManager.allow_cold_extrude = (thermalManager.extrude_min_temp == 0);
}
if (parser.seen('P'))
thermalManager.allow_cold_extrude = (thermalManager.extrude_min_temp == 0) || parser.value_bool();
else if (!seen_S) {
// Report current state
SERIAL_ECHO_START();
SERIAL_ECHOPAIR("Cold extrudes are ", (thermalManager.allow_cold_extrude ? "en" : "dis"));
SERIAL_ECHOPAIR("abled (min temp ", thermalManager.extrude_min_temp);
SERIAL_ECHOLNPGM("C)");
}
}
#endif // PREVENT_COLD_EXTRUSION
/**
* M303: PID relay autotune
*
* S sets the target temperature. (default 150C / 70C)
* E (-1 for the bed) (default 0)
* C
* U with a non-zero value will apply the result to current settings
*/
inline void gcode_M303() {
#if HAS_PID_HEATING
const int e = parser.intval('E'), c = parser.intval('C', 5);
const bool u = parser.boolval('U');
int16_t temp = parser.celsiusval('S', e < 0 ? 70 : 150);
if (WITHIN(e, 0, HOTENDS - 1))
target_extruder = e;
#if DISABLED(BUSY_WHILE_HEATING)
KEEPALIVE_STATE(NOT_BUSY);
#endif
thermalManager.PID_autotune(temp, e, c, u);
#if DISABLED(BUSY_WHILE_HEATING)
KEEPALIVE_STATE(IN_HANDLER);
#endif
#else
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_ERR_M303_DISABLED);
#endif
}
#if ENABLED(MORGAN_SCARA)
bool SCARA_move_to_cal(const uint8_t delta_a, const uint8_t delta_b) {
if (IsRunning()) {
forward_kinematics_SCARA(delta_a, delta_b);
destination[X_AXIS] = cartes[X_AXIS];
destination[Y_AXIS] = cartes[Y_AXIS];
destination[Z_AXIS] = current_position[Z_AXIS];
prepare_move_to_destination();
return true;
}
return false;
}
/**
* M360: SCARA calibration: Move to cal-position ThetaA (0 deg calibration)
*/
inline bool gcode_M360() {
SERIAL_ECHOLNPGM(" Cal: Theta 0");
return SCARA_move_to_cal(0, 120);
}
/**
* M361: SCARA calibration: Move to cal-position ThetaB (90 deg calibration - steps per degree)
*/
inline bool gcode_M361() {
SERIAL_ECHOLNPGM(" Cal: Theta 90");
return SCARA_move_to_cal(90, 130);
}
/**
* M362: SCARA calibration: Move to cal-position PsiA (0 deg calibration)
*/
inline bool gcode_M362() {
SERIAL_ECHOLNPGM(" Cal: Psi 0");
return SCARA_move_to_cal(60, 180);
}
/**
* M363: SCARA calibration: Move to cal-position PsiB (90 deg calibration - steps per degree)
*/
inline bool gcode_M363() {
SERIAL_ECHOLNPGM(" Cal: Psi 90");
return SCARA_move_to_cal(50, 90);
}
/**
* M364: SCARA calibration: Move to cal-position PsiC (90 deg to Theta calibration position)
*/
inline bool gcode_M364() {
SERIAL_ECHOLNPGM(" Cal: Theta-Psi 90");
return SCARA_move_to_cal(45, 135);
}
#endif // SCARA
#if ENABLED(EXT_SOLENOID)
void enable_solenoid(const uint8_t num) {
switch (num) {
case 0:
OUT_WRITE(SOL0_PIN, HIGH);
break;
#if HAS_SOLENOID_1 && EXTRUDERS > 1
case 1:
OUT_WRITE(SOL1_PIN, HIGH);
break;
#endif
#if HAS_SOLENOID_2 && EXTRUDERS > 2
case 2:
OUT_WRITE(SOL2_PIN, HIGH);
break;
#endif
#if HAS_SOLENOID_3 && EXTRUDERS > 3
case 3:
OUT_WRITE(SOL3_PIN, HIGH);
break;
#endif
#if HAS_SOLENOID_4 && EXTRUDERS > 4
case 4:
OUT_WRITE(SOL4_PIN, HIGH);
break;
#endif
default:
SERIAL_ECHO_START();
SERIAL_ECHOLNPGM(MSG_INVALID_SOLENOID);
break;
}
}
void enable_solenoid_on_active_extruder() { enable_solenoid(active_extruder); }
void disable_all_solenoids() {
OUT_WRITE(SOL0_PIN, LOW);
#if HAS_SOLENOID_1 && EXTRUDERS > 1
OUT_WRITE(SOL1_PIN, LOW);
#endif
#if HAS_SOLENOID_2 && EXTRUDERS > 2
OUT_WRITE(SOL2_PIN, LOW);
#endif
#if HAS_SOLENOID_3 && EXTRUDERS > 3
OUT_WRITE(SOL3_PIN, LOW);
#endif
#if HAS_SOLENOID_4 && EXTRUDERS > 4
OUT_WRITE(SOL4_PIN, LOW);
#endif
}
/**
* M380: Enable solenoid on the active extruder
*/
inline void gcode_M380() { enable_solenoid_on_active_extruder(); }
/**
* M381: Disable all solenoids
*/
inline void gcode_M381() { disable_all_solenoids(); }
#endif // EXT_SOLENOID
/**
* M400: Finish all moves
*/
inline void gcode_M400() { stepper.synchronize(); }
#if HAS_BED_PROBE
/**
* M401: Engage Z Servo endstop if available
*/
inline void gcode_M401() { DEPLOY_PROBE(); }
/**
* M402: Retract Z Servo endstop if enabled
*/
inline void gcode_M402() { STOW_PROBE(); }
#endif // HAS_BED_PROBE
#if ENABLED(FILAMENT_WIDTH_SENSOR)
/**
* M404: Display or set (in current units) the nominal filament width (3mm, 1.75mm ) W<3.0>
*/
inline void gcode_M404() {
if (parser.seen('W')) {
filament_width_nominal = parser.value_linear_units();
planner.volumetric_area_nominal = CIRCLE_AREA(filament_width_nominal * 0.5);
}
else {
SERIAL_PROTOCOLPGM("Filament dia (nominal mm):");
SERIAL_PROTOCOLLN(filament_width_nominal);
}
}
/**
* M405: Turn on filament sensor for control
*/
inline void gcode_M405() {
// This is technically a linear measurement, but since it's quantized to centimeters and is a different
// unit than everything else, it uses parser.value_byte() instead of parser.value_linear_units().
if (parser.seen('D')) {
meas_delay_cm = parser.value_byte();
NOMORE(meas_delay_cm, MAX_MEASUREMENT_DELAY);
}
if (filwidth_delay_index[1] == -1) { // Initialize the ring buffer if not done since startup
const int8_t temp_ratio = thermalManager.widthFil_to_size_ratio();
for (uint8_t i = 0; i < COUNT(measurement_delay); ++i)
measurement_delay[i] = temp_ratio;
filwidth_delay_index[0] = filwidth_delay_index[1] = 0;
}
filament_sensor = true;
}
/**
* M406: Turn off filament sensor for control
*/
inline void gcode_M406() {
filament_sensor = false;
planner.calculate_volumetric_multipliers(); // Restore correct 'volumetric_multiplier' value
}
/**
* M407: Get measured filament diameter on serial output
*/
inline void gcode_M407() {
SERIAL_PROTOCOLPGM("Filament dia (measured mm):");
SERIAL_PROTOCOLLN(filament_width_meas);
}
#endif // FILAMENT_WIDTH_SENSOR
void quickstop_stepper() {
stepper.quick_stop();
stepper.synchronize();
set_current_from_steppers_for_axis(ALL_AXES);
SYNC_PLAN_POSITION_KINEMATIC();
}
#if HAS_LEVELING
/**
* M420: Enable/Disable Bed Leveling and/or set the Z fade height.
*
* S[bool] Turns leveling on or off
* Z[height] Sets the Z fade height (0 or none to disable)
* V[bool] Verbose - Print the leveling grid
*
* With AUTO_BED_LEVELING_UBL only:
*
* L[index] Load UBL mesh from index (0 is default)
*/
inline void gcode_M420() {
const float oldpos[] = { current_position[X_AXIS], current_position[Y_AXIS], current_position[Z_AXIS] };
#if ENABLED(AUTO_BED_LEVELING_UBL)
// L to load a mesh from the EEPROM
if (parser.seen('L')) {
#if ENABLED(EEPROM_SETTINGS)
const int8_t storage_slot = parser.has_value() ? parser.value_int() : ubl.storage_slot;
const int16_t a = settings.calc_num_meshes();
if (!a) {
SERIAL_PROTOCOLLNPGM("?EEPROM storage not available.");
return;
}
if (!WITHIN(storage_slot, 0, a - 1)) {
SERIAL_PROTOCOLLNPGM("?Invalid storage slot.");
SERIAL_PROTOCOLLNPAIR("?Use 0 to ", a - 1);
return;
}
settings.load_mesh(storage_slot);
ubl.storage_slot = storage_slot;
#else
SERIAL_PROTOCOLLNPGM("?EEPROM storage not available.");
return;
#endif
}
// L to load a mesh from the EEPROM
if (parser.seen('L') || parser.seen('V')) {
ubl.display_map(0); // Currently only supports one map type
SERIAL_ECHOLNPAIR("ubl.mesh_is_valid = ", ubl.mesh_is_valid());
SERIAL_ECHOLNPAIR("ubl.storage_slot = ", ubl.storage_slot);
}
#endif // AUTO_BED_LEVELING_UBL
// V to print the matrix or mesh
if (parser.seen('V')) {
#if ABL_PLANAR
planner.bed_level_matrix.debug(PSTR("Bed Level Correction Matrix:"));
#else
if (leveling_is_valid()) {
#if ENABLED(AUTO_BED_LEVELING_BILINEAR)
print_bilinear_leveling_grid();
#if ENABLED(ABL_BILINEAR_SUBDIVISION)
print_bilinear_leveling_grid_virt();
#endif
#elif ENABLED(MESH_BED_LEVELING)
SERIAL_ECHOLNPGM("Mesh Bed Level data:");
mbl.report_mesh();
#endif
}
#endif
}
#if ENABLED(ENABLE_LEVELING_FADE_HEIGHT)
if (parser.seen('Z')) set_z_fade_height(parser.value_linear_units(), false);
#endif
bool to_enable = false;
if (parser.seen('S')) {
to_enable = parser.value_bool();
set_bed_leveling_enabled(to_enable);
}
const bool new_status = planner.leveling_active;
if (to_enable && !new_status) {
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_ERR_M420_FAILED);
}
SERIAL_ECHO_START();
SERIAL_ECHOLNPAIR("Bed Leveling ", new_status ? MSG_ON : MSG_OFF);
#if ENABLED(ENABLE_LEVELING_FADE_HEIGHT)
SERIAL_ECHO_START();
SERIAL_ECHOPGM("Fade Height ");
if (planner.z_fade_height > 0.0)
SERIAL_ECHOLN(planner.z_fade_height);
else
SERIAL_ECHOLNPGM(MSG_OFF);
#endif
// Report change in position
if (memcmp(oldpos, current_position, sizeof(oldpos)))
report_current_position();
}
#endif
#if ENABLED(MESH_BED_LEVELING)
/**
* M421: Set a single Mesh Bed Leveling Z coordinate
*
* Usage:
* M421 X Y Z
* M421 X Y Q
* M421 I J Z
* M421 I J Q
*/
inline void gcode_M421() {
const bool hasX = parser.seen('X'), hasI = parser.seen('I');
const int8_t ix = hasI ? parser.value_int() : hasX ? mbl.probe_index_x(parser.value_linear_units()) : -1;
const bool hasY = parser.seen('Y'), hasJ = parser.seen('J');
const int8_t iy = hasJ ? parser.value_int() : hasY ? mbl.probe_index_y(parser.value_linear_units()) : -1;
const bool hasZ = parser.seen('Z'), hasQ = !hasZ && parser.seen('Q');
if (int(hasI && hasJ) + int(hasX && hasY) != 1 || !(hasZ || hasQ)) {
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_ERR_M421_PARAMETERS);
}
else if (ix < 0 || iy < 0) {
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_ERR_MESH_XY);
}
else
mbl.set_z(ix, iy, parser.value_linear_units() + (hasQ ? mbl.z_values[ix][iy] : 0));
}
#elif ENABLED(AUTO_BED_LEVELING_BILINEAR)
/**
* M421: Set a single Mesh Bed Leveling Z coordinate
*
* Usage:
* M421 I J Z
* M421 I J Q
*/
inline void gcode_M421() {
int8_t ix = parser.intval('I', -1), iy = parser.intval('J', -1);
const bool hasI = ix >= 0,
hasJ = iy >= 0,
hasZ = parser.seen('Z'),
hasQ = !hasZ && parser.seen('Q');
if (!hasI || !hasJ || !(hasZ || hasQ)) {
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_ERR_M421_PARAMETERS);
}
else if (!WITHIN(ix, 0, GRID_MAX_POINTS_X - 1) || !WITHIN(iy, 0, GRID_MAX_POINTS_Y - 1)) {
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_ERR_MESH_XY);
}
else {
z_values[ix][iy] = parser.value_linear_units() + (hasQ ? z_values[ix][iy] : 0);
#if ENABLED(ABL_BILINEAR_SUBDIVISION)
bed_level_virt_interpolate();
#endif
}
}
#elif ENABLED(AUTO_BED_LEVELING_UBL)
/**
* M421: Set a single Mesh Bed Leveling Z coordinate
*
* Usage:
* M421 I J Z
* M421 I J Q
* M421 C Z
* M421 C Q
*/
inline void gcode_M421() {
int8_t ix = parser.intval('I', -1), iy = parser.intval('J', -1);
const bool hasI = ix >= 0,
hasJ = iy >= 0,
hasC = parser.seen('C'),
hasZ = parser.seen('Z'),
hasQ = !hasZ && parser.seen('Q');
if (hasC) {
const mesh_index_pair location = ubl.find_closest_mesh_point_of_type(REAL, current_position[X_AXIS], current_position[Y_AXIS], USE_NOZZLE_AS_REFERENCE, NULL);
ix = location.x_index;
iy = location.y_index;
}
if (int(hasC) + int(hasI && hasJ) != 1 || !(hasZ || hasQ)) {
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_ERR_M421_PARAMETERS);
}
else if (!WITHIN(ix, 0, GRID_MAX_POINTS_X - 1) || !WITHIN(iy, 0, GRID_MAX_POINTS_Y - 1)) {
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_ERR_MESH_XY);
}
else
ubl.z_values[ix][iy] = parser.value_linear_units() + (hasQ ? ubl.z_values[ix][iy] : 0);
}
#endif // AUTO_BED_LEVELING_UBL
#if HAS_M206_COMMAND
/**
* M428: Set home_offset based on the distance between the
* current_position and the nearest "reference point."
* If an axis is past center its endstop position
* is the reference-point. Otherwise it uses 0. This allows
* the Z offset to be set near the bed when using a max endstop.
*
* M428 can't be used more than 2cm away from 0 or an endstop.
*
* Use M206 to set these values directly.
*/
inline void gcode_M428() {
if (axis_unhomed_error()) return;
float diff[XYZ];
LOOP_XYZ(i) {
diff[i] = base_home_pos((AxisEnum)i) - current_position[i];
if (!WITHIN(diff[i], -20, 20) && home_dir((AxisEnum)i) > 0)
diff[i] = -current_position[i];
if (!WITHIN(diff[i], -20, 20)) {
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_ERR_M428_TOO_FAR);
LCD_ALERTMESSAGEPGM("Err: Too far!");
BUZZ(200, 40);
return;
}
}
LOOP_XYZ(i) set_home_offset((AxisEnum)i, diff[i]);
report_current_position();
LCD_MESSAGEPGM(MSG_HOME_OFFSETS_APPLIED);
BUZZ(100, 659);
BUZZ(100, 698);
}
#endif // HAS_M206_COMMAND
/**
* M500: Store settings in EEPROM
*/
inline void gcode_M500() {
(void)settings.save();
}
/**
* M501: Read settings from EEPROM
*/
inline void gcode_M501() {
(void)settings.load();
}
/**
* M502: Revert to default settings
*/
inline void gcode_M502() {
(void)settings.reset();
}
#if DISABLED(DISABLE_M503)
/**
* M503: print settings currently in memory
*/
inline void gcode_M503() {
(void)settings.report(parser.seen('S') && !parser.value_bool());
}
#endif
#if ENABLED(EEPROM_SETTINGS)
/**
* M504: Validate EEPROM Contents
*/
inline void gcode_M504() {
if (settings.validate()) {
SERIAL_ECHO_START();
SERIAL_ECHOLNPGM("EEPROM OK");
}
}
#endif
#if ENABLED(ABORT_ON_ENDSTOP_HIT_FEATURE_ENABLED)
/**
* M540: Set whether SD card print should abort on endstop hit (M540 S<0|1>)
*/
inline void gcode_M540() {
if (parser.seen('S')) stepper.abort_on_endstop_hit = parser.value_bool();
}
#endif // ABORT_ON_ENDSTOP_HIT_FEATURE_ENABLED
#if HAS_BED_PROBE
inline void gcode_M851() {
SERIAL_ECHO_START();
SERIAL_ECHOPGM(MSG_PROBE_Z_OFFSET);
if (parser.seen('Z')) {
const float value = parser.value_linear_units();
if (!WITHIN(value, Z_PROBE_OFFSET_RANGE_MIN, Z_PROBE_OFFSET_RANGE_MAX)) {
SERIAL_ECHOLNPGM(" " MSG_Z_MIN " " STRINGIFY(Z_PROBE_OFFSET_RANGE_MIN) " " MSG_Z_MAX " " STRINGIFY(Z_PROBE_OFFSET_RANGE_MAX));
return;
}
zprobe_zoffset = value;
}
SERIAL_ECHOLNPAIR(": ", zprobe_zoffset);
}
#endif // HAS_BED_PROBE
#if ENABLED(SKEW_CORRECTION_GCODE)
/**
* M852: Get or set the machine skew factors. Reports current values with no arguments.
*
* S[xy_factor] - Alias for 'I'
* I[xy_factor] - New XY skew factor
* J[xz_factor] - New XZ skew factor
* K[yz_factor] - New YZ skew factor
*/
inline void gcode_M852() {
uint8_t ijk = 0, badval = 0, setval = 0;
if (parser.seen('I') || parser.seen('S')) {
++ijk;
const float value = parser.value_linear_units();
if (WITHIN(value, SKEW_FACTOR_MIN, SKEW_FACTOR_MAX)) {
if (planner.xy_skew_factor != value) {
planner.xy_skew_factor = value;
++setval;
}
}
else
++badval;
}
#if ENABLED(SKEW_CORRECTION_FOR_Z)
if (parser.seen('J')) {
++ijk;
const float value = parser.value_linear_units();
if (WITHIN(value, SKEW_FACTOR_MIN, SKEW_FACTOR_MAX)) {
if (planner.xz_skew_factor != value) {
planner.xz_skew_factor = value;
++setval;
}
}
else
++badval;
}
if (parser.seen('K')) {
++ijk;
const float value = parser.value_linear_units();
if (WITHIN(value, SKEW_FACTOR_MIN, SKEW_FACTOR_MAX)) {
if (planner.yz_skew_factor != value) {
planner.yz_skew_factor = value;
++setval;
}
}
else
++badval;
}
#endif
if (badval)
SERIAL_ECHOLNPGM(MSG_SKEW_MIN " " STRINGIFY(SKEW_FACTOR_MIN) " " MSG_SKEW_MAX " " STRINGIFY(SKEW_FACTOR_MAX));
// When skew is changed the current position changes
if (setval) {
set_current_from_steppers_for_axis(ALL_AXES);
SYNC_PLAN_POSITION_KINEMATIC();
report_current_position();
}
if (!ijk) {
SERIAL_ECHO_START();
SERIAL_ECHO(MSG_SKEW_FACTOR " XY: ");
SERIAL_ECHO_F(planner.xy_skew_factor, 6);
SERIAL_EOL();
#if ENABLED(SKEW_CORRECTION_FOR_Z)
SERIAL_ECHOPAIR(" XZ: ", planner.xz_skew_factor);
SERIAL_ECHOLNPAIR(" YZ: ", planner.yz_skew_factor);
#else
SERIAL_EOL();
#endif
}
}
#endif // SKEW_CORRECTION_GCODE
#if ENABLED(ADVANCED_PAUSE_FEATURE)
/**
* M600: Pause for filament change
*
* E[distance] - Retract the filament this far
* Z[distance] - Move the Z axis by this distance
* X[position] - Move to this X position, with Y
* Y[position] - Move to this Y position, with X
* U[distance] - Retract distance for removal (manual reload)
* L[distance] - Extrude distance for insertion (manual reload)
* B[count] - Number of times to beep, -1 for indefinite (if equipped with a buzzer)
* T[toolhead] - Select extruder for filament change
*
* Default values are used for omitted arguments.
*/
inline void gcode_M600() {
point_t park_point = NOZZLE_PARK_POINT;
if (get_target_extruder_from_command(600)) return;
// Show initial message
#if ENABLED(ULTIPANEL)
lcd_advanced_pause_show_message(ADVANCED_PAUSE_MESSAGE_INIT, ADVANCED_PAUSE_MODE_PAUSE_PRINT, target_extruder);
#endif
#if ENABLED(HOME_BEFORE_FILAMENT_CHANGE)
// Don't allow filament change without homing first
if (axis_unhomed_error()) home_all_axes();
#endif
#if EXTRUDERS > 1
// Change toolhead if specified
uint8_t active_extruder_before_filament_change = active_extruder;
if (active_extruder != target_extruder)
tool_change(target_extruder, 0, true);
#endif
// Initial retract before move to filament change position
const float retract = -FABS(parser.seen('E') ? parser.value_axis_units(E_AXIS) : 0
#ifdef PAUSE_PARK_RETRACT_LENGTH
+ (PAUSE_PARK_RETRACT_LENGTH)
#endif
);
// Lift Z axis
if (parser.seenval('Z'))
park_point.z = parser.linearval('Z');
// Move XY axes to filament change position or given position
if (parser.seenval('X'))
park_point.x = parser.linearval('X');
if (parser.seenval('Y'))
park_point.y = parser.linearval('Y');
#if HOTENDS > 1 && DISABLED(DUAL_X_CARRIAGE)
park_point.x += (active_extruder ? hotend_offset[X_AXIS][active_extruder] : 0);
park_point.y += (active_extruder ? hotend_offset[Y_AXIS][active_extruder] : 0);
#endif
// Unload filament
const float unload_length = -FABS(parser.seen('U') ? parser.value_axis_units(E_AXIS) :
filament_change_unload_length[active_extruder]);
// Load filament
const float load_length = FABS(parser.seen('L') ? parser.value_axis_units(E_AXIS) :
filament_change_load_length[active_extruder]);
const int beep_count = parser.intval('B',
#ifdef FILAMENT_CHANGE_ALERT_BEEPS
FILAMENT_CHANGE_ALERT_BEEPS
#else
-1
#endif
);
const bool job_running = print_job_timer.isRunning();
if (pause_print(retract, park_point, unload_length, true)) {
wait_for_filament_reload(beep_count);
resume_print(load_length, ADVANCED_PAUSE_EXTRUDE_LENGTH, beep_count);
}
#if EXTRUDERS > 1
// Restore toolhead if it was changed
if (active_extruder_before_filament_change != active_extruder)
tool_change(active_extruder_before_filament_change, 0, true);
#endif
// Resume the print job timer if it was running
if (job_running) print_job_timer.start();
}
/**
* M603: Configure filament change
*
* T[toolhead] - Select extruder to configure, active extruder if not specified
* U[distance] - Retract distance for removal, for the specified extruder
* L[distance] - Extrude distance for insertion, for the specified extruder
*
*/
inline void gcode_M603() {
if (get_target_extruder_from_command(603)) return;
// Unload length
if (parser.seen('U')) {
filament_change_unload_length[target_extruder] = FABS(parser.value_axis_units(E_AXIS));
#if ENABLED(PREVENT_LENGTHY_EXTRUDE)
NOMORE(filament_change_unload_length[target_extruder], EXTRUDE_MAXLENGTH);
#endif
}
// Load length
if (parser.seen('L')) {
filament_change_load_length[target_extruder] = FABS(parser.value_axis_units(E_AXIS));
#if ENABLED(PREVENT_LENGTHY_EXTRUDE)
NOMORE(filament_change_load_length[target_extruder], EXTRUDE_MAXLENGTH);
#endif
}
}
#endif // ADVANCED_PAUSE_FEATURE
#if ENABLED(MK2_MULTIPLEXER)
inline void select_multiplexed_stepper(const uint8_t e) {
stepper.synchronize();
disable_e_steppers();
WRITE(E_MUX0_PIN, TEST(e, 0) ? HIGH : LOW);
WRITE(E_MUX1_PIN, TEST(e, 1) ? HIGH : LOW);
WRITE(E_MUX2_PIN, TEST(e, 2) ? HIGH : LOW);
safe_delay(100);
}
#endif // MK2_MULTIPLEXER
#if ENABLED(DUAL_X_CARRIAGE)
/**
* M605: Set dual x-carriage movement mode
*
* M605 S0: Full control mode. The slicer has full control over x-carriage movement
* M605 S1: Auto-park mode. The inactive head will auto park/unpark without slicer involvement
* M605 S2 [Xnnn] [Rmmm]: Duplication mode. The second extruder will duplicate the first with nnn
* units x-offset and an optional differential hotend temperature of
* mmm degrees. E.g., with "M605 S2 X100 R2" the second extruder will duplicate
* the first with a spacing of 100mm in the x direction and 2 degrees hotter.
*
* Note: the X axis should be homed after changing dual x-carriage mode.
*/
inline void gcode_M605() {
stepper.synchronize();
if (parser.seen('S')) dual_x_carriage_mode = (DualXMode)parser.value_byte();
switch (dual_x_carriage_mode) {
case DXC_FULL_CONTROL_MODE:
case DXC_AUTO_PARK_MODE:
break;
case DXC_DUPLICATION_MODE:
if (parser.seen('X')) duplicate_extruder_x_offset = max(parser.value_linear_units(), X2_MIN_POS - x_home_pos(0));
if (parser.seen('R')) duplicate_extruder_temp_offset = parser.value_celsius_diff();
SERIAL_ECHO_START();
SERIAL_ECHOPGM(MSG_HOTEND_OFFSET);
SERIAL_CHAR(' ');
SERIAL_ECHO(hotend_offset[X_AXIS][0]);
SERIAL_CHAR(',');
SERIAL_ECHO(hotend_offset[Y_AXIS][0]);
SERIAL_CHAR(' ');
SERIAL_ECHO(duplicate_extruder_x_offset);
SERIAL_CHAR(',');
SERIAL_ECHOLN(hotend_offset[Y_AXIS][1]);
break;
default:
dual_x_carriage_mode = DEFAULT_DUAL_X_CARRIAGE_MODE;
break;
}
active_extruder_parked = false;
extruder_duplication_enabled = false;
delayed_move_time = 0;
}
#elif ENABLED(DUAL_NOZZLE_DUPLICATION_MODE)
inline void gcode_M605() {
stepper.synchronize();
extruder_duplication_enabled = parser.intval('S') == (int)DXC_DUPLICATION_MODE;
SERIAL_ECHO_START();
SERIAL_ECHOLNPAIR(MSG_DUPLICATION_MODE, extruder_duplication_enabled ? MSG_ON : MSG_OFF);
}
#endif // DUAL_NOZZLE_DUPLICATION_MODE
#if ENABLED(FILAMENT_LOAD_UNLOAD_GCODES)
/**
* M701: Load filament
*
* T[extruder] - Optional extruder number. Current extruder if omitted.
* Z[distance] - Move the Z axis by this distance
* L[distance] - Extrude distance for insertion (positive value) (manual reload)
*
* Default values are used for omitted arguments.
*/
inline void gcode_M701() {
point_t park_point = NOZZLE_PARK_POINT;
if (get_target_extruder_from_command(701)) return;
// Z axis lift
if (parser.seenval('Z')) park_point.z = parser.linearval('Z');
// Load filament
const float load_length = FABS(parser.seen('L') ? parser.value_axis_units(E_AXIS) :
filament_change_load_length[target_extruder]);
// Show initial "wait for load" message
#if ENABLED(ULTIPANEL)
lcd_advanced_pause_show_message(ADVANCED_PAUSE_MESSAGE_LOAD, ADVANCED_PAUSE_MODE_LOAD_FILAMENT, target_extruder);
#endif
#if EXTRUDERS > 1
// Change toolhead if specified
uint8_t active_extruder_before_filament_change = active_extruder;
if (active_extruder != target_extruder)
tool_change(target_extruder, 0, true);
#endif
// Lift Z axis
if (park_point.z > 0)
do_blocking_move_to_z(min(current_position[Z_AXIS] + park_point.z, Z_MAX_POS), NOZZLE_PARK_Z_FEEDRATE);
load_filament(load_length, ADVANCED_PAUSE_EXTRUDE_LENGTH, FILAMENT_CHANGE_ALERT_BEEPS, true,
thermalManager.wait_for_heating(target_extruder), ADVANCED_PAUSE_MODE_LOAD_FILAMENT);
// Restore Z axis
if (park_point.z > 0)
do_blocking_move_to_z(max(current_position[Z_AXIS] - park_point.z, Z_MIN_POS), NOZZLE_PARK_Z_FEEDRATE);
#if EXTRUDERS > 1
// Restore toolhead if it was changed
if (active_extruder_before_filament_change != active_extruder)
tool_change(active_extruder_before_filament_change, 0, true);
#endif
// Show status screen
#if ENABLED(ULTIPANEL)
lcd_advanced_pause_show_message(ADVANCED_PAUSE_MESSAGE_STATUS);
#endif
}
/**
* M702: Unload filament
*
* T[extruder] - Optional extruder number. If omitted, current extruder
* (or ALL extruders with FILAMENT_UNLOAD_ALL_EXTRUDERS).
* Z[distance] - Move the Z axis by this distance
* U[distance] - Retract distance for removal (manual reload)
*
* Default values are used for omitted arguments.
*/
inline void gcode_M702() {
point_t park_point = NOZZLE_PARK_POINT;
if (get_target_extruder_from_command(702)) return;
// Z axis lift
if (parser.seenval('Z')) park_point.z = parser.linearval('Z');
// Show initial message
#if ENABLED(ULTIPANEL)
lcd_advanced_pause_show_message(ADVANCED_PAUSE_MESSAGE_UNLOAD, ADVANCED_PAUSE_MODE_UNLOAD_FILAMENT, target_extruder);
#endif
#if EXTRUDERS > 1
// Change toolhead if specified
uint8_t active_extruder_before_filament_change = active_extruder;
if (active_extruder != target_extruder)
tool_change(target_extruder, 0, true);
#endif
// Lift Z axis
if (park_point.z > 0)
do_blocking_move_to_z(min(current_position[Z_AXIS] + park_point.z, Z_MAX_POS), NOZZLE_PARK_Z_FEEDRATE);
// Unload filament
#if EXTRUDERS > 1 && ENABLED(FILAMENT_UNLOAD_ALL_EXTRUDERS)
if (!parser.seenval('T')) {
HOTEND_LOOP() {
if (e != active_extruder) tool_change(e, 0, true);
unload_filament(-filament_change_unload_length[e], true, ADVANCED_PAUSE_MODE_UNLOAD_FILAMENT);
}
}
else
#endif
{
// Unload length
const float unload_length = -FABS(parser.seen('U') ? parser.value_axis_units(E_AXIS) :
filament_change_unload_length[target_extruder]);
unload_filament(unload_length, true, ADVANCED_PAUSE_MODE_UNLOAD_FILAMENT);
}
// Restore Z axis
if (park_point.z > 0)
do_blocking_move_to_z(max(current_position[Z_AXIS] - park_point.z, Z_MIN_POS), NOZZLE_PARK_Z_FEEDRATE);
#if EXTRUDERS > 1
// Restore toolhead if it was changed
if (active_extruder_before_filament_change != active_extruder)
tool_change(active_extruder_before_filament_change, 0, true);
#endif
// Show status screen
#if ENABLED(ULTIPANEL)
lcd_advanced_pause_show_message(ADVANCED_PAUSE_MESSAGE_STATUS);
#endif
}
#endif // FILAMENT_LOAD_UNLOAD_GCODES
#if ENABLED(LIN_ADVANCE)
/**
* M900: Set and/or Get advance K factor and WH/D ratio
*
* K Set advance K factor
* R Set ratio directly (overrides WH/D)
* W H D Set ratio from WH/D
*/
inline void gcode_M900() {
stepper.synchronize();
const float newK = parser.floatval('K', -1);
if (newK >= 0) planner.extruder_advance_k = newK;
float newR = parser.floatval('R', -1);
if (newR < 0) {
const float newD = parser.floatval('D', -1),
newW = parser.floatval('W', -1),
newH = parser.floatval('H', -1);
if (newD >= 0 && newW >= 0 && newH >= 0)
newR = newD ? (newW * newH) / (sq(newD * 0.5) * M_PI) : 0;
}
if (newR >= 0) planner.advance_ed_ratio = newR;
SERIAL_ECHO_START();
SERIAL_ECHOPAIR("Advance K=", planner.extruder_advance_k);
SERIAL_ECHOPGM(" E/D=");
const float ratio = planner.advance_ed_ratio;
if (ratio) SERIAL_ECHO(ratio); else SERIAL_ECHOPGM("Auto");
SERIAL_EOL();
}
#endif // LIN_ADVANCE
#if HAS_TRINAMIC
#if ENABLED(TMC_DEBUG)
inline void gcode_M122() {
if (parser.seen('S'))
tmc_set_report_status(parser.value_bool());
else
tmc_report_all();
}
#endif // TMC_DEBUG
/**
* M906: Set motor current in milliamps using axis codes X, Y, Z, E
* Report driver currents when no axis specified
*/
inline void gcode_M906() {
uint16_t values[XYZE];
LOOP_XYZE(i) values[i] = parser.intval(axis_codes[i]);
#define TMC_SET_GET_CURRENT(P,Q) do { \
if (values[P##_AXIS]) tmc_set_current(stepper##Q, extended_axis_codes[TMC_##Q], values[P##_AXIS]); \
else tmc_get_current(stepper##Q, extended_axis_codes[TMC_##Q]); } while(0)
#if X_IS_TRINAMIC
TMC_SET_GET_CURRENT(X,X);
#endif
#if X2_IS_TRINAMIC
TMC_SET_GET_CURRENT(X,X2);
#endif
#if Y_IS_TRINAMIC
TMC_SET_GET_CURRENT(Y,Y);
#endif
#if Y2_IS_TRINAMIC
TMC_SET_GET_CURRENT(Y,Y2);
#endif
#if Z_IS_TRINAMIC
TMC_SET_GET_CURRENT(Z,Z);
#endif
#if Z2_IS_TRINAMIC
TMC_SET_GET_CURRENT(Z,Z2);
#endif
#if E0_IS_TRINAMIC
TMC_SET_GET_CURRENT(E,E0);
#endif
#if E1_IS_TRINAMIC
TMC_SET_GET_CURRENT(E,E1);
#endif
#if E2_IS_TRINAMIC
TMC_SET_GET_CURRENT(E,E2);
#endif
#if E3_IS_TRINAMIC
TMC_SET_GET_CURRENT(E,E3);
#endif
#if E4_IS_TRINAMIC
TMC_SET_GET_CURRENT(E,E4);
#endif
}
/**
* M911: Report TMC stepper driver overtemperature pre-warn flag
* The flag is held by the library and persist until manually cleared by M912
*/
inline void gcode_M911() {
#if ENABLED(X_IS_TMC2130) || (ENABLED(X_IS_TMC2208) && PIN_EXISTS(X_SERIAL_RX)) || ENABLED(IS_TRAMS)
tmc_report_otpw(stepperX, extended_axis_codes[TMC_X]);
#endif
#if ENABLED(Y_IS_TMC2130) || (ENABLED(Y_IS_TMC2208) && PIN_EXISTS(Y_SERIAL_RX)) || ENABLED(IS_TRAMS)
tmc_report_otpw(stepperY, extended_axis_codes[TMC_Y]);
#endif
#if ENABLED(Z_IS_TMC2130) || (ENABLED(Z_IS_TMC2208) && PIN_EXISTS(Z_SERIAL_RX)) || ENABLED(IS_TRAMS)
tmc_report_otpw(stepperZ, extended_axis_codes[TMC_Z]);
#endif
#if ENABLED(E0_IS_TMC2130) || (ENABLED(E0_IS_TMC2208) && PIN_EXISTS(E0_SERIAL_RX)) || ENABLED(IS_TRAMS)
tmc_report_otpw(stepperE0, extended_axis_codes[TMC_E0]);
#endif
}
/**
* M912: Clear TMC stepper driver overtemperature pre-warn flag held by the library
*/
inline void gcode_M912() {
const bool clearX = parser.seen(axis_codes[X_AXIS]), clearY = parser.seen(axis_codes[Y_AXIS]), clearZ = parser.seen(axis_codes[Z_AXIS]), clearE = parser.seen(axis_codes[E_AXIS]),
clearAll = (!clearX && !clearY && !clearZ && !clearE) || (clearX && clearY && clearZ && clearE);
#if ENABLED(X_IS_TMC2130) || ENABLED(IS_TRAMS) || (ENABLED(X_IS_TMC2208) && PIN_EXISTS(X_SERIAL_RX))
if (clearX || clearAll) tmc_clear_otpw(stepperX, extended_axis_codes[TMC_X]);
#endif
#if ENABLED(X2_IS_TMC2130) || (ENABLED(X2_IS_TMC2208) && PIN_EXISTS(X_SERIAL_RX))
if (clearX || clearAll) tmc_clear_otpw(stepperX, extended_axis_codes[TMC_X]);
#endif
#if ENABLED(Y_IS_TMC2130) || (ENABLED(Y_IS_TMC2208) && PIN_EXISTS(Y_SERIAL_RX))
if (clearY || clearAll) tmc_clear_otpw(stepperY, extended_axis_codes[TMC_Y]);
#endif
#if ENABLED(Z_IS_TMC2130) || (ENABLED(Z_IS_TMC2208) && PIN_EXISTS(Z_SERIAL_RX))
if (clearZ || clearAll) tmc_clear_otpw(stepperZ, extended_axis_codes[TMC_Z]);
#endif
#if ENABLED(E0_IS_TMC2130) || (ENABLED(E0_IS_TMC2208) && PIN_EXISTS(E0_SERIAL_RX))
if (clearE || clearAll) tmc_clear_otpw(stepperE0, extended_axis_codes[TMC_E0]);
#endif
}
/**
* M913: Set HYBRID_THRESHOLD speed.
*/
#if ENABLED(HYBRID_THRESHOLD)
inline void gcode_M913() {
uint16_t values[XYZE];
LOOP_XYZE(i) values[i] = parser.intval(axis_codes[i]);
#define TMC_SET_GET_PWMTHRS(P,Q) do { \
if (values[P##_AXIS]) tmc_set_pwmthrs(stepper##Q, extended_axis_codes[TMC_##Q], values[P##_AXIS], planner.axis_steps_per_mm[P##_AXIS]); \
else tmc_get_pwmthrs(stepper##Q, extended_axis_codes[TMC_##Q], planner.axis_steps_per_mm[P##_AXIS]); } while(0)
#if X_IS_TRINAMIC
TMC_SET_GET_PWMTHRS(X,X);
#endif
#if X2_IS_TRINAMIC
TMC_SET_GET_PWMTHRS(X,X2);
#endif
#if Y_IS_TRINAMIC
TMC_SET_GET_PWMTHRS(Y,Y);
#endif
#if Y2_IS_TRINAMIC
TMC_SET_GET_PWMTHRS(Y,Y2);
#endif
#if Z_IS_TRINAMIC
TMC_SET_GET_PWMTHRS(Z,Z);
#endif
#if Z2_IS_TRINAMIC
TMC_SET_GET_PWMTHRS(Z,Z2);
#endif
#if E0_IS_TRINAMIC
TMC_SET_GET_PWMTHRS(E,E0);
#endif
#if E1_IS_TRINAMIC
TMC_SET_GET_PWMTHRS(E,E1);
#endif
#if E2_IS_TRINAMIC
TMC_SET_GET_PWMTHRS(E,E2);
#endif
#if E3_IS_TRINAMIC
TMC_SET_GET_PWMTHRS(E,E3);
#endif
#if E4_IS_TRINAMIC
TMC_SET_GET_PWMTHRS(E,E4);
#endif
}
#endif // HYBRID_THRESHOLD
/**
* M914: Set SENSORLESS_HOMING sensitivity.
*/
#if ENABLED(SENSORLESS_HOMING)
inline void gcode_M914() {
#define TMC_SET_GET_SGT(P,Q) do { \
if (parser.seen(axis_codes[X_AXIS])) tmc_set_sgt(stepperX, extended_axis_codes[TMC_X], parser.value_int()); \
else tmc_get_sgt(stepperX, extended_axis_codes[TMC_X]); } while(0)
#if ENABLED(X_IS_TMC2130) || ENABLED(IS_TRAMS)
TMC_SET_GET_SGT(X,X);
#endif
#if ENABLED(X2_IS_TMC2130)
TMC_SET_GET_SGT(X,X2);
#endif
#if ENABLED(Y_IS_TMC2130) || ENABLED(IS_TRAMS)
TMC_SET_GET_SGT(Y,Y);
#endif
#if ENABLED(Y2_IS_TMC2130)
TMC_SET_GET_SGT(Y,Y2);
#endif
}
#endif // SENSORLESS_HOMING
/**
* TMC Z axis calibration routine
*/
#if ENABLED(TMC_Z_CALIBRATION)
inline void gcode_M915() {
uint16_t _rms = parser.seenval('S') ? parser.value_int() : CALIBRATION_CURRENT;
uint16_t _z = parser.seenval('Z') ? parser.value_int() : CALIBRATION_EXTRA_HEIGHT;
if (!axis_known_position[Z_AXIS]) {
SERIAL_ECHOLNPGM("\nPlease home Z axis first");
return;
}
#if Z_IS_TRINAMIC
uint16_t Z_current_1 = stepperZ.getCurrent(),
stepperZ.setCurrent(_rms, R_SENSE, HOLD_MULTIPLIER);
#endif
#if Z2_IS_TRINAMIC
uint16_t Z2_current_1 = stepperZ.getCurrent();
stepperZ2.setCurrent(_rms, R_SENSE, HOLD_MULTIPLIER);
#endif
SERIAL_ECHOPAIR("\nCalibration current: Z", _rms);
soft_endstops_enabled = false;
do_blocking_move_to_z(Z_MAX_POS+_z);
#if Z_IS_TRINAMIC
stepperZ.setCurrent(Z_current_1, R_SENSE, HOLD_MULTIPLIER);
#endif
#if Z2_IS_TRINAMIC
stepperZ2.setCurrent(Z2_current_1, R_SENSE, HOLD_MULTIPLIER);
#endif
do_blocking_move_to_z(Z_MAX_POS);
soft_endstops_enabled = true;
SERIAL_ECHOLNPGM("\nHoming Z due to lost steps");
enqueue_and_echo_commands_P(PSTR("G28 Z"));
}
#endif
#endif // HAS_TRINAMIC
/**
* M907: Set digital trimpot motor current using axis codes X, Y, Z, E, B, S
*/
inline void gcode_M907() {
#if HAS_DIGIPOTSS
LOOP_XYZE(i) if (parser.seen(axis_codes[i])) stepper.digipot_current(i, parser.value_int());
if (parser.seen('B')) stepper.digipot_current(4, parser.value_int());
if (parser.seen('S')) for (uint8_t i = 0; i <= 4; i++) stepper.digipot_current(i, parser.value_int());
#elif HAS_MOTOR_CURRENT_PWM
#if PIN_EXISTS(MOTOR_CURRENT_PWM_XY)
if (parser.seen('X')) stepper.digipot_current(0, parser.value_int());
#endif
#if PIN_EXISTS(MOTOR_CURRENT_PWM_Z)
if (parser.seen('Z')) stepper.digipot_current(1, parser.value_int());
#endif
#if PIN_EXISTS(MOTOR_CURRENT_PWM_E)
if (parser.seen('E')) stepper.digipot_current(2, parser.value_int());
#endif
#endif
#if ENABLED(DIGIPOT_I2C)
// this one uses actual amps in floating point
LOOP_XYZE(i) if (parser.seen(axis_codes[i])) digipot_i2c_set_current(i, parser.value_float());
// for each additional extruder (named B,C,D,E..., channels 4,5,6,7...)
for (uint8_t i = NUM_AXIS; i < DIGIPOT_I2C_NUM_CHANNELS; i++) if (parser.seen('B' + i - (NUM_AXIS))) digipot_i2c_set_current(i, parser.value_float());
#endif
#if ENABLED(DAC_STEPPER_CURRENT)
if (parser.seen('S')) {
const float dac_percent = parser.value_float();
for (uint8_t i = 0; i <= 4; i++) dac_current_percent(i, dac_percent);
}
LOOP_XYZE(i) if (parser.seen(axis_codes[i])) dac_current_percent(i, parser.value_float());
#endif
}
#if HAS_DIGIPOTSS || ENABLED(DAC_STEPPER_CURRENT)
/**
* M908: Control digital trimpot directly (M908 P S)
*/
inline void gcode_M908() {
#if HAS_DIGIPOTSS
stepper.digitalPotWrite(
parser.intval('P'),
parser.intval('S')
);
#endif
#ifdef DAC_STEPPER_CURRENT
dac_current_raw(
parser.byteval('P', -1),
parser.ushortval('S', 0)
);
#endif
}
#if ENABLED(DAC_STEPPER_CURRENT) // As with Printrbot RevF
inline void gcode_M909() { dac_print_values(); }
inline void gcode_M910() { dac_commit_eeprom(); }
#endif
#endif // HAS_DIGIPOTSS || DAC_STEPPER_CURRENT
#if HAS_MICROSTEPS
// M350 Set microstepping mode. Warning: Steps per unit remains unchanged. S code sets stepping mode for all drivers.
inline void gcode_M350() {
if (parser.seen('S')) for (int i = 0; i <= 4; i++) stepper.microstep_mode(i, parser.value_byte());
LOOP_XYZE(i) if (parser.seen(axis_codes[i])) stepper.microstep_mode(i, parser.value_byte());
if (parser.seen('B')) stepper.microstep_mode(4, parser.value_byte());
stepper.microstep_readings();
}
/**
* M351: Toggle MS1 MS2 pins directly with axis codes X Y Z E B
* S# determines MS1 or MS2, X# sets the pin high/low.
*/
inline void gcode_M351() {
if (parser.seenval('S')) switch (parser.value_byte()) {
case 1:
LOOP_XYZE(i) if (parser.seenval(axis_codes[i])) stepper.microstep_ms(i, parser.value_byte(), -1);
if (parser.seenval('B')) stepper.microstep_ms(4, parser.value_byte(), -1);
break;
case 2:
LOOP_XYZE(i) if (parser.seenval(axis_codes[i])) stepper.microstep_ms(i, -1, parser.value_byte());
if (parser.seenval('B')) stepper.microstep_ms(4, -1, parser.value_byte());
break;
}
stepper.microstep_readings();
}
#endif // HAS_MICROSTEPS
#if HAS_CASE_LIGHT
#ifndef INVERT_CASE_LIGHT
#define INVERT_CASE_LIGHT false
#endif
uint8_t case_light_brightness; // LCD routine wants INT
bool case_light_on;
#if ENABLED(CASE_LIGHT_USE_NEOPIXEL)
LEDColor case_light_color =
#ifdef CASE_LIGHT_NEOPIXEL_COLOR
CASE_LIGHT_NEOPIXEL_COLOR
#else
{ 255, 255, 255, 255 }
#endif
;
#endif
void update_case_light() {
const uint8_t i = case_light_on ? case_light_brightness : 0, n10ct = INVERT_CASE_LIGHT ? 255 - i : i;
#if ENABLED(CASE_LIGHT_USE_NEOPIXEL)
leds.set_color(
MakeLEDColor(case_light_color.r, case_light_color.g, case_light_color.b, case_light_color.w, n10ct),
false
);
#else // !CASE_LIGHT_USE_NEOPIXEL
SET_OUTPUT(CASE_LIGHT_PIN);
if (USEABLE_HARDWARE_PWM(CASE_LIGHT_PIN))
analogWrite(CASE_LIGHT_PIN, n10ct);
else {
const bool s = case_light_on ? !INVERT_CASE_LIGHT : INVERT_CASE_LIGHT;
WRITE(CASE_LIGHT_PIN, s ? HIGH : LOW);
}
#endif // !CASE_LIGHT_USE_NEOPIXEL
}
#endif // HAS_CASE_LIGHT
/**
* M355: Turn case light on/off and set brightness
*
* P Set case light brightness (PWM pin required - ignored otherwise)
*
* S Set case light on/off
*
* When S turns on the light on a PWM pin then the current brightness level is used/restored
*
* M355 P200 S0 turns off the light & sets the brightness level
* M355 S1 turns on the light with a brightness of 200 (assuming a PWM pin)
*/
inline void gcode_M355() {
#if HAS_CASE_LIGHT
uint8_t args = 0;
if (parser.seenval('P')) ++args, case_light_brightness = parser.value_byte();
if (parser.seenval('S')) ++args, case_light_on = parser.value_bool();
if (args) update_case_light();
// always report case light status
SERIAL_ECHO_START();
if (!case_light_on) {
SERIAL_ECHOLN("Case light: off");
}
else {
if (!USEABLE_HARDWARE_PWM(CASE_LIGHT_PIN)) SERIAL_ECHOLN("Case light: on");
else SERIAL_ECHOLNPAIR("Case light: ", (int)case_light_brightness);
}
#else
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_ERR_M355_NONE);
#endif // HAS_CASE_LIGHT
}
#if ENABLED(MIXING_EXTRUDER)
/**
* M163: Set a single mix factor for a mixing extruder
* This is called "weight" by some systems.
*
* S[index] The channel index to set
* P[float] The mix value
*
*/
inline void gcode_M163() {
const int mix_index = parser.intval('S');
if (mix_index < MIXING_STEPPERS) {
float mix_value = parser.floatval('P');
NOLESS(mix_value, 0.0);
mixing_factor[mix_index] = RECIPROCAL(mix_value);
}
}
#if MIXING_VIRTUAL_TOOLS > 1
/**
* M164: Store the current mix factors as a virtual tool.
*
* S[index] The virtual tool to store
*
*/
inline void gcode_M164() {
const int tool_index = parser.intval('S');
if (tool_index < MIXING_VIRTUAL_TOOLS) {
normalize_mix();
for (uint8_t i = 0; i < MIXING_STEPPERS; i++)
mixing_virtual_tool_mix[tool_index][i] = mixing_factor[i];
}
}
#endif
#if ENABLED(DIRECT_MIXING_IN_G1)
/**
* M165: Set multiple mix factors for a mixing extruder.
* Factors that are left out will be set to 0.
* All factors together must add up to 1.0.
*
* A[factor] Mix factor for extruder stepper 1
* B[factor] Mix factor for extruder stepper 2
* C[factor] Mix factor for extruder stepper 3
* D[factor] Mix factor for extruder stepper 4
* H[factor] Mix factor for extruder stepper 5
* I[factor] Mix factor for extruder stepper 6
*
*/
inline void gcode_M165() { gcode_get_mix(); }
#endif
#endif // MIXING_EXTRUDER
/**
* M999: Restart after being stopped
*
* Default behaviour is to flush the serial buffer and request
* a resend to the host starting on the last N line received.
*
* Sending "M999 S1" will resume printing without flushing the
* existing command buffer.
*
*/
inline void gcode_M999() {
Running = true;
lcd_reset_alert_level();
if (parser.boolval('S')) return;
// gcode_LastN = Stopped_gcode_LastN;
FlushSerialRequestResend();
}
#if ENABLED(SWITCHING_EXTRUDER)
#if EXTRUDERS > 3
#define REQ_ANGLES 4
#define _SERVO_NR (e < 2 ? SWITCHING_EXTRUDER_SERVO_NR : SWITCHING_EXTRUDER_E23_SERVO_NR)
#else
#define REQ_ANGLES 2
#define _SERVO_NR SWITCHING_EXTRUDER_SERVO_NR
#endif
inline void move_extruder_servo(const uint8_t e) {
constexpr int16_t angles[] = SWITCHING_EXTRUDER_SERVO_ANGLES;
static_assert(COUNT(angles) == REQ_ANGLES, "SWITCHING_EXTRUDER_SERVO_ANGLES needs " STRINGIFY(REQ_ANGLES) " angles.");
stepper.synchronize();
#if EXTRUDERS & 1
if (e < EXTRUDERS - 1)
#endif
{
MOVE_SERVO(_SERVO_NR, angles[e]);
safe_delay(500);
}
}
#endif // SWITCHING_EXTRUDER
#if ENABLED(SWITCHING_NOZZLE)
inline void move_nozzle_servo(const uint8_t e) {
const int16_t angles[2] = SWITCHING_NOZZLE_SERVO_ANGLES;
stepper.synchronize();
MOVE_SERVO(SWITCHING_NOZZLE_SERVO_NR, angles[e]);
safe_delay(500);
}
#endif
inline void invalid_extruder_error(const uint8_t e) {
SERIAL_ECHO_START();
SERIAL_CHAR('T');
SERIAL_ECHO_F(e, DEC);
SERIAL_CHAR(' ');
SERIAL_ECHOLN(MSG_INVALID_EXTRUDER);
}
#if ENABLED(PARKING_EXTRUDER)
#if ENABLED(PARKING_EXTRUDER_SOLENOIDS_INVERT)
#define PE_MAGNET_ON_STATE !PARKING_EXTRUDER_SOLENOIDS_PINS_ACTIVE
#else
#define PE_MAGNET_ON_STATE PARKING_EXTRUDER_SOLENOIDS_PINS_ACTIVE
#endif
void pe_set_magnet(const uint8_t extruder_num, const uint8_t state) {
switch (extruder_num) {
case 1: OUT_WRITE(SOL1_PIN, state); break;
default: OUT_WRITE(SOL0_PIN, state); break;
}
#if PARKING_EXTRUDER_SOLENOIDS_DELAY > 0
dwell(PARKING_EXTRUDER_SOLENOIDS_DELAY);
#endif
}
inline void pe_activate_magnet(const uint8_t extruder_num) { pe_set_magnet(extruder_num, PE_MAGNET_ON_STATE); }
inline void pe_deactivate_magnet(const uint8_t extruder_num) { pe_set_magnet(extruder_num, !PE_MAGNET_ON_STATE); }
#endif // PARKING_EXTRUDER
#if HAS_FANMUX
void fanmux_switch(const uint8_t e) {
WRITE(FANMUX0_PIN, TEST(e, 0) ? HIGH : LOW);
#if PIN_EXISTS(FANMUX1)
WRITE(FANMUX1_PIN, TEST(e, 1) ? HIGH : LOW);
#if PIN_EXISTS(FANMUX2)
WRITE(FANMUX2, TEST(e, 2) ? HIGH : LOW);
#endif
#endif
}
FORCE_INLINE void fanmux_init(void) {
SET_OUTPUT(FANMUX0_PIN);
#if PIN_EXISTS(FANMUX1)
SET_OUTPUT(FANMUX1_PIN);
#if PIN_EXISTS(FANMUX2)
SET_OUTPUT(FANMUX2_PIN);
#endif
#endif
fanmux_switch(0);
}
#endif // HAS_FANMUX
/**
* Perform a tool-change, which may result in moving the
* previous tool out of the way and the new tool into place.
*/
void tool_change(const uint8_t tmp_extruder, const float fr_mm_s/*=0.0*/, bool no_move/*=false*/) {
#if ENABLED(MIXING_EXTRUDER) && MIXING_VIRTUAL_TOOLS > 1
if (tmp_extruder >= MIXING_VIRTUAL_TOOLS)
return invalid_extruder_error(tmp_extruder);
// T0-Tnnn: Switch virtual tool by changing the mix
for (uint8_t j = 0; j < MIXING_STEPPERS; j++)
mixing_factor[j] = mixing_virtual_tool_mix[tmp_extruder][j];
#else // !MIXING_EXTRUDER || MIXING_VIRTUAL_TOOLS <= 1
if (tmp_extruder >= EXTRUDERS)
return invalid_extruder_error(tmp_extruder);
#if HOTENDS > 1
const float old_feedrate_mm_s = fr_mm_s > 0.0 ? fr_mm_s : feedrate_mm_s;
feedrate_mm_s = fr_mm_s > 0.0 ? fr_mm_s : XY_PROBE_FEEDRATE_MM_S;
if (tmp_extruder != active_extruder) {
if (!no_move && axis_unhomed_error()) {
no_move = true;
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) SERIAL_ECHOLNPGM("No move on toolchange");
#endif
}
// Save current position to destination, for use later
set_destination_from_current();
#if ENABLED(DUAL_X_CARRIAGE)
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) {
SERIAL_ECHOPGM("Dual X Carriage Mode ");
switch (dual_x_carriage_mode) {
case DXC_FULL_CONTROL_MODE: SERIAL_ECHOLNPGM("DXC_FULL_CONTROL_MODE"); break;
case DXC_AUTO_PARK_MODE: SERIAL_ECHOLNPGM("DXC_AUTO_PARK_MODE"); break;
case DXC_DUPLICATION_MODE: SERIAL_ECHOLNPGM("DXC_DUPLICATION_MODE"); break;
}
}
#endif
const float xhome = x_home_pos(active_extruder);
if (dual_x_carriage_mode == DXC_AUTO_PARK_MODE
&& IsRunning()
&& (delayed_move_time || current_position[X_AXIS] != xhome)
) {
float raised_z = current_position[Z_AXIS] + TOOLCHANGE_PARK_ZLIFT;
#if ENABLED(MAX_SOFTWARE_ENDSTOPS)
NOMORE(raised_z, soft_endstop_max[Z_AXIS]);
#endif
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) {
SERIAL_ECHOLNPAIR("Raise to ", raised_z);
SERIAL_ECHOLNPAIR("MoveX to ", xhome);
SERIAL_ECHOLNPAIR("Lower to ", current_position[Z_AXIS]);
}
#endif
// Park old head: 1) raise 2) move to park position 3) lower
for (uint8_t i = 0; i < 3; i++)
planner.buffer_line(
i == 0 ? current_position[X_AXIS] : xhome,
current_position[Y_AXIS],
i == 2 ? current_position[Z_AXIS] : raised_z,
current_position[E_AXIS],
planner.max_feedrate_mm_s[i == 1 ? X_AXIS : Z_AXIS],
active_extruder
);
stepper.synchronize();
}
// Apply Y & Z extruder offset (X offset is used as home pos with Dual X)
current_position[Y_AXIS] -= hotend_offset[Y_AXIS][active_extruder] - hotend_offset[Y_AXIS][tmp_extruder];
current_position[Z_AXIS] -= hotend_offset[Z_AXIS][active_extruder] - hotend_offset[Z_AXIS][tmp_extruder];
// Activate the new extruder ahead of calling set_axis_is_at_home!
active_extruder = tmp_extruder;
// This function resets the max/min values - the current position may be overwritten below.
set_axis_is_at_home(X_AXIS);
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) DEBUG_POS("New Extruder", current_position);
#endif
// Only when auto-parking are carriages safe to move
if (dual_x_carriage_mode != DXC_AUTO_PARK_MODE) no_move = true;
switch (dual_x_carriage_mode) {
case DXC_FULL_CONTROL_MODE:
// New current position is the position of the activated extruder
current_position[X_AXIS] = inactive_extruder_x_pos;
// Save the inactive extruder's position (from the old current_position)
inactive_extruder_x_pos = destination[X_AXIS];
break;
case DXC_AUTO_PARK_MODE:
// record raised toolhead position for use by unpark
COPY(raised_parked_position, current_position);
raised_parked_position[Z_AXIS] += TOOLCHANGE_UNPARK_ZLIFT;
#if ENABLED(MAX_SOFTWARE_ENDSTOPS)
NOMORE(raised_parked_position[Z_AXIS], soft_endstop_max[Z_AXIS]);
#endif
active_extruder_parked = true;
delayed_move_time = 0;
break;
case DXC_DUPLICATION_MODE:
// If the new extruder is the left one, set it "parked"
// This triggers the second extruder to move into the duplication position
active_extruder_parked = (active_extruder == 0);
if (active_extruder_parked)
current_position[X_AXIS] = inactive_extruder_x_pos;
else
current_position[X_AXIS] = destination[X_AXIS] + duplicate_extruder_x_offset;
inactive_extruder_x_pos = destination[X_AXIS];
extruder_duplication_enabled = false;
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) {
SERIAL_ECHOLNPAIR("Set inactive_extruder_x_pos=", inactive_extruder_x_pos);
SERIAL_ECHOLNPGM("Clear extruder_duplication_enabled");
}
#endif
break;
}
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) {
SERIAL_ECHOLNPAIR("Active extruder parked: ", active_extruder_parked ? "yes" : "no");
DEBUG_POS("New extruder (parked)", current_position);
}
#endif
// No extra case for HAS_ABL in DUAL_X_CARRIAGE. Does that mean they don't work together?
#else // !DUAL_X_CARRIAGE
#if ENABLED(PARKING_EXTRUDER) // Dual Parking extruder
const float z_diff = hotend_offset[Z_AXIS][active_extruder] - hotend_offset[Z_AXIS][tmp_extruder];
float z_raise = PARKING_EXTRUDER_SECURITY_RAISE;
if (!no_move) {
const float parkingposx[] = PARKING_EXTRUDER_PARKING_X,
midpos = (parkingposx[0] + parkingposx[1]) * 0.5 + hotend_offset[X_AXIS][active_extruder],
grabpos = parkingposx[tmp_extruder] + hotend_offset[X_AXIS][active_extruder]
+ (tmp_extruder == 0 ? -(PARKING_EXTRUDER_GRAB_DISTANCE) : PARKING_EXTRUDER_GRAB_DISTANCE);
/**
* Steps:
* 1. Raise Z-Axis to give enough clearance
* 2. Move to park position of old extruder
* 3. Disengage magnetic field, wait for delay
* 4. Move near new extruder
* 5. Engage magnetic field for new extruder
* 6. Move to parking incl. offset of new extruder
* 7. Lower Z-Axis
*/
// STEP 1
#if ENABLED(DEBUG_LEVELING_FEATURE)
SERIAL_ECHOLNPGM("Starting Autopark");
if (DEBUGGING(LEVELING)) DEBUG_POS("current position:", current_position);
#endif
current_position[Z_AXIS] += z_raise;
#if ENABLED(DEBUG_LEVELING_FEATURE)
SERIAL_ECHOLNPGM("(1) Raise Z-Axis ");
if (DEBUGGING(LEVELING)) DEBUG_POS("Moving to Raised Z-Position", current_position);
#endif
planner.buffer_line_kinematic(current_position, planner.max_feedrate_mm_s[Z_AXIS], active_extruder);
stepper.synchronize();
// STEP 2
current_position[X_AXIS] = parkingposx[active_extruder] + hotend_offset[X_AXIS][active_extruder];
#if ENABLED(DEBUG_LEVELING_FEATURE)
SERIAL_ECHOLNPAIR("(2) Park extruder ", active_extruder);
if (DEBUGGING(LEVELING)) DEBUG_POS("Moving ParkPos", current_position);
#endif
planner.buffer_line_kinematic(current_position, planner.max_feedrate_mm_s[X_AXIS], active_extruder);
stepper.synchronize();
// STEP 3
#if ENABLED(DEBUG_LEVELING_FEATURE)
SERIAL_ECHOLNPGM("(3) Disengage magnet ");
#endif
pe_deactivate_magnet(active_extruder);
// STEP 4
#if ENABLED(DEBUG_LEVELING_FEATURE)
SERIAL_ECHOLNPGM("(4) Move to position near new extruder");
#endif
current_position[X_AXIS] += (active_extruder == 0 ? 10 : -10); // move 10mm away from parked extruder
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) DEBUG_POS("Moving away from parked extruder", current_position);
#endif
planner.buffer_line_kinematic(current_position, planner.max_feedrate_mm_s[X_AXIS], active_extruder);
stepper.synchronize();
// STEP 5
#if ENABLED(DEBUG_LEVELING_FEATURE)
SERIAL_ECHOLNPGM("(5) Engage magnetic field");
#endif
#if ENABLED(PARKING_EXTRUDER_SOLENOIDS_INVERT)
pe_activate_magnet(active_extruder); //just save power for inverted magnets
#endif
pe_activate_magnet(tmp_extruder);
// STEP 6
current_position[X_AXIS] = grabpos + (tmp_extruder == 0 ? (+10) : (-10));
planner.buffer_line_kinematic(current_position, planner.max_feedrate_mm_s[X_AXIS], active_extruder);
current_position[X_AXIS] = grabpos;
#if ENABLED(DEBUG_LEVELING_FEATURE)
SERIAL_ECHOLNPAIR("(6) Unpark extruder ", tmp_extruder);
if (DEBUGGING(LEVELING)) DEBUG_POS("Move UnparkPos", current_position);
#endif
planner.buffer_line_kinematic(current_position, planner.max_feedrate_mm_s[X_AXIS]/2, active_extruder);
stepper.synchronize();
// Step 7
current_position[X_AXIS] = midpos - hotend_offset[X_AXIS][tmp_extruder];
#if ENABLED(DEBUG_LEVELING_FEATURE)
SERIAL_ECHOLNPGM("(7) Move midway between hotends");
if (DEBUGGING(LEVELING)) DEBUG_POS("Move midway to new extruder", current_position);
#endif
planner.buffer_line_kinematic(current_position, planner.max_feedrate_mm_s[X_AXIS], active_extruder);
stepper.synchronize();
#if ENABLED(DEBUG_LEVELING_FEATURE)
SERIAL_ECHOLNPGM("Autopark done.");
#endif
}
else { // nomove == true
// Only engage magnetic field for new extruder
pe_activate_magnet(tmp_extruder);
#if ENABLED(PARKING_EXTRUDER_SOLENOIDS_INVERT)
pe_activate_magnet(active_extruder); // Just save power for inverted magnets
#endif
}
current_position[Z_AXIS] -= hotend_offset[Z_AXIS][tmp_extruder] - hotend_offset[Z_AXIS][active_extruder]; // Apply Zoffset
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) DEBUG_POS("Applying Z-offset", current_position);
#endif
#endif // dualParking extruder
#if ENABLED(SWITCHING_NOZZLE)
#define DONT_SWITCH (SWITCHING_EXTRUDER_SERVO_NR == SWITCHING_NOZZLE_SERVO_NR)
// <0 if the new nozzle is higher, >0 if lower. A bigger raise when lower.
const float z_diff = hotend_offset[Z_AXIS][active_extruder] - hotend_offset[Z_AXIS][tmp_extruder],
z_raise = 0.3 + (z_diff > 0.0 ? z_diff : 0.0);
// Always raise by some amount (destination copied from current_position earlier)
current_position[Z_AXIS] += z_raise;
planner.buffer_line_kinematic(current_position, planner.max_feedrate_mm_s[Z_AXIS], active_extruder);
move_nozzle_servo(tmp_extruder);
#endif
/**
* Set current_position to the position of the new nozzle.
* Offsets are based on linear distance, so we need to get
* the resulting position in coordinate space.
*
* - With grid or 3-point leveling, offset XYZ by a tilted vector
* - With mesh leveling, update Z for the new position
* - Otherwise, just use the raw linear distance
*
* Software endstops are altered here too. Consider a case where:
* E0 at X=0 ... E1 at X=10
* When we switch to E1 now X=10, but E1 can't move left.
* To express this we apply the change in XY to the software endstops.
* E1 can move farther right than E0, so the right limit is extended.
*
* Note that we don't adjust the Z software endstops. Why not?
* Consider a case where Z=0 (here) and switching to E1 makes Z=1
* because the bed is 1mm lower at the new position. As long as
* the first nozzle is out of the way, the carriage should be
* allowed to move 1mm lower. This technically "breaks" the
* Z software endstop. But this is technically correct (and
* there is no viable alternative).
*/
#if ABL_PLANAR
// Offset extruder, make sure to apply the bed level rotation matrix
vector_3 tmp_offset_vec = vector_3(hotend_offset[X_AXIS][tmp_extruder],
hotend_offset[Y_AXIS][tmp_extruder],
0),
act_offset_vec = vector_3(hotend_offset[X_AXIS][active_extruder],
hotend_offset[Y_AXIS][active_extruder],
0),
offset_vec = tmp_offset_vec - act_offset_vec;
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) {
tmp_offset_vec.debug(PSTR("tmp_offset_vec"));
act_offset_vec.debug(PSTR("act_offset_vec"));
offset_vec.debug(PSTR("offset_vec (BEFORE)"));
}
#endif
offset_vec.apply_rotation(planner.bed_level_matrix.transpose(planner.bed_level_matrix));
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) offset_vec.debug(PSTR("offset_vec (AFTER)"));
#endif
// Adjustments to the current position
const float xydiff[2] = { offset_vec.x, offset_vec.y };
current_position[Z_AXIS] += offset_vec.z;
#else // !ABL_PLANAR
const float xydiff[2] = {
hotend_offset[X_AXIS][tmp_extruder] - hotend_offset[X_AXIS][active_extruder],
hotend_offset[Y_AXIS][tmp_extruder] - hotend_offset[Y_AXIS][active_extruder]
};
#if ENABLED(MESH_BED_LEVELING)
if (planner.leveling_active) {
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) SERIAL_ECHOPAIR("Z before MBL: ", current_position[Z_AXIS]);
#endif
float x2 = current_position[X_AXIS] + xydiff[X_AXIS],
y2 = current_position[Y_AXIS] + xydiff[Y_AXIS],
z1 = current_position[Z_AXIS], z2 = z1;
planner.apply_leveling(current_position[X_AXIS], current_position[Y_AXIS], z1);
planner.apply_leveling(x2, y2, z2);
current_position[Z_AXIS] += z2 - z1;
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING))
SERIAL_ECHOLNPAIR(" after: ", current_position[Z_AXIS]);
#endif
}
#endif // MESH_BED_LEVELING
#endif // !HAS_ABL
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) {
SERIAL_ECHOPAIR("Offset Tool XY by { ", xydiff[X_AXIS]);
SERIAL_ECHOPAIR(", ", xydiff[Y_AXIS]);
SERIAL_ECHOLNPGM(" }");
}
#endif
// The newly-selected extruder XY is actually at...
current_position[X_AXIS] += xydiff[X_AXIS];
current_position[Y_AXIS] += xydiff[Y_AXIS];
// Set the new active extruder
active_extruder = tmp_extruder;
#endif // !DUAL_X_CARRIAGE
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) DEBUG_POS("Sync After Toolchange", current_position);
#endif
// Tell the planner the new "current position"
SYNC_PLAN_POSITION_KINEMATIC();
// Move to the "old position" (move the extruder into place)
#if ENABLED(SWITCHING_NOZZLE)
destination[Z_AXIS] += z_diff; // Include the Z restore with the "move back"
#endif
if (!no_move && IsRunning()) {
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) DEBUG_POS("Move back", destination);
#endif
// Move back to the original (or tweaked) position
do_blocking_move_to(destination[X_AXIS], destination[Y_AXIS], destination[Z_AXIS]);
}
#if ENABLED(SWITCHING_NOZZLE)
else {
// Move back down. (Including when the new tool is higher.)
do_blocking_move_to_z(destination[Z_AXIS], planner.max_feedrate_mm_s[Z_AXIS]);
}
#endif
} // (tmp_extruder != active_extruder)
stepper.synchronize();
#if ENABLED(EXT_SOLENOID) && !ENABLED(PARKING_EXTRUDER)
disable_all_solenoids();
enable_solenoid_on_active_extruder();
#endif // EXT_SOLENOID
feedrate_mm_s = old_feedrate_mm_s;
#else // HOTENDS <= 1
UNUSED(fr_mm_s);
UNUSED(no_move);
#if ENABLED(MK2_MULTIPLEXER)
if (tmp_extruder >= E_STEPPERS)
return invalid_extruder_error(tmp_extruder);
select_multiplexed_stepper(tmp_extruder);
#endif
// Set the new active extruder
active_extruder = tmp_extruder;
#endif // HOTENDS <= 1
#if ENABLED(SWITCHING_EXTRUDER) && !DONT_SWITCH
stepper.synchronize();
move_extruder_servo(active_extruder);
#endif
#if HAS_FANMUX
fanmux_switch(active_extruder);
#endif
SERIAL_ECHO_START();
SERIAL_ECHOLNPAIR(MSG_ACTIVE_EXTRUDER, (int)active_extruder);
#endif // !MIXING_EXTRUDER || MIXING_VIRTUAL_TOOLS <= 1
}
/**
* T0-T3: Switch tool, usually switching extruders
*
* F[units/min] Set the movement feedrate
* S1 Don't move the tool in XY after change
*/
inline void gcode_T(const uint8_t tmp_extruder) {
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) {
SERIAL_ECHOPAIR(">>> gcode_T(", tmp_extruder);
SERIAL_CHAR(')');
SERIAL_EOL();
DEBUG_POS("BEFORE", current_position);
}
#endif
#if HOTENDS == 1 || (ENABLED(MIXING_EXTRUDER) && MIXING_VIRTUAL_TOOLS > 1)
tool_change(tmp_extruder);
#elif HOTENDS > 1
tool_change(
tmp_extruder,
MMM_TO_MMS(parser.linearval('F')),
(tmp_extruder == active_extruder) || parser.boolval('S')
);
#endif
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) {
DEBUG_POS("AFTER", current_position);
SERIAL_ECHOLNPGM("<<< gcode_T");
}
#endif
}
/**
* Process the parsed command and dispatch it to its handler
*/
void process_parsed_command() {
KEEPALIVE_STATE(IN_HANDLER);
// Handle a known G, M, or T
switch (parser.command_letter) {
case 'G': switch (parser.codenum) {
case 0: case 1: gcode_G0_G1( // G0: Fast Move, G1: Linear Move
#if IS_SCARA
parser.codenum == 0
#endif
); break;
#if ENABLED(ARC_SUPPORT) && DISABLED(SCARA)
case 2: case 3: gcode_G2_G3(parser.codenum == 2); break; // G2: CW ARC, G3: CCW ARC
#endif
case 4: gcode_G4(); break; // G4: Dwell
#if ENABLED(BEZIER_CURVE_SUPPORT)
case 5: gcode_G5(); break; // G5: Cubic B_spline
#endif
#if ENABLED(FWRETRACT)
case 10: gcode_G10(); break; // G10: Retract
case 11: gcode_G11(); break; // G11: Prime
#endif
#if ENABLED(NOZZLE_CLEAN_FEATURE)
case 12: gcode_G12(); break; // G12: Clean Nozzle
#endif
#if ENABLED(CNC_WORKSPACE_PLANES)
case 17: gcode_G17(); break; // G17: Select Plane XY
case 18: gcode_G18(); break; // G18: Select Plane ZX
case 19: gcode_G19(); break; // G19: Select Plane YZ
#endif
#if ENABLED(INCH_MODE_SUPPORT)
case 20: gcode_G20(); break; // G20: Inch Units
case 21: gcode_G21(); break; // G21: Millimeter Units
#endif
#if ENABLED(G26_MESH_VALIDATION)
case 26: gcode_G26(); break; // G26: Mesh Validation Pattern
#endif
#if ENABLED(NOZZLE_PARK_FEATURE)
case 27: gcode_G27(); break; // G27: Park Nozzle
#endif
case 28: gcode_G28(false); break; // G28: Home one or more axes
#if HAS_LEVELING
case 29: gcode_G29(); break; // G29: Detailed Z probe
#endif
#if HAS_BED_PROBE
case 30: gcode_G30(); break; // G30: Single Z probe
#endif
#if ENABLED(Z_PROBE_SLED)
case 31: gcode_G31(); break; // G31: Dock sled
case 32: gcode_G32(); break; // G32: Undock sled
#endif
#if ENABLED(DELTA_AUTO_CALIBRATION)
case 33: gcode_G33(); break; // G33: Delta Auto-Calibration
#endif
#if ENABLED(G38_PROBE_TARGET)
case 38:
if (parser.subcode == 2 || parser.subcode == 3)
gcode_G38(parser.subcode == 2); // G38.2, G38.3: Probe towards object
break;
#endif
#if HAS_MESH
case 42: gcode_G42(); break; // G42: Move to mesh point
#endif
case 90: relative_mode = false; break; // G90: Absolute coordinates
case 91: relative_mode = true; break; // G91: Relative coordinates
case 92: gcode_G92(); break; // G92: Set Position
#if ENABLED(DEBUG_GCODE_PARSER)
case 800: parser.debug(); break; // G800: GCode Parser Test for G
#endif
}
break;
case 'M': switch (parser.codenum) {
#if HAS_RESUME_CONTINUE
case 0: case 1: gcode_M0_M1(); break; // M0: Unconditional stop, M1: Conditional stop
#endif
#if ENABLED(SPINDLE_LASER_ENABLE)
case 3: gcode_M3_M4(true); break; // M3: Laser/CW-Spindle Power
case 4: gcode_M3_M4(false); break; // M4: Laser/CCW-Spindle Power
case 5: gcode_M5(); break; // M5: Laser/Spindle OFF
#endif
case 17: gcode_M17(); break; // M17: Enable all steppers
#if ENABLED(SDSUPPORT)
case 20: gcode_M20(); break; // M20: List SD Card
case 21: gcode_M21(); break; // M21: Init SD Card
case 22: gcode_M22(); break; // M22: Release SD Card
case 23: gcode_M23(); break; // M23: Select File
case 24: gcode_M24(); break; // M24: Start SD Print
case 25: gcode_M25(); break; // M25: Pause SD Print
case 26: gcode_M26(); break; // M26: Set SD Index
case 27: gcode_M27(); break; // M27: Get SD Status
case 28: gcode_M28(); break; // M28: Start SD Write
case 29: gcode_M29(); break; // M29: Stop SD Write
case 30: gcode_M30(); break; // M30: Delete File
case 32: gcode_M32(); break; // M32: Select file, Start SD Print
#if ENABLED(LONG_FILENAME_HOST_SUPPORT)
case 33: gcode_M33(); break; // M33: Report longname path
#endif
#if ENABLED(SDCARD_SORT_ALPHA) && ENABLED(SDSORT_GCODE)
case 34: gcode_M34(); break; // M34: Set SD card sorting options
#endif
case 928: gcode_M928(); break; // M928: Start SD write
#endif // SDSUPPORT
case 31: gcode_M31(); break; // M31: Report print job elapsed time
case 42: gcode_M42(); break; // M42: Change pin state
#if ENABLED(PINS_DEBUGGING)
case 43: gcode_M43(); break; // M43: Read/monitor pin and endstop states
#endif
#if ENABLED(Z_MIN_PROBE_REPEATABILITY_TEST)
case 48: gcode_M48(); break; // M48: Z probe repeatability test
#endif
#if ENABLED(G26_MESH_VALIDATION)
case 49: gcode_M49(); break; // M49: Toggle the G26 Debug Flag
#endif
#if ENABLED(ULTRA_LCD) && ENABLED(LCD_SET_PROGRESS_MANUALLY)
case 73: gcode_M73(); break; // M73: Set Print Progress %
#endif
case 75: gcode_M75(); break; // M75: Start Print Job Timer
case 76: gcode_M76(); break; // M76: Pause Print Job Timer
case 77: gcode_M77(); break; // M77: Stop Print Job Timer
#if ENABLED(PRINTCOUNTER)
case 78: gcode_M78(); break; // M78: Report Print Statistics
#endif
#if ENABLED(M100_FREE_MEMORY_WATCHER)
case 100: gcode_M100(); break; // M100: Free Memory Report
#endif
case 104: gcode_M104(); break; // M104: Set Hotend Temperature
case 110: gcode_M110(); break; // M110: Set Current Line Number
case 111: gcode_M111(); break; // M111: Set Debug Flags
#if DISABLED(EMERGENCY_PARSER)
case 108: gcode_M108(); break; // M108: Cancel Waiting
case 112: gcode_M112(); break; // M112: Emergency Stop
case 410: gcode_M410(); break; // M410: Quickstop. Abort all planned moves
#endif
#if ENABLED(HOST_KEEPALIVE_FEATURE)
case 113: gcode_M113(); break; // M113: Set Host Keepalive Interval
#endif
case 140: gcode_M140(); break; // M140: Set Bed Temperature
case 105: gcode_M105(); KEEPALIVE_STATE(NOT_BUSY); return; // M105: Report Temperatures (and say "ok")
#if ENABLED(AUTO_REPORT_TEMPERATURES) && (HAS_TEMP_HOTEND || HAS_TEMP_BED)
case 155: gcode_M155(); break; // M155: Set Temperature Auto-report Interval
#endif
case 109: gcode_M109(); break; // M109: Set Hotend Temperature. Wait for target.
#if HAS_TEMP_BED
case 190: gcode_M190(); break; // M190: Set Bed Temperature. Wait for target.
#endif
#if FAN_COUNT > 0
case 106: gcode_M106(); break; // M106: Set Fan Speed
case 107: gcode_M107(); break; // M107: Fan Off
#endif
#if ENABLED(PARK_HEAD_ON_PAUSE)
case 125: gcode_M125(); break; // M125: Park (for Filament Change)
#endif
#if ENABLED(BARICUDA)
#if HAS_HEATER_1
case 126: gcode_M126(); break; // M126: Valve 1 Open
case 127: gcode_M127(); break; // M127: Valve 1 Closed
#endif
#if HAS_HEATER_2
case 128: gcode_M128(); break; // M128: Valve 2 Open
case 129: gcode_M129(); break; // M129: Valve 2 Closed
#endif
#endif
#if HAS_POWER_SWITCH
case 80: gcode_M80(); break; // M80: Turn on Power Supply
#endif
case 81: gcode_M81(); break; // M81: Turn off Power and Power Supply
case 82: gcode_M82(); break; // M82: Disable Relative E-Axis
case 83: gcode_M83(); break; // M83: Set Relative E-Axis
case 18: case 84: gcode_M18_M84(); break; // M18/M84: Disable Steppers / Set Timeout
case 85: gcode_M85(); break; // M85: Set inactivity stepper shutdown timeout
case 92: gcode_M92(); break; // M92: Set steps-per-unit
case 114: gcode_M114(); break; // M114: Report Current Position
case 115: gcode_M115(); break; // M115: Capabilities Report
case 117: gcode_M117(); break; // M117: Set LCD message text
case 118: gcode_M118(); break; // M118: Print a message in the host console
case 119: gcode_M119(); break; // M119: Report Endstop states
case 120: gcode_M120(); break; // M120: Enable Endstops
case 121: gcode_M121(); break; // M121: Disable Endstops
#if ENABLED(ULTIPANEL)
case 145: gcode_M145(); break; // M145: Set material heatup parameters
#endif
#if ENABLED(TEMPERATURE_UNITS_SUPPORT)
case 149: gcode_M149(); break; // M149: Set Temperature Units, C F K
#endif
#if HAS_COLOR_LEDS
case 150: gcode_M150(); break; // M150: Set Status LED Color
#endif
#if ENABLED(MIXING_EXTRUDER)
case 163: gcode_M163(); break; // M163: Set Mixing Component
#if MIXING_VIRTUAL_TOOLS > 1
case 164: gcode_M164(); break; // M164: Save Current Mix
#endif
#if ENABLED(DIRECT_MIXING_IN_G1)
case 165: gcode_M165(); break; // M165: Set Multiple Mixing Components
#endif
#endif
#if DISABLED(NO_VOLUMETRICS)
case 200: gcode_M200(); break; // M200: Set Filament Diameter, Volumetric Extrusion
#endif
case 201: gcode_M201(); break; // M201: Set Max Printing Acceleration (units/sec^2)
#if 0
case 202: gcode_M202(); break; // M202: Not used for Sprinter/grbl gen6
#endif
case 203: gcode_M203(); break; // M203: Set Max Feedrate (units/sec)
case 204: gcode_M204(); break; // M204: Set Acceleration
case 205: gcode_M205(); break; // M205: Set Advanced settings
#if HAS_M206_COMMAND
case 206: gcode_M206(); break; // M206: Set Home Offsets
case 428: gcode_M428(); break; // M428: Set Home Offsets based on current position
#endif
#if ENABLED(FWRETRACT)
case 207: gcode_M207(); break; // M207: Set Retract Length, Feedrate, Z lift
case 208: gcode_M208(); break; // M208: Set Additional Prime Length and Feedrate
case 209:
if (MIN_AUTORETRACT <= MAX_AUTORETRACT) gcode_M209(); // M209: Turn Auto-Retract on/off
break;
#endif
case 211: gcode_M211(); break; // M211: Enable/Disable/Report Software Endstops
#if HOTENDS > 1
case 218: gcode_M218(); break; // M218: Set Tool Offset
#endif
case 220: gcode_M220(); break; // M220: Set Feedrate Percentage
case 221: gcode_M221(); break; // M221: Set Flow Percentage
case 226: gcode_M226(); break; // M226: Wait for Pin State
#if defined(CHDK) || HAS_PHOTOGRAPH
case 240: gcode_M240(); break; // M240: Trigger Camera
#endif
#if HAS_LCD_CONTRAST
case 250: gcode_M250(); break; // M250: Set LCD Contrast
#endif
#if ENABLED(EXPERIMENTAL_I2CBUS)
case 260: gcode_M260(); break; // M260: Send Data to i2c slave
case 261: gcode_M261(); break; // M261: Request Data from i2c slave
#endif
#if HAS_SERVOS
case 280: gcode_M280(); break; // M280: Set Servo Position
#endif
#if ENABLED(BABYSTEPPING)
case 290: gcode_M290(); break; // M290: Babystepping
#endif
#if HAS_BUZZER
case 300: gcode_M300(); break; // M300: Add Tone/Buzz to Queue
#endif
#if ENABLED(PIDTEMP)
case 301: gcode_M301(); break; // M301: Set Hotend PID parameters
#endif
#if ENABLED(PREVENT_COLD_EXTRUSION)
case 302: gcode_M302(); break; // M302: Set Minimum Extrusion Temp
#endif
case 303: gcode_M303(); break; // M303: PID Autotune
#if ENABLED(PIDTEMPBED)
case 304: gcode_M304(); break; // M304: Set Bed PID parameters
#endif
#if HAS_MICROSTEPS
case 350: gcode_M350(); break; // M350: Set microstepping mode. Warning: Steps per unit remains unchanged. S code sets stepping mode for all drivers.
case 351: gcode_M351(); break; // M351: Toggle MS1 MS2 pins directly, S# determines MS1 or MS2, X# sets the pin high/low.
#endif
case 355: gcode_M355(); break; // M355: Set Case Light brightness
#if ENABLED(MORGAN_SCARA)
case 360: if (gcode_M360()) return; break; // M360: SCARA Theta pos1
case 361: if (gcode_M361()) return; break; // M361: SCARA Theta pos2
case 362: if (gcode_M362()) return; break; // M362: SCARA Psi pos1
case 363: if (gcode_M363()) return; break; // M363: SCARA Psi pos2
case 364: if (gcode_M364()) return; break; // M364: SCARA Psi pos3 (90 deg to Theta)
#endif
case 400: gcode_M400(); break; // M400: Synchronize. Wait for moves to finish.
#if HAS_BED_PROBE
case 401: gcode_M401(); break; // M401: Deploy Probe
case 402: gcode_M402(); break; // M402: Stow Probe
#endif
#if ENABLED(FILAMENT_WIDTH_SENSOR)
case 404: gcode_M404(); break; // M404: Set/Report Nominal Filament Width
case 405: gcode_M405(); break; // M405: Enable Filament Width Sensor
case 406: gcode_M406(); break; // M406: Disable Filament Width Sensor
case 407: gcode_M407(); break; // M407: Report Measured Filament Width
#endif
#if HAS_LEVELING
case 420: gcode_M420(); break; // M420: Set Bed Leveling Enabled / Fade
#endif
#if HAS_MESH
case 421: gcode_M421(); break; // M421: Set a Mesh Z value
#endif
case 500: gcode_M500(); break; // M500: Store Settings in EEPROM
case 501: gcode_M501(); break; // M501: Read Settings from EEPROM
case 502: gcode_M502(); break; // M502: Revert Settings to defaults
#if DISABLED(DISABLE_M503)
case 503: gcode_M503(); break; // M503: Report Settings (in SRAM)
#endif
#if ENABLED(EEPROM_SETTINGS)
case 504: gcode_M504(); break; // M504: Validate EEPROM
#endif
#if ENABLED(ABORT_ON_ENDSTOP_HIT_FEATURE_ENABLED)
case 540: gcode_M540(); break; // M540: Set Abort on Endstop Hit for SD Printing
#endif
#if ENABLED(ADVANCED_PAUSE_FEATURE)
case 600: gcode_M600(); break; // M600: Pause for Filament Change
case 603: gcode_M603(); break; // M603: Configure Filament Change
#endif
#if ENABLED(DUAL_X_CARRIAGE) || ENABLED(DUAL_NOZZLE_DUPLICATION_MODE)
case 605: gcode_M605(); break; // M605: Set Dual X Carriage movement mode
#endif
#if ENABLED(DELTA)
case 665: gcode_M665(); break; // M665: Delta Configuration
#endif
#if ENABLED(DELTA) || ENABLED(X_DUAL_ENDSTOPS) || ENABLED(Y_DUAL_ENDSTOPS) || ENABLED(Z_DUAL_ENDSTOPS)
case 666: gcode_M666(); break; // M666: DELTA/Dual Endstop Adjustment
#endif
#if ENABLED(FILAMENT_LOAD_UNLOAD_GCODES)
case 701: gcode_M701(); break; // M701: Load Filament
case 702: gcode_M702(); break; // M702: Unload Filament
#endif
#if ENABLED(DEBUG_GCODE_PARSER)
case 800: parser.debug(); break; // M800: GCode Parser Test for M
#endif
#if HAS_BED_PROBE
case 851: gcode_M851(); break; // M851: Set Z Probe Z Offset
#endif
#if ENABLED(SKEW_CORRECTION_GCODE)
case 852: gcode_M852(); break; // M852: Set Skew factors
#endif
#if ENABLED(I2C_POSITION_ENCODERS)
case 860: gcode_M860(); break; // M860: Report encoder module position
case 861: gcode_M861(); break; // M861: Report encoder module status
case 862: gcode_M862(); break; // M862: Perform axis test
case 863: gcode_M863(); break; // M863: Calibrate steps/mm
case 864: gcode_M864(); break; // M864: Change module address
case 865: gcode_M865(); break; // M865: Check module firmware version
case 866: gcode_M866(); break; // M866: Report axis error count
case 867: gcode_M867(); break; // M867: Toggle error correction
case 868: gcode_M868(); break; // M868: Set error correction threshold
case 869: gcode_M869(); break; // M869: Report axis error
#endif
#if ENABLED(LIN_ADVANCE)
case 900: gcode_M900(); break; // M900: Set Linear Advance K factor
#endif
case 907: gcode_M907(); break; // M907: Set Digital Trimpot Motor Current using axis codes.
#if HAS_DIGIPOTSS || ENABLED(DAC_STEPPER_CURRENT)
case 908: gcode_M908(); break; // M908: Direct Control Digital Trimpot
#if ENABLED(DAC_STEPPER_CURRENT)
case 909: gcode_M909(); break; // M909: Print Digipot/DAC current value (As with Printrbot RevF)
case 910: gcode_M910(); break; // M910: Commit Digipot/DAC value to External EEPROM (As with Printrbot RevF)
#endif
#endif
#if ENABLED(HAVE_TMC2130) || ENABLED(HAVE_TMC2208)
#if ENABLED(TMC_DEBUG)
case 122: gcode_M122(); break; // M122: Debug TMC steppers
#endif
case 906: gcode_M906(); break; // M906: Set motor current in milliamps using axis codes X, Y, Z, E
case 911: gcode_M911(); break; // M911: Report TMC prewarn triggered flags
case 912: gcode_M912(); break; // M911: Clear TMC prewarn triggered flags
#if ENABLED(HYBRID_THRESHOLD)
case 913: gcode_M913(); break; // M913: Set HYBRID_THRESHOLD speed.
#endif
#if ENABLED(SENSORLESS_HOMING)
case 914: gcode_M914(); break; // M914: Set SENSORLESS_HOMING sensitivity.
#endif
#if ENABLED(TMC_Z_CALIBRATION)
case 915: gcode_M915(); break; // M915: TMC Z axis calibration routine
#endif
#endif
case 999: gcode_M999(); break; // M999: Restart after being Stopped
}
break;
case 'T': gcode_T(parser.codenum); break; // T: Tool Select
default: parser.unknown_command_error();
}
KEEPALIVE_STATE(NOT_BUSY);
ok_to_send();
}
void process_next_command() {
char * const current_command = command_queue[cmd_queue_index_r];
if (DEBUGGING(ECHO)) {
SERIAL_ECHO_START();
SERIAL_ECHOLN(current_command);
#if ENABLED(M100_FREE_MEMORY_WATCHER)
SERIAL_ECHOPAIR("slot:", cmd_queue_index_r);
M100_dump_routine(" Command Queue:", (const char*)command_queue, (const char*)(command_queue + sizeof(command_queue)));
#endif
}
// Parse the next command in the queue
parser.parse(current_command);
process_parsed_command();
}
/**
* Send a "Resend: nnn" message to the host to
* indicate that a command needs to be re-sent.
*/
void FlushSerialRequestResend() {
//char command_queue[cmd_queue_index_r][100]="Resend:";
MYSERIAL.flush();
SERIAL_PROTOCOLPGM(MSG_RESEND);
SERIAL_PROTOCOLLN(gcode_LastN + 1);
ok_to_send();
}
/**
* Send an "ok" message to the host, indicating
* that a command was successfully processed.
*
* If ADVANCED_OK is enabled also include:
* N Line number of the command, if any
* P Planner space remaining
* B Block queue space remaining
*/
void ok_to_send() {
refresh_cmd_timeout();
if (!send_ok[cmd_queue_index_r]) return;
SERIAL_PROTOCOLPGM(MSG_OK);
#if ENABLED(ADVANCED_OK)
char* p = command_queue[cmd_queue_index_r];
if (*p == 'N') {
SERIAL_PROTOCOL(' ');
SERIAL_ECHO(*p++);
while (NUMERIC_SIGNED(*p))
SERIAL_ECHO(*p++);
}
SERIAL_PROTOCOLPGM(" P"); SERIAL_PROTOCOL(int(BLOCK_BUFFER_SIZE - planner.movesplanned() - 1));
SERIAL_PROTOCOLPGM(" B"); SERIAL_PROTOCOL(BUFSIZE - commands_in_queue);
#endif
SERIAL_EOL();
}
#if HAS_SOFTWARE_ENDSTOPS
/**
* Constrain the given coordinates to the software endstops.
*
* For DELTA/SCARA the XY constraint is based on the smallest
* radius within the set software endstops.
*/
void clamp_to_software_endstops(float target[XYZ]) {
if (!soft_endstops_enabled) return;
#if IS_KINEMATIC
const float dist_2 = HYPOT2(target[X_AXIS], target[Y_AXIS]);
if (dist_2 > soft_endstop_radius_2) {
const float ratio = soft_endstop_radius / SQRT(dist_2); // 200 / 300 = 0.66
target[X_AXIS] *= ratio;
target[Y_AXIS] *= ratio;
}
#else
#if ENABLED(MIN_SOFTWARE_ENDSTOP_X)
NOLESS(target[X_AXIS], soft_endstop_min[X_AXIS]);
#endif
#if ENABLED(MIN_SOFTWARE_ENDSTOP_Y)
NOLESS(target[Y_AXIS], soft_endstop_min[Y_AXIS]);
#endif
#if ENABLED(MAX_SOFTWARE_ENDSTOP_X)
NOMORE(target[X_AXIS], soft_endstop_max[X_AXIS]);
#endif
#if ENABLED(MAX_SOFTWARE_ENDSTOP_Y)
NOMORE(target[Y_AXIS], soft_endstop_max[Y_AXIS]);
#endif
#endif
#if ENABLED(MIN_SOFTWARE_ENDSTOP_Z)
NOLESS(target[Z_AXIS], soft_endstop_min[Z_AXIS]);
#endif
#if ENABLED(MAX_SOFTWARE_ENDSTOP_Z)
NOMORE(target[Z_AXIS], soft_endstop_max[Z_AXIS]);
#endif
}
#endif
#if ENABLED(AUTO_BED_LEVELING_BILINEAR)
#if ENABLED(ABL_BILINEAR_SUBDIVISION)
#define ABL_BG_SPACING(A) bilinear_grid_spacing_virt[A]
#define ABL_BG_FACTOR(A) bilinear_grid_factor_virt[A]
#define ABL_BG_POINTS_X ABL_GRID_POINTS_VIRT_X
#define ABL_BG_POINTS_Y ABL_GRID_POINTS_VIRT_Y
#define ABL_BG_GRID(X,Y) z_values_virt[X][Y]
#else
#define ABL_BG_SPACING(A) bilinear_grid_spacing[A]
#define ABL_BG_FACTOR(A) bilinear_grid_factor[A]
#define ABL_BG_POINTS_X GRID_MAX_POINTS_X
#define ABL_BG_POINTS_Y GRID_MAX_POINTS_Y
#define ABL_BG_GRID(X,Y) z_values[X][Y]
#endif
// Get the Z adjustment for non-linear bed leveling
float bilinear_z_offset(const float raw[XYZ]) {
static float z1, d2, z3, d4, L, D, ratio_x, ratio_y,
last_x = -999.999, last_y = -999.999;
// Whole units for the grid line indices. Constrained within bounds.
static int8_t gridx, gridy, nextx, nexty,
last_gridx = -99, last_gridy = -99;
// XY relative to the probed area
const float rx = raw[X_AXIS] - bilinear_start[X_AXIS],
ry = raw[Y_AXIS] - bilinear_start[Y_AXIS];
#if ENABLED(EXTRAPOLATE_BEYOND_GRID)
// Keep using the last grid box
#define FAR_EDGE_OR_BOX 2
#else
// Just use the grid far edge
#define FAR_EDGE_OR_BOX 1
#endif
if (last_x != rx) {
last_x = rx;
ratio_x = rx * ABL_BG_FACTOR(X_AXIS);
const float gx = constrain(FLOOR(ratio_x), 0, ABL_BG_POINTS_X - FAR_EDGE_OR_BOX);
ratio_x -= gx; // Subtract whole to get the ratio within the grid box
#if DISABLED(EXTRAPOLATE_BEYOND_GRID)
// Beyond the grid maintain height at grid edges
NOLESS(ratio_x, 0); // Never < 0.0. (> 1.0 is ok when nextx==gridx.)
#endif
gridx = gx;
nextx = min(gridx + 1, ABL_BG_POINTS_X - 1);
}
if (last_y != ry || last_gridx != gridx) {
if (last_y != ry) {
last_y = ry;
ratio_y = ry * ABL_BG_FACTOR(Y_AXIS);
const float gy = constrain(FLOOR(ratio_y), 0, ABL_BG_POINTS_Y - FAR_EDGE_OR_BOX);
ratio_y -= gy;
#if DISABLED(EXTRAPOLATE_BEYOND_GRID)
// Beyond the grid maintain height at grid edges
NOLESS(ratio_y, 0); // Never < 0.0. (> 1.0 is ok when nexty==gridy.)
#endif
gridy = gy;
nexty = min(gridy + 1, ABL_BG_POINTS_Y - 1);
}
if (last_gridx != gridx || last_gridy != gridy) {
last_gridx = gridx;
last_gridy = gridy;
// Z at the box corners
z1 = ABL_BG_GRID(gridx, gridy); // left-front
d2 = ABL_BG_GRID(gridx, nexty) - z1; // left-back (delta)
z3 = ABL_BG_GRID(nextx, gridy); // right-front
d4 = ABL_BG_GRID(nextx, nexty) - z3; // right-back (delta)
}
// Bilinear interpolate. Needed since ry or gridx has changed.
L = z1 + d2 * ratio_y; // Linear interp. LF -> LB
const float R = z3 + d4 * ratio_y; // Linear interp. RF -> RB
D = R - L;
}
const float offset = L + ratio_x * D; // the offset almost always changes
/*
static float last_offset = 0;
if (FABS(last_offset - offset) > 0.2) {
SERIAL_ECHOPGM("Sudden Shift at ");
SERIAL_ECHOPAIR("x=", rx);
SERIAL_ECHOPAIR(" / ", bilinear_grid_spacing[X_AXIS]);
SERIAL_ECHOLNPAIR(" -> gridx=", gridx);
SERIAL_ECHOPAIR(" y=", ry);
SERIAL_ECHOPAIR(" / ", bilinear_grid_spacing[Y_AXIS]);
SERIAL_ECHOLNPAIR(" -> gridy=", gridy);
SERIAL_ECHOPAIR(" ratio_x=", ratio_x);
SERIAL_ECHOLNPAIR(" ratio_y=", ratio_y);
SERIAL_ECHOPAIR(" z1=", z1);
SERIAL_ECHOPAIR(" z2=", z2);
SERIAL_ECHOPAIR(" z3=", z3);
SERIAL_ECHOLNPAIR(" z4=", z4);
SERIAL_ECHOPAIR(" L=", L);
SERIAL_ECHOPAIR(" R=", R);
SERIAL_ECHOLNPAIR(" offset=", offset);
}
last_offset = offset;
//*/
return offset;
}
#endif // AUTO_BED_LEVELING_BILINEAR
#if ENABLED(DELTA)
/**
* Recalculate factors used for delta kinematics whenever
* settings have been changed (e.g., by M665).
*/
void recalc_delta_settings() {
const float trt[ABC] = DELTA_RADIUS_TRIM_TOWER,
drt[ABC] = DELTA_DIAGONAL_ROD_TRIM_TOWER;
delta_tower[A_AXIS][X_AXIS] = cos(RADIANS(210 + delta_tower_angle_trim[A_AXIS])) * (delta_radius + trt[A_AXIS]); // front left tower
delta_tower[A_AXIS][Y_AXIS] = sin(RADIANS(210 + delta_tower_angle_trim[A_AXIS])) * (delta_radius + trt[A_AXIS]);
delta_tower[B_AXIS][X_AXIS] = cos(RADIANS(330 + delta_tower_angle_trim[B_AXIS])) * (delta_radius + trt[B_AXIS]); // front right tower
delta_tower[B_AXIS][Y_AXIS] = sin(RADIANS(330 + delta_tower_angle_trim[B_AXIS])) * (delta_radius + trt[B_AXIS]);
delta_tower[C_AXIS][X_AXIS] = cos(RADIANS( 90 + delta_tower_angle_trim[C_AXIS])) * (delta_radius + trt[C_AXIS]); // back middle tower
delta_tower[C_AXIS][Y_AXIS] = sin(RADIANS( 90 + delta_tower_angle_trim[C_AXIS])) * (delta_radius + trt[C_AXIS]);
delta_diagonal_rod_2_tower[A_AXIS] = sq(delta_diagonal_rod + drt[A_AXIS]);
delta_diagonal_rod_2_tower[B_AXIS] = sq(delta_diagonal_rod + drt[B_AXIS]);
delta_diagonal_rod_2_tower[C_AXIS] = sq(delta_diagonal_rod + drt[C_AXIS]);
update_software_endstops(Z_AXIS);
axis_homed[X_AXIS] = axis_homed[Y_AXIS] = axis_homed[Z_AXIS] = false;
}
#if ENABLED(DELTA_FAST_SQRT)
/**
* Fast inverse sqrt from Quake III Arena
* See: https://en.wikipedia.org/wiki/Fast_inverse_square_root
*/
float Q_rsqrt(const float number) {
long i;
float x2, y;
const float threehalfs = 1.5f;
x2 = number * 0.5f;
y = number;
i = * ( long * ) &y; // evil floating point bit level hacking
i = 0x5F3759DF - ( i >> 1 ); // what the f***?
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
return y;
}
#endif
/**
* Delta Inverse Kinematics
*
* Calculate the tower positions for a given machine
* position, storing the result in the delta[] array.
*
* This is an expensive calculation, requiring 3 square
* roots per segmented linear move, and strains the limits
* of a Mega2560 with a Graphical Display.
*
* Suggested optimizations include:
*
* - Disable the home_offset (M206) and/or position_shift (G92)
* features to remove up to 12 float additions.
*
* - Use a fast-inverse-sqrt function and add the reciprocal.
* (see above)
*/
#define DELTA_DEBUG() do { \
SERIAL_ECHOPAIR("cartesian X:", raw[X_AXIS]); \
SERIAL_ECHOPAIR(" Y:", raw[Y_AXIS]); \
SERIAL_ECHOLNPAIR(" Z:", raw[Z_AXIS]); \
SERIAL_ECHOPAIR("delta A:", delta[A_AXIS]); \
SERIAL_ECHOPAIR(" B:", delta[B_AXIS]); \
SERIAL_ECHOLNPAIR(" C:", delta[C_AXIS]); \
}while(0)
void inverse_kinematics(const float raw[XYZ]) {
DELTA_IK(raw);
// DELTA_DEBUG();
}
/**
* Calculate the highest Z position where the
* effector has the full range of XY motion.
*/
float delta_safe_distance_from_top() {
float cartesian[XYZ] = { 0, 0, 0 };
inverse_kinematics(cartesian);
float distance = delta[A_AXIS];
cartesian[Y_AXIS] = DELTA_PRINTABLE_RADIUS;
inverse_kinematics(cartesian);
return FABS(distance - delta[A_AXIS]);
}
/**
* Delta Forward Kinematics
*
* See the Wikipedia article "Trilateration"
* https://en.wikipedia.org/wiki/Trilateration
*
* Establish a new coordinate system in the plane of the
* three carriage points. This system has its origin at
* tower1, with tower2 on the X axis. Tower3 is in the X-Y
* plane with a Z component of zero.
* We will define unit vectors in this coordinate system
* in our original coordinate system. Then when we calculate
* the Xnew, Ynew and Znew values, we can translate back into
* the original system by moving along those unit vectors
* by the corresponding values.
*
* Variable names matched to Marlin, c-version, and avoid the
* use of any vector library.
*
* by Andreas Hardtung 2016-06-07
* based on a Java function from "Delta Robot Kinematics V3"
* by Steve Graves
*
* The result is stored in the cartes[] array.
*/
void forward_kinematics_DELTA(float z1, float z2, float z3) {
// Create a vector in old coordinates along x axis of new coordinate
const float p12[] = {
delta_tower[B_AXIS][X_AXIS] - delta_tower[A_AXIS][X_AXIS],
delta_tower[B_AXIS][Y_AXIS] - delta_tower[A_AXIS][Y_AXIS],
z2 - z1
},
// Get the Magnitude of vector.
d = SQRT(sq(p12[0]) + sq(p12[1]) + sq(p12[2])),
// Create unit vector by dividing by magnitude.
ex[3] = { p12[0] / d, p12[1] / d, p12[2] / d },
// Get the vector from the origin of the new system to the third point.
p13[3] = {
delta_tower[C_AXIS][X_AXIS] - delta_tower[A_AXIS][X_AXIS],
delta_tower[C_AXIS][Y_AXIS] - delta_tower[A_AXIS][Y_AXIS],
z3 - z1
},
// Use the dot product to find the component of this vector on the X axis.
i = ex[0] * p13[0] + ex[1] * p13[1] + ex[2] * p13[2],
// Create a vector along the x axis that represents the x component of p13.
iex[] = { ex[0] * i, ex[1] * i, ex[2] * i };
// Subtract the X component from the original vector leaving only Y. We use the
// variable that will be the unit vector after we scale it.
float ey[3] = { p13[0] - iex[0], p13[1] - iex[1], p13[2] - iex[2] };
// The magnitude of Y component
const float j = SQRT(sq(ey[0]) + sq(ey[1]) + sq(ey[2]));
// Convert to a unit vector
ey[0] /= j; ey[1] /= j; ey[2] /= j;
// The cross product of the unit x and y is the unit z
// float[] ez = vectorCrossProd(ex, ey);
const float ez[3] = {
ex[1] * ey[2] - ex[2] * ey[1],
ex[2] * ey[0] - ex[0] * ey[2],
ex[0] * ey[1] - ex[1] * ey[0]
},
// We now have the d, i and j values defined in Wikipedia.
// Plug them into the equations defined in Wikipedia for Xnew, Ynew and Znew
Xnew = (delta_diagonal_rod_2_tower[A_AXIS] - delta_diagonal_rod_2_tower[B_AXIS] + sq(d)) / (d * 2),
Ynew = ((delta_diagonal_rod_2_tower[A_AXIS] - delta_diagonal_rod_2_tower[C_AXIS] + HYPOT2(i, j)) / 2 - i * Xnew) / j,
Znew = SQRT(delta_diagonal_rod_2_tower[A_AXIS] - HYPOT2(Xnew, Ynew));
// Start from the origin of the old coordinates and add vectors in the
// old coords that represent the Xnew, Ynew and Znew to find the point
// in the old system.
cartes[X_AXIS] = delta_tower[A_AXIS][X_AXIS] + ex[0] * Xnew + ey[0] * Ynew - ez[0] * Znew;
cartes[Y_AXIS] = delta_tower[A_AXIS][Y_AXIS] + ex[1] * Xnew + ey[1] * Ynew - ez[1] * Znew;
cartes[Z_AXIS] = z1 + ex[2] * Xnew + ey[2] * Ynew - ez[2] * Znew;
}
void forward_kinematics_DELTA(float point[ABC]) {
forward_kinematics_DELTA(point[A_AXIS], point[B_AXIS], point[C_AXIS]);
}
#endif // DELTA
/**
* Get the stepper positions in the cartes[] array.
* Forward kinematics are applied for DELTA and SCARA.
*
* The result is in the current coordinate space with
* leveling applied. The coordinates need to be run through
* unapply_leveling to obtain machine coordinates suitable
* for current_position, etc.
*/
void get_cartesian_from_steppers() {
#if ENABLED(DELTA)
forward_kinematics_DELTA(
stepper.get_axis_position_mm(A_AXIS),
stepper.get_axis_position_mm(B_AXIS),
stepper.get_axis_position_mm(C_AXIS)
);
#else
#if IS_SCARA
forward_kinematics_SCARA(
stepper.get_axis_position_degrees(A_AXIS),
stepper.get_axis_position_degrees(B_AXIS)
);
#else
cartes[X_AXIS] = stepper.get_axis_position_mm(X_AXIS);
cartes[Y_AXIS] = stepper.get_axis_position_mm(Y_AXIS);
#endif
cartes[Z_AXIS] = stepper.get_axis_position_mm(Z_AXIS);
#endif
}
/**
* Set the current_position for an axis based on
* the stepper positions, removing any leveling that
* may have been applied.
*
* To prevent small shifts in axis position always call
* SYNC_PLAN_POSITION_KINEMATIC after updating axes with this.
*
* To keep hosts in sync, always call report_current_position
* after updating the current_position.
*/
void set_current_from_steppers_for_axis(const AxisEnum axis) {
get_cartesian_from_steppers();
#if PLANNER_LEVELING
planner.unapply_leveling(cartes);
#endif
if (axis == ALL_AXES)
COPY(current_position, cartes);
else
current_position[axis] = cartes[axis];
}
#if IS_CARTESIAN
#if ENABLED(SEGMENT_LEVELED_MOVES)
/**
* Prepare a segmented move on a CARTESIAN setup.
*
* This calls planner.buffer_line several times, adding
* small incremental moves. This allows the planner to
* apply more detailed bed leveling to the full move.
*/
inline void segmented_line_to_destination(const float &fr_mm_s, const float segment_size=LEVELED_SEGMENT_LENGTH) {
const float xdiff = destination[X_AXIS] - current_position[X_AXIS],
ydiff = destination[Y_AXIS] - current_position[Y_AXIS];
// If the move is only in Z/E don't split up the move
if (!xdiff && !ydiff) {
planner.buffer_line_kinematic(destination, fr_mm_s, active_extruder);
return;
}
// Remaining cartesian distances
const float zdiff = destination[Z_AXIS] - current_position[Z_AXIS],
ediff = destination[E_AXIS] - current_position[E_AXIS];
// Get the linear distance in XYZ
// If the move is very short, check the E move distance
// No E move either? Game over.
float cartesian_mm = SQRT(sq(xdiff) + sq(ydiff) + sq(zdiff));
if (UNEAR_ZERO(cartesian_mm)) cartesian_mm = FABS(ediff);
if (UNEAR_ZERO(cartesian_mm)) return;
// The length divided by the segment size
// At least one segment is required
uint16_t segments = cartesian_mm / segment_size;
NOLESS(segments, 1);
// The approximate length of each segment
const float inv_segments = 1.0 / float(segments),
segment_distance[XYZE] = {
xdiff * inv_segments,
ydiff * inv_segments,
zdiff * inv_segments,
ediff * inv_segments
};
// SERIAL_ECHOPAIR("mm=", cartesian_mm);
// SERIAL_ECHOLNPAIR(" segments=", segments);
// Get the raw current position as starting point
float raw[XYZE];
COPY(raw, current_position);
// Calculate and execute the segments
while (--segments) {
static millis_t next_idle_ms = millis() + 200UL;
thermalManager.manage_heater(); // This returns immediately if not really needed.
if (ELAPSED(millis(), next_idle_ms)) {
next_idle_ms = millis() + 200UL;
idle();
}
LOOP_XYZE(i) raw[i] += segment_distance[i];
planner.buffer_line_kinematic(raw, fr_mm_s, active_extruder);
}
// Since segment_distance is only approximate,
// the final move must be to the exact destination.
planner.buffer_line_kinematic(destination, fr_mm_s, active_extruder);
}
#elif ENABLED(MESH_BED_LEVELING)
/**
* Prepare a mesh-leveled linear move in a Cartesian setup,
* splitting the move where it crosses mesh borders.
*/
void mesh_line_to_destination(const float fr_mm_s, uint8_t x_splits=0xFF, uint8_t y_splits=0xFF) {
// Get current and destination cells for this line
int cx1 = mbl.cell_index_x(current_position[X_AXIS]),
cy1 = mbl.cell_index_y(current_position[Y_AXIS]),
cx2 = mbl.cell_index_x(destination[X_AXIS]),
cy2 = mbl.cell_index_y(destination[Y_AXIS]);
NOMORE(cx1, GRID_MAX_POINTS_X - 2);
NOMORE(cy1, GRID_MAX_POINTS_Y - 2);
NOMORE(cx2, GRID_MAX_POINTS_X - 2);
NOMORE(cy2, GRID_MAX_POINTS_Y - 2);
// Start and end in the same cell? No split needed.
if (cx1 == cx2 && cy1 == cy2) {
buffer_line_to_destination(fr_mm_s);
set_current_from_destination();
return;
}
#define MBL_SEGMENT_END(A) (current_position[A ##_AXIS] + (destination[A ##_AXIS] - current_position[A ##_AXIS]) * normalized_dist)
float normalized_dist, end[XYZE];
const int8_t gcx = max(cx1, cx2), gcy = max(cy1, cy2);
// Crosses on the X and not already split on this X?
// The x_splits flags are insurance against rounding errors.
if (cx2 != cx1 && TEST(x_splits, gcx)) {
// Split on the X grid line
CBI(x_splits, gcx);
COPY(end, destination);
destination[X_AXIS] = mbl.index_to_xpos[gcx];
normalized_dist = (destination[X_AXIS] - current_position[X_AXIS]) / (end[X_AXIS] - current_position[X_AXIS]);
destination[Y_AXIS] = MBL_SEGMENT_END(Y);
}
// Crosses on the Y and not already split on this Y?
else if (cy2 != cy1 && TEST(y_splits, gcy)) {
// Split on the Y grid line
CBI(y_splits, gcy);
COPY(end, destination);
destination[Y_AXIS] = mbl.index_to_ypos[gcy];
normalized_dist = (destination[Y_AXIS] - current_position[Y_AXIS]) / (end[Y_AXIS] - current_position[Y_AXIS]);
destination[X_AXIS] = MBL_SEGMENT_END(X);
}
else {
// Must already have been split on these border(s)
buffer_line_to_destination(fr_mm_s);
set_current_from_destination();
return;
}
destination[Z_AXIS] = MBL_SEGMENT_END(Z);
destination[E_AXIS] = MBL_SEGMENT_END(E);
// Do the split and look for more borders
mesh_line_to_destination(fr_mm_s, x_splits, y_splits);
// Restore destination from stack
COPY(destination, end);
mesh_line_to_destination(fr_mm_s, x_splits, y_splits);
}
#elif ENABLED(AUTO_BED_LEVELING_BILINEAR)
#define CELL_INDEX(A,V) ((V - bilinear_start[A##_AXIS]) * ABL_BG_FACTOR(A##_AXIS))
/**
* Prepare a bilinear-leveled linear move on Cartesian,
* splitting the move where it crosses grid borders.
*/
void bilinear_line_to_destination(const float fr_mm_s, uint16_t x_splits=0xFFFF, uint16_t y_splits=0xFFFF) {
// Get current and destination cells for this line
int cx1 = CELL_INDEX(X, current_position[X_AXIS]),
cy1 = CELL_INDEX(Y, current_position[Y_AXIS]),
cx2 = CELL_INDEX(X, destination[X_AXIS]),
cy2 = CELL_INDEX(Y, destination[Y_AXIS]);
cx1 = constrain(cx1, 0, ABL_BG_POINTS_X - 2);
cy1 = constrain(cy1, 0, ABL_BG_POINTS_Y - 2);
cx2 = constrain(cx2, 0, ABL_BG_POINTS_X - 2);
cy2 = constrain(cy2, 0, ABL_BG_POINTS_Y - 2);
// Start and end in the same cell? No split needed.
if (cx1 == cx2 && cy1 == cy2) {
buffer_line_to_destination(fr_mm_s);
set_current_from_destination();
return;
}
#define LINE_SEGMENT_END(A) (current_position[A ##_AXIS] + (destination[A ##_AXIS] - current_position[A ##_AXIS]) * normalized_dist)
float normalized_dist, end[XYZE];
const int8_t gcx = max(cx1, cx2), gcy = max(cy1, cy2);
// Crosses on the X and not already split on this X?
// The x_splits flags are insurance against rounding errors.
if (cx2 != cx1 && TEST(x_splits, gcx)) {
// Split on the X grid line
CBI(x_splits, gcx);
COPY(end, destination);
destination[X_AXIS] = bilinear_start[X_AXIS] + ABL_BG_SPACING(X_AXIS) * gcx;
normalized_dist = (destination[X_AXIS] - current_position[X_AXIS]) / (end[X_AXIS] - current_position[X_AXIS]);
destination[Y_AXIS] = LINE_SEGMENT_END(Y);
}
// Crosses on the Y and not already split on this Y?
else if (cy2 != cy1 && TEST(y_splits, gcy)) {
// Split on the Y grid line
CBI(y_splits, gcy);
COPY(end, destination);
destination[Y_AXIS] = bilinear_start[Y_AXIS] + ABL_BG_SPACING(Y_AXIS) * gcy;
normalized_dist = (destination[Y_AXIS] - current_position[Y_AXIS]) / (end[Y_AXIS] - current_position[Y_AXIS]);
destination[X_AXIS] = LINE_SEGMENT_END(X);
}
else {
// Must already have been split on these border(s)
buffer_line_to_destination(fr_mm_s);
set_current_from_destination();
return;
}
destination[Z_AXIS] = LINE_SEGMENT_END(Z);
destination[E_AXIS] = LINE_SEGMENT_END(E);
// Do the split and look for more borders
bilinear_line_to_destination(fr_mm_s, x_splits, y_splits);
// Restore destination from stack
COPY(destination, end);
bilinear_line_to_destination(fr_mm_s, x_splits, y_splits);
}
#endif // AUTO_BED_LEVELING_BILINEAR
#endif // IS_CARTESIAN
#if !UBL_SEGMENTED
#if IS_KINEMATIC
/**
* Prepare a linear move in a DELTA or SCARA setup.
*
* This calls planner.buffer_line several times, adding
* small incremental moves for DELTA or SCARA.
*
* For Unified Bed Leveling (Delta or Segmented Cartesian)
* the ubl.prepare_segmented_line_to method replaces this.
*/
inline bool prepare_kinematic_move_to(const float (&rtarget)[XYZE]) {
// Get the top feedrate of the move in the XY plane
const float _feedrate_mm_s = MMS_SCALED(feedrate_mm_s);
const float xdiff = rtarget[X_AXIS] - current_position[X_AXIS],
ydiff = rtarget[Y_AXIS] - current_position[Y_AXIS];
// If the move is only in Z/E don't split up the move
if (!xdiff && !ydiff) {
planner.buffer_line_kinematic(rtarget, _feedrate_mm_s, active_extruder);
return false; // caller will update current_position
}
// Fail if attempting move outside printable radius
if (!position_is_reachable(rtarget[X_AXIS], rtarget[Y_AXIS])) return true;
// Remaining cartesian distances
const float zdiff = rtarget[Z_AXIS] - current_position[Z_AXIS],
ediff = rtarget[E_AXIS] - current_position[E_AXIS];
// Get the linear distance in XYZ
// If the move is very short, check the E move distance
// No E move either? Game over.
float cartesian_mm = SQRT(sq(xdiff) + sq(ydiff) + sq(zdiff));
if (UNEAR_ZERO(cartesian_mm)) cartesian_mm = FABS(ediff);
if (UNEAR_ZERO(cartesian_mm)) return true;
// Minimum number of seconds to move the given distance
const float seconds = cartesian_mm / _feedrate_mm_s;
// The number of segments-per-second times the duration
// gives the number of segments
uint16_t segments = delta_segments_per_second * seconds;
// For SCARA minimum segment size is 0.25mm
#if IS_SCARA
NOMORE(segments, cartesian_mm * 4);
#endif
// At least one segment is required
NOLESS(segments, 1);
// The approximate length of each segment
const float inv_segments = 1.0 / float(segments),
segment_distance[XYZE] = {
xdiff * inv_segments,
ydiff * inv_segments,
zdiff * inv_segments,
ediff * inv_segments
};
// SERIAL_ECHOPAIR("mm=", cartesian_mm);
// SERIAL_ECHOPAIR(" seconds=", seconds);
// SERIAL_ECHOLNPAIR(" segments=", segments);
#if ENABLED(SCARA_FEEDRATE_SCALING)
// SCARA needs to scale the feed rate from mm/s to degrees/s
// i.e., Complete the angular vector in the given time.
const float inv_segment_length = min(10.0, float(segments) / cartesian_mm), // 1/mm/segs
inverse_secs = inv_segment_length * _feedrate_mm_s;
float oldA = stepper.get_axis_position_degrees(A_AXIS),
oldB = stepper.get_axis_position_degrees(B_AXIS);
#endif
// Get the current position as starting point
float raw[XYZE];
COPY(raw, current_position);
// Calculate and execute the segments
while (--segments) {
static millis_t next_idle_ms = millis() + 200UL;
thermalManager.manage_heater(); // This returns immediately if not really needed.
if (ELAPSED(millis(), next_idle_ms)) {
next_idle_ms = millis() + 200UL;
idle();
}
LOOP_XYZE(i) raw[i] += segment_distance[i];
#if ENABLED(DELTA)
DELTA_IK(raw); // Delta can inline its kinematics
#else
inverse_kinematics(raw);
#endif
ADJUST_DELTA(raw); // Adjust Z if bed leveling is enabled
#if ENABLED(SCARA_FEEDRATE_SCALING)
// For SCARA scale the feed rate from mm/s to degrees/s
// i.e., Complete the angular vector in the given time.
planner.buffer_segment(delta[A_AXIS], delta[B_AXIS], raw[Z_AXIS], raw[E_AXIS], HYPOT(delta[A_AXIS] - oldA, delta[B_AXIS] - oldB) * inverse_secs, active_extruder);
oldA = delta[A_AXIS]; oldB = delta[B_AXIS];
#else
planner.buffer_line(delta[A_AXIS], delta[B_AXIS], delta[C_AXIS], raw[E_AXIS], _feedrate_mm_s, active_extruder);
#endif
}
// Ensure last segment arrives at target location.
#if ENABLED(SCARA_FEEDRATE_SCALING)
inverse_kinematics(rtarget);
ADJUST_DELTA(rtarget);
planner.buffer_segment(delta[A_AXIS], delta[B_AXIS], rtarget[Z_AXIS], rtarget[E_AXIS], HYPOT(delta[A_AXIS] - oldA, delta[B_AXIS] - oldB) * inverse_secs, active_extruder);
#else
planner.buffer_line_kinematic(rtarget, _feedrate_mm_s, active_extruder);
#endif
return false; // caller will update current_position
}
#else // !IS_KINEMATIC
/**
* Prepare a linear move in a Cartesian setup.
*
* When a mesh-based leveling system is active, moves are segmented
* according to the configuration of the leveling system.
*
* Returns true if current_position[] was set to destination[]
*/
inline bool prepare_move_to_destination_cartesian() {
#if HAS_MESH
if (planner.leveling_active && planner.leveling_active_at_z(destination[Z_AXIS])) {
#if ENABLED(AUTO_BED_LEVELING_UBL)
ubl.line_to_destination_cartesian(MMS_SCALED(feedrate_mm_s), active_extruder); // UBL's motion routine needs to know about
return true; // all moves, including Z-only moves.
#elif ENABLED(SEGMENT_LEVELED_MOVES)
segmented_line_to_destination(MMS_SCALED(feedrate_mm_s));
return false; // caller will update current_position
#else
/**
* For MBL and ABL-BILINEAR only segment moves when X or Y are involved.
* Otherwise fall through to do a direct single move.
*/
if (current_position[X_AXIS] != destination[X_AXIS] || current_position[Y_AXIS] != destination[Y_AXIS]) {
#if ENABLED(MESH_BED_LEVELING)
mesh_line_to_destination(MMS_SCALED(feedrate_mm_s));
#elif ENABLED(AUTO_BED_LEVELING_BILINEAR)
bilinear_line_to_destination(MMS_SCALED(feedrate_mm_s));
#endif
return true;
}
#endif
}
#endif // HAS_MESH
buffer_line_to_destination(MMS_SCALED(feedrate_mm_s));
return false; // caller will update current_position
}
#endif // !IS_KINEMATIC
#endif // !UBL_SEGMENTED
#if ENABLED(DUAL_X_CARRIAGE)
/**
* Unpark the carriage, if needed
*/
inline bool dual_x_carriage_unpark() {
if (active_extruder_parked)
switch (dual_x_carriage_mode) {
case DXC_FULL_CONTROL_MODE: break;
case DXC_AUTO_PARK_MODE:
if (current_position[E_AXIS] == destination[E_AXIS]) {
// This is a travel move (with no extrusion)
// Skip it, but keep track of the current position
// (so it can be used as the start of the next non-travel move)
if (delayed_move_time != 0xFFFFFFFFUL) {
set_current_from_destination();
NOLESS(raised_parked_position[Z_AXIS], destination[Z_AXIS]);
delayed_move_time = millis();
return true;
}
}
// unpark extruder: 1) raise, 2) move into starting XY position, 3) lower
for (uint8_t i = 0; i < 3; i++)
planner.buffer_line(
i == 0 ? raised_parked_position[X_AXIS] : current_position[X_AXIS],
i == 0 ? raised_parked_position[Y_AXIS] : current_position[Y_AXIS],
i == 2 ? current_position[Z_AXIS] : raised_parked_position[Z_AXIS],
current_position[E_AXIS],
i == 1 ? PLANNER_XY_FEEDRATE() : planner.max_feedrate_mm_s[Z_AXIS],
active_extruder
);
delayed_move_time = 0;
active_extruder_parked = false;
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) SERIAL_ECHOLNPGM("Clear active_extruder_parked");
#endif
break;
case DXC_DUPLICATION_MODE:
if (active_extruder == 0) {
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) {
SERIAL_ECHOPAIR("Set planner X", inactive_extruder_x_pos);
SERIAL_ECHOLNPAIR(" ... Line to X", current_position[X_AXIS] + duplicate_extruder_x_offset);
}
#endif
// move duplicate extruder into correct duplication position.
planner.set_position_mm(
inactive_extruder_x_pos,
current_position[Y_AXIS],
current_position[Z_AXIS],
current_position[E_AXIS]
);
planner.buffer_line(
current_position[X_AXIS] + duplicate_extruder_x_offset,
current_position[Y_AXIS], current_position[Z_AXIS], current_position[E_AXIS],
planner.max_feedrate_mm_s[X_AXIS], 1
);
SYNC_PLAN_POSITION_KINEMATIC();
stepper.synchronize();
extruder_duplication_enabled = true;
active_extruder_parked = false;
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) SERIAL_ECHOLNPGM("Set extruder_duplication_enabled\nClear active_extruder_parked");
#endif
}
else {
#if ENABLED(DEBUG_LEVELING_FEATURE)
if (DEBUGGING(LEVELING)) SERIAL_ECHOLNPGM("Active extruder not 0");
#endif
}
break;
}
return false;
}
#endif // DUAL_X_CARRIAGE
/**
* Prepare a single move and get ready for the next one
*
* This may result in several calls to planner.buffer_line to
* do smaller moves for DELTA, SCARA, mesh moves, etc.
*
* Make sure current_position[E] and destination[E] are good
* before calling or cold/lengthy extrusion may get missed.
*/
void prepare_move_to_destination() {
clamp_to_software_endstops(destination);
refresh_cmd_timeout();
#if ENABLED(PREVENT_COLD_EXTRUSION) || ENABLED(PREVENT_LENGTHY_EXTRUDE)
if (!DEBUGGING(DRYRUN)) {
if (destination[E_AXIS] != current_position[E_AXIS]) {
#if ENABLED(PREVENT_COLD_EXTRUSION)
if (thermalManager.tooColdToExtrude(active_extruder)) {
current_position[E_AXIS] = destination[E_AXIS]; // Behave as if the move really took place, but ignore E part
SERIAL_ECHO_START();
SERIAL_ECHOLNPGM(MSG_ERR_COLD_EXTRUDE_STOP);
}
#endif // PREVENT_COLD_EXTRUSION
#if ENABLED(PREVENT_LENGTHY_EXTRUDE)
if (FABS(destination[E_AXIS] - current_position[E_AXIS]) * planner.e_factor[active_extruder] > (EXTRUDE_MAXLENGTH)) {
current_position[E_AXIS] = destination[E_AXIS]; // Behave as if the move really took place, but ignore E part
SERIAL_ECHO_START();
SERIAL_ECHOLNPGM(MSG_ERR_LONG_EXTRUDE_STOP);
}
#endif // PREVENT_LENGTHY_EXTRUDE
}
}
#endif
#if ENABLED(DUAL_X_CARRIAGE)
if (dual_x_carriage_unpark()) return;
#endif
if (
#if UBL_SEGMENTED
ubl.prepare_segmented_line_to(destination, MMS_SCALED(feedrate_mm_s))
#elif IS_KINEMATIC
prepare_kinematic_move_to(destination)
#else
prepare_move_to_destination_cartesian()
#endif
) return;
set_current_from_destination();
}
#if ENABLED(ARC_SUPPORT)
#if N_ARC_CORRECTION < 1
#undef N_ARC_CORRECTION
#define N_ARC_CORRECTION 1
#endif
/**
* Plan an arc in 2 dimensions
*
* The arc is approximated by generating many small linear segments.
* The length of each segment is configured in MM_PER_ARC_SEGMENT (Default 1mm)
* Arcs should only be made relatively large (over 5mm), as larger arcs with
* larger segments will tend to be more efficient. Your slicer should have
* options for G2/G3 arc generation. In future these options may be GCode tunable.
*/
void plan_arc(
const float (&cart)[XYZE], // Destination position
const float (&offset)[2], // Center of rotation relative to current_position
const bool clockwise // Clockwise?
) {
#if ENABLED(CNC_WORKSPACE_PLANES)
AxisEnum p_axis, q_axis, l_axis;
switch (workspace_plane) {
default:
case PLANE_XY: p_axis = X_AXIS; q_axis = Y_AXIS; l_axis = Z_AXIS; break;
case PLANE_ZX: p_axis = Z_AXIS; q_axis = X_AXIS; l_axis = Y_AXIS; break;
case PLANE_YZ: p_axis = Y_AXIS; q_axis = Z_AXIS; l_axis = X_AXIS; break;
}
#else
constexpr AxisEnum p_axis = X_AXIS, q_axis = Y_AXIS, l_axis = Z_AXIS;
#endif
// Radius vector from center to current location
float r_P = -offset[0], r_Q = -offset[1];
const float radius = HYPOT(r_P, r_Q),
center_P = current_position[p_axis] - r_P,
center_Q = current_position[q_axis] - r_Q,
rt_X = cart[p_axis] - center_P,
rt_Y = cart[q_axis] - center_Q,
linear_travel = cart[l_axis] - current_position[l_axis],
extruder_travel = cart[E_AXIS] - current_position[E_AXIS];
// CCW angle of rotation between position and target from the circle center. Only one atan2() trig computation required.
float angular_travel = ATAN2(r_P * rt_Y - r_Q * rt_X, r_P * rt_X + r_Q * rt_Y);
if (angular_travel < 0) angular_travel += RADIANS(360);
if (clockwise) angular_travel -= RADIANS(360);
// Make a circle if the angular rotation is 0 and the target is current position
if (angular_travel == 0 && current_position[p_axis] == cart[p_axis] && current_position[q_axis] == cart[q_axis])
angular_travel = RADIANS(360);
const float mm_of_travel = HYPOT(angular_travel * radius, FABS(linear_travel));
if (mm_of_travel < 0.001) return;
uint16_t segments = FLOOR(mm_of_travel / (MM_PER_ARC_SEGMENT));
NOLESS(segments, 1);
/**
* Vector rotation by transformation matrix: r is the original vector, r_T is the rotated vector,
* and phi is the angle of rotation. Based on the solution approach by Jens Geisler.
* r_T = [cos(phi) -sin(phi);
* sin(phi) cos(phi)] * r ;
*
* For arc generation, the center of the circle is the axis of rotation and the radius vector is
* defined from the circle center to the initial position. Each line segment is formed by successive
* vector rotations. This requires only two cos() and sin() computations to form the rotation
* matrix for the duration of the entire arc. Error may accumulate from numerical round-off, since
* all double numbers are single precision on the Arduino. (True double precision will not have
* round off issues for CNC applications.) Single precision error can accumulate to be greater than
* tool precision in some cases. Therefore, arc path correction is implemented.
*
* Small angle approximation may be used to reduce computation overhead further. This approximation
* holds for everything, but very small circles and large MM_PER_ARC_SEGMENT values. In other words,
* theta_per_segment would need to be greater than 0.1 rad and N_ARC_CORRECTION would need to be large
* to cause an appreciable drift error. N_ARC_CORRECTION~=25 is more than small enough to correct for
* numerical drift error. N_ARC_CORRECTION may be on the order a hundred(s) before error becomes an
* issue for CNC machines with the single precision Arduino calculations.
*
* This approximation also allows plan_arc to immediately insert a line segment into the planner
* without the initial overhead of computing cos() or sin(). By the time the arc needs to be applied
* a correction, the planner should have caught up to the lag caused by the initial plan_arc overhead.
* This is important when there are successive arc motions.
*/
// Vector rotation matrix values
float raw[XYZE];
const float theta_per_segment = angular_travel / segments,
linear_per_segment = linear_travel / segments,
extruder_per_segment = extruder_travel / segments,
sin_T = theta_per_segment,
cos_T = 1 - 0.5 * sq(theta_per_segment); // Small angle approximation
// Initialize the linear axis
raw[l_axis] = current_position[l_axis];
// Initialize the extruder axis
raw[E_AXIS] = current_position[E_AXIS];
const float fr_mm_s = MMS_SCALED(feedrate_mm_s);
millis_t next_idle_ms = millis() + 200UL;
#if N_ARC_CORRECTION > 1
int8_t arc_recalc_count = N_ARC_CORRECTION;
#endif
#if ENABLED(SCARA_FEEDRATE_SCALING)
// SCARA needs to scale the feed rate from mm/s to degrees/s
const float inv_segment_length = 1.0 / (MM_PER_ARC_SEGMENT),
inverse_secs = inv_segment_length * fr_mm_s;
float oldA = stepper.get_axis_position_degrees(A_AXIS),
oldB = stepper.get_axis_position_degrees(B_AXIS);
#endif
for (uint16_t i = 1; i < segments; i++) { // Iterate (segments-1) times
thermalManager.manage_heater();
if (ELAPSED(millis(), next_idle_ms)) {
next_idle_ms = millis() + 200UL;
idle();
}
#if N_ARC_CORRECTION > 1
if (--arc_recalc_count) {
// Apply vector rotation matrix to previous r_P / 1
const float r_new_Y = r_P * sin_T + r_Q * cos_T;
r_P = r_P * cos_T - r_Q * sin_T;
r_Q = r_new_Y;
}
else
#endif
{
#if N_ARC_CORRECTION > 1
arc_recalc_count = N_ARC_CORRECTION;
#endif
// Arc correction to radius vector. Computed only every N_ARC_CORRECTION increments.
// Compute exact location by applying transformation matrix from initial radius vector(=-offset).
// To reduce stuttering, the sin and cos could be computed at different times.
// For now, compute both at the same time.
const float cos_Ti = cos(i * theta_per_segment), sin_Ti = sin(i * theta_per_segment);
r_P = -offset[0] * cos_Ti + offset[1] * sin_Ti;
r_Q = -offset[0] * sin_Ti - offset[1] * cos_Ti;
}
// Update raw location
raw[p_axis] = center_P + r_P;
raw[q_axis] = center_Q + r_Q;
raw[l_axis] += linear_per_segment;
raw[E_AXIS] += extruder_per_segment;
clamp_to_software_endstops(raw);
#if ENABLED(SCARA_FEEDRATE_SCALING)
// For SCARA scale the feed rate from mm/s to degrees/s
// i.e., Complete the angular vector in the given time.
inverse_kinematics(raw);
ADJUST_DELTA(raw);
planner.buffer_segment(delta[A_AXIS], delta[B_AXIS], raw[Z_AXIS], raw[E_AXIS], HYPOT(delta[A_AXIS] - oldA, delta[B_AXIS] - oldB) * inverse_secs, active_extruder);
oldA = delta[A_AXIS]; oldB = delta[B_AXIS];
#else
planner.buffer_line_kinematic(raw, fr_mm_s, active_extruder);
#endif
}
// Ensure last segment arrives at target location.
#if ENABLED(SCARA_FEEDRATE_SCALING)
inverse_kinematics(cart);
ADJUST_DELTA(cart);
planner.buffer_segment(delta[A_AXIS], delta[B_AXIS], cart[Z_AXIS], cart[E_AXIS], HYPOT(delta[A_AXIS] - oldA, delta[B_AXIS] - oldB) * inverse_secs, active_extruder);
#else
planner.buffer_line_kinematic(cart, fr_mm_s, active_extruder);
#endif
// As far as the parser is concerned, the position is now == target. In reality the
// motion control system might still be processing the action and the real tool position
// in any intermediate location.
set_current_from_destination();
} // plan_arc
#endif // ARC_SUPPORT
#if ENABLED(BEZIER_CURVE_SUPPORT)
void plan_cubic_move(const float (&offset)[4]) {
cubic_b_spline(current_position, destination, offset, MMS_SCALED(feedrate_mm_s), active_extruder);
// As far as the parser is concerned, the position is now == destination. In reality the
// motion control system might still be processing the action and the real tool position
// in any intermediate location.
set_current_from_destination();
}
#endif // BEZIER_CURVE_SUPPORT
#if ENABLED(USE_CONTROLLER_FAN)
void controllerFan() {
static millis_t lastMotorOn = 0, // Last time a motor was turned on
nextMotorCheck = 0; // Last time the state was checked
const millis_t ms = millis();
if (ELAPSED(ms, nextMotorCheck)) {
nextMotorCheck = ms + 2500UL; // Not a time critical function, so only check every 2.5s
if (X_ENABLE_READ == X_ENABLE_ON || Y_ENABLE_READ == Y_ENABLE_ON || Z_ENABLE_READ == Z_ENABLE_ON || thermalManager.soft_pwm_amount_bed > 0
|| E0_ENABLE_READ == E_ENABLE_ON // If any of the drivers are enabled...
#if E_STEPPERS > 1
|| E1_ENABLE_READ == E_ENABLE_ON
#if HAS_X2_ENABLE
|| X2_ENABLE_READ == X_ENABLE_ON
#endif
#if E_STEPPERS > 2
|| E2_ENABLE_READ == E_ENABLE_ON
#if E_STEPPERS > 3
|| E3_ENABLE_READ == E_ENABLE_ON
#if E_STEPPERS > 4
|| E4_ENABLE_READ == E_ENABLE_ON
#endif // E_STEPPERS > 4
#endif // E_STEPPERS > 3
#endif // E_STEPPERS > 2
#endif // E_STEPPERS > 1
) {
lastMotorOn = ms; //... set time to NOW so the fan will turn on
}
// Fan off if no steppers have been enabled for CONTROLLERFAN_SECS seconds
uint8_t speed = (!lastMotorOn || ELAPSED(ms, lastMotorOn + (CONTROLLERFAN_SECS) * 1000UL)) ? 0 : CONTROLLERFAN_SPEED;
// allows digital or PWM fan output to be used (see M42 handling)
WRITE(CONTROLLER_FAN_PIN, speed);
analogWrite(CONTROLLER_FAN_PIN, speed);
}
}
#endif // USE_CONTROLLER_FAN
#if ENABLED(MORGAN_SCARA)
/**
* Morgan SCARA Forward Kinematics. Results in cartes[].
* Maths and first version by QHARLEY.
* Integrated into Marlin and slightly restructured by Joachim Cerny.
*/
void forward_kinematics_SCARA(const float &a, const float &b) {
float a_sin = sin(RADIANS(a)) * L1,
a_cos = cos(RADIANS(a)) * L1,
b_sin = sin(RADIANS(b)) * L2,
b_cos = cos(RADIANS(b)) * L2;
cartes[X_AXIS] = a_cos + b_cos + SCARA_OFFSET_X; //theta
cartes[Y_AXIS] = a_sin + b_sin + SCARA_OFFSET_Y; //theta+phi
/*
SERIAL_ECHOPAIR("SCARA FK Angle a=", a);
SERIAL_ECHOPAIR(" b=", b);
SERIAL_ECHOPAIR(" a_sin=", a_sin);
SERIAL_ECHOPAIR(" a_cos=", a_cos);
SERIAL_ECHOPAIR(" b_sin=", b_sin);
SERIAL_ECHOLNPAIR(" b_cos=", b_cos);
SERIAL_ECHOPAIR(" cartes[X_AXIS]=", cartes[X_AXIS]);
SERIAL_ECHOLNPAIR(" cartes[Y_AXIS]=", cartes[Y_AXIS]);
//*/
}
/**
* Morgan SCARA Inverse Kinematics. Results in delta[].
*
* See http://forums.reprap.org/read.php?185,283327
*
* Maths and first version by QHARLEY.
* Integrated into Marlin and slightly restructured by Joachim Cerny.
*/
void inverse_kinematics(const float raw[XYZ]) {
static float C2, S2, SK1, SK2, THETA, PSI;
float sx = raw[X_AXIS] - SCARA_OFFSET_X, // Translate SCARA to standard X Y
sy = raw[Y_AXIS] - SCARA_OFFSET_Y; // With scaling factor.
if (L1 == L2)
C2 = HYPOT2(sx, sy) / L1_2_2 - 1;
else
C2 = (HYPOT2(sx, sy) - (L1_2 + L2_2)) / (2.0 * L1 * L2);
S2 = SQRT(1 - sq(C2));
// Unrotated Arm1 plus rotated Arm2 gives the distance from Center to End
SK1 = L1 + L2 * C2;
// Rotated Arm2 gives the distance from Arm1 to Arm2
SK2 = L2 * S2;
// Angle of Arm1 is the difference between Center-to-End angle and the Center-to-Elbow
THETA = ATAN2(SK1, SK2) - ATAN2(sx, sy);
// Angle of Arm2
PSI = ATAN2(S2, C2);
delta[A_AXIS] = DEGREES(THETA); // theta is support arm angle
delta[B_AXIS] = DEGREES(THETA + PSI); // equal to sub arm angle (inverted motor)
delta[C_AXIS] = raw[Z_AXIS];
/*
DEBUG_POS("SCARA IK", raw);
DEBUG_POS("SCARA IK", delta);
SERIAL_ECHOPAIR(" SCARA (x,y) ", sx);
SERIAL_ECHOPAIR(",", sy);
SERIAL_ECHOPAIR(" C2=", C2);
SERIAL_ECHOPAIR(" S2=", S2);
SERIAL_ECHOPAIR(" Theta=", THETA);
SERIAL_ECHOLNPAIR(" Phi=", PHI);
//*/
}
#endif // MORGAN_SCARA
#if ENABLED(TEMP_STAT_LEDS)
static bool red_led = false;
static millis_t next_status_led_update_ms = 0;
void handle_status_leds(void) {
if (ELAPSED(millis(), next_status_led_update_ms)) {
next_status_led_update_ms += 500; // Update every 0.5s
float max_temp = 0.0;
#if HAS_TEMP_BED
max_temp = MAX3(max_temp, thermalManager.degTargetBed(), thermalManager.degBed());
#endif
HOTEND_LOOP()
max_temp = MAX3(max_temp, thermalManager.degHotend(e), thermalManager.degTargetHotend(e));
const bool new_led = (max_temp > 55.0) ? true : (max_temp < 54.0) ? false : red_led;
if (new_led != red_led) {
red_led = new_led;
#if PIN_EXISTS(STAT_LED_RED)
WRITE(STAT_LED_RED_PIN, new_led ? HIGH : LOW);
#if PIN_EXISTS(STAT_LED_BLUE)
WRITE(STAT_LED_BLUE_PIN, new_led ? LOW : HIGH);
#endif
#else
WRITE(STAT_LED_BLUE_PIN, new_led ? HIGH : LOW);
#endif
}
}
}
#endif
#if ENABLED(FILAMENT_RUNOUT_SENSOR)
void handle_filament_runout() {
if (!filament_ran_out) {
filament_ran_out = true;
enqueue_and_echo_commands_P(PSTR(FILAMENT_RUNOUT_SCRIPT));
stepper.synchronize();
}
}
#endif // FILAMENT_RUNOUT_SENSOR
#if ENABLED(FAST_PWM_FAN)
void setPwmFrequency(uint8_t pin, int val) {
val &= 0x07;
switch (digitalPinToTimer(pin)) {
#ifdef TCCR0A
#if !AVR_AT90USB1286_FAMILY
case TIMER0A:
#endif
case TIMER0B: //_SET_CS(0, val);
break;
#endif
#ifdef TCCR1A
case TIMER1A: case TIMER1B: //_SET_CS(1, val);
break;
#endif
#if defined(TCCR2) || defined(TCCR2A)
#ifdef TCCR2
case TIMER2:
#endif
#ifdef TCCR2A
case TIMER2A: case TIMER2B:
#endif
_SET_CS(2, val); break;
#endif
#ifdef TCCR3A
case TIMER3A: case TIMER3B: case TIMER3C: _SET_CS(3, val); break;
#endif
#ifdef TCCR4A
case TIMER4A: case TIMER4B: case TIMER4C: _SET_CS(4, val); break;
#endif
#ifdef TCCR5A
case TIMER5A: case TIMER5B: case TIMER5C: _SET_CS(5, val); break;
#endif
}
}
#endif // FAST_PWM_FAN
void enable_all_steppers() {
enable_X();
enable_Y();
enable_Z();
enable_E0();
enable_E1();
enable_E2();
enable_E3();
enable_E4();
}
void disable_e_stepper(const uint8_t e) {
switch (e) {
case 0: disable_E0(); break;
case 1: disable_E1(); break;
case 2: disable_E2(); break;
case 3: disable_E3(); break;
case 4: disable_E4(); break;
}
}
void disable_e_steppers() {
disable_E0();
disable_E1();
disable_E2();
disable_E3();
disable_E4();
}
void disable_all_steppers() {
disable_X();
disable_Y();
disable_Z();
disable_e_steppers();
}
/**
* Manage several activities:
* - Check for Filament Runout
* - Keep the command buffer full
* - Check for maximum inactive time between commands
* - Check for maximum inactive time between stepper commands
* - Check if pin CHDK needs to go LOW
* - Check for KILL button held down
* - Check for HOME button held down
* - Check if cooling fan needs to be switched on
* - Check if an idle but hot extruder needs filament extruded (EXTRUDER_RUNOUT_PREVENT)
*/
void manage_inactivity(bool ignore_stepper_queue/*=false*/) {
#if ENABLED(FILAMENT_RUNOUT_SENSOR)
if ((IS_SD_PRINTING || print_job_timer.isRunning()) && (READ(FIL_RUNOUT_PIN) == FIL_RUNOUT_INVERTING))
handle_filament_runout();
#endif
if (commands_in_queue < BUFSIZE) get_available_commands();
const millis_t ms = millis();
if (max_inactive_time && ELAPSED(ms, previous_cmd_ms + max_inactive_time)) {
SERIAL_ERROR_START();
SERIAL_ECHOLNPAIR(MSG_KILL_INACTIVE_TIME, parser.command_ptr);
kill(PSTR(MSG_KILLED));
}
// Prevent steppers timing-out in the middle of M600
#if ENABLED(ADVANCED_PAUSE_FEATURE) && ENABLED(PAUSE_PARK_NO_STEPPER_TIMEOUT)
#define MOVE_AWAY_TEST !did_pause_print
#else
#define MOVE_AWAY_TEST true
#endif
if (MOVE_AWAY_TEST && stepper_inactive_time && ELAPSED(ms, previous_cmd_ms + stepper_inactive_time)
&& !ignore_stepper_queue && !planner.blocks_queued()) {
#if ENABLED(DISABLE_INACTIVE_X)
disable_X();
#endif
#if ENABLED(DISABLE_INACTIVE_Y)
disable_Y();
#endif
#if ENABLED(DISABLE_INACTIVE_Z)
disable_Z();
#endif
#if ENABLED(DISABLE_INACTIVE_E)
disable_e_steppers();
#endif
#if ENABLED(AUTO_BED_LEVELING_UBL) && ENABLED(ULTIPANEL) // Only needed with an LCD
ubl.lcd_map_control = defer_return_to_status = false;
#endif
}
#ifdef CHDK // Check if pin should be set to LOW after M240 set it to HIGH
if (chdkActive && ELAPSED(ms, chdkHigh + CHDK_DELAY)) {
chdkActive = false;
WRITE(CHDK, LOW);
}
#endif
#if HAS_KILL
// Check if the kill button was pressed and wait just in case it was an accidental
// key kill key press
// -------------------------------------------------------------------------------
static int killCount = 0; // make the inactivity button a bit less responsive
const int KILL_DELAY = 750;
if (!READ(KILL_PIN))
killCount++;
else if (killCount > 0)
killCount--;
// Exceeded threshold and we can confirm that it was not accidental
// KILL the machine
// ----------------------------------------------------------------
if (killCount >= KILL_DELAY) {
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_KILL_BUTTON);
kill(PSTR(MSG_KILLED));
}
#endif
#if HAS_HOME
// Check to see if we have to home, use poor man's debouncer
// ---------------------------------------------------------
static int homeDebounceCount = 0; // poor man's debouncing count
const int HOME_DEBOUNCE_DELAY = 2500;
if (!IS_SD_PRINTING && !READ(HOME_PIN)) {
if (!homeDebounceCount) {
enqueue_and_echo_commands_P(PSTR("G28"));
LCD_MESSAGEPGM(MSG_AUTO_HOME);
}
if (homeDebounceCount < HOME_DEBOUNCE_DELAY)
homeDebounceCount++;
else
homeDebounceCount = 0;
}
#endif
#if ENABLED(USE_CONTROLLER_FAN)
controllerFan(); // Check if fan should be turned on to cool stepper drivers down
#endif
#if ENABLED(EXTRUDER_RUNOUT_PREVENT)
if (ELAPSED(ms, previous_cmd_ms + (EXTRUDER_RUNOUT_SECONDS) * 1000UL)
&& thermalManager.degHotend(active_extruder) > EXTRUDER_RUNOUT_MINTEMP) {
#if ENABLED(SWITCHING_EXTRUDER)
const bool oldstatus = E0_ENABLE_READ;
enable_E0();
#else // !SWITCHING_EXTRUDER
bool oldstatus;
switch (active_extruder) {
default: oldstatus = E0_ENABLE_READ; enable_E0(); break;
#if E_STEPPERS > 1
case 1: oldstatus = E1_ENABLE_READ; enable_E1(); break;
#if E_STEPPERS > 2
case 2: oldstatus = E2_ENABLE_READ; enable_E2(); break;
#if E_STEPPERS > 3
case 3: oldstatus = E3_ENABLE_READ; enable_E3(); break;
#if E_STEPPERS > 4
case 4: oldstatus = E4_ENABLE_READ; enable_E4(); break;
#endif // E_STEPPERS > 4
#endif // E_STEPPERS > 3
#endif // E_STEPPERS > 2
#endif // E_STEPPERS > 1
}
#endif // !SWITCHING_EXTRUDER
previous_cmd_ms = ms; // refresh_cmd_timeout()
const float olde = current_position[E_AXIS];
current_position[E_AXIS] += EXTRUDER_RUNOUT_EXTRUDE;
planner.buffer_line_kinematic(current_position, MMM_TO_MMS(EXTRUDER_RUNOUT_SPEED), active_extruder);
current_position[E_AXIS] = olde;
planner.set_e_position_mm(olde);
stepper.synchronize();
#if ENABLED(SWITCHING_EXTRUDER)
E0_ENABLE_WRITE(oldstatus);
#else
switch (active_extruder) {
case 0: E0_ENABLE_WRITE(oldstatus); break;
#if E_STEPPERS > 1
case 1: E1_ENABLE_WRITE(oldstatus); break;
#if E_STEPPERS > 2
case 2: E2_ENABLE_WRITE(oldstatus); break;
#if E_STEPPERS > 3
case 3: E3_ENABLE_WRITE(oldstatus); break;
#if E_STEPPERS > 4
case 4: E4_ENABLE_WRITE(oldstatus); break;
#endif // E_STEPPERS > 4
#endif // E_STEPPERS > 3
#endif // E_STEPPERS > 2
#endif // E_STEPPERS > 1
}
#endif // !SWITCHING_EXTRUDER
}
#endif // EXTRUDER_RUNOUT_PREVENT
#if ENABLED(DUAL_X_CARRIAGE)
// handle delayed move timeout
if (delayed_move_time && ELAPSED(ms, delayed_move_time + 1000UL) && IsRunning()) {
// travel moves have been received so enact them
delayed_move_time = 0xFFFFFFFFUL; // force moves to be done
set_destination_from_current();
prepare_move_to_destination();
}
#endif
#if ENABLED(TEMP_STAT_LEDS)
handle_status_leds();
#endif
#if ENABLED(MONITOR_DRIVER_STATUS)
monitor_tmc_driver();
#endif
planner.check_axes_activity();
}
/**
* Standard idle routine keeps the machine alive
*/
void idle(
#if ENABLED(ADVANCED_PAUSE_FEATURE)
bool no_stepper_sleep/*=false*/
#endif
) {
#if ENABLED(MAX7219_DEBUG)
Max7219_idle_tasks();
#endif // MAX7219_DEBUG
lcd_update();
host_keepalive();
#if ENABLED(AUTO_REPORT_TEMPERATURES) && (HAS_TEMP_HOTEND || HAS_TEMP_BED)
thermalManager.auto_report_temperatures();
#endif
manage_inactivity(
#if ENABLED(ADVANCED_PAUSE_FEATURE)
no_stepper_sleep
#endif
);
thermalManager.manage_heater();
#if ENABLED(PRINTCOUNTER)
print_job_timer.tick();
#endif
#if HAS_BUZZER && DISABLED(LCD_USE_I2C_BUZZER)
buzzer.tick();
#endif
#if ENABLED(I2C_POSITION_ENCODERS)
if (planner.blocks_queued() &&
( (blockBufferIndexRef != planner.block_buffer_head) ||
((lastUpdateMillis + I2CPE_MIN_UPD_TIME_MS) < millis())) ) {
blockBufferIndexRef = planner.block_buffer_head;
I2CPEM.update();
lastUpdateMillis = millis();
}
#endif
}
/**
* Kill all activity and lock the machine.
* After this the machine will need to be reset.
*/
void kill(const char* lcd_msg) {
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_ERR_KILLED);
thermalManager.disable_all_heaters();
disable_all_steppers();
#if ENABLED(ULTRA_LCD)
kill_screen(lcd_msg);
#else
UNUSED(lcd_msg);
#endif
_delay_ms(600); // Wait a short time (allows messages to get out before shutting down.
cli(); // Stop interrupts
_delay_ms(250); //Wait to ensure all interrupts routines stopped
thermalManager.disable_all_heaters(); //turn off heaters again
#ifdef ACTION_ON_KILL
SERIAL_ECHOLNPGM("//action:" ACTION_ON_KILL);
#endif
#if HAS_POWER_SWITCH
SET_INPUT(PS_ON_PIN);
#endif
suicide();
while (1) {
#if ENABLED(USE_WATCHDOG)
watchdog_reset();
#endif
} // Wait for reset
}
/**
* Turn off heaters and stop the print in progress
* After a stop the machine may be resumed with M999
*/
void stop() {
thermalManager.disable_all_heaters(); // 'unpause' taken care of in here
#if ENABLED(PROBING_FANS_OFF)
if (fans_paused) fans_pause(false); // put things back the way they were
#endif
if (IsRunning()) {
Stopped_gcode_LastN = gcode_LastN; // Save last g_code for restart
SERIAL_ERROR_START();
SERIAL_ERRORLNPGM(MSG_ERR_STOPPED);
LCD_MESSAGEPGM(MSG_STOPPED);
safe_delay(350); // allow enough time for messages to get out before stopping
Running = false;
}
}
/**
* Marlin entry-point: Set up before the program loop
* - Set up the kill pin, filament runout, power hold
* - Start the serial port
* - Print startup messages and diagnostics
* - Get EEPROM or default settings
* - Initialize managers for:
* • temperature
* • planner
* • watchdog
* • stepper
* • photo pin
* • servos
* • LCD controller
* • Digipot I2C
* • Z probe sled
* • status LEDs
*/
void setup() {
#if ENABLED(MAX7219_DEBUG)
Max7219_init();
#endif
#if ENABLED(DISABLE_JTAG)
// Disable JTAG on AT90USB chips to free up pins for IO
MCUCR = 0x80;
MCUCR = 0x80;
#endif
#if ENABLED(FILAMENT_RUNOUT_SENSOR)
setup_filrunoutpin();
#endif
setup_killpin();
setup_powerhold();
#if HAS_STEPPER_RESET
disableStepperDrivers();
#endif
MYSERIAL.begin(BAUDRATE);
SERIAL_PROTOCOLLNPGM("start");
SERIAL_ECHO_START();
#if ENABLED(HAVE_TMC2208)
tmc2208_serial_begin();
#endif
// Check startup - does nothing if bootloader sets MCUSR to 0
byte mcu = MCUSR;
if (mcu & 1) SERIAL_ECHOLNPGM(MSG_POWERUP);
if (mcu & 2) SERIAL_ECHOLNPGM(MSG_EXTERNAL_RESET);
if (mcu & 4) SERIAL_ECHOLNPGM(MSG_BROWNOUT_RESET);
if (mcu & 8) SERIAL_ECHOLNPGM(MSG_WATCHDOG_RESET);
if (mcu & 32) SERIAL_ECHOLNPGM(MSG_SOFTWARE_RESET);
MCUSR = 0;
SERIAL_ECHOPGM(MSG_MARLIN);
SERIAL_CHAR(' ');
SERIAL_ECHOLNPGM(SHORT_BUILD_VERSION);
SERIAL_EOL();
#if defined(STRING_DISTRIBUTION_DATE) && defined(STRING_CONFIG_H_AUTHOR)
SERIAL_ECHO_START();
SERIAL_ECHOPGM(MSG_CONFIGURATION_VER);
SERIAL_ECHOPGM(STRING_DISTRIBUTION_DATE);
SERIAL_ECHOLNPGM(MSG_AUTHOR STRING_CONFIG_H_AUTHOR);
SERIAL_ECHO_START();
SERIAL_ECHOLNPGM("Compiled: " __DATE__);
#endif
SERIAL_ECHO_START();
SERIAL_ECHOPAIR(MSG_FREE_MEMORY, freeMemory());
SERIAL_ECHOLNPAIR(MSG_PLANNER_BUFFER_BYTES, (int)sizeof(block_t)*BLOCK_BUFFER_SIZE);
// Send "ok" after commands by default
for (int8_t i = 0; i < BUFSIZE; i++) send_ok[i] = true;
// Load data from EEPROM if available (or use defaults)
// This also updates variables in the planner, elsewhere
(void)settings.load();
#if HAS_M206_COMMAND
// Initialize current position based on home_offset
COPY(current_position, home_offset);
#else
ZERO(current_position);
#endif
// Vital to init stepper/planner equivalent for current_position
SYNC_PLAN_POSITION_KINEMATIC();
thermalManager.init(); // Initialize temperature loop
#if ENABLED(USE_WATCHDOG)
watchdog_init();
#endif
stepper.init(); // Initialize stepper, this enables interrupts!
servo_init();
#if HAS_PHOTOGRAPH
OUT_WRITE(PHOTOGRAPH_PIN, LOW);
#endif
#if HAS_CASE_LIGHT
case_light_on = CASE_LIGHT_DEFAULT_ON;
case_light_brightness = CASE_LIGHT_DEFAULT_BRIGHTNESS;
update_case_light();
#endif
#if ENABLED(SPINDLE_LASER_ENABLE)
OUT_WRITE(SPINDLE_LASER_ENABLE_PIN, !SPINDLE_LASER_ENABLE_INVERT); // init spindle to off
#if SPINDLE_DIR_CHANGE
OUT_WRITE(SPINDLE_DIR_PIN, SPINDLE_INVERT_DIR ? 255 : 0); // init rotation to clockwise (M3)
#endif
#if ENABLED(SPINDLE_LASER_PWM)
SET_OUTPUT(SPINDLE_LASER_PWM_PIN);
analogWrite(SPINDLE_LASER_PWM_PIN, SPINDLE_LASER_PWM_INVERT ? 255 : 0); // set to lowest speed
#endif
#endif
#if HAS_BED_PROBE
endstops.enable_z_probe(false);
#endif
#if ENABLED(USE_CONTROLLER_FAN)
SET_OUTPUT(CONTROLLER_FAN_PIN); //Set pin used for driver cooling fan
#endif
#if HAS_STEPPER_RESET
enableStepperDrivers();
#endif
#if ENABLED(DIGIPOT_I2C)
digipot_i2c_init();
#endif
#if ENABLED(DAC_STEPPER_CURRENT)
dac_init();
#endif
#if (ENABLED(Z_PROBE_SLED) || ENABLED(SOLENOID_PROBE)) && HAS_SOLENOID_1
OUT_WRITE(SOL1_PIN, LOW); // turn it off
#endif
#if HAS_HOME
SET_INPUT_PULLUP(HOME_PIN);
#endif
#if PIN_EXISTS(STAT_LED_RED)
OUT_WRITE(STAT_LED_RED_PIN, LOW); // turn it off
#endif
#if PIN_EXISTS(STAT_LED_BLUE)
OUT_WRITE(STAT_LED_BLUE_PIN, LOW); // turn it off
#endif
#if HAS_COLOR_LEDS
leds.setup();
#endif
#if ENABLED(RGB_LED) || ENABLED(RGBW_LED)
SET_OUTPUT(RGB_LED_R_PIN);
SET_OUTPUT(RGB_LED_G_PIN);
SET_OUTPUT(RGB_LED_B_PIN);
#if ENABLED(RGBW_LED)
SET_OUTPUT(RGB_LED_W_PIN);
#endif
#endif
#if ENABLED(MK2_MULTIPLEXER)
SET_OUTPUT(E_MUX0_PIN);
SET_OUTPUT(E_MUX1_PIN);
SET_OUTPUT(E_MUX2_PIN);
#endif
#if HAS_FANMUX
fanmux_init();
#endif
lcd_init();
#if ENABLED(SHOW_BOOTSCREEN)
lcd_bootscreen();
#endif
#if ENABLED(MIXING_EXTRUDER) && MIXING_VIRTUAL_TOOLS > 1
// Virtual Tools 0, 1, 2, 3 = Filament 1, 2, 3, 4, etc.
for (uint8_t t = 0; t < MIXING_VIRTUAL_TOOLS && t < MIXING_STEPPERS; t++)
for (uint8_t i = 0; i < MIXING_STEPPERS; i++)
mixing_virtual_tool_mix[t][i] = (t == i) ? 1.0 : 0.0;
// Remaining virtual tools are 100% filament 1
#if MIXING_STEPPERS < MIXING_VIRTUAL_TOOLS
for (uint8_t t = MIXING_STEPPERS; t < MIXING_VIRTUAL_TOOLS; t++)
for (uint8_t i = 0; i < MIXING_STEPPERS; i++)
mixing_virtual_tool_mix[t][i] = (i == 0) ? 1.0 : 0.0;
#endif
// Initialize mixing to tool 0 color
for (uint8_t i = 0; i < MIXING_STEPPERS; i++)
mixing_factor[i] = mixing_virtual_tool_mix[0][i];
#endif
#if ENABLED(BLTOUCH)
// Make sure any BLTouch error condition is cleared
bltouch_command(BLTOUCH_RESET);
set_bltouch_deployed(true);
set_bltouch_deployed(false);
#endif
#if ENABLED(I2C_POSITION_ENCODERS)
I2CPEM.init();
#endif
#if ENABLED(EXPERIMENTAL_I2CBUS) && I2C_SLAVE_ADDRESS > 0
i2c.onReceive(i2c_on_receive);
i2c.onRequest(i2c_on_request);
#endif
#if ENABLED(ENDSTOP_INTERRUPTS_FEATURE)
setup_endstop_interrupts();
#endif
#if ENABLED(SWITCHING_EXTRUDER) && !DONT_SWITCH
move_extruder_servo(0); // Initialize extruder servo
#endif
#if ENABLED(SWITCHING_NOZZLE)
move_nozzle_servo(0); // Initialize nozzle servo
#endif
#if ENABLED(PARKING_EXTRUDER)
#if ENABLED(PARKING_EXTRUDER_SOLENOIDS_INVERT)
pe_activate_magnet(0);
pe_activate_magnet(1);
#else
pe_deactivate_magnet(0);
pe_deactivate_magnet(1);
#endif
#endif
#if ENABLED(MKS_12864OLED) || ENABLED(MKS_12864OLED_SSD1306)
SET_OUTPUT(LCD_PINS_DC);
OUT_WRITE(LCD_PINS_RS, LOW);
delay(1000);
WRITE(LCD_PINS_RS, HIGH);
#endif
}
/**
* The main Marlin program loop
*
* - Save or log commands to SD
* - Process available commands (if not saving)
* - Call heater manager
* - Call inactivity manager
* - Call endstop manager
* - Call LCD update
*/
void loop() {
if (commands_in_queue < BUFSIZE) get_available_commands();
#if ENABLED(SDSUPPORT)
card.checkautostart(false);
#endif
if (commands_in_queue) {
#if ENABLED(SDSUPPORT)
if (card.saving) {
char* command = command_queue[cmd_queue_index_r];
if (strstr_P(command, PSTR("M29"))) {
// M29 closes the file
card.closefile();
SERIAL_PROTOCOLLNPGM(MSG_FILE_SAVED);
#ifndef USBCON
#if ENABLED(SERIAL_STATS_DROPPED_RX)
SERIAL_ECHOLNPAIR("Dropped bytes: ", customizedSerial.dropped());
#endif
#if ENABLED(SERIAL_STATS_MAX_RX_QUEUED)
SERIAL_ECHOLNPAIR("Max RX Queue Size: ", customizedSerial.rxMaxEnqueued());
#endif
#endif // !USBCON
ok_to_send();
}
else {
// Write the string from the read buffer to SD
card.write_command(command);
if (card.logging)
process_next_command(); // The card is saving because it's logging
else
ok_to_send();
}
}
else
process_next_command();
#else
process_next_command();
#endif // SDSUPPORT
// The queue may be reset by a command handler or by code invoked by idle() within a handler
if (commands_in_queue) {
--commands_in_queue;
if (++cmd_queue_index_r >= BUFSIZE) cmd_queue_index_r = 0;
}
}
endstops.report_state();
idle();
}