Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion controllers/easynav_regulated_pp_controller/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ EasyNav's design differs from Nav2's `controller_server` in ways that require so
| `use_velocity_scaled_lookahead_dist` | Use velocity-scaled lookahead distance instead of the fixed `lookahead_dist`. |
| `rotate_to_heading_angular_vel` | Angular velocity used while rotating in place. |
| `use_rotate_to_heading` | Enable rotate-in-place behaviors (rough path heading and final goal heading). |
| `rotate_to_heading_min_angle` | Angle to the carrot beyond which the robot rotates in place first. |
| `rotate_to_heading_min_angle` | Angle to the carrot beyond which the robot rotates in place first. Hysteretic: leaving rotate-in-place mode requires the angle to drop to *half* this value (well-aligned), not just back under it, to avoid chattering between rotate-in-place and curve-follow mode near sharp turns where the carrot itself is geometrically unstable tick to tick. |
| `use_regulated_linear_velocity_scaling` | Enable curvature-based velocity regulation. |
| `regulated_linear_scaling_min_radius` | Turning radius below which curvature regulation kicks in. |
| `regulated_linear_scaling_min_speed` | Minimum velocity kept under regulation. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ class RegulatedPurePursuitController : public ControllerMethodBase
double robot_yaw);

/// \brief Whether the robot should rotate in place towards \p angle_to_path.
bool shouldRotateToPath(double angle_to_path) const;
bool shouldRotateToPath(double angle_to_path, bool currently_rotating) const;
Comment on lines 168 to +177

/// \brief Computes a kinematically-feasible rotate-in-place command towards \p angle_to_target.
void rotateToHeading(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,14 @@ RegulatedPurePursuitController::toRobotFrame(
}

bool
RegulatedPurePursuitController::shouldRotateToPath(double angle_to_path) const
RegulatedPurePursuitController::shouldRotateToPath(
double angle_to_path, bool currently_rotating) const
{
return use_rotate_to_heading_ && std::fabs(angle_to_path) > rotate_to_heading_min_angle_;
if (!use_rotate_to_heading_) {return false;}
const double threshold = currently_rotating ?
0.5 * rotate_to_heading_min_angle_ :
rotate_to_heading_min_angle_;
return std::fabs(angle_to_path) > threshold;
}

void
Expand Down Expand Up @@ -489,7 +494,7 @@ RegulatedPurePursuitController::update_rt(NavState & nav_state)
const double regulation_curvature = heuristics::calculateCurvature(
curvature_local.x, curvature_local.y);

if (shouldRotateToPath(angle_to_path)) {
if (shouldRotateToPath(angle_to_path, is_rotating_to_heading_)) {
is_rotating_to_heading_ = true;
rotateToHeading(linear_vel, angular_vel, angle_to_path, dt);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ class FriendRegulatedPurePursuitController : public easynav::RegulatedPurePursui
using easynav::RegulatedPurePursuitController::toRobotFrame;
using easynav::RegulatedPurePursuitController::remainingPathDistance;
using easynav::RegulatedPurePursuitController::findClosestPoseIndex;
using easynav::RegulatedPurePursuitController::shouldRotateToPath;

void setRotateToHeadingParams(bool use_rotate_to_heading, double min_angle)
{
use_rotate_to_heading_ = use_rotate_to_heading;
rotate_to_heading_min_angle_ = min_angle;
}
};

TEST(RegulatedPurePursuitControllerHelpers, LookAheadPointInterpolatesOnSegment)
Expand Down Expand Up @@ -214,6 +221,33 @@ TEST(RegulatedPurePursuitControllerHelpers, LookAheadPointClampsToPathEnd)
EXPECT_NEAR(carrot.x, 1.0, 1e-6);
}

TEST(RegulatedPurePursuitControllerHelpers, ShouldRotateToPathHasHysteresis)
{
// Regression test for the oscillation reported near sharp turns exiting an inflated area: a
// single fixed threshold, re-evaluated fresh every tick with no memory of the previous
// decision, let the controller chatter between rotate-in-place and curve-follow mode whenever
// the (geometrically unstable, near a sharp corner) angle-to-path hovered near it. The fix
// requires a *smaller* angle to leave rotate-in-place mode than the one that entered it.
FriendRegulatedPurePursuitController controller;
controller.setRotateToHeadingParams(/*use_rotate_to_heading=*/true, /*min_angle=*/0.785);

// Below the entry threshold, and not currently rotating: stay in curve-follow mode.
EXPECT_FALSE(controller.shouldRotateToPath(0.5, /*currently_rotating=*/false));
// Above the entry threshold: start rotating in place.
EXPECT_TRUE(controller.shouldRotateToPath(0.9, /*currently_rotating=*/false));

// The crux of the fix: once rotating, an angle that would never have *started* a rotation
// (0.5 < 0.785) must not end one already in progress -- the old single-threshold code would
// have flipped back to curve-follow mode here, which is exactly the observed oscillation.
EXPECT_TRUE(controller.shouldRotateToPath(0.5, /*currently_rotating=*/true));
// Only once well-aligned (below half the entry threshold) does it stop rotating.
EXPECT_FALSE(controller.shouldRotateToPath(0.3, /*currently_rotating=*/true));

// Disabled outright regardless of angle or state.
controller.setRotateToHeadingParams(/*use_rotate_to_heading=*/false, /*min_angle=*/0.785);
EXPECT_FALSE(controller.shouldRotateToPath(3.0, /*currently_rotating=*/true));
}

TEST(RegulatedPurePursuitControllerHelpers, ToRobotFrameRotatesAndTranslates)
{
geometry_msgs::msg::Point global_point;
Expand Down
Loading