Skip to content

Commit 2963e23

Browse files
committed
Add alt-entry profiles and fix sub-cycle brake transitions
- Alt-entry: pre-compute alternate profile for N+1 with v0 matching brake exit velocity, RT picks closer match at junction - Sub-cycle gap: replace two-stage with single-stage position-control when main profile < 1 servo cycle (eliminates displacement dip) - Spill-over: write stop profiles on downstream segments when deceleration crosses segment boundaries - Feed-hold recovery: abort stale cursor walk on resume, use predictStateAtTime for phase 2 transition
1 parent 23619fb commit 2963e23

3 files changed

Lines changed: 311 additions & 48 deletions

File tree

src/emc/motion_planning/motion_planning_9d.cc

Lines changed: 272 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2180,6 +2180,7 @@ static int recomputeDownstreamProfiles(TP_STRUCT *tp,
21802180

21812181
recomputed++;
21822182
}
2183+
21832184
return recomputed;
21842185
}
21852186

@@ -2265,6 +2266,88 @@ static double estimateExitAtFeed(TP_STRUCT const *tp, TC_STRUCT const *tc,
22652266
tc->maxaccel, default_jerk, max_vel, target);
22662267
}
22672268

2269+
/**
2270+
* @brief Write an alternate entry profile for the next segment.
2271+
*
2272+
* When a brake branch on the current segment changes its exit velocity,
2273+
* the next segment's pre-computed profile has a stale entry velocity.
2274+
* This writes an alternate profile with v0 = brake's exit velocity.
2275+
* RT picks whichever profile (main or alt_entry) has v0 closer to the
2276+
* actual junction velocity — smooth regardless of whether brake was taken.
2277+
*
2278+
* Called BEFORE commitBranch so the cushion is in place before the jump.
2279+
*/
2280+
static void writeAltEntry(TP_STRUCT *tp, TC_STRUCT *tc,
2281+
double branch_exit_vel, double new_feed_scale)
2282+
{
2283+
if (tc->term_cond != TC_TERM_COND_TANGENT) return;
2284+
2285+
TC_STRUCT *next = tcqItem_user(&tp->queue, 1);
2286+
if (!next) return;
2287+
2288+
// Skip if alt_entry wouldn't differ meaningfully from main profile
2289+
if (__atomic_load_n(&next->shared_9d.profile.valid, __ATOMIC_ACQUIRE)) {
2290+
double main_v0 = next->shared_9d.profile.v[0];
2291+
if (fabs(branch_exit_vel - main_v0) < 0.1) return;
2292+
}
2293+
2294+
double next_vel_limit = getEffectiveVelLimit(tp, next);
2295+
double next_max_vel = next_vel_limit * new_feed_scale;
2296+
next_max_vel = applyVLimit(tp, next, next_max_vel);
2297+
double next_jerk = next->maxjerk > 0 ? next->maxjerk : g_handoff_config.default_max_jerk;
2298+
2299+
// Determine target exit velocity for next segment
2300+
double unscaled_fv = atomicLoadDouble(&next->shared_9d.final_vel);
2301+
double target_exit_vel = (next->term_cond == TC_TERM_COND_TANGENT)
2302+
? fmin(unscaled_fv * new_feed_scale, next_max_vel)
2303+
: 0.0;
2304+
target_exit_vel = applyKinkVelCap(target_exit_vel, unscaled_fv, next_max_vel, next->kink_vel);
2305+
2306+
double achievable_exit = findAchievableExitVelocity(
2307+
branch_exit_vel, 0.0,
2308+
next->target, next->maxaccel, next_jerk,
2309+
next_max_vel, target_exit_vel);
2310+
2311+
// Compute Ruckig profile for next segment with adjusted entry velocity
2312+
try {
2313+
ruckig::Ruckig<1> otg(g_handoff_config.servo_cycle_time_sec);
2314+
ruckig::InputParameter<1> input;
2315+
ruckig::Trajectory<1> traj;
2316+
2317+
input.current_position = {0.0};
2318+
input.current_velocity = {branch_exit_vel};
2319+
input.current_acceleration = {0.0};
2320+
input.target_position = {next->target};
2321+
input.target_velocity = {achievable_exit};
2322+
input.target_acceleration = {0.0};
2323+
input.max_velocity = {next_max_vel};
2324+
input.max_acceleration = {next->maxaccel};
2325+
input.max_jerk = {next_jerk};
2326+
2327+
auto result = otg.calculate(input, traj);
2328+
if (result != ruckig::Result::Working && result != ruckig::Result::Finished)
2329+
return;
2330+
2331+
ruckig_profile_t alt_profile;
2332+
memset(&alt_profile, 0, sizeof(alt_profile));
2333+
copyRuckigProfile(traj, &alt_profile);
2334+
if (!alt_profile.valid) return;
2335+
if (profileHasNegativeVelocity(&alt_profile)) return;
2336+
2337+
alt_profile.computed_feed_scale = new_feed_scale;
2338+
alt_profile.computed_vel_limit = next_vel_limit;
2339+
alt_profile.computed_vLimit = tp->vLimit;
2340+
2341+
// Write alt_entry — cushion in place before the jump
2342+
next->shared_9d.alt_entry.profile = alt_profile;
2343+
next->shared_9d.alt_entry.v0 = branch_exit_vel;
2344+
__atomic_thread_fence(__ATOMIC_RELEASE);
2345+
__atomic_store_n(&next->shared_9d.alt_entry.valid, 1, __ATOMIC_RELEASE);
2346+
} catch (...) {
2347+
// Alt-entry is best-effort; failure falls back to v0 correction in RT
2348+
}
2349+
}
2350+
22682351
/**
22692352
* @brief Compute a branch trajectory for feed override change
22702353
*
@@ -2289,6 +2372,11 @@ static double estimateExitAtFeed(TP_STRUCT const *tp, TC_STRUCT const *tc,
22892372
*
22902373
* @return true if branch was successfully computed and committed
22912374
*/
2375+
static void computeSpillOver(const ruckig_profile_t *profile,
2376+
double remaining_dist,
2377+
double *spill_vel,
2378+
double *spill_acc);
2379+
22922380
bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale)
22932381
{
22942382
if (!tp || !tc) return false;
@@ -2532,6 +2620,75 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale)
25322620

