@@ -1095,6 +1095,39 @@ static void applyBidirectionalReachability(double &v_entry, double &v_exit,
10951095 v_entry = fmin (v_entry, v_bwd);
10961096}
10971097
1098+ /* *
1099+ * @brief Compute minimum exit velocity achievable by maximum jerk-limited
1100+ * braking from entry_vel over the given distance.
1101+ *
1102+ * Used for cross-feed deceleration corridors: when a segment enters at
1103+ * old-feed velocity but needs to converge toward new-feed max_vel,
1104+ * this gives the physically achievable exit after one segment of maximum
1105+ * braking. Returns 0 if full stop is possible within the distance.
1106+ */
1107+ static double jerkLimitedMinExitVelocity (double v_entry, double distance,
1108+ double max_acc, double max_jrk)
1109+ {
1110+ if (v_entry <= 0 || distance <= 0 ) return 0 ;
1111+
1112+ // Can we brake to 0 within the distance?
1113+ double d_to_zero = jerkLimitedBrakingDistance (v_entry, 0 , max_acc, max_jrk);
1114+ if (d_to_zero <= distance) return 0 ;
1115+
1116+ // Binary search: find minimum v_f such that braking distance fits.
1117+ // jerkLimitedBrakingDistance(v_entry, v_f) decreases as v_f increases
1118+ // (less deceleration → shorter distance).
1119+ double lo = 0 , hi = v_entry;
1120+ for (int i = 0 ; i < 30 ; i++) {
1121+ double mid = 0.5 * (lo + hi);
1122+ double d_brake = jerkLimitedBrakingDistance (v_entry, mid, max_acc, max_jrk);
1123+ if (d_brake <= distance)
1124+ hi = mid; // Can brake to mid — try lower
1125+ else
1126+ lo = mid; // Can't brake to mid — need higher exit
1127+ if (hi - lo < 1e-6 ) break ;
1128+ }
1129+ return hi;
1130+ }
10981131
10991132/* *
11001133 * @brief Compute chain exit cap: maximum safe exit velocity for the active
@@ -1663,6 +1696,14 @@ static bool computeAndStoreProfile(TC_STRUCT *tc, const RuckigProfileParams &p)
16631696 copyRuckigProfile (traj, &tc->shared_9d .profile );
16641697 return true ;
16651698 } else {
1699+ if (FO_TRACE) {
1700+ rtapi_print_msg (RTAPI_MSG_ERR,
1701+ " FO_RUCKIG_FAIL result=%d seg=%d v0=%.3f vf=%.3f "
1702+ " max_v=%.3f max_a=%.3f max_j=%.1f dist=%.4f feed=%.3f\n " ,
1703+ (int )result, tc->id ,
1704+ p.v_entry , p.v_exit , p.max_vel ,
1705+ p.max_acc , p.max_jrk , p.target_dist , p.feed_scale );
1706+ }
16661707 __atomic_store_n (&tc->shared_9d .profile .valid , 0 , __ATOMIC_RELEASE);
16671708 return false ;
16681709 }
@@ -2471,6 +2512,18 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
24712512 }
24722513 prev_exit_vel_known = true ;
24732514 }
2515+ if (FO_TRACE) {
2516+ rtapi_print_msg (RTAPI_MSG_ERR,
2517+ " FO_ACTIVE_SKIP seg=%d term=%d prof_valid=%d "
2518+ " prof_feed=%.3f v0_override=%.1f prev_known=%d "
2519+ " prev_exit=%.3f bv=%d\n " ,
2520+ tc->id , tc->term_cond , tc->shared_9d .profile .valid ,
2521+ tc->shared_9d .profile .computed_feed_scale ,
2522+ v0_override, prev_exit_vel_known ? 1 : 0 ,
2523+ prev_exit_vel,
2524+ __atomic_load_n (&tc->shared_9d .branch .valid ,
2525+ __ATOMIC_ACQUIRE));
2526+ }
24742527 continue ;
24752528 }
24762529
@@ -2481,9 +2534,13 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
24812534 double max_acc = tcGetTangentialMaxAccel_9D_user (tc);
24822535 double max_jrk = tc->maxjerk > 0 ? tc->maxjerk : default_jerk;
24832536
2484- // Entry velocity (physical mm/s), capped by current segment's max_vel
2537+ // Entry velocity (physical mm/s). Do NOT cap by max_vel here —
2538+ // cross-feed junctions (BRANCH_SKIP_REPLAN) can have prev_exit at
2539+ // old feed exceeding new feed's max_vel. The Ruckig call below
2540+ // uses fmax(scaled_v_entry, max_vel) to allow deceleration from above
2541+ // the limit, matching the two-stage brake pattern in computeBranch().
24852542 double scaled_v_entry = prev_exit_vel_known
2486- ? fmin ( prev_exit_vel, max_vel) : 0.0 ;
2543+ ? prev_exit_vel : 0.0 ;
24872544 // Cap entry by previous segment's kink_vel
24882545 {
24892546 TC_STRUCT *prev_tc = (i > 0 ) ? tcqItem_user (queue, i - 1 ) : NULL ;
@@ -2498,11 +2555,17 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
24982555 scaled_v_exit = applyKinkVelCap (scaled_v_exit, v_exit, max_vel, tc->kink_vel );
24992556 double desired_fvel = scaled_v_exit;
25002557
2558+ // Cross-feed deceleration corridor detection
2559+ bool cross_feed_corridor = (scaled_v_entry > max_vel * 1.01 );
2560+
25012561 // Forward-lookahead: cap exit by what the next segment can actually
25022562 // enter (jerk-limited braking to its exit vel). The backward pass
25032563 // uses trapezoidal for v_f≈0 which overestimates; this corrects the
25042564 // junction before the profile is committed.
2505- if (scaled_v_exit > 0.0 && i + 1 < queue_len) {
2565+ // Skip when in cross-feed corridor — the lookahead uses backward-pass
2566+ // exits at the wrong feed scale, producing overly tight caps that
2567+ // cascade forward and crush entry velocities on short segments.
2568+ if (!cross_feed_corridor && scaled_v_exit > 0.0 && i + 1 < queue_len) {
25062569 TC_STRUCT *next_tc = tcqItem_user (queue, i + 1 );
25072570 if (next_tc && next_tc->target > 1e-9 ) {
25082571 double nf = snap.forSegment (next_tc);
@@ -2511,11 +2574,6 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
25112574 double na = tcGetTangentialMaxAccel_9D_user (next_tc);
25122575 double nj = next_tc->maxjerk > 0 ? next_tc->maxjerk : default_jerk;
25132576 double reach = jerkLimitedMaxEntryVelocity (nv_exit, next_tc->target , na, nj);
2514- // Also cap by the successor's max velocity — the successor
2515- // can't enter faster than its own max_vel regardless of
2516- // deceleration capacity. Without this, the predecessor
2517- // exits above the successor's max_vel, creating a junction
2518- // gap that no branch can fix (too little remaining distance).
25192577 double nvel_limit = getEffectiveVelLimit (tp, next_tc);
25202578 double next_max = applyVLimit (tp, next_tc, nvel_limit * nf);
25212579 reach = fmin (reach, next_max);
@@ -2526,6 +2584,22 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
25262584 }
25272585 }
25282586
2587+ // Cross-feed deceleration corridor: when entry exceeds the
2588+ // feed-scaled max_vel (e.g. after BRANCH_SKIP_REPLAN from 200%→10%),
2589+ // compute the physics-based minimum exit velocity from maximum
2590+ // jerk-limited braking over this segment's distance. This gives
2591+ // natural multi-segment deceleration: each segment brakes as hard
2592+ // as physics allow, chaining to the next until velocity converges
2593+ // to max_vel. The forward lookahead is skipped (above) because
2594+ // it uses backward-pass exits at the wrong feed scale.
2595+ if (cross_feed_corridor && tc->term_cond == TC_TERM_COND_TANGENT) {
2596+ double min_exit = jerkLimitedMinExitVelocity (
2597+ scaled_v_entry, tc->target , max_acc, max_jrk);
2598+ // Use the larger of: physics minimum, or max_vel (don't undershoot target)
2599+ scaled_v_exit = fmin (fmax (min_exit, max_vel), scaled_v_entry);
2600+ desired_fvel = scaled_v_exit;
2601+ }
2602+
25292603 // --- SKIP: profile already consistent with propagated v0 and current limits ---
25302604 if (tc->shared_9d .profile .valid ) {
25312605 bool entry_ok = !prev_exit_vel_known ||
@@ -2596,14 +2670,41 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
25962670 }
25972671
25982672 // --- Compute and store Ruckig profile ---
2673+ double pre_bidir_v_entry = scaled_v_entry;
2674+ double pre_bidir_v_exit = scaled_v_exit;
25992675 atomicStoreDouble (&tc->shared_9d .entry_vel , scaled_v_entry);
26002676
26012677 applyBidirectionalReachability (scaled_v_entry, scaled_v_exit,
26022678 tc->target , max_acc, max_jrk);
26032679
2680+ // Log cross-feed or zero-entry profile writes (first 5 per pass)
2681+ if (FO_TRACE && fo_dirty_count < 5 &&
2682+ (fabs (pre_bidir_v_entry) < 0.01 ||
2683+ fabs (feed_scale - tc->shared_9d .profile .computed_feed_scale ) > 0.1 )) {
2684+ rtapi_print_msg (RTAPI_MSG_ERR,
2685+ " FO_FWDWRITE i=%d seg=%d type=%d "
2686+ " v_entry=%.3f(bidir:%.3f) v_exit=%.3f(bidir:%.3f) "
2687+ " feed=%.3f old_feed=%.3f prev_known=%d "
2688+ " max_vel=%.3f target=%.4f active=%d\n " ,
2689+ i, tc->id , tc->motion_type ,
2690+ pre_bidir_v_entry, scaled_v_entry,
2691+ pre_bidir_v_exit, scaled_v_exit,
2692+ feed_scale, tc->shared_9d .profile .computed_feed_scale ,
2693+ prev_exit_vel_known ? 1 : 0 ,
2694+ max_vel, tc->target , tc->active );
2695+ }
2696+
26042697 try {
2698+ // Raise Ruckig max_velocity when entry exceeds feed-scaled limit
2699+ // (cross-feed junction). Same pattern as computeBranch() two-stage
2700+ // brake: fmax(state.velocity, new_max_vel).
2701+ // The 0.1% margin avoids Ruckig ErrorExecutionTimeCalculation
2702+ // (-110) which fires when max_velocity == current_velocity
2703+ // exactly — a degenerate case that hits every cross-feed
2704+ // corridor segment since fmax(v0, max_vel) == v0 by definition.
2705+ double ruckig_max_vel = fmax (scaled_v_entry, max_vel) * 1.001 ;
26052706 RuckigProfileParams rp = {scaled_v_entry, scaled_v_exit,
2606- max_vel , max_acc, max_jrk, tc->target ,
2707+ ruckig_max_vel , max_acc, max_jrk, tc->target ,
26072708 feed_scale, vel_limit, tp->vLimit , desired_fvel};
26082709 if (computeAndStoreProfile (tc, rp)) {
26092710 fo_dirty_count++;
@@ -2617,9 +2718,25 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
26172718 prev_exit_vel = 0.0 ;
26182719 }
26192720 } else {
2721+ if (FO_TRACE && cross_feed_corridor) {
2722+ rtapi_print_msg (RTAPI_MSG_ERR,
2723+ " FO_CORRIDOR_FAIL i=%d seg=%d v_entry=%.3f v_exit=%.3f "
2724+ " ruckig_max_vel=%.3f max_acc=%.3f max_jrk=%.1f "
2725+ " target=%.4f feed=%.3f corridor=%d\n " ,
2726+ i, tc->id , scaled_v_entry, scaled_v_exit,
2727+ ruckig_max_vel, max_acc, max_jrk,
2728+ tc->target , feed_scale, cross_feed_corridor ? 1 : 0 );
2729+ }
26202730 prev_exit_vel = 0.0 ;
26212731 }
26222732 } catch (...) {
2733+ if (FO_TRACE && cross_feed_corridor) {
2734+ rtapi_print_msg (RTAPI_MSG_ERR,
2735+ " FO_CORRIDOR_THROW i=%d seg=%d v_entry=%.3f v_exit=%.3f "
2736+ " target=%.4f feed=%.3f\n " ,
2737+ i, tc->id , scaled_v_entry, scaled_v_exit,
2738+ tc->target , feed_scale);
2739+ }
26232740 __atomic_store_n (&tc->shared_9d .profile .valid , 0 , __ATOMIC_RELEASE);
26242741 prev_exit_vel = 0.0 ;
26252742 }
@@ -2858,6 +2975,14 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
28582975 // set by RT, so we also check new_feed_scale to catch abort.
28592976 handoff_time = elapsed;
28602977 window_end_time = handoff_time + window_sec + 1.0 ;
2978+ } else if (tc->term_cond == TC_TERM_COND_TANGENT) {
2979+ // TANGENT segment: bypass margin — the branch doesn't need
2980+ // to decelerate to zero, it exits into a blend at whatever
2981+ // velocity is achievable. Let Ruckig + findAchievableExitVelocity
2982+ // decide what's feasible rather than pre-rejecting based on a
2983+ // conservative margin that can exceed short segment durations.
2984+ handoff_time = elapsed;
2985+ window_end_time = handoff_time + window_sec + 1.0 ;
28612986 } else {
28622987 handoff_time = profile_duration - min_handoff_margin;
28632988 if (handoff_time <= elapsed) {
@@ -2895,6 +3020,8 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
28953020 }
28963021
28973022 if (!state.valid ) {
3023+ if (FO_TRACE) rtapi_print_msg (RTAPI_MSG_ERR,
3024+ " FO_BRANCH_REJECT seg=%d reason=STATE_INVALID\n " , tc->id );
28983025 return false ;
28993026 }
29003027
@@ -2909,6 +3036,10 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
29093036 // Calculate remaining distance after handoff
29103037 double remaining = tc->target - state.position ;
29113038 if (remaining < 1e-6 ) {
3039+ if (FO_TRACE) rtapi_print_msg (RTAPI_MSG_ERR,
3040+ " FO_BRANCH_REJECT seg=%d reason=NO_DISTANCE remaining=%.6f "
3041+ " pos=%.3f target=%.3f\n " ,
3042+ tc->id , remaining, state.position , tc->target );
29123043 tc->shared_9d .requested_feed_scale = new_feed_scale;
29133044 return false ;
29143045 }
@@ -2920,6 +3051,11 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
29203051 if (state.acceleration < -0.5 * tc->maxaccel && state.velocity > 0 ) {
29213052 double stop_dist = (state.velocity * state.velocity ) / (2.0 * tc->maxaccel );
29223053 if (remaining <= stop_dist * 1.5 ) {
3054+ if (FO_TRACE) rtapi_print_msg (RTAPI_MSG_ERR,
3055+ " FO_BRANCH_REJECT seg=%d reason=DECEL_LOCKED v=%.1f a=%.1f "
3056+ " remaining=%.3f stop_dist=%.3f\n " ,
3057+ tc->id , state.velocity , state.acceleration ,
3058+ remaining, stop_dist);
29233059 tc->shared_9d .requested_feed_scale = new_feed_scale;
29243060 return false ;
29253061 }
@@ -3095,6 +3231,10 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
30953231 }
30963232 } else {
30973233 // Even minimum feed can't satisfy — segment is genuinely locked
3234+ if (FO_TRACE) rtapi_print_msg (RTAPI_MSG_ERR,
3235+ " FO_BRANCH_REJECT seg=%d reason=FEED_LIMIT_LOCKED "
3236+ " exit_hard=%.1f remaining=%.3f v=%.1f\n " ,
3237+ tc->id , exit_hard_limit, remaining, state.velocity );
30983238 return false ;
30993239 }
31003240 }
@@ -3126,12 +3266,18 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
31263266
31273267 auto result = otg.calculate (input, traj);
31283268 if (result != ruckig::Result::Working && result != ruckig::Result::Finished) {
3269+ if (FO_TRACE) rtapi_print_msg (RTAPI_MSG_ERR,
3270+ " FO_BRANCH_REJECT seg=%d reason=BRAKE_RUCKIG v=%.1f a=%.1f "
3271+ " target_v=%.1f\n " ,
3272+ tc->id , state.velocity , state.acceleration , stage1_target_vel);
31293273 return false ;
31303274 }
31313275
31323276 // Brake profile is clean 7-phase, use phase-based sampling
31333277 copyRuckigProfile (traj, &brake_profile);
31343278 if (!brake_profile.valid ) {
3279+ if (FO_TRACE) rtapi_print_msg (RTAPI_MSG_ERR,
3280+ " FO_BRANCH_REJECT seg=%d reason=BRAKE_COPY\n " , tc->id );
31353281 return false ;
31363282 }
31373283
@@ -3189,17 +3335,29 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
31893335 // reject the branch and let the existing profile finish.
31903336 // Phase 4 TODO (Blending): review PARABOLIC handling here.
31913337 if (tc->term_cond != TC_TERM_COND_TANGENT && achievable_exit > 0.01 ) {
3338+ if (FO_TRACE) rtapi_print_msg (RTAPI_MSG_ERR,
3339+ " FO_BRANCH_REJECT seg=%d reason=CANT_STOP "
3340+ " achievable=%.1f remaining_after_brake=%.3f\n " ,
3341+ tc->id , achievable_exit, remaining_after_brake);
31923342 return false ;
31933343 }
31943344
31953345 // Kink-limited junctions: reject if can't decel to kink_vel
31963346 if (tc->kink_vel > 0 && achievable_exit > tc->kink_vel + 0.01 ) {
3347+ if (FO_TRACE) rtapi_print_msg (RTAPI_MSG_ERR,
3348+ " FO_BRANCH_REJECT seg=%d reason=KINK_VEL "
3349+ " achievable=%.1f kink=%.1f\n " ,
3350+ tc->id , achievable_exit, tc->kink_vel );
31973351 return false ;
31983352 }
31993353
32003354 // Downstream reachability: reject if can't decel to what
32013355 // next segment accepts. Feed change deferred to next cycle.
32023356 if (achievable_exit > downstream_exit_cap + 0.01 ) {
3357+ if (FO_TRACE) rtapi_print_msg (RTAPI_MSG_ERR,
3358+ " FO_BRANCH_REJECT seg=%d reason=DS_CAP "
3359+ " achievable=%.1f ds_cap=%.1f\n " ,
3360+ tc->id , achievable_exit, downstream_exit_cap);
32033361 tc->shared_9d .requested_feed_scale = new_feed_scale;
32043362 return false ;
32053363 }
@@ -3301,6 +3459,13 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
33013459 if (!use_single_stage &&
33023460 (profileHasNegativeVelocity (&brake_profile) ||
33033461 profileHasNegativeVelocity (&main_profile))) {
3462+ if (FO_TRACE) rtapi_print_msg (RTAPI_MSG_ERR,
3463+ " FO_BRANCH_REJECT seg=%d reason=NEG_VEL brake_neg=%d main_neg=%d "
3464+ " v=%.1f a=%.1f remaining=%.3f\n " ,
3465+ tc->id ,
3466+ profileHasNegativeVelocity (&brake_profile) ? 1 : 0 ,
3467+ profileHasNegativeVelocity (&main_profile) ? 1 : 0 ,
3468+ state.velocity , state.acceleration , remaining);
33043469 return false ;
33053470 }
33063471
@@ -3799,12 +3964,41 @@ static void executeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double feed)
37993964 }
38003965 }
38013966
3802- g_feed_mgr.planning .branch_failed = !ok;
38033967 if (!ok) {
3804- g_feed_mgr.planning .branch_failed_seg = tc->id ;
3805- // No rewrite happened — nothing to converge. Clear countdown
3806- // so gate reopens as soon as GATE_CLEAR fires (RT moves to next seg).
3807- g_feed_mgr.planning .convergence_remaining = 0 ;
3968+ // Branch can't fit within active segment (e.g. FEED_LIMIT_LOCKED —
3969+ // not enough remaining distance to brake to downstream exit cap).
3970+ // Instead of waiting for RT to advance past this segment, commit the
3971+ // new feed immediately and replan downstream. Active segment finishes
3972+ // at old profile (RT is safe). Gate stays closed; the convergence
3973+ // countdown from closeGate() ensures safe_depth segments are converged
3974+ // at the new feed before the gate reopens.
3975+
3976+ g_feed_mgr.committed_feed = emcmotStatus
3977+ ? emcmotStatus->feed_scale : 1.0 ;
3978+ g_feed_mgr.committed_rapid = emcmotStatus
3979+ ? emcmotStatus->rapid_scale : 1.0 ;
3980+
3981+ double budget = g_feed_mgr.planning .getBudget (
3982+ g_handoff_config.servo_cycle_time_sec );
3983+ double t0 = etime_user ();
3984+ int segs = replanForward (tp, -1.0 , budget);
3985+ double replan_s = etime_user () - t0;
3986+
3987+ // Update EMA costs + converged_depth (same tracker as success path).
3988+ g_feed_mgr.planning .onBranchCycleComplete (0.0 , replan_s, segs);
3989+
3990+ if (FO_TRACE) {
3991+ int qlen = tcqLen_user (&tp->queue );
3992+ rtapi_print_msg (RTAPI_MSG_ERR,
3993+ " FO cy=%d BRANCH_SKIP_REPLAN committed=%.3f segs=%d/%d "
3994+ " budget=%.0fus actual=%.0fus needs_replan=%d\n " ,
3995+ g_fo_cycle, g_feed_mgr.committed_feed ,
3996+ segs, qlen, budget * 1e6 , replan_s * 1e6 ,
3997+ g_needs_replan ? 1 : 0 );
3998+ }
3999+ // Gate stays closed — continuation path in checkFeedOverride handles
4000+ // remaining segments. convergence_remaining stays at 20 (from
4001+ // closeGate), so gate opens only after safe_depth converge + countdown.
38084002 }
38094003
38104004 if (ok) {
0 commit comments