Nav2 Navigation Stack - rolling  main
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_graceful_controller/graceful_controller.hpp"
23 #include "nav2_costmap_2d/costmap_filters/filter_values.hpp"
24 
25 namespace nav2_graceful_controller
26 {
27 
29  const nav2::LifecycleNode::WeakPtr & parent,
30  std::string name, const std::shared_ptr<tf2_ros::Buffer> tf,
31  const std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros)
32 {
33  nav2::LifecycleNode::SharedPtr node = parent.lock();
34  if (!node) {
35  throw nav2_core::ControllerException("Unable to lock node!");
36  }
37 
38  costmap_ros_ = costmap_ros;
39  tf_buffer_ = tf;
40  plugin_name_ = name;
41  logger_ = node->get_logger();
42 
43  // Handles storage and dynamic configuration of parameters.
44  // Returns pointer to data current param settings.
45  param_handler_ = std::make_unique<ParameterHandler>(
46  node, plugin_name_, logger_,
47  costmap_ros_->getCostmap()->getSizeInMetersX());
48  params_ = param_handler_->getParams();
49 
50  // Handles global path transformations
51  path_handler_ = std::make_unique<PathHandler>(
52  params_->transform_tolerance, tf_buffer_, costmap_ros_);
53 
54  // Handles the control law to generate the velocity commands
55  control_law_ = std::make_unique<SmoothControlLaw>(
56  params_->k_phi, params_->k_delta, params_->beta, params_->lambda, params_->slowdown_radius,
57  params_->v_linear_min, params_->v_linear_max, params_->v_angular_max);
58 
59  // Initialize footprint collision checker
60  if(params_->use_collision_detection) {
61  collision_checker_ = std::make_unique<nav2_costmap_2d::
62  FootprintCollisionChecker<nav2_costmap_2d::Costmap2D *>>(costmap_ros_->getCostmap());
63  }
64 
65  // Publishers
66  transformed_plan_pub_ = node->create_publisher<nav_msgs::msg::Path>("transformed_global_plan");
67  local_plan_pub_ = node->create_publisher<nav_msgs::msg::Path>("local_plan");
68  motion_target_pub_ = node->create_publisher<geometry_msgs::msg::PoseStamped>("motion_target");
69  slowdown_pub_ = node->create_publisher<visualization_msgs::msg::Marker>("slowdown");
70 
71  RCLCPP_INFO(logger_, "Configured Graceful Motion Controller: %s", plugin_name_.c_str());
72 }
73 
75 {
76  RCLCPP_INFO(
77  logger_,
78  "Cleaning up controller: %s of type graceful_controller::GracefulController",
79  plugin_name_.c_str());
80  transformed_plan_pub_.reset();
81  local_plan_pub_.reset();
82  motion_target_pub_.reset();
83  slowdown_pub_.reset();
84  collision_checker_.reset();
85  path_handler_.reset();
86  param_handler_.reset();
87  control_law_.reset();
88 }
89 
91 {
92  RCLCPP_INFO(
93  logger_,
94  "Activating controller: %s of type nav2_graceful_controller::GracefulController",
95  plugin_name_.c_str());
96  transformed_plan_pub_->on_activate();
97  local_plan_pub_->on_activate();
98  motion_target_pub_->on_activate();
99  slowdown_pub_->on_activate();
100  param_handler_->activate();
101 }
102 
104 {
105  RCLCPP_INFO(
106  logger_,
107  "Deactivating controller: %s of type nav2_graceful_controller::GracefulController",
108  plugin_name_.c_str());
109  transformed_plan_pub_->on_deactivate();
110  local_plan_pub_->on_deactivate();
111  motion_target_pub_->on_deactivate();
112  slowdown_pub_->on_deactivate();
113  param_handler_->deactivate();
114 }
115 
116 geometry_msgs::msg::TwistStamped GracefulController::computeVelocityCommands(
117  const geometry_msgs::msg::PoseStamped & pose,
118  const geometry_msgs::msg::Twist & /*velocity*/,
119  nav2_core::GoalChecker * goal_checker)
120 {
121  std::lock_guard<std::mutex> param_lock(param_handler_->getMutex());
122 
123  geometry_msgs::msg::TwistStamped cmd_vel;
124  cmd_vel.header = pose.header;
125 
126  // Update for the current goal checker's state
127  geometry_msgs::msg::Pose pose_tolerance;
128  geometry_msgs::msg::Twist velocity_tolerance;
129  if (!goal_checker->getTolerances(pose_tolerance, velocity_tolerance)) {
130  RCLCPP_WARN(logger_, "Unable to retrieve goal checker's tolerances!");
131  } else {
132  goal_dist_tolerance_ = pose_tolerance.position.x;
133  }
134 
135  // Update the smooth control law with the new params
136  control_law_->setCurvatureConstants(
137  params_->k_phi, params_->k_delta, params_->beta, params_->lambda);
138  control_law_->setSlowdownRadius(params_->slowdown_radius);
139  control_law_->setSpeedLimit(params_->v_linear_min, params_->v_linear_max, params_->v_angular_max);
140 
141  // Transform path to robot base frame
142  auto transformed_plan = path_handler_->transformGlobalPlan(
143  pose, params_->max_robot_pose_search_dist);
144 
145  // Add proper orientations to plan, if needed
146  validateOrientations(transformed_plan.poses);
147 
148  // Publish plan for visualization
149  transformed_plan_pub_->publish(transformed_plan);
150 
151  // Transform local frame to global frame to use in collision checking
152  geometry_msgs::msg::TransformStamped costmap_transform;
153  try {
154  costmap_transform = tf_buffer_->lookupTransform(
155  costmap_ros_->getGlobalFrameID(), costmap_ros_->getBaseFrameID(),
156  tf2::TimePointZero);
157  } catch (tf2::TransformException & ex) {
158  RCLCPP_ERROR(
159  logger_, "Could not transform %s to %s: %s",
160  costmap_ros_->getBaseFrameID().c_str(), costmap_ros_->getGlobalFrameID().c_str(),
161  ex.what());
162  throw ex;
163  }
164 
165  // Compute distance to goal as the path's integrated distance to account for path curvatures
166  double dist_to_goal = nav2_util::geometry_utils::calculate_path_length(transformed_plan);
167 
168  // If we've reached the XY goal tolerance, just rotate
169  if (dist_to_goal < goal_dist_tolerance_ || goal_reached_) {
170  goal_reached_ = true;
171  double angle_to_goal = tf2::getYaw(transformed_plan.poses.back().pose.orientation);
172  // Check for collisions between our current pose and goal pose
173  size_t num_steps = fabs(angle_to_goal) / params_->in_place_collision_resolution;
174  // Need to check at least the end pose
175  num_steps = std::max(static_cast<size_t>(1), num_steps);
176  bool collision_free = true;
177  for (size_t i = 1; i <= num_steps; ++i) {
178  double step = static_cast<double>(i) / static_cast<double>(num_steps);
179  double yaw = step * angle_to_goal;
180  geometry_msgs::msg::PoseStamped next_pose;
181  next_pose.header.frame_id = costmap_ros_->getBaseFrameID();
182  next_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(yaw);
183  geometry_msgs::msg::PoseStamped costmap_pose;
184  tf2::doTransform(next_pose, costmap_pose, costmap_transform);
185  if (params_->use_collision_detection && inCollision(
186  costmap_pose.pose.position.x, costmap_pose.pose.position.y,
187  tf2::getYaw(costmap_pose.pose.orientation)))
188  {
189  collision_free = false;
190  break;
191  }
192  }
193  // Compute velocity if rotation is possible
194  if (collision_free) {
195  cmd_vel.twist = rotateToTarget(angle_to_goal);
196  return cmd_vel;
197  }
198  // Else, fall through and see if we should follow control law longer
199  }
200 
201  // Find a valid target pose and its trajectory
202  nav_msgs::msg::Path local_plan;
203  geometry_msgs::msg::PoseStamped target_pose;
204 
205  double dist_to_target;
206  std::vector<double> target_distances;
207  computeDistanceAlongPath(transformed_plan.poses, target_distances);
208 
209  bool is_first_iteration = true;
210  for (int i = transformed_plan.poses.size() - 1; i >= 0; --i) {
211  if (is_first_iteration) {
212  // Calculate target pose through lookahead interpolation to get most accurate
213  // lookahead point, if possible
214  dist_to_target = params_->max_lookahead;
215  // Interpolate after goal false for graceful controller
216  // Requires interpolating the orientation which is not yet implemented
217  // Updates dist_to_target for target_pose returned if using the point on the path
218  target_pose = nav2_util::getLookAheadPoint(dist_to_target, transformed_plan, false);
219  is_first_iteration = false;
220  } else {
221  // Underlying control law needs a single target pose, which should:
222  // * Be as far away as possible from the robot (for smoothness)
223  // * But no further than the max_lookahed_ distance
224  // * Be feasible to reach in a collision free manner
225  dist_to_target = target_distances[i];
226  target_pose = transformed_plan.poses[i];
227  }
228 
229  // Compute velocity at this moment if valid target pose is found
230  if (validateTargetPose(
231  target_pose, dist_to_target, dist_to_goal, local_plan, costmap_transform, cmd_vel))
232  {
233  // Publish the selected target_pose
234  motion_target_pub_->publish(target_pose);
235  // Publish marker for slowdown radius around motion target for debugging / visualization
236  auto slowdown_marker = nav2_graceful_controller::createSlowdownMarker(
237  target_pose, params_->slowdown_radius);
238  slowdown_pub_->publish(slowdown_marker);
239  // Publish the local plan
240  local_plan.header = transformed_plan.header;
241  local_plan_pub_->publish(local_plan);
242  // Successfully found velocity command
243  return cmd_vel;
244  }
245  }
246 
247  throw nav2_core::NoValidControl("Collision detected in trajectory");
248 }
249 
250 void GracefulController::setPlan(const nav_msgs::msg::Path & path)
251 {
252  path_handler_->setPlan(path);
253  goal_reached_ = false;
254  do_initial_rotation_ = true;
255 }
256 
258  const double & speed_limit, const bool & percentage)
259 {
260  std::lock_guard<std::mutex> param_lock(param_handler_->getMutex());
261 
262  if (speed_limit == nav2_costmap_2d::NO_SPEED_LIMIT) {
263  params_->v_linear_max = params_->v_linear_max_initial;
264  params_->v_angular_max = params_->v_angular_max_initial;
265  } else {
266  if (percentage) {
267  // Speed limit is expressed in % from maximum speed of robot
268  params_->v_linear_max = std::max(
269  params_->v_linear_max_initial * speed_limit / 100.0, params_->v_linear_min);
270  params_->v_angular_max = params_->v_angular_max_initial * speed_limit / 100.0;
271  } else {
272  // Speed limit is expressed in m/s
273  params_->v_linear_max = std::max(speed_limit, params_->v_linear_min);
274  // Limit the angular velocity to be proportional to the linear velocity
275  params_->v_angular_max = params_->v_angular_max_initial *
276  speed_limit / params_->v_linear_max_initial;
277  }
278  }
279 }
280 
282  geometry_msgs::msg::PoseStamped & target_pose,
283  double dist_to_target,
284  double dist_to_goal,
285  nav_msgs::msg::Path & trajectory,
286  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  if (dist_to_goal < params_->max_lookahead) {
295  if (params_->prefer_final_rotation) {
296  // Avoid instability and big sweeping turns at the end of paths by
297  // ignoring final heading
298  double yaw = std::atan2(target_pose.pose.position.y, target_pose.pose.position.x);
299  target_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(yaw);
300  }
301  } else if (dist_to_target < params_->min_lookahead) {
302  // Make sure target is far enough away to avoid instability
303  return false;
304  }
305 
306  // Flip the orientation of the motion target if the robot is moving backwards
307  bool reversing = false;
308  if (params_->allow_backward && target_pose.pose.position.x < 0.0) {
309  reversing = true;
310  target_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(
311  tf2::getYaw(target_pose.pose.orientation) + M_PI);
312  }
313 
314  // Actually simulate the path
315  if (simulateTrajectory(target_pose, costmap_transform, trajectory, cmd_vel, reversing)) {
316  // Successfully simulated to target_pose
317  return true;
318  }
319 
320  // Validation not successful
321  return false;
322 }
323 
325  const geometry_msgs::msg::PoseStamped & motion_target,
326  const geometry_msgs::msg::TransformStamped & costmap_transform,
327  nav_msgs::msg::Path & trajectory,
328  geometry_msgs::msg::TwistStamped & cmd_vel,
329  bool backward)
330 {
331  trajectory.poses.clear();
332 
333  // First pose is robot current pose
334  geometry_msgs::msg::PoseStamped next_pose;
335  next_pose.header.frame_id = costmap_ros_->getBaseFrameID();
336  next_pose.pose.orientation.w = 1.0;
337 
338  // Should we simulate rotation initially?
339  bool sim_initial_rotation = do_initial_rotation_ && params_->initial_rotation;
340  double angle_to_target =
341  std::atan2(motion_target.pose.position.y, motion_target.pose.position.x);
342  if (fabs(angle_to_target) < params_->initial_rotation_tolerance) {
343  sim_initial_rotation = false;
344  do_initial_rotation_ = false;
345  }
346 
347  double distance = std::numeric_limits<double>::max();
348  double resolution_ = costmap_ros_->getCostmap()->getResolution();
349  double dt = (params_->v_linear_max > 0.0) ? resolution_ / params_->v_linear_max : 0.0;
350 
351  // Set max iter to avoid infinite loop
352  unsigned int max_iter = 3 *
353  std::hypot(motion_target.pose.position.x, motion_target.pose.position.y) / resolution_;
354 
355  // Generate path
356  do{
357  if (sim_initial_rotation) {
358  // Compute rotation velocity
359  double next_pose_yaw = tf2::getYaw(next_pose.pose.orientation);
360  auto cmd = rotateToTarget(angle_to_target - next_pose_yaw);
361 
362  // If this is first iteration, this is our current target velocity
363  if (trajectory.poses.empty()) {cmd_vel.twist = cmd;}
364 
365  // Are we done simulating initial rotation?
366  if (fabs(angle_to_target - next_pose_yaw) < params_->initial_rotation_tolerance) {
367  sim_initial_rotation = false;
368  }
369 
370  // Forward simulate rotation command
371  next_pose_yaw += cmd_vel.twist.angular.z * dt;
372  next_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(next_pose_yaw);
373  } else {
374  // If this is first iteration, this is our current target velocity
375  if (trajectory.poses.empty()) {
376  cmd_vel.twist = control_law_->calculateRegularVelocity(
377  motion_target.pose, next_pose.pose, backward);
378  }
379 
380  // Apply velocities to calculate next pose
381  next_pose.pose = control_law_->calculateNextPose(
382  dt, motion_target.pose, next_pose.pose, backward);
383  }
384 
385  // Add the pose to the trajectory for visualization
386  trajectory.poses.push_back(next_pose);
387 
388  // Check for collision
389  geometry_msgs::msg::PoseStamped global_pose;
390  tf2::doTransform(next_pose, global_pose, costmap_transform);
391  if (params_->use_collision_detection && inCollision(
392  global_pose.pose.position.x, global_pose.pose.position.y,
393  tf2::getYaw(global_pose.pose.orientation)))
394  {
395  return false;
396  }
397 
398  // Check if we reach the goal
399  distance = nav2_util::geometry_utils::euclidean_distance(motion_target.pose, next_pose.pose);
400  }while(distance > resolution_ && trajectory.poses.size() < max_iter);
401 
402  return true;
403 }
404 
405 geometry_msgs::msg::Twist GracefulController::rotateToTarget(double angle_to_target)
406 {
407  geometry_msgs::msg::Twist vel;
408  vel.linear.x = 0.0;
409  vel.angular.z = params_->rotation_scaling_factor * angle_to_target * params_->v_angular_max;
410  vel.angular.z = std::copysign(1.0, vel.angular.z) * std::max(abs(vel.angular.z),
411  params_->v_angular_min_in_place);
412  return vel;
413 }
414 
415 bool GracefulController::inCollision(const double & x, const double & y, const double & theta)
416 {
417  unsigned int mx, my;
418  if (!costmap_ros_->getCostmap()->worldToMap(x, y, mx, my)) {
419  RCLCPP_WARN(
420  logger_, "The path is not in the costmap. Cannot check for collisions. "
421  "Proceed at your own risk, slow the robot, or increase your costmap size.");
422  return false;
423  }
424 
425  // Calculate the cost of the footprint at the robot's current position depending
426  // on the shape of the footprint
427  bool is_tracking_unknown =
428  costmap_ros_->getLayeredCostmap()->isTrackingUnknown();
429  bool consider_footprint = !costmap_ros_->getUseRadius();
430 
431  double footprint_cost;
432  if (consider_footprint) {
433  footprint_cost = collision_checker_->footprintCostAtPose(
434  x, y, theta, costmap_ros_->getRobotFootprint());
435  } else {
436  footprint_cost = collision_checker_->pointCost(mx, my);
437  }
438 
439  switch (static_cast<unsigned char>(footprint_cost)) {
440  case (nav2_costmap_2d::LETHAL_OBSTACLE):
441  return true;
442  case (nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE):
443  return consider_footprint ? false : true;
444  case (nav2_costmap_2d::NO_INFORMATION):
445  return is_tracking_unknown ? false : true;
446  }
447 
448  return false;
449 }
450 
452  const std::vector<geometry_msgs::msg::PoseStamped> & poses,
453  std::vector<double> & distances)
454 {
455  distances.resize(poses.size());
456  // Do the first pose from robot
457  double d = std::hypot(poses[0].pose.position.x, poses[0].pose.position.y);
458  distances[0] = d;
459  // Compute remaining poses
460  for (size_t i = 1; i < poses.size(); ++i) {
461  d += nav2_util::geometry_utils::euclidean_distance(poses[i - 1].pose, poses[i].pose);
462  distances[i] = d;
463  }
464 }
465 
467  std::vector<geometry_msgs::msg::PoseStamped> & path)
468 {
469  // We never change the orientation of the first & last pose
470  // So we need at least three poses to do anything here
471  if (path.size() < 3) {return;}
472 
473  // Check if we actually need to add orientations
474  double initial_yaw = tf2::getYaw(path[1].pose.orientation);
475  for (size_t i = 2; i < path.size() - 1; ++i) {
476  double this_yaw = tf2::getYaw(path[i].pose.orientation);
477  if (angles::shortest_angular_distance(this_yaw, initial_yaw) > 1e-6) {return;}
478  }
479 
480  // For each pose, point at the next one
481  // NOTE: control loop will handle reversing logic
482  for (size_t i = 0; i < path.size() - 1; ++i) {
483  // Get relative yaw angle
484  double dx = path[i + 1].pose.position.x - path[i].pose.position.x;
485  double dy = path[i + 1].pose.position.y - path[i].pose.position.y;
486  double yaw = std::atan2(dy, dx);
487  path[i].pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(yaw);
488  }
489 }
490 
491 } // namespace nav2_graceful_controller
492 
493 // Register this controller as a nav2_core plugin
494 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 getTolerances(geometry_msgs::msg::Pose &pose_tolerance, geometry_msgs::msg::Twist &vel_tolerance)=0
Get the maximum possible tolerances used for goal checking in the major types. Any field without a va...
void activate() override
Activate controller state machine.
void setPlan(const nav_msgs::msg::Path &path) override
nav2_core setPlan - Sets the global plan.
void computeDistanceAlongPath(const std::vector< geometry_msgs::msg::PoseStamped > &poses, std::vector< double > &distances)
Compute the distance to each pose in a path.
void deactivate() override
Deactivate controller state machine.
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...
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 setSpeedLimit(const double &speed_limit, const bool &percentage) override
Limits the maximum linear speed of the robot.
void configure(const nav2::LifecycleNode::WeakPtr &parent, std::string name, std::shared_ptr< tf2_ros::Buffer > tf, std::shared_ptr< nav2_costmap_2d::Costmap2DROS > costmap_ros) override
Configure controller state machine.
geometry_msgs::msg::TwistStamped computeVelocityCommands(const geometry_msgs::msg::PoseStamped &pose, const geometry_msgs::msg::Twist &velocity, nav2_core::GoalChecker *goal_checker) override
Compute the best command given the current pose and velocity.
bool inCollision(const double &x, const double &y, const double &theta)
Checks if the robot is in collision.
void validateOrientations(std::vector< geometry_msgs::msg::PoseStamped > &path)
Control law requires proper orientations, not all planners provide them.
bool validateTargetPose(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.