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