Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions Marlin/Configuration_adv.h
Original file line number Diff line number Diff line change
Expand Up @@ -1237,16 +1237,29 @@

#define FTM_POLYS // Disable POLY5/6 to save ~3k of Flash. Preserves TRAPEZOIDAL.
#if ENABLED(FTM_POLYS)
#define FTM_TRAJECTORY_TYPE TRAPEZOIDAL // Block acceleration profile (TRAPEZOIDAL, POLY5, POLY6)
// TRAPEZOIDAL: Continuous Velocity. Max acceleration is respected.
// POLY5: Like POLY6 with 1.5x but uses less CPU.
// POLY6: Continuous Acceleration (aka S_CURVE).
// POLY trajectories not only reduce resonances without rounding corners, but also
// reduce extruder strain due to linear advance.

#define FTM_POLY6_ACCELERATION_OVERSHOOT 1.875f // Max acceleration overshoot factor for POLY6 (1.25 to 1.875)
#endif

/**
* FTM Constant-Jolt Trajectory (7-phase S-curve).
* Jolt is the rate of change of acceleration, not related to Marlin's "classic jerk."
* Ramps acceleration gradually so max acceleration is limited by max speed and distance traveled.
*/
//#define FTM_CONSTANT_JOLT
#if ENABLED(FTM_CONSTANT_JOLT)
#define FTM_DEFAULT_JOLT 250.0f // (m/s³) Default jolt for constant-jolt trajectory.
// Higher values print faster at the cost of increased resonance and extruder stress
#endif

// Block acceleration profile
// :[ 'TRAPEZOIDAL', 'POLY5', 'POLY6', 'CONSTANT_JOLT' ]
#define FTM_TRAJECTORY_TYPE TRAPEZOIDAL // TRAPEZOIDAL: Continuous Velocity. Max acceleration is respected.
// POLY5: Like POLY6 with 1.5x but uses less CPU. Requires FTM_POLYS.
// POLY6: Continuous Acceleration (aka S_CURVE). Requires FTM_POLYS.
// CONSTANT_JOLT: 7-phase S-curve. Requires FTM_CONSTANT_JOLT.
// POLY trajectories not only reduce resonances without rounding corners, but
// also reduce extruder strain due to linear advance.

/**
* Advanced configuration
*/
Expand Down
2 changes: 1 addition & 1 deletion Marlin/Version.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
* here we define this default string as the date where the latest release
* version was tagged.
*/
//#define STRING_DISTRIBUTION_DATE "2026-06-28"
//#define STRING_DISTRIBUTION_DATE "2026-06-29"