25332621
bool committed = commitBranch(&tc->shared_9d, &main_profile, NULL, 0.0, 0.0,
25342622
handoff_time, state.position, 0.0, window_end_time);
2623+
2624+
// Spill-over: if stop distance exceeds remaining segment, the
2625+
// machine will cross into downstream segments mid-deceleration.
2626+
// Write alt-entry stop profiles so RT has correct v0 at each
2627+
// boundary (same pattern as tpRequestAbortBranch_9D).
2628+
if (committed && tc->term_cond == TC_TERM_COND_TANGENT) {
2629+
double spill_vel, spill_acc;
2630+
computeSpillOver(&main_profile, remaining, &spill_vel, &spill_acc);
2631+
2632+
if (spill_vel > TP_VEL_EPSILON) {
2633+
TC_QUEUE_STRUCT *queue = &tp->queue;
2634+
int queue_len = tcqLen_user(queue);
2635+
double entry_vel = spill_vel;
2636+
double entry_acc = spill_acc;
2637+
2638+
for (int i = 1; i < queue_len && entry_vel > TP_VEL_EPSILON; i++) {
2639+
TC_STRUCT *next = tcqItem_user(queue, i);
2640+
if (!next || next->target < 1e-9) break;
2641+
2642+
double this_entry_vel = entry_vel;
2643+
double next_vel_limit = getEffectiveVelLimit(tp, next);
2644+
double next_jerk = next->maxjerk > 0 ? next->maxjerk : default_jerk;
2645+
2646+
try {
2647+
ruckig::Ruckig<1> otg_spill(g_handoff_config.servo_cycle_time_sec);
2648+
ruckig::InputParameter<1> in_spill;
2649+
ruckig::Trajectory<1> traj_spill;
2650+
2651+
in_spill.control_interface = ruckig::ControlInterface::Velocity;
2652+
in_spill.current_position = {0.0};
2653+
in_spill.current_velocity = {entry_vel};
2654+
in_spill.current_acceleration = {entry_acc};
2655+
in_spill.target_velocity = {0.0};
2656+
in_spill.target_acceleration = {0.0};
2657+
in_spill.max_velocity = {next_vel_limit};
2658+
in_spill.max_acceleration = {next->maxaccel};
2659+
in_spill.max_jerk = {next_jerk};
2660+
2661+
auto result = otg_spill.calculate(in_spill, traj_spill);
2662+
if (result != ruckig::Result::Working &&
2663+
result != ruckig::Result::Finished)
2664+
break;
2665+
2666+
ruckig_profile_t stop_prof;
2667+
memset(&stop_prof, 0, sizeof(stop_prof));
2668+
copyRuckigProfile(traj_spill, &stop_prof);
2669+
if (!stop_prof.valid || profileHasNegativeVelocity(&stop_prof))
2670+
break;
2671+
stop_prof.computed_feed_scale = 0.0;
2672+
stop_prof.computed_vel_limit = next_vel_limit;
2673+
stop_prof.computed_vLimit = tp->vLimit;
2674+
2675+
// Check if this segment also spills over
2676+
computeSpillOver(&stop_prof, next->target,
2677+
&entry_vel, &entry_acc);
2678+
2679+
// Write as alt-entry so RT picks it at the junction
2680+
next->shared_9d.alt_entry.profile = stop_prof;
2681+
next->shared_9d.alt_entry.v0 = this_entry_vel;
2682+
__atomic_thread_fence(__ATOMIC_RELEASE);
2683+
__atomic_store_n(&next->shared_9d.alt_entry.valid, 1,
2684+
__ATOMIC_RELEASE);
2685+
} catch (...) {
2686+
break;
2687+
}
2688+
}
2689+
}
2690+
}
2691+
25352692
return committed;
25362693
}
25372694

