Nav2 Navigation Stack - lyrical  lyrical
ROS 2 Navigation Stack
graceful_controller.cpp
1 // Copyright (c) 2023 Alberto J. Tudela Roldán
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #include <memory>
16 #include <mutex>
17 
18 #include "angles/angles.h"
19 #include "nav2_core/controller_exceptions.hpp"
20 #include "nav2_util/geometry_utils.hpp"
21 #include "nav2_util/controller_utils.hpp"
22 #include "nav2_util/path_utils.hpp"
23 #include "nav2_graceful_controller/graceful_controller.hpp"
24 #include "nav2_costmap_2d/costmap_filters/filter_values.hpp"
25 #include "nav2_ros_common/tf2_factories.hpp"
26 
27 namespace nav2_graceful_controller
28 {
29 
31  const nav2::LifecycleNode::WeakPtr & parent,
32  std::string name, const nav2::TransformBuffer::SharedPtr tf,
33  const std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros)
34 {
35  nav2::LifecycleNode::SharedPtr node = parent.lock();
36  if (!node) {
37  throw nav2_core::ControllerException("Unable to lock node!");
38  }
39 
40  costmap_ros_ = costmap_ros;
41  tf_buffer_ = tf;
42  plugin_name_ = name;
43  logger_ = node->get_logger();
44 
45  // Handles storage and dynamic configuration of parameters.
46  // Returns pointer to data current param settings.
47  param_handler_ = std::make_unique<ParameterHandler>(
48  node, plugin_name_, logger_);
49  params_ = param_handler_->getParams();
50 
51  // Handles the control law to generate the velocity commands
52  control_law_ = std::make_unique<SmoothControlLaw>(
53  params_->k_phi, params_->k_delta, params_->beta, params_->lambda,
54  params_->slowdown_radius, params_->deceleration_max,
55  params_->v_linear_min, params_->v_linear_max, params_->v_angular_max);
56 
57  // Initialize footprint collision checker
58  if (params_->use_collision_detection) {
59  collision_checker_ = std::make_unique<nav2_costmap_2d::
60  FootprintCollisionChecker<nav2_costmap_2d::Costmap2D *>>(costmap_ros_->getCostmap());
61  }
62 
63  double max_valid_cost = costmap_ros_->getUseRadius() ?
64  static_cast<double>(nav2_costmap_2d::MAX_NON_OBSTACLE) :
65  static_cast<double>(nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE);
66  if (max_valid_cost - static_cast<double>(params_->obstacle_cost_margin) < 0.0) {
67  RCLCPP_WARN(
68  logger_, "obstacle_cost_margin (%d) is higher than max cost (%d).",
69  params_->obstacle_cost_margin, nav2_costmap_2d::MAX_NON_OBSTACLE);
70  throw nav2_core::ControllerException("obstacle_cost_margin is higher than max cost.");
71  }
72 
73  // Publishers
74  local_plan_pub_ = node->create_publisher<nav_msgs::msg::Path>("local_plan");
75  motion_target_pub_ = node->create_publisher<geometry_msgs::msg::PoseStamped>("motion_target");
76  slowdown_pub_ = node->create_publisher<visualization_msgs::msg::Marker>("slowdown");
77 
78  RCLCPP_INFO(logger_, "Configured Graceful Motion Controller: %s", plugin_name_.c_str());
79 }
80 
82 {
83  RCLCPP_INFO(
84  logger_,
85  "Cleaning up controller: %s of type graceful_controller::GracefulController",
86  plugin_name_.c_str());
87  local_plan_pub_.reset();
88  motion_target_pub_.reset();
89  slowdown_pub_.reset();
90  collision_checker_.reset();
91  param_handler_.reset();
92  control_law_.reset();
93 }
94 
96 {
97  RCLCPP_INFO(
98  logger_,
99  "Activating controller: %s of type nav2_graceful_controller::GracefulController",
100  plugin_name_.c_str());
101  local_plan_pub_->on_activate();
102  motion_target_pub_->on_activate();
103  slowdown_pub_->on_activate();
104  param_handler_->activate();
105 }
106 
108 {
109  RCLCPP_INFO(
110  logger_,
111  "Deactivating controller: %s of type nav2_graceful_controller::GracefulController",
112  plugin_name_.c_str());
113  local_plan_pub_->on_deactivate();
114  motion_target_pub_->on_deactivate();
115  slowdown_pub_->on_deactivate();
116  param_handler_->deactivate();
117 }
118 
119 geometry_msgs::msg::TwistStamped GracefulController::computeVelocityCommands(
120  const geometry_msgs::msg::PoseStamped & pose,
121  const geometry_msgs::msg::Twist & velocity,
122  nav2_core::GoalChecker * goal_checker,
123  const nav_msgs::msg::Path & transformed_global_plan,
124  const geometry_msgs::msg::PoseStamped & global_goal)
125 {
126  std::lock_guard<std::mutex> param_lock(param_handler_->getMutex());
127 
128  geometry_msgs::msg::TwistStamped cmd_vel;
129  cmd_vel.header = pose.header;
130 
131  // Transform the plan from costmap's global frame to robot base frame
132  nav_msgs::msg::Path transformed_plan;
133  if (!nav2_util::transformPathInTargetFrame(
134  transformed_global_plan, transformed_plan, *tf_buffer_,
135  costmap_ros_->getBaseFrameID(), costmap_ros_->getTransformTolerance()))
136  {
138  "Unable to transform plan pose into local frame");
139  }
140 
141  // Update the smooth control law with the new params
142  control_law_->setCurvatureConstants(
143  params_->k_phi, params_->k_delta, params_->beta, params_->lambda);
144  control_law_->setSlowdownRadius(params_->slowdown_radius);
145  control_law_->setMaxDeceleration(params_->deceleration_max);
146  control_law_->setSpeedLimit(params_->v_linear_min, params_->v_linear_max, params_->v_angular_max);
147  // Add proper orientations to plan, if needed
148  validateOrientations(transformed_plan.poses);
149 
150  // Transform local frame to global frame to use in collision checking
151  geometry_msgs::msg::TransformStamped costmap_transform;
152  try {
153  costmap_transform = tf_buffer_->lookupTransform(
154  costmap_ros_->getGlobalFrameID(), costmap_ros_->getBaseFrameID(),
155  tf2::TimePointZero);
156  } catch (tf2::TransformException & ex) {
157  RCLCPP_ERROR(
158  logger_, "Could not transform %s to %s: %s",
159  costmap_ros_->getBaseFrameID().c_str(), costmap_ros_->getGlobalFrameID().c_str(),
160  ex.what());
161  throw ex;
162  }
163 
164  // Compute distance to goal as the path's integrated distance to account for path curvatures
165  double dist_to_goal = nav2_util::geometry_utils::calculate_path_length(transformed_plan);
166 
167  // If we've reached the XY goal tolerance, just rotate.
168  // Feed the goal checker the GLOBAL-frame plan (same frame as `pose` / `global_goal`), not the
169  // base_link-frame `transformed_plan` used for control.
170  if (goal_checker->isGoalXYReached(pose.pose, global_goal.pose, velocity,
171  transformed_global_plan))
172  {
173  double angle_to_goal = tf2::getYaw(transformed_plan.poses.back().pose.orientation);
174  // Check for collisions between our current pose and goal pose
175  size_t num_steps = fabs(angle_to_goal) / params_->in_place_collision_resolution;
176  // Need to check at least the end pose
177  num_steps = std::max(static_cast<size_t>(1), num_steps);
178  bool collision_free = true;
179  for (size_t i = 1; i <= num_steps; ++i) {
180  double step = static_cast<double>(i) / static_cast<double>(num_steps);
181  double yaw = step * angle_to_goal;
182  geometry_msgs::msg::PoseStamped next_pose;
183  next_pose.header.frame_id = costmap_ros_->getBaseFrameID();
184  next_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(yaw);
185  geometry_msgs::msg::PoseStamped costmap_pose;
186  tf2::doTransform(next_pose, costmap_pose, costmap_transform);
187  if (params_->use_collision_detection && inCollision(
188  costmap_pose.pose.position.x, costmap_pose.pose.position.y,
189  tf2::getYaw(costmap_pose.pose.orientation)))
190  {
191  collision_free = false;
192  break;
193  }
194  }
195  // Compute velocity if rotation is possible
196  if (collision_free) {
197  cmd_vel.twist = rotateToTarget(angle_to_goal);
198  return cmd_vel;
199  }
200  // Else, fall through and see if we should follow control law longer
201  }
202 
203  // Find a valid target pose and its trajectory
204  nav_msgs::msg::Path local_plan;
205  geometry_msgs::msg::PoseStamped target_pose;
206 
207  double dist_to_target;
208  std::vector<double> target_distances;
209  computeDistanceAlongPath(transformed_plan.poses, target_distances);
210 
211  bool is_first_iteration = true;
212  for (int i = transformed_plan.poses.size() - 1; i >= 0; --i) {
213  if (is_first_iteration) {
214  // Calculate target pose through lookahead interpolation to get most accurate
215  // lookahead point, if possible
216  dist_to_target = params_->max_lookahead;
217  // Interpolate after goal false for graceful controller
218  // Requires interpolating the orientation which is not yet implemented
219  // Updates dist_to_target for target_pose returned if using the point on the path
220  target_pose = nav2_util::getLookAheadPoint(dist_to_target, transformed_plan, false);
221  is_first_iteration = false;
222  } else {
223  // Underlying control law needs a single target pose, which should:
224  // * Be as far away as possible from the robot (for smoothness)
225  // * But no further than the max_lookahed_ distance
226  // * Be feasible to reach in a collision free manner
227  dist_to_target = target_distances[i];
228  target_pose = transformed_plan.poses[i];
229  }
230 
231  // Compute velocity at this moment if valid target pose is found
232  if (
233  validateTargetPoseOnApproach(target_pose, dist_to_target, dist_to_goal, local_plan,
234  costmap_transform, cmd_vel) ||
235  validateTargetPose(target_pose, dist_to_target, local_plan, costmap_transform, cmd_vel))
236  {
237  // Publish the selected target_pose
238  motion_target_pub_->publish(std::make_unique<geometry_msgs::msg::PoseStamped>(target_pose));
239  // Publish marker for slowdown radius around motion target for debugging / visualization
240  auto slowdown_marker = nav2_graceful_controller::createSlowdownMarker(
241  target_pose, params_->slowdown_radius);
242  slowdown_pub_->publish(std::make_unique<visualization_msgs::msg::Marker>(slowdown_marker));
243  // Publish the local plan
244  local_plan.header = transformed_plan.header;
245  local_plan_pub_->publish(std::make_unique<nav_msgs::msg::Path>(local_plan));
246  // Successfully found velocity command
247  return cmd_vel;
248  }
249  }
250 
251  throw nav2_core::NoValidControl("Collision detected in trajectory");
252 }
253 
254 void GracefulController::newPathReceived(const nav_msgs::msg::Path & /*raw_global_path*/)
255 {
256  do_initial_rotation_ = true;
257  safe_approach_angle_.reset();
258 }
259 
261  const double & speed_limit, const bool & percentage)
262 {
263  std::lock_guard<std::mutex> param_lock(param_handler_->getMutex());
264 
265  if (speed_limit == nav2_costmap_2d::NO_SPEED_LIMIT) {
266  params_->v_linear_max = params_->v_linear_max_initial;
267  params_->v_angular_max = params_->v_angular_max_initial;
268  } else {
269  if (percentage) {
270  // Speed limit is expressed in % from maximum speed of robot
271  params_->v_linear_max = std::max(
272  params_->v_linear_max_initial * speed_limit / 100.0, params_->v_linear_min);
273  params_->v_angular_max = params_->v_angular_max_initial * speed_limit / 100.0;
274  } else {
275  // Speed limit is expressed in m/s
276  params_->v_linear_max = std::max(speed_limit, params_->v_linear_min);
277  // Limit the angular velocity to be proportional to the linear velocity
278  params_->v_angular_max = params_->v_angular_max_initial *
279  speed_limit / params_->v_linear_max_initial;
280  }
281  }
282 }
283 
285  geometry_msgs::msg::PoseStamped & target_pose, double dist_to_target,
286  nav_msgs::msg::Path & trajectory, geometry_msgs::msg::TransformStamped & costmap_transform,
287  geometry_msgs::msg::TwistStamped & cmd_vel)
288 {
289  // Continue if target_pose is too far away from robot
290  if (dist_to_target > params_->max_lookahead) {
291  return false;
292  }
293 
294  // Flip the orientation of the motion target if the robot is moving backwards
295  bool reversing = false;
296  if (params_->allow_backward && target_pose.pose.position.x < 0.0) {
297  reversing = true;
298  target_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(
299  tf2::getYaw(target_pose.pose.orientation) + M_PI);
300  }
301 
302  // Actually simulate the path
303  double sim_linear_velocity = params_->v_linear_max;
304  do {
305  control_law_->setSpeedLimit(params_->v_linear_min, sim_linear_velocity, params_->v_angular_max);
306  if (simulateTrajectory(target_pose, costmap_transform, trajectory, cmd_vel, reversing)) {
307  // Successfully simulated to target_pose
308  return true;
309  }
310  // Reduce velocity and try again for same target_pose
311  sim_linear_velocity -= params_->footprint_scaling_step;
312  } while (sim_linear_velocity >= params_->footprint_scaling_linear_vel);
313 
314  // Validation not successful
315  return false;
316 }
317 
319  geometry_msgs::msg::PoseStamped & target_pose, double dist_to_target, double dist_to_goal,
320  nav_msgs::msg::Path & trajectory, geometry_msgs::msg::TransformStamped & costmap_transform,
321  geometry_msgs::msg::TwistStamped & cmd_vel)
322 {
323  // Not approaching goal with large lookahead and don't evaluate shortcut trajectories
324  // when we do not prefer rotating to goal at the end.
325  if (dist_to_goal >= params_->max_lookahead || !params_->prefer_final_rotation) {
326  return false;
327  }
328  // Avoid instability and big sweeping turns at the end of paths by
329  // ignoring final heading
330  double yaw = std::atan2(target_pose.pose.position.y, target_pose.pose.position.x);
331  target_pose.pose.orientation =
332  nav2_util::geometry_utils::orientationAroundZAxis(yaw);
333 
334  if (validateTargetPose(target_pose, dist_to_target, trajectory, costmap_transform, cmd_vel)) {
335  // Determine the maximum valid cost based on robot footprint type
336  double max_valid_cost =
337  costmap_ros_->getUseRadius() ? static_cast<double>(nav2_costmap_2d::MAX_NON_OBSTACLE) :
338  static_cast<double>(nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE);
339 
340  // Check if the final rotation path is risky
341  double safety_threshold = max_valid_cost - static_cast<double>(params_->obstacle_cost_margin);
342  if (getMaxCost(trajectory, costmap_transform) >= safety_threshold) {
343  // Try to find a better approach by searching spiral curves
345  target_pose, dist_to_target, costmap_transform, max_valid_cost, trajectory, cmd_vel);
346  }
347  return true;
348  }
349  return false;
350 }
351 
353  const geometry_msgs::msg::PoseStamped & motion_target,
354  const geometry_msgs::msg::TransformStamped & costmap_transform,
355  nav_msgs::msg::Path & trajectory,
356  geometry_msgs::msg::TwistStamped & cmd_vel,
357  bool backward)
358 {
359  trajectory.poses.clear();
360 
361  // First pose is robot current pose
362  geometry_msgs::msg::PoseStamped next_pose;
363  next_pose.header.frame_id = costmap_ros_->getBaseFrameID();
364  next_pose.pose.orientation.w = 1.0;
365 
366  // Should we simulate rotation initially?
367  bool sim_initial_rotation = do_initial_rotation_ && params_->initial_rotation;
368  double angle_to_target =
369  std::atan2(motion_target.pose.position.y, motion_target.pose.position.x);
370  if (fabs(angle_to_target) < params_->initial_rotation_tolerance) {
371  sim_initial_rotation = false;
372  do_initial_rotation_ = false;
373  }
374 
375  double distance = std::numeric_limits<double>::max();
376  double resolution = costmap_ros_->getCostmap()->getResolution();
377  double dt = (params_->v_linear_max > 0.0) ? resolution / params_->v_linear_max : 0.0;
378 
379  // Set max iter to avoid infinite loop
380  unsigned int max_iter = 3 *
381  std::hypot(motion_target.pose.position.x, motion_target.pose.position.y) / resolution;
382 
383  // Generate path
384  do{
385  if (sim_initial_rotation) {
386  // Compute rotation velocity
387  double next_pose_yaw = tf2::getYaw(next_pose.pose.orientation);
388  auto cmd = rotateToTarget(angle_to_target - next_pose_yaw);
389 
390  // If this is first iteration, this is our current target velocity
391  if (trajectory.poses.empty()) {cmd_vel.twist = cmd;}
392 
393  // Are we done simulating initial rotation?
394  if (fabs(angle_to_target - next_pose_yaw) < params_->initial_rotation_tolerance) {
395  sim_initial_rotation = false;
396  }
397 
398  // Forward simulate rotation command
399  next_pose_yaw += cmd_vel.twist.angular.z * dt;
400  next_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(next_pose_yaw);
401  } else {
402  // If this is first iteration, this is our current target velocity
403  if (trajectory.poses.empty()) {
404  cmd_vel.twist = control_law_->calculateRegularVelocity(
405  motion_target.pose, next_pose.pose, backward);
406  }
407 
408  // Apply velocities to calculate next pose
409  next_pose.pose = control_law_->calculateNextPose(
410  dt, motion_target.pose, next_pose.pose, backward);
411  }
412 
413  // Add the pose to the trajectory for visualization
414  trajectory.poses.push_back(next_pose);
415 
416  // Compute footprint scaling
417  double footprint_scaling = 1.0;
418  if (cmd_vel.twist.linear.x > params_->footprint_scaling_linear_vel) {
419  // Scaling = (vel_x - scaling_vel_x) / (max_vel_x - scaling_vel_x)
420  double ratio = params_->v_linear_max - params_->footprint_scaling_linear_vel;
421  // Avoid divide by zero
422  if (ratio > 0) {
423  ratio = (cmd_vel.twist.linear.x - params_->footprint_scaling_linear_vel) / ratio;
424  footprint_scaling += ratio * params_->footprint_scaling_factor;
425  }
426  }
427 
428  // Check for collision
429  geometry_msgs::msg::PoseStamped global_pose;
430  tf2::doTransform(next_pose, global_pose, costmap_transform);
431  if (params_->use_collision_detection && inCollision(
432  global_pose.pose.position.x, global_pose.pose.position.y,
433  tf2::getYaw(global_pose.pose.orientation), footprint_scaling))
434  {
435  return false;
436  }
437 
438  // Check if we reach the goal
439  distance = nav2_util::geometry_utils::euclidean_distance(motion_target.pose, next_pose.pose);
440  }while(distance > resolution && trajectory.poses.size() < max_iter);
441 
442  return true;
443 }
444 
445 geometry_msgs::msg::Twist GracefulController::rotateToTarget(double angle_to_target)
446 {
447  geometry_msgs::msg::Twist vel;
448  vel.linear.x = 0.0;
449  vel.angular.z = params_->rotation_scaling_factor * angle_to_target * params_->v_angular_max;
450  vel.angular.z = std::copysign(1.0, vel.angular.z) * std::max(
451  abs(vel.angular.z),
452  params_->v_angular_min_in_place);
453  return vel;
454 }
455 
457  const nav_msgs::msg::Path & path, geometry_msgs::msg::TransformStamped & costmap_transform)
458 {
459  double max_cost = 0.0;
460 
461  for (const auto & pose : path.poses) {
462  geometry_msgs::msg::PoseStamped costmap_pose;
463  tf2::doTransform(pose, costmap_pose, costmap_transform);
464  unsigned int mx, my;
465  if (costmap_ros_->getCostmap()->worldToMap(costmap_pose.pose.position.x,
466  costmap_pose.pose.position.y, mx, my))
467  {
468  max_cost = std::max(max_cost, collision_checker_->pointCost(mx, my));
469  }
470  }
471 
472  return max_cost;
473 }
474 
476  const double & x, const double & y, const double & theta,
477  double inflation_scale)
478 {
479  unsigned int mx, my;
480  if (!costmap_ros_->getCostmap()->worldToMap(x, y, mx, my)) {
481  RCLCPP_WARN(
482  logger_, "The path is not in the costmap. Cannot check for collisions. "
483  "Proceed at your own risk, slow the robot, or increase your costmap size.");
484  return false;
485  }
486 
487  if (inflation_scale < 1.0) {
488  RCLCPP_WARN(logger_, "Inflation ratio cannot be less than 1.0");
489  throw nav2_core::NoValidControl("Inflation ratio less than 1.0");
490  }
491 
492  // Calculate the cost of the footprint at the robot's current position depending
493  // on the shape of the footprint
494  bool is_tracking_unknown =
495  costmap_ros_->getLayeredCostmap()->isTrackingUnknown();
496  bool consider_footprint = !costmap_ros_->getUseRadius();
497 
498  double footprint_cost;
499  if (consider_footprint) {
500  std::vector<geometry_msgs::msg::Point> spec = costmap_ros_->getRobotFootprint();
501  if (spec.size() > 3) {
502  for (auto & point : spec) {
503  point.x *= inflation_scale;
504  point.y *= inflation_scale;
505  }
506  }
507  footprint_cost = collision_checker_->footprintCostAtPose(x, y, theta, spec);
508  } else {
509  footprint_cost = collision_checker_->pointCost(mx, my);
510  }
511 
512  switch (static_cast<unsigned char>(footprint_cost)) {
513  case (nav2_costmap_2d::LETHAL_OBSTACLE):
514  return true;
515  case (nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE):
516  return consider_footprint ? false : true;
517  case (nav2_costmap_2d::NO_INFORMATION):
518  return is_tracking_unknown ? false : true;
519  }
520 
521  return false;
522 }
523 
525  const std::vector<geometry_msgs::msg::PoseStamped> & poses,
526  std::vector<double> & distances)
527 {
528  distances.resize(poses.size());
529  // Do the first pose from robot
530  double d = std::hypot(poses[0].pose.position.x, poses[0].pose.position.y);
531  distances[0] = d;
532  // Compute remaining poses
533  for (size_t i = 1; i < poses.size(); ++i) {
534  d += nav2_util::geometry_utils::euclidean_distance(poses[i - 1].pose, poses[i].pose);
535  distances[i] = d;
536  }
537 }
538 
540  std::vector<geometry_msgs::msg::PoseStamped> & path)
541 {
542  // We never change the orientation of the first & last pose
543  // So we need at least three poses to do anything here
544  if (path.size() < 3) {return;}
545 
546  // Check if we actually need to add orientations
547  double initial_yaw = tf2::getYaw(path[1].pose.orientation);
548  for (size_t i = 2; i < path.size() - 1; ++i) {
549  double this_yaw = tf2::getYaw(path[i].pose.orientation);
550  if (angles::shortest_angular_distance(this_yaw, initial_yaw) > 1e-6) {return;}
551  }
552 
553  // For each pose, point at the next one
554  // NOTE: control loop will handle reversing logic
555  for (size_t i = 0; i < path.size() - 1; ++i) {
556  // Get relative yaw angle
557  double dx = path[i + 1].pose.position.x - path[i].pose.position.x;
558  double dy = path[i + 1].pose.position.y - path[i].pose.position.y;
559  double yaw = std::atan2(dy, dx);
560  path[i].pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(yaw);
561  }
562 }
563 
565  geometry_msgs::msg::PoseStamped & target_pose, double dist_to_target,
566  geometry_msgs::msg::TransformStamped & costmap_transform, double safety_cost,
567  nav_msgs::msg::Path & best_trajectory, geometry_msgs::msg::TwistStamped & best_cmd_vel)
568 {
569  bool found_valid = false;
570  double best_eta = std::numeric_limits<double>::max();
571 
572  for (int i = 0; i < 2 * M_PI / params_->final_rotation_search_step; ++i) {
573  double angle = static_cast<double>(i) * params_->final_rotation_search_step;
574  // Prioritize previously selected approach angles
575  if (safe_approach_angle_.has_value()) {
576  angle += safe_approach_angle_.value();
577  }
578 
579  // Create candidate pose
580  auto candidate_pose = target_pose;
581  candidate_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(angle);
582 
583  nav_msgs::msg::Path candidate_path = best_trajectory;
584  geometry_msgs::msg::TwistStamped candidate_cmd_vel = best_cmd_vel;
585 
586  // Validate the candidate
587  if (validateTargetPose(
588  candidate_pose, dist_to_target, candidate_path, costmap_transform,
589  candidate_cmd_vel))
590  {
591  double candidate_cost = getMaxCost(candidate_path, costmap_transform);
592 
593  bool reversing = false;
594  if (params_->allow_backward && target_pose.pose.position.x < 0.0) {
595  reversing = true;
596  }
597  // Calculate ETA
598  double eta = 0.0;
599  for (size_t j = 1; j < candidate_path.poses.size(); ++j) {
600  auto current_pose = candidate_path.poses[j - 1];
601  auto next_pose = candidate_path.poses[j];
602  auto cmd = control_law_->calculateRegularVelocity(candidate_pose.pose, current_pose.pose,
603  reversing);
604  double speed = std::abs(cmd.linear.x);
605  // Avoid division by zero
606  speed = std::max(speed, 1e-3);
607  double step_dist = nav2_util::geometry_utils::euclidean_distance(
608  current_pose.pose, next_pose.pose);
609  double step_time = step_dist / speed;
610  eta += step_time;
611  }
612 
613  // Selection logic: Pick the fastest among the safe ones
614  if (eta < best_eta) {
615  best_eta = eta;
616  if (candidate_cost < safety_cost) {
617  best_trajectory = candidate_path;
618  best_cmd_vel = candidate_cmd_vel;
619  target_pose = candidate_pose;
620  found_valid = true;
621  // Reuse known safe approach angle if still valid
622  if (safe_approach_angle_.value_or(1e3 /*Never in (-PI, PI]*/) == angle) {
623  break;
624  }
625  safe_approach_angle_ = angle;
626  }
627  }
628  }
629  }
630 
631  return found_valid;
632 }
633 
634 } // namespace nav2_graceful_controller
635 
636 // Register this controller as a nav2_core plugin
637 PLUGINLIB_EXPORT_CLASS(
controller interface that acts as a virtual base class for all controller plugins
Definition: controller.hpp:60
Function-object for checking whether a goal has been reached.
virtual bool isGoalXYReached(const geometry_msgs::msg::Pose &query_pose, const geometry_msgs::msg::Pose &goal_pose, const geometry_msgs::msg::Twist &velocity, const nav_msgs::msg::Path &transformed_global_plan)=0
Check if XY goal position has been reached (without considering yaw) This is useful for controllers t...
void activate() override
Activate controller state machine.
void computeDistanceAlongPath(const std::vector< geometry_msgs::msg::PoseStamped > &poses, std::vector< double > &distances)
Compute the distance to each pose in a path.
bool validateTargetPose(geometry_msgs::msg::PoseStamped &target_pose, double dist_to_target, nav_msgs::msg::Path &trajectory, geometry_msgs::msg::TransformStamped &costmap_transform, geometry_msgs::msg::TwistStamped &cmd_vel)
Validate a given target pose for calculating command velocity.
bool validateTargetPoseOnApproach(geometry_msgs::msg::PoseStamped &target_pose, double dist_to_target, double dist_to_goal, nav_msgs::msg::Path &trajectory, geometry_msgs::msg::TransformStamped &costmap_transform, geometry_msgs::msg::TwistStamped &cmd_vel)
Validate a given target pose for calculating command velocity on approach to goal.
void deactivate() override
Deactivate controller state machine.
bool inCollision(const double &x, const double &y, const double &theta, double inflation_scale=1.0)
Checks if the robot is in collision.
bool findBestApproachTrajectory(geometry_msgs::msg::PoseStamped &target_pose, double dist_to_target, geometry_msgs::msg::TransformStamped &costmap_transform, double safety_cost, nav_msgs::msg::Path &best_trajectory, geometry_msgs::msg::TwistStamped &best_cmd_vel)
Find the best approach trajectory by searching multiple orientations.
bool simulateTrajectory(const geometry_msgs::msg::PoseStamped &motion_target, const geometry_msgs::msg::TransformStamped &costmap_transform, nav_msgs::msg::Path &trajectory, geometry_msgs::msg::TwistStamped &cmd_vel, bool backward)
Simulate trajectory calculating in every step the new velocity command based on a new curvature value...
geometry_msgs::msg::TwistStamped computeVelocityCommands(const geometry_msgs::msg::PoseStamped &pose, const geometry_msgs::msg::Twist &velocity, nav2_core::GoalChecker *goal_checker, const nav_msgs::msg::Path &transformed_global_plan, const geometry_msgs::msg::PoseStamped &global_goal) override
Compute the best command given the current pose and velocity.
void configure(const nav2::LifecycleNode::WeakPtr &parent, std::string name, nav2::TransformBuffer::SharedPtr tf, std::shared_ptr< nav2_costmap_2d::Costmap2DROS > costmap_ros) override
Configure controller state machine.
void cleanup() override
Cleanup controller state machine.
geometry_msgs::msg::Twist rotateToTarget(double angle_to_target)
Rotate the robot to face the motion target with maximum angular velocity.
void newPathReceived(const nav_msgs::msg::Path &raw_global_path) override
nav2_core newPathReceived - Receives a new plan from the Planner Server
void setSpeedLimit(const double &speed_limit, const bool &percentage) override
Limits the maximum linear speed of the robot.
void validateOrientations(std::vector< geometry_msgs::msg::PoseStamped > &path)
Control law requires proper orientations, not all planners provide them.
double getMaxCost(const nav_msgs::msg::Path &path, geometry_msgs::msg::TransformStamped &costmap_transform)
Get the maximum cost of a path.