- 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')) {
+ endstops.monitor_flag = parser.value_bool();
+ SERIAL_PROTOCOLPGM("endstop monitor ");
+ serialprintPGM(endstops.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 pin_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 (pin_t pin = first_pin; pin <= last_pin; pin++) {
+ if (!ignore_protection && pin_is_protected(pin)) 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 (pin_t pin = first_pin; pin <= last_pin; pin++) {
+ if (!ignore_protection && pin_is_protected(pin)) 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 (pin_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 ProbePtRaise raise_after = parser.boolval('E') ? PROBE_PT_STOW : PROBE_PT_RAISE;
+
+ 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 (!position_is_reachable_by_probe(X_probe_location, Y_probe_location)) {
+ SERIAL_PROTOCOLLNPGM("? (X,Y) out of bounds.");
+ return;
+ }
+
+ 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();
+
+ float 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, raise_after, 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_PRINTABLE_RADIUS),
+ 0.3333333333 * (DELTA_PRINTABLE_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++) {
+ float 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, raise_after);
+
+ // 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
+ */
+ float 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
+
+ #ifdef Z_AFTER_PROBING
+ move_z_after_probing();
+ #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_reset_status();
+ }
+ #endif
+ }
+
+ #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_SENSOR
+ SERIAL_PROTOCOLPGM(MSG_OK);
+ thermalManager.print_heaterstates();
+ #else // !HAS_TEMP_SENSOR
+ SERIAL_ERROR_START();
+ SERIAL_ERRORLNPGM(MSG_ERR_NO_THERMISTORS);
+ #endif
+
+ SERIAL_EOL();
+}
+
+#if ENABLED(AUTO_REPORT_TEMPERATURES)
+
+ /**
+ * 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, 255U);
+ }
+ }
+
+ /**
+ * 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'),
+ set_temp = no_wait_for_cooling || parser.seenval('R');
+ if (set_temp) {
+ 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_reset_status();
+ }
+ else
+ print_job_timer.start();
+ #endif
+
+ #if ENABLED(ULTRA_LCD)
+ const bool heating = thermalManager.isHeatingHotend(target_extruder);
+ if (heating || !no_wait_for_cooling)
+ #if HOTENDS > 1
+ lcd_status_printf_P(0, heating ? PSTR("E%i " MSG_HEATING) : PSTR("E%i " MSG_COOLING), target_extruder + 1);
+ #else
+ lcd_setstatusPGM(heating ? PSTR("E " MSG_HEATING) : PSTR("E " MSG_COOLING));
+ #endif
+ #endif
+ }
+
+ #if ENABLED(AUTOTEMP)
+ planner.autotemp_M104_M109();
+ #endif
+
+ if (!set_temp) return;
+
+ #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, old_temp = 9999;
+ 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();
+ reset_stepper_timeout(); // Keep steppers powered
+
+ 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 = ABS(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 < float(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_reset_status();
+ #if ENABLED(PRINTER_EVENT_LEDS)
+ leds.set_white();
+ #endif
+ }
+
+ #if DISABLED(BUSY_WHILE_HEATING)
+ KEEPALIVE_STATE(IN_HANDLER);
+ #endif
+}
+
+#if HAS_HEATED_BED
+
+ /**
+ * M140: Set bed temperature
+ */
+ inline void gcode_M140() {
+ if (DEBUGGING(DRYRUN)) return;
+ if (parser.seenval('S')) thermalManager.setTargetBed(parser.value_celsius());
+ }
+
+ #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;
+
+ 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;
+
+ lcd_setstatusPGM(thermalManager.isHeatingBed() ? PSTR(MSG_BED_HEATING) : PSTR(MSG_BED_COOLING));
+
+ #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 = 127;
+ #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();
+ reset_stepper_timeout(); // Keep steppers powered
+
+ 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 = ABS(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 < float(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_reset_status();
+ #if DISABLED(BUSY_WHILE_HEATING)
+ KEEPALIVE_STATE(IN_HANDLER);
+ #endif
+ }
+
+#endif // HAS_HEATED_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');
+
+ static const 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
+ ;
+
+ static const 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_ptr(&debug_strings[i]));
+ }
+ }
+ }
+ else {
+ SERIAL_ECHOPGM(MSG_DEBUG_OFF);
+ #if !defined(__AVR__) || !defined(USBCON)
+ #if ENABLED(SERIAL_STATS_RX_BUFFER_OVERRUNS)
+ SERIAL_ECHOPAIR("\nBuffer Overruns: ", customizedSerial.buffer_overruns());
+ #endif
+
+ #if ENABLED(SERIAL_STATS_RX_FRAMING_ERRORS)
+ SERIAL_ECHOPAIR("\nFraming Errors: ", customizedSerial.framing_errors());
+ #endif
+
+ #if ENABLED(SERIAL_STATS_DROPPED_RX)
+ SERIAL_ECHOPAIR("\nDropped bytes: ", customizedSerial.dropped());
+ #endif
+
+ #if ENABLED(SERIAL_STATS_MAX_RX_QUEUED)
+ SERIAL_ECHOPAIR("\nMax RX Queue Size: ", customizedSerial.rxMaxEnqueued());
+ #endif
+ #endif // !__AVR__ || !USBCON
+ }
+ 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
+
+#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;
+ }
+
+ PSU_ON();
+
+ /**
+ * 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 DISABLED(AUTO_POWER_CONTROL)
+ delay(100); // Wait for power to settle
+ restore_stepper_drivers();
+ #endif
+
+ #if ENABLED(ULTIPANEL)
+ lcd_reset_status();
+ #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();
+ planner.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
+ suicide();
+ #elif HAS_POWER_SWITCH
+ PSU_OFF();
+ #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_CART] = false; }
+
+/**
+ * M83: Set E codes relative while in Absolute Coordinates (G90) mode
+ */
+inline void gcode_M83() { axis_relative_modes[E_CART] = 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) {
+ planner.finish_and_disable();
+ }
+ else {
+ planner.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
+ if (ubl.lcd_map_control) 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.
+ * (for Hangprinter: A, B, C, D, 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_NUM_AXIS(i) {
+ if (parser.seen(RAW_AXIS_CODES(i))) {
+ if (i == E_AXIS) {
+ const float value = parser.value_per_axis_unit((AxisEnum)(E_AXIS + TARGET_EXTRUDER));
+ if (value < 20) {
+ const float factor = planner.axis_steps_per_mm[E_AXIS + TARGET_EXTRUDER] / value; // increase e constants if M92 E14 is given for netfab.
+ #if DISABLED(JUNCTION_DEVIATION)
+ planner.max_jerk[E_AXIS] *= factor;
+ #endif
+ 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 {
+ #if ENABLED(LINE_BUILDUP_COMPENSATION_FEATURE)
+ SERIAL_ECHOLNPGM("Warning: "
+ "M92 A, B, C, and D only affect acceleration planning "
+ "when BUILDUP_COMPENSATION_FEATURE is enabled.");
+ #endif
+ 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_PROTOCOLPAIR("X:", LOGICAL_X_POSITION(current_position[X_AXIS]));
+ SERIAL_PROTOCOLPAIR(" Y:", LOGICAL_Y_POSITION(current_position[Y_AXIS]));
+ SERIAL_PROTOCOLPAIR(" Z:", LOGICAL_Z_POSITION(current_position[Z_AXIS]));
+ SERIAL_PROTOCOLPAIR(" E:", current_position[E_CART]);
+
+ #if ENABLED(HANGPRINTER)
+ SERIAL_EOL();
+ SERIAL_PROTOCOLPAIR("A:", line_lengths[A_AXIS]);
+ SERIAL_PROTOCOLPAIR(" B:", line_lengths[B_AXIS]);
+ SERIAL_PROTOCOLPAIR(" C:", line_lengths[C_AXIS]);
+ SERIAL_PROTOCOLLNPAIR(" D:", line_lengths[D_AXIS]);
+ #endif
+
+ stepper.report_positions();
+
+ #if IS_SCARA
+ SERIAL_PROTOCOLPAIR("SCARA Theta:", planner.get_axis_position_degrees(A_AXIS));
+ SERIAL_PROTOCOLLNPAIR(" Psi+Theta:", planner.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() {
+
+ 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_xyz(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
+
+ planner.synchronize();
+
+ SERIAL_PROTOCOLPGM("Stepper:");
+ LOOP_NUM_AXIS(i) {
+ SERIAL_CHAR(' ');
+ SERIAL_CHAR(RAW_AXIS_CODES(i));
+ SERIAL_CHAR(':');
+ SERIAL_PROTOCOL(stepper.position((AxisEnum)i));
+ }
+ SERIAL_EOL();
+
+ #if IS_SCARA
+ const float deg[XYZ] = {
+ planner.get_axis_position_degrees(A_AXIS),
+ planner.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], planner.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_CART] - current_position[E_CART]
+ };
+ 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')) return report_current_position_detail();
+ #endif
+
+ planner.synchronize();
+
+ const uint16_t sval = parser.ushortval('S');
+
+ #if ENABLED(MECHADUINO_I2C_COMMANDS)
+ if (sval == 1) return report_axis_position_from_encoder_data();
+ #endif
+
+ if (sval == 2) return report_xyz_from_stepper_position();
+
+ 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
+ );
+
+ // AUTOREPORT_SD_STATUS (M27 extension)
+ cap_line(PSTR("AUTOREPORT_SD_STATUS")
+ #if ENABLED(AUTO_REPORT_SD_STATUS)
+ , true
+ #endif
+ );
+
+ // THERMAL_PROTECTION
+ cap_line(PSTR("THERMAL_PROTECTION")
+ #if ENABLED(THERMAL_PROTECTION_HOTENDS) && ENABLED(THERMAL_PROTECTION_BED)
+ , true
+ #endif
+ );
+
+ #endif // EXTENDED_CAPABILITIES_REPORT
+}
+
+/**
+ * M117: Set LCD Status Message
+ */
+inline void gcode_M117() {
+ if (parser.string_arg[0])
+ lcd_setstatus(parser.string_arg);
+ else
+ lcd_reset_status();
+}
+
+/**
+ * M118: Display a message in the host console.
+ *
+ * A1 Prepend '// ' for an action command, as in OctoPrint
+ * E1 Have the host 'echo:' the text
+ */
+inline void gcode_M118() {
+ bool hasE = false, hasA = false;
+ char *p = parser.string_arg;
+ for (uint8_t i = 2; i--;)
+ if ((p[0] == 'A' || p[0] == 'E') && p[1] == '1') {
+ if (p[0] == 'A') hasA = true;
+ if (p[0] == 'E') hasE = true;
+ p += 2;
+ while (*p == ' ') ++p;
+ }
+ if (hasE) SERIAL_ECHO_START();
+ if (hasA) SERIAL_ECHOPGM("// ");
+ SERIAL_ECHOLN(p);
+}
+
+/**
+ * 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 = -ABS(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) && DISABLED(DELTA)
+ 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)) )
+ 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_NUM_AXIS(i) {
+ if (parser.seen(RAW_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_NUM_AXIS(i)
+ if (parser.seen(RAW_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
+ */
+inline void gcode_M204() {
+ bool report = true;
+ if (parser.seenval('S')) { // Kept for legacy compatibility. Should NOT BE USED for new developments.
+ planner.travel_acceleration = planner.acceleration = parser.value_linear_units();
+ report = false;
+ }
+ if (parser.seenval('P')) {
+ planner.acceleration = parser.value_linear_units();
+ report = false;
+ }
+ if (parser.seenval('R')) {
+ planner.retract_acceleration = parser.value_linear_units();
+ report = false;
+ }
+ if (parser.seenval('T')) {
+ planner.travel_acceleration = parser.value_linear_units();
+ report = false;
+ }
+ if (report) {
+ SERIAL_ECHOPAIR("Acceleration: P", planner.acceleration);
+ SERIAL_ECHOPAIR(" R", planner.retract_acceleration);
+ SERIAL_ECHOLNPAIR(" T", planner.travel_acceleration);
+ }
+}
+
+/**
+ * M205: Set Advanced Settings
+ *
+ * Q = Min Segment Time (µs)
+ * S = Min Feed Rate (units/s)
+ * T = Min Travel Feed Rate (units/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)
+ * J = Junction Deviation (mm) (Requires JUNCTION_DEVIATION)
+ */
+inline void gcode_M205() {
+ if (parser.seen('Q')) planner.min_segment_time_us = parser.value_ulong();
+ 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 ENABLED(JUNCTION_DEVIATION)
+ if (parser.seen('J')) {
+ const float junc_dev = parser.value_linear_units();
+ if (WITHIN(junc_dev, 0.01f, 0.3f)) {
+ planner.junction_deviation_mm = junc_dev;
+ planner.recalculate_max_e_jerk();
+ }
+ else {
+ SERIAL_ERROR_START();
+ SERIAL_ERRORLNPGM("?J out of range (0.01 to 0.3)");
+ }
+ }
+ #else
+ #if ENABLED(HANGPRINTER)
+ if (parser.seen('A')) planner.max_jerk[A_AXIS] = parser.value_linear_units();
+ if (parser.seen('B')) planner.max_jerk[B_AXIS] = parser.value_linear_units();
+ if (parser.seen('C')) planner.max_jerk[C_AXIS] = parser.value_linear_units();
+ if (parser.seen('D')) planner.max_jerk[D_AXIS] = parser.value_linear_units();
+ #else
+ 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 HAS_MESH
+ if (planner.max_jerk[Z_AXIS] <= 0.1f)
+ SERIAL_ECHOLNPGM("WARNING! Low Z Jerk may lead to unwanted pauses.");
+ #endif
+ }
+ #endif
+ if (parser.seen('E')) planner.max_jerk[E_AXIS] = parser.value_linear_units();
+ #endif
+}
+
+#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 = Gamma (Tower 3) angle trim
+ */
+ 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(HANGPRINTER)
+ /**
+ * M665: Set HANGPRINTER settings
+ *
+ * Parameters:
+ *
+ * W[anchor_A_y] - A-anchor's y coordinate (see note)
+ * E[anchor_A_z] - A-anchor's z coordinate (see note)
+ * R[anchor_B_x] - B-anchor's x coordinate (see note)
+ * T[anchor_B_y] - B-anchor's y coordinate (see note)
+ * Y[anchor_B_z] - B-anchor's z coordinate (see note)
+ * U[anchor_C_x] - C-anchor's x coordinate (see note)
+ * I[anchor_C_y] - C-anchor's y coordinate (see note)
+ * O[anchor_C_z] - C-anchor's z coordinate (see note)
+ * P[anchor_D_z] - D-anchor's z coordinate (see note)
+ * S[segments-per-second] - Segments-per-second
+ *
+ * Note: All xyz coordinates are measured relative to the line's pivot point in the mover,
+ * when it is at its home position (nozzle in (0,0,0), and lines tight).
+ * The y-axis is defined to be horizontal right above/below the A-lines when mover is at home.
+ * The z-axis is along the vertical direction.
+ */
+ inline void gcode_M665() {
+ if (parser.seen('W')) anchor_A_y = parser.value_float();
+ if (parser.seen('E')) anchor_A_z = parser.value_float();
+ if (parser.seen('R')) anchor_B_x = parser.value_float();
+ if (parser.seen('T')) anchor_B_y = parser.value_float();
+ if (parser.seen('Y')) anchor_B_z = parser.value_float();
+ if (parser.seen('U')) anchor_C_x = parser.value_float();
+ if (parser.seen('I')) anchor_C_y = parser.value_float();
+ if (parser.seen('O')) anchor_C_z = parser.value_float();
+ if (parser.seen('P')) anchor_D_z = parser.value_float();
+ if (parser.seen('S')) delta_segments_per_second = parser.value_float();
+ recalc_hangprinter_settings();
+ }
+
+#elif ENABLED(X_DUAL_ENDSTOPS) || ENABLED(Y_DUAL_ENDSTOPS) || ENABLED(Z_DUAL_ENDSTOPS)
+
+ /**
+ * M666: Set Dual Endstops offsets for X, Y, and/or Z.
+ * With no parameters report current offsets.
+ */
+ inline void gcode_M666() {
+ bool report = true;
+ #if ENABLED(X_DUAL_ENDSTOPS)
+ if (parser.seenval('X')) {
+ endstops.x_endstop_adj = parser.value_linear_units();
+ report = false;
+ }
+ #endif
+ #if ENABLED(Y_DUAL_ENDSTOPS)
+ if (parser.seenval('Y')) {
+ endstops.y_endstop_adj = parser.value_linear_units();
+ report = false;
+ }
+ #endif
+ #if ENABLED(Z_DUAL_ENDSTOPS)
+ if (parser.seenval('Z')) {
+ endstops.z_endstop_adj = parser.value_linear_units();
+ report = false;
+ }
+ #endif
+ if (report) {
+ SERIAL_ECHOPGM("Dual Endstop Adjustment (mm): ");
+ #if ENABLED(X_DUAL_ENDSTOPS)
+ SERIAL_ECHOPAIR(" X", endstops.x_endstop_adj);
+ #endif
+ #if ENABLED(Y_DUAL_ENDSTOPS)
+ SERIAL_ECHOPAIR(" Y", endstops.y_endstop_adj);
+ #endif
+ #if ENABLED(Z_DUAL_ENDSTOPS)
+ SERIAL_ECHOPAIR(" Z", endstops.z_endstop_adj);
+ #endif
+ SERIAL_EOL();
+ }
+ }
+
+#endif // X_DUAL_ENDSTOPS || Y_DUAL_ENDSTOPS || 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/get hotend offset (in linear units)
+ *
+ * T
+ * X
+ * Y
+ * Z - Available with DUAL_X_CARRIAGE, SWITCHING_NOZZLE, and PARKING_EXTRUDER
+ */
+ inline void gcode_M218() {
+ if (get_target_extruder_from_command(218) || target_extruder == 0) return;
+
+ bool report = true;
+ if (parser.seenval('X')) {
+ hotend_offset[X_AXIS][target_extruder] = parser.value_linear_units();
+ report = false;
+ }
+ if (parser.seenval('Y')) {
+ hotend_offset[Y_AXIS][target_extruder] = parser.value_linear_units();
+ report = false;
+ }
+
+ #if HAS_HOTEND_OFFSET_Z
+ if (parser.seenval('Z')) {
+ hotend_offset[Z_AXIS][target_extruder] = parser.value_linear_units();
+ report = false;
+ }
+ #endif
+
+ if (report) {
+ 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 HAS_HOTEND_OFFSET_Z
+ SERIAL_CHAR(',');
+ SERIAL_ECHO(hotend_offset[Z_AXIS][e]);
+ #endif
+ }
+ SERIAL_EOL();
+ }
+
+ #if ENABLED(DELTA)
+ if (target_extruder == active_extruder)
+ do_blocking_move_to_xy(current_position[X_AXIS], current_position[Y_AXIS], planner.max_feedrate_mm_s[X_AXIS]);
+ #endif
+ }
+
+#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);
+ }
+ else {
+ SERIAL_ECHO_START();
+ SERIAL_CHAR('E');
+ SERIAL_CHAR('0' + target_extruder);
+ SERIAL_ECHOPAIR(" Flow: ", planner.flow_percentage[target_extruder]);
+ SERIAL_CHAR('%');
+ SERIAL_EOL();
+ }
+}
+
+/**
+ * M226: Wait until the specified pin reaches the state required (M226 P S)
+ */
+inline void gcode_M226() {
+ if (parser.seen('P')) {
+ const int pin = parser.value_int(), pin_state = parser.intval('S', -1);
+ if (WITHIN(pin_state, -1, 1) && pin > -1) {
+ if (pin_is_protected(pin))
+ protected_pin_err();
+ else {
+ int target = LOW;
+ planner.synchronize();
+ pinMode(pin, INPUT);
+ switch (pin_state) {
+ case 1: target = HIGH; break;
+ case 0: target = LOW; break;
+ case -1: target = !digitalRead(pin); break;
+ }
+ while (digitalRead(pin) != target) idle();
+ }
+ } // pin_state -1 0 1 && pin > -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_ERRORLNPGM("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[int] 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')) thermalManager.lpq_len = parser.value_float();
+ NOMORE(thermalManager.lpq_len, LPQ_MAX_LEN);
+ NOLESS(thermalManager.lpq_len, 0);
+ #endif
+
+ thermalManager.update_pid();
+ 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_ERRORLNPGM(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() { planner.synchronize(); }
+
+#if HAS_BED_PROBE
+
+ /**
+ * M401: Deploy and activate the Z probe
+ */
+ inline void gcode_M401() {
+ DEPLOY_PROBE();
+ report_current_position();
+ }
+
+ /**
+ * M402: Deactivate and stow the Z probe
+ */
+ inline void gcode_M402() {
+ STOW_PROBE();
+ #ifdef Z_AFTER_PROBING
+ move_z_after_probing();
+ #endif
+ report_current_position();
+ }
+
+#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() {
+ planner.quick_stop();
+ planner.synchronize();
+ set_current_from_steppers_for_axis(ALL_AXES);
+ SYNC_PLAN_POSITION_KINEMATIC();
+}
+
+#if HAS_LEVELING
+
+ //#define M420_C_USE_MEAN
+
+ /**
+ * 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)
+ * T[map] 0:Human-readable 1:CSV 2:"LCD" 4:Compact
+ *
+ * With mesh-based leveling only:
+ *
+ * C Center mesh on the mean of the lowest and highest
+ */
+ inline void gcode_M420() {
+ const bool seen_S = parser.seen('S');
+ bool to_enable = seen_S ? parser.value_bool() : planner.leveling_active;
+
+ // If disabling leveling do it right away
+ // (Don't disable for just M420 or M420 V)
+ if (seen_S && !to_enable) set_bed_leveling_enabled(false);
+
+ 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')) {
+
+ set_bed_leveling_enabled(false);
+
+ #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 or V display the map info
+ if (parser.seen('L') || parser.seen('V')) {
+ ubl.display_map(parser.byteval('T'));
+ SERIAL_ECHOPGM("Mesh is ");
+ if (!ubl.mesh_is_valid()) SERIAL_ECHOPGM("in");
+ SERIAL_ECHOLNPAIR("valid\nStorage slot: ", ubl.storage_slot);
+ }
+
+ #endif // AUTO_BED_LEVELING_UBL
+
+ #if HAS_MESH
+
+ #if ENABLED(MESH_BED_LEVELING)
+ #define Z_VALUES(X,Y) mbl.z_values[X][Y]
+ #else
+ #define Z_VALUES(X,Y) z_values[X][Y]
+ #endif
+
+ // Subtract the given value or the mean from all mesh values
+ if (leveling_is_valid() && parser.seen('C')) {
+ const float cval = parser.value_float();
+ #if ENABLED(AUTO_BED_LEVELING_UBL)
+
+ set_bed_leveling_enabled(false);
+ ubl.adjust_mesh_to_mean(true, cval);
+
+ #else
+
+ #if ENABLED(M420_C_USE_MEAN)
+
+ // Get the sum and average of all mesh values
+ float mesh_sum = 0;
+ for (uint8_t x = GRID_MAX_POINTS_X; x--;)
+ for (uint8_t y = GRID_MAX_POINTS_Y; y--;)
+ mesh_sum += Z_VALUES(x, y);
+ const float zmean = mesh_sum / float(GRID_MAX_POINTS);
+
+ #else
+
+ // Find the low and high mesh values
+ float lo_val = 100, hi_val = -100;
+ for (uint8_t x = GRID_MAX_POINTS_X; x--;)
+ for (uint8_t y = GRID_MAX_POINTS_Y; y--;) {
+ const float z = Z_VALUES(x, y);
+ NOMORE(lo_val, z);
+ NOLESS(hi_val, z);
+ }
+ // Take the mean of the lowest and highest
+ const float zmean = (lo_val + hi_val) / 2.0 + cval;
+
+ #endif
+
+ // If not very close to 0, adjust the mesh
+ if (!NEAR_ZERO(zmean)) {
+ set_bed_leveling_enabled(false);
+ // Subtract the mean from all values
+ for (uint8_t x = GRID_MAX_POINTS_X; x--;)
+ for (uint8_t y = GRID_MAX_POINTS_Y; y--;)
+ Z_VALUES(x, y) -= zmean;
+ #if ENABLED(ABL_BILINEAR_SUBDIVISION)
+ bed_level_virt_interpolate();
+ #endif
+ }
+
+ #endif
+ }
+
+ #endif // HAS_MESH
+
+ // 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
+
+ // Enable leveling if specified, or if previously active
+ set_bed_leveling_enabled(to_enable);
+
+ // Error if leveling failed to enable or reenable
+ if (to_enable && !planner.leveling_active) {
+ SERIAL_ERROR_START();
+ SERIAL_ERRORLNPGM(MSG_ERR_M420_FAILED);
+ }
+
+ SERIAL_ECHO_START();
+ SERIAL_ECHOLNPAIR("Bed Leveling ", planner.leveling_active ? 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 // HAS_LEVELING
+
+#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