/**
* The protocol for communication to the host. Protocol indicates communication
Expand Down
45 changes: 37 additions & 8 deletions Marlin/src/gcode/feature/ft_motion/M494.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,20 @@
#include "../../../lcd/marlinui.h"

void say_ftm_settings() {
#if ANY(FTM_POLYS, FTM_SMOOTHING)
#if ANY(FTM_POLYS, FTM_SMOOTHING, FTM_CONSTANT_JOLT)
const ft_config_t &c = ftMotion.cfg;
#endif

#if ENABLED(FTM_POLYS)
#if HAS_FTM_TRAJECTORY_SELECTION
SERIAL_ECHOLN(F(" Trajectory: "), ftMotion.getTrajectoryName(), C('('), (uint8_t)ftMotion.getTrajectoryType(), C(')'));
if (ftMotion.getTrajectoryType() == TrajectoryType::POLY6)
SERIAL_ECHOLNPGM(" Poly6 Overshoot: ", p_float_t(c.poly6_acceleration_overshoot, 3));
#if ENABLED(FTM_POLYS)
if (ftMotion.getTrajectoryType() == TrajectoryType::POLY6)
SERIAL_ECHOLNPGM(" Poly6 Overshoot: ", p_float_t(c.poly6_acceleration_overshoot, 3));
#endif
#if ENABLED(FTM_CONSTANT_JOLT)
if (ftMotion.getTrajectoryType() == TrajectoryType::CONSTANT_JOLT)
SERIAL_ECHOLNPGM(" Jolt: ", p_float_t(c.jolt / 1000.0f, 1), " m/s^3");
#endif
#endif

#if ENABLED(FTM_SMOOTHING)
Expand All @@ -52,7 +58,7 @@ void GcodeSuite::M494_report(const bool forReplay/*=true*/) {
report_heading_etc(forReplay, F("FT Motion"));
SERIAL_ECHOPGM(" M494 T", (uint8_t)ftMotion.getTrajectoryType());

#if ANY(FTM_POLYS, FTM_SMOOTHING)
#if ANY(FTM_POLYS, FTM_SMOOTHING, FTM_CONSTANT_JOLT)
const ft_config_t &c = ftMotion.cfg;
#endif

Expand All @@ -70,15 +76,21 @@ void GcodeSuite::M494_report(const bool forReplay/*=true*/) {
SERIAL_ECHOPGM(" O", c.poly6_acceleration_overshoot);
#endif

#if ENABLED(FTM_CONSTANT_JOLT)
if (ftMotion.getTrajectoryType() == TrajectoryType::CONSTANT_JOLT)
SERIAL_ECHOPGM(" J", c.jolt / 1000.0f);
#endif

SERIAL_EOL();
}

/**
* M494: Set Fixed-time Motion Control parameters
*
* Parameters:
* T<type> Set trajectory generator type (0=TRAPEZOIDAL, 1=POLY5, 2=POLY6)
* T<type> Set trajectory generator type (0=TRAPEZOIDAL, 1=POLY5, 2=POLY6, 3=CONSTANT_JOLT)
* O<overshoot> Set acceleration overshoot for POLY6 (1.25-1.875)
* J<jolt> Set maximum jolt for CONSTANT_JOLT (m/s³, positive)
* X<time> Set smoothing time for the X axis
* Y<time> Set smoothing time for the Y axis
* Z<time> Set smoothing time for the Z axis
Expand All @@ -87,16 +99,23 @@ void GcodeSuite::M494_report(const bool forReplay/*=true*/) {
void GcodeSuite::M494() {
bool report = !parser.seen_any();

#if ENABLED(FTM_POLYS)
#if HAS_FTM_TRAJECTORY_SELECTION

// Parse trajectory type parameter.
if (parser.seenval('T')) {
if (ftMotion.updateTrajectoryType(TrajectoryType(parser.value_int())))
report = true;
else
SERIAL_ECHOLN(F("?Invalid "), F("(T)rajectory type value. Use 0=TRAPEZOIDAL, 1=POLY5, 2=POLY6"));
SERIAL_ECHOLN(F("?Invalid (T)rajectory type value. (0=TRAPEZOIDAL"
TERN_(FTM_POLYS, ", 1=POLY5, 2=POLY6")
TERN_(FTM_CONSTANT_JOLT, ", 3=CONSTANT_JOLT")
")"));
}

#endif // HAS_FTM_TRAJECTORY_SELECTION

#if ENABLED(FTM_POLYS)

// Parse overshoot parameter.
if (parser.seenval('O')) {
const float val = parser.value_float();
Expand All @@ -110,6 +129,16 @@ void GcodeSuite::M494() {

#endif // FTM_POLYS

#if ENABLED(FTM_CONSTANT_JOLT)

// Parse jolt parameter (user provides m/s³, stored internally as mm/s³).
if (parser.seenval('J')) {
ftMotion.cfg.set_jolt(parser.value_float() * 1000.0f);
report = true;
}

#endif // FTM_CONSTANT_JOLT

#if ENABLED(FTM_SMOOTHING)

auto smooth_set = [](AxisEnum axis, char axis_name) {
Expand Down
7 changes: 7 additions & 0 deletions Marlin/src/inc/Conditionals-4-adv.h
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,13 @@
#if ANY(FTM_DIR_CHANGE_HOLD_X, FTM_DIR_CHANGE_HOLD_Y, FTM_DIR_CHANGE_HOLD_Z, FTM_DIR_CHANGE_HOLD_E)
#define HAS_FTM_DIR_CHANGE_HOLD 1
#endif
#if ANY(FTM_POLYS, FTM_CONSTANT_JOLT)
#define HAS_FTM_TRAJECTORY_SELECTION 1
#endif
// Default trajectory type when not explicitly set
#ifndef FTM_TRAJECTORY_TYPE
#define FTM_TRAJECTORY_TYPE TRAPEZOIDAL
#endif
#endif

// Standard Motion
Expand Down
2 changes: 1 addition & 1 deletion Marlin/src/inc/Version.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
* version was tagged.
*/
#ifndef STRING_DISTRIBUTION_DATE
#define STRING_DISTRIBUTION_DATE "2026-06-28"
#define STRING_DISTRIBUTION_DATE "2026-06-29"
#endif

/**
Expand Down
2 changes: 2 additions & 0 deletions Marlin/src/lcd/language/language_en.h
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,8 @@ namespace LanguageNarrow_en {
LSTR MSG_FTM_TRAPEZOIDAL = _UxGT("Trapezoidal");
LSTR MSG_FTM_POLY5 = _UxGT("5th Order");
LSTR MSG_FTM_POLY6 = _UxGT("6th Order");
LSTR MSG_FTM_CONSTANT_JOLT = _UxGT("Constant Jolt");
LSTR MSG_FTM_JOLT = _UxGT("Jolt (m/s3)");
LSTR MSG_FTM_TRAJECTORY = _UxGT("Trajectory: $");
LSTR MSG_FILAMENT_EN = _UxGT("Filament *");
LSTR MSG_SEGMENTS_PER_SECOND = _UxGT("Segments/Sec");
Expand Down
75 changes: 51 additions & 24 deletions Marlin/src/lcd/menu/menu_motion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ void menu_move() {
END_MENU();
}

#if ENABLED(FTM_POLYS)
#if HAS_FTM_TRAJECTORY_SELECTION

void menu_ftm_trajectory_generator() {
const TrajectoryType traj_type = ftMotion.getTrajectoryType();
Expand All @@ -438,17 +438,24 @@ void menu_move() {
if (traj_type != TrajectoryType::TRAPEZOIDAL) ACTION_ITEM(MSG_FTM_TRAPEZOIDAL, []{
queue.inject(TS(F("M494"), 'T', int(TrajectoryType::TRAPEZOIDAL))); ui.go_back();
});
if (traj_type != TrajectoryType::POLY5) ACTION_ITEM(MSG_FTM_POLY5, []{
queue.inject(TS(F("M494"), 'T', int(TrajectoryType::POLY5))); ui.go_back();
});
if (traj_type != TrajectoryType::POLY6) ACTION_ITEM(MSG_FTM_POLY6, []{
queue.inject(TS(F("M494"), 'T', int(TrajectoryType::POLY6))); ui.go_back();
});
#if ENABLED(FTM_POLYS)
if (traj_type != TrajectoryType::POLY5) ACTION_ITEM(MSG_FTM_POLY5, []{
queue.inject(TS(F("M494"), 'T', int(TrajectoryType::POLY5))); ui.go_back();
});
if (traj_type != TrajectoryType::POLY6) ACTION_ITEM(MSG_FTM_POLY6, []{
queue.inject(TS(F("M494"), 'T', int(TrajectoryType::POLY6))); ui.go_back();
});
#endif
#if ENABLED(FTM_CONSTANT_JOLT)
if (traj_type != TrajectoryType::CONSTANT_JOLT) ACTION_ITEM(MSG_FTM_CONSTANT_JOLT, []{
queue.inject(TS(F("M494"), 'T', int(TrajectoryType::CONSTANT_JOLT))); ui.go_back();
});
#endif

END_MENU();
}

#endif // FTM_POLYS
#endif // HAS_FTM_TRAJECTORY_SELECTION

#if HAS_DYNAMIC_FREQ

Expand Down Expand Up @@ -548,14 +555,24 @@ void menu_move() {
// Show only when FT Motion is active (or optionally always show)
if (TERN(FT_MOTION_NO_MENU_TOGGLE, true, c.active)) {

#if ENABLED(FTM_POLYS)
#if HAS_FTM_TRAJECTORY_SELECTION
SUBMENU_S(ftMotion.getTrajectoryName(), MSG_FTM_TRAJECTORY, menu_ftm_trajectory_generator);
if (ftMotion.getTrajectoryType() == TrajectoryType::POLY6) {
editable.decimal = c.poly6_acceleration_overshoot;
EDIT_ITEM(float42_52, MSG_FTM_POLY6_OVERSHOOT, &editable.decimal, 1.25f, 1.875f, []{
queue.inject(TS(F("M494"), 'O', editable.decimal));
});
}
#if ENABLED(FTM_POLYS)
if (ftMotion.getTrajectoryType() == TrajectoryType::POLY6) {
editable.decimal = c.poly6_acceleration_overshoot;
EDIT_ITEM(float42_52, MSG_FTM_POLY6_OVERSHOOT, &editable.decimal, 1.25f, 1.875f, []{
queue.inject(TS(F("M494"), 'O', editable.decimal));
});
}
#endif
#if ENABLED(FTM_CONSTANT_JOLT)
if (ftMotion.getTrajectoryType() == TrajectoryType::CONSTANT_JOLT) {
editable.decimal = c.jolt / 1000.0f;
EDIT_ITEM(float4, MSG_FTM_JOLT, &editable.decimal, 1.0f, 10000.0f, []{
queue.inject(TS(F("M494"), 'J', editable.decimal));
});
}
#endif
#endif

CARTES_MAP(_FTM_AXIS_SUBMENU);
Expand All @@ -576,7 +593,7 @@ void menu_move() {
// Copy Flash strings to RAM for C-string substitution
// For U8G paged rendering check and skip extra string copy

#if ENABLED(FTM_POLYS)
#if HAS_FTM_TRAJECTORY_SELECTION
#if CACHE_FOR_SPEED
bool got_t = false;
#endif
Expand All @@ -592,7 +609,7 @@ void menu_move() {

#else // !__AVR__

#if ENABLED(FTM_POLYS)
#if HAS_FTM_TRAJECTORY_SELECTION
auto _traj_name = []{ return ftMotion.getTrajectoryName(); };
#endif

Expand All @@ -601,14 +618,24 @@ void menu_move() {
START_MENU();
BACK_ITEM(MSG_TUNE);

#if ENABLED(FTM_POLYS)
#if HAS_FTM_TRAJECTORY_SELECTION
SUBMENU_S(_traj_name(), MSG_FTM_TRAJECTORY, menu_ftm_trajectory_generator);
if (ftMotion.getTrajectoryType() == TrajectoryType::POLY6) {
editable.decimal = ftMotion.cfg.poly6_acceleration_overshoot;
EDIT_ITEM(float42_52, MSG_FTM_POLY6_OVERSHOOT, &editable.decimal, 1.25f, 1.875f, []{
queue.inject(TS(F("M494"), 'O', editable.decimal));
});
}
#if ENABLED(FTM_POLYS)
if (ftMotion.getTrajectoryType() == TrajectoryType::POLY6) {
editable.decimal = ftMotion.cfg.poly6_acceleration_overshoot;
EDIT_ITEM(float42_52, MSG_FTM_POLY6_OVERSHOOT, &editable.decimal, 1.25f, 1.875f, []{
queue.inject(TS(F("M494"), 'O', editable.decimal));
});
}
#endif
#if ENABLED(FTM_CONSTANT_JOLT)
if (ftMotion.getTrajectoryType() == TrajectoryType::CONSTANT_JOLT) {
editable.decimal = ftMotion.cfg.jolt / 1000.0f;
EDIT_ITEM(float4, MSG_FTM_JOLT, &editable.decimal, 1.0f, 10000.0f, []{
queue.inject(TS(F("M494"), 'J', editable.decimal));
});
}
#endif
#endif

SHAPED_MAP(_FTM_AXIS_SUBMENU);
Expand Down
Loading
Loading