@@ -2672,6 +2829,8 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale)
26722829
brake_profile.computed_vLimit = tp->vLimit;
26732830
}
26742831

2832+
bool use_single_stage = false; // set true if sub-cycle gap detected
2833+
26752834
// Stage 2: Main profile (position control)
26762835
// Start from stage1_target_vel (after brake/stabilize), go to segment end
26772836
{
@@ -2753,11 +2912,73 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale)
27532912
main_profile.computed_feed_scale = effective_feed_scale;
27542913
main_profile.computed_vel_limit = vel_limit;
27552914
main_profile.computed_vLimit = tp->vLimit;
2915+
2916+
// Sub-cycle gap fix: when the main profile is shorter than
2917+
// one servo cycle, the brake→main transition causes a
2918+
// displacement dip (jerk spike). Replace with a single-stage
2919+
// position-control profile from current state to segment end.
2920+
{
2921+
double cycle_time = g_handoff_config.servo_cycle_time_sec;
2922+
if (main_profile.duration < cycle_time) {
2923+
ruckig::InputParameter<1> alt_input;
2924+
ruckig::Trajectory<1> alt_traj;
2925+
2926+
double alt_target_exit = (tc->term_cond == TC_TERM_COND_TANGENT)
2927+
? fmin(atomicLoadDouble(&tc->shared_9d.final_vel) * new_feed_scale, new_max_vel)
2928+
: 0.0;
2929+
alt_target_exit = applyKinkVelCap(alt_target_exit,
2930+
atomicLoadDouble(&tc->shared_9d.final_vel), new_max_vel, tc->kink_vel);
2931+
alt_target_exit = fmin(alt_target_exit, downstream_exit_cap);
2932+
double alt_achievable = findAchievableExitVelocity(
2933+
state.velocity, state.acceleration,
2934+
remaining, tc->maxaccel, default_jerk,
2935+
fmax(state.velocity, new_max_vel), alt_target_exit);
2936+
2937+
alt_input.control_interface = ruckig::ControlInterface::Position;
2938+
alt_input.current_position = {0.0};
2939+
alt_input.current_velocity = {state.velocity};
2940+
alt_input.current_acceleration = {state.acceleration};
2941+
alt_input.target_position = {remaining};
2942+
alt_input.target_velocity = {alt_achievable};
2943+
alt_input.target_acceleration = {0.0};
2944+
alt_input.max_velocity = {fmax(state.velocity, new_max_vel)};
2945+
alt_input.max_acceleration = {tc->maxaccel};
2946+
alt_input.max_jerk = {default_jerk};
2947+
2948+
auto alt_result = otg.calculate(alt_input, alt_traj);
2949+
bool alt_ok = (alt_result == ruckig::Result::Working ||
2950+
alt_result == ruckig::Result::Finished);
2951+
2952+
ruckig_profile_t alt_profile = {};
2953+
if (alt_ok) {
2954+
copyRuckigProfile(alt_traj, &alt_profile);
2955+
alt_ok = alt_profile.valid &&
2956+
!profileHasNegativeVelocity(&alt_profile);
2957+
}
2958+
2959+
if (alt_ok) {
2960+
// Replace two-stage with single-stage
2961+
main_profile = alt_profile;
2962+
main_profile.computed_feed_scale = effective_feed_scale;
2963+
main_profile.computed_vel_limit = vel_limit;
2964+
main_profile.computed_vLimit = tp->vLimit;
2965+
achievable_exit = alt_achievable;
2966+
if (alt_achievable > alt_target_exit + 0.01) {
2967+
effective_feed_scale = alt_achievable / vel_limit;
2968+
if (effective_feed_scale > 1.0) effective_feed_scale = 1.0;
2969+
}
2970+
tc->shared_9d.achieved_exit_vel = alt_achievable;
2971+
use_single_stage = true;
2972+
}
2973+
}
2974+
}
27562975
}
27572976

27582977
// Fix A: Reject if brake or main profile contains backward motion
2759-
if (profileHasNegativeVelocity(&brake_profile) ||
2760-
profileHasNegativeVelocity(&main_profile)) {
2978+
// (skip brake check when using single-stage — no brake profile)
2979+
if (!use_single_stage &&
2980+
(profileHasNegativeVelocity(&brake_profile) ||
2981+
profileHasNegativeVelocity(&main_profile))) {
27612982
rtapi_print_msg(RTAPI_MSG_DBG,
27622983
"Branch: REJECT seg=%d reason=negvel(2stg) brake_neg=%d main_neg=%d "
27632984
"brake_dist=%.4f seg_remaining=%.4f seg_target=%.4f progress=%.4f "
@@ -2777,13 +2998,22 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale)
27772998
g_ruckig_timing.count++;
27782999
if (calc_us > g_ruckig_timing.max_us) g_ruckig_timing.max_us = calc_us;
27793000

2780-
// Commit with both profiles
2781-
// brake_target_vel = stage1_target_vel (what brake/stabilize brings velocity to)
2782-
bool committed = commitBranch(&tc->shared_9d, &main_profile, &brake_profile,
2783-
stage1_target_vel, brake_end_position,
2784-
handoff_time, state.position,
2785-
effective_feed_scale, window_end_time,
2786-
tc->shared_9d.achieved_exit_vel);
3001+
// Cushion before jump: write alt-entry on N+1 before committing brake on N
3002+
writeAltEntry(tp, tc, tc->shared_9d.achieved_exit_vel, effective_feed_scale);
3003+
3004+
// Commit profile(s)
3005+
// When use_single_stage, commit as single-stage (no brake).
3006+
// Otherwise commit two-stage with brake_target_vel = stage1_target_vel.
3007+
bool committed = use_single_stage
3008+
? commitBranch(&tc->shared_9d, &main_profile, NULL, 0.0, 0.0,
3009+
handoff_time, state.position,
3010+
effective_feed_scale, window_end_time,
3011+
tc->shared_9d.achieved_exit_vel)
3012+
: commitBranch(&tc->shared_9d, &main_profile, &brake_profile,
3013+
stage1_target_vel, brake_end_position,
3014+
handoff_time, state.position,
3015+
effective_feed_scale, window_end_time,
3016+
tc->shared_9d.achieved_exit_vel);
27873017

27883018
return committed;
27893019

@@ -2900,6 +3130,9 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale)
29003130
g_ruckig_timing.count++;
29013131
if (calc_us > g_ruckig_timing.max_us) g_ruckig_timing.max_us = calc_us;
29023132

3133+
// Cushion before jump: write alt-entry on N+1 before committing branch on N
3134+
writeAltEntry(tp, tc, tc->shared_9d.achieved_exit_vel, effective_feed_scale);
3135+
29033136
bool committed = commitBranch(&tc->shared_9d, &main_profile, NULL, 0.0, 0.0,
29043137
handoff_time, state.position,
29053138
effective_feed_scale, window_end_time,
@@ -3165,9 +3398,23 @@ extern "C" void manageBranches(TP_STRUCT *tp)
31653398
// Feed change detected.
31663399
// If cursor walk is active, queue this change (next-in-line, freely overwritable).
31673400
if (g_recompute_cursor > 0) {
3168-
g_next_feed_scale = current_feed;
3169-
g_next_rapid_scale = emcmotStatus->rapid_scale;
3170-
return;
3401+
// Recovery from feed-hold: abort the hold cursor and restart fresh,
3402+
// just like program start. The hold cursor is walking at feed≈0,
3403+
// skipping segments with valid profiles. Those profiles are stale
3404+
// (computed at the pre-hold feed) and RT will hit them with wrong v0.
3405+
// By falling through to the normal feed-change path, we get
3406+
// invalidateNextNSegments + synchronous recomputeDownstreamProfiles
3407+
// before RT can advance into the stale segments.
3408+
if (g_recompute_feed_scale < 0.001 && current_feed > 0.001) {
3409+
g_recompute_cursor = 0;
3410+
g_next_feed_scale = -1.0;
3411+
g_next_rapid_scale = -1.0;
3412+
// Fall through to process recovery feed immediately
3413+
} else {
3414+
g_next_feed_scale = current_feed;
3415+
g_next_rapid_scale = emcmotStatus->rapid_scale;
3416+
return;
3417+
}
31713418
}
31723419

31733420
// Cursor idle — start processing this feed change.
@@ -3182,30 +3429,20 @@ extern "C" void manageBranches(TP_STRUCT *tp)
31823429

31833430
// Two-phase feed hold: when user requests 0%, first decelerate to 0.1%
31843431
// using normal Ruckig (Phase 1), then engage real 0% to stop completely
3185-
// (Phase 2). Phase 1 writes 0.001 profiles downstream so that Phase 2's
3186-
// createFeedHoldProfile passes the DONT_CLOBBER check (0.001 > 0.001 is false).
3187-
// Use profile_feed (what's actually running) not canonical_feed (per-segment, can be stale).
3432+
// (Phase 2). Phase 1 is a normal Ruckig branch — no special treatment.
3433+
// Phase 2 only fires once the machine has actually decelerated to near the
3434+
// 0.1% target velocity, checked via predictStateAtTime (not profile feed
3435+
// label, which drops to 0.001 immediately on commit).
31883436
static const double MINIMUM_RUCKIG_FEED = 0.001; // 0.1%
3189-
static int last_phase = 0; // Track state changes for logging
3190-
if (snap_feed < 0.001 && profile_feed > MINIMUM_RUCKIG_FEED + 0.005) {
3191-
// Phase 1: profile still at high feed, decelerate to 0.1% first
3192-
snap_feed = MINIMUM_RUCKIG_FEED;
3193-
if (last_phase != 1) {
3194-
rtapi_print_msg(RTAPI_MSG_DBG,
3195-
"PHASE1 seg %d: profile_feed=%.3f > 0.006, override to 0.1%%\n",
3196-
tc->id, profile_feed);
3197-
last_phase = 1;
3198-
}
3199-
} else if (snap_feed < 0.001) {
3200-
// Phase 2: profile at 0.1% or lower, allow real 0%
3201-
if (last_phase != 2) {
3202-
rtapi_print_msg(RTAPI_MSG_DBG,
3203-
"PHASE2 seg %d: profile_feed=%.3f <= 0.006, allowing real 0%%\n",
3204-
tc->id, profile_feed);
3205-
last_phase = 2;
3437+
if (snap_feed < 0.001) {
3438+
double elapsed = atomicLoadDouble(&tc->elapsed_time);
3439+
PredictedState probe = predictStateAtTime(tc, elapsed);
3440+
double vel_at_min_feed = tc->reqvel * MINIMUM_RUCKIG_FEED;
3441+
if (!probe.valid || probe.velocity > vel_at_min_feed) {
3442+
// Phase 1: still decelerating, use normal Ruckig at 0.1%
3443+
snap_feed = MINIMUM_RUCKIG_FEED;
32063444
}
3207-
} else {
3208-
last_phase = 0;
3445+
// else: Phase 2 — deceleration complete, allow real 0%
32093446
}
32103447

32113448
int commit_seg;
@@ -3811,6 +4048,7 @@ extern "C" void checkFeedOverride(TP_STRUCT *tp)
38114048
g_recompute_first_batch_done = true;
38124049
// Update throughput estimate (EMA, alpha=0.3)
38134050
g_segments_per_tick = 0.7 * g_segments_per_tick + 0.3 * recomputed;
4051+
38144052
}
38154053

38164054
// Advance or finish

0 commit comments

Comments
 (0)