Nav2 Navigation Stack - jazzy  jazzy
ROS 2 Navigation Stack
nav2_rotation_shim_controller.cpp
1 // Copyright (c) 2021 Samsung Research America
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 <algorithm>
16 #include <string>
17 #include <memory>
18 #include <vector>
19 #include <utility>
20 
21 #include "nav2_rotation_shim_controller/nav2_rotation_shim_controller.hpp"
22 
23 using rcl_interfaces::msg::ParameterType;
24 
25 namespace nav2_rotation_shim_controller
26 {
27 
29 : lp_loader_("nav2_core", "nav2_core::Controller"),
30  primary_controller_(nullptr),
31  path_updated_(false),
32  in_rotation_(false)
33 {
34 }
35 
37  const rclcpp_lifecycle::LifecycleNode::WeakPtr & parent,
38  std::string name, std::shared_ptr<tf2_ros::Buffer> tf,
39  std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros)
40 {
41  position_goal_checker_ = std::make_unique<nav2_controller::PositionGoalChecker>();
42  position_goal_checker_->initialize(parent, plugin_name_ + ".position_checker", costmap_ros);
43  plugin_name_ = name;
44  node_ = parent;
45  auto node = parent.lock();
46 
47  tf_ = tf;
48  costmap_ros_ = costmap_ros;
49  logger_ = node->get_logger();
50  clock_ = node->get_clock();
51 
52  std::string primary_controller;
53  double control_frequency;
54  nav2_util::declare_parameter_if_not_declared(
55  node, plugin_name_ + ".angular_dist_threshold", rclcpp::ParameterValue(0.785)); // 45 deg
56  nav2_util::declare_parameter_if_not_declared(
57  node, plugin_name_ + ".angular_disengage_threshold", rclcpp::ParameterValue(0.785 / 2.0));
58  nav2_util::declare_parameter_if_not_declared(
59  node, plugin_name_ + ".forward_sampling_distance", rclcpp::ParameterValue(0.5));
60  nav2_util::declare_parameter_if_not_declared(
61  node, plugin_name_ + ".rotate_to_heading_angular_vel", rclcpp::ParameterValue(1.8));
62  nav2_util::declare_parameter_if_not_declared(
63  node, plugin_name_ + ".max_angular_accel", rclcpp::ParameterValue(3.2));
64  nav2_util::declare_parameter_if_not_declared(
65  node, plugin_name_ + ".max_cost_threshold",
66  rclcpp::ParameterValue(static_cast<double>(nav2_costmap_2d::LETHAL_OBSTACLE)));
67  nav2_util::declare_parameter_if_not_declared(
68  node, plugin_name_ + ".simulate_ahead_time", rclcpp::ParameterValue(1.0));
69  nav2_util::declare_parameter_if_not_declared(
70  node, plugin_name_ + ".primary_controller", rclcpp::PARAMETER_STRING);
71  nav2_util::declare_parameter_if_not_declared(
72  node, plugin_name_ + ".rotate_to_goal_heading", rclcpp::ParameterValue(false));
73  nav2_util::declare_parameter_if_not_declared(
74  node, plugin_name_ + ".rotate_to_heading_once", rclcpp::ParameterValue(false));
75  nav2_util::declare_parameter_if_not_declared(
76  node, plugin_name_ + ".closed_loop", rclcpp::ParameterValue(true));
77  nav2_util::declare_parameter_if_not_declared(
78  node, plugin_name_ + ".use_path_orientations", rclcpp::ParameterValue(false));
79 
80  node->get_parameter(plugin_name_ + ".angular_dist_threshold", angular_dist_threshold_);
81  node->get_parameter(plugin_name_ + ".angular_disengage_threshold", angular_disengage_threshold_);
82  node->get_parameter(plugin_name_ + ".forward_sampling_distance", forward_sampling_distance_);
83  node->get_parameter(
84  plugin_name_ + ".rotate_to_heading_angular_vel",
85  rotate_to_heading_angular_vel_);
86  node->get_parameter(plugin_name_ + ".max_angular_accel", max_angular_accel_);
87  node->get_parameter(plugin_name_ + ".max_cost_threshold", max_cost_threshold_);
88  node->get_parameter(plugin_name_ + ".simulate_ahead_time", simulate_ahead_time_);
89 
90  primary_controller = node->get_parameter(plugin_name_ + ".primary_controller").as_string();
91  node->get_parameter("controller_frequency", control_frequency);
92  control_duration_ = 1.0 / control_frequency;
93 
94  node->get_parameter(plugin_name_ + ".rotate_to_goal_heading", rotate_to_goal_heading_);
95  node->get_parameter(plugin_name_ + ".rotate_to_heading_once", rotate_to_heading_once_);
96  node->get_parameter(plugin_name_ + ".closed_loop", closed_loop_);
97  node->get_parameter(plugin_name_ + ".use_path_orientations", use_path_orientations_);
98 
99  try {
100  primary_controller_ = lp_loader_.createUniqueInstance(primary_controller);
101  RCLCPP_INFO(
102  logger_, "Created internal controller for rotation shimming: %s of type %s",
103  plugin_name_.c_str(), primary_controller.c_str());
104  } catch (const pluginlib::PluginlibException & ex) {
105  RCLCPP_FATAL(
106  logger_,
107  "Failed to create internal controller for rotation shimming. Exception: %s", ex.what());
108  return;
109  }
110 
111  primary_controller_->configure(parent, name, tf, costmap_ros);
112 
113  // initialize collision checker and set costmap
114  collision_checker_ = std::make_unique<nav2_costmap_2d::
115  FootprintCollisionChecker<nav2_costmap_2d::Costmap2D *>>(costmap_ros->getCostmap());
116 }
117 
119 {
120  RCLCPP_INFO(
121  logger_,
122  "Activating controller: %s of type "
123  "nav2_rotation_shim_controller::RotationShimController",
124  plugin_name_.c_str());
125 
126  primary_controller_->activate();
127  in_rotation_ = false;
128  last_angular_vel_ = std::numeric_limits<double>::max();
129 
130  auto node = node_.lock();
131  dyn_params_handler_ = node->add_on_set_parameters_callback(
132  std::bind(
134  this, std::placeholders::_1));
135  position_goal_checker_->reset();
136 }
137 
139 {
140  RCLCPP_INFO(
141  logger_,
142  "Deactivating controller: %s of type "
143  "nav2_rotation_shim_controller::RotationShimController",
144  plugin_name_.c_str());
145 
146  primary_controller_->deactivate();
147 
148  if (auto node = node_.lock()) {
149  node->remove_on_set_parameters_callback(dyn_params_handler_.get());
150  }
151  dyn_params_handler_.reset();
152 }
153 
155 {
156  RCLCPP_INFO(
157  logger_,
158  "Cleaning up controller: %s of type "
159  "nav2_rotation_shim_controller::RotationShimController",
160  plugin_name_.c_str());
161 
162  primary_controller_->cleanup();
163  primary_controller_.reset();
164  position_goal_checker_.reset();
165 }
166 
167 geometry_msgs::msg::TwistStamped RotationShimController::computeVelocityCommands(
168  const geometry_msgs::msg::PoseStamped & pose,
169  const geometry_msgs::msg::Twist & velocity,
170  nav2_core::GoalChecker * goal_checker)
171 {
172  // Rotate to goal heading when in goal xy tolerance
173  if (rotate_to_goal_heading_) {
174  std::lock_guard<std::mutex> lock_reinit(mutex_);
175 
176  try {
177  geometry_msgs::msg::PoseStamped sampled_pt_goal = getSampledPathGoal();
178 
179  if (!nav2_util::transformPoseInTargetFrame(
180  sampled_pt_goal, sampled_pt_goal, *tf_,
181  pose.header.frame_id))
182  {
183  throw nav2_core::ControllerTFError("Failed to transform pose to base frame!");
184  }
185 
186  geometry_msgs::msg::Pose pose_tolerance;
187  geometry_msgs::msg::Twist vel_tolerance;
188  goal_checker->getTolerances(pose_tolerance, vel_tolerance);
189  position_goal_checker_->setXYGoalTolerance(pose_tolerance.position.x);
190 
191  if (position_goal_checker_->isGoalReached(pose.pose, sampled_pt_goal.pose, velocity)) {
192  double pose_yaw = tf2::getYaw(pose.pose.orientation);
193  double goal_yaw = tf2::getYaw(sampled_pt_goal.pose.orientation);
194 
195  double angular_distance_to_heading = angles::shortest_angular_distance(pose_yaw, goal_yaw);
196 
197  auto cmd_vel = computeRotateToHeadingCommand(angular_distance_to_heading, pose, velocity);
198  last_angular_vel_ = cmd_vel.twist.angular.z;
199  return cmd_vel;
200  }
201  } catch (const std::runtime_error & e) {
202  RCLCPP_INFO(
203  logger_,
204  "Rotation Shim Controller was unable to find a goal point,"
205  " a rotational collision was detected, or TF failed to transform"
206  " into base frame! what(): %s", e.what());
207  }
208  }
209 
210  if (path_updated_) {
211  nav2_costmap_2d::Costmap2D * costmap = costmap_ros_->getCostmap();
212  std::unique_lock<nav2_costmap_2d::Costmap2D::mutex_t> lock(*(costmap->getMutex()));
213 
214  std::lock_guard<std::mutex> lock_reinit(mutex_);
215  try {
216  auto sampled_pt = getSampledPathPt();
217  double angular_distance_to_heading;
218  if (use_path_orientations_) {
219  angular_distance_to_heading = angles::shortest_angular_distance(
220  tf2::getYaw(pose.pose.orientation),
221  tf2::getYaw(sampled_pt.pose.orientation));
222  } else {
223  geometry_msgs::msg::Pose sampled_pt_base = transformPoseToBaseFrame(sampled_pt);
224  angular_distance_to_heading = std::atan2(
225  sampled_pt_base.position.y,
226  sampled_pt_base.position.x);
227  }
228 
229  double angular_thresh =
230  in_rotation_ ? angular_disengage_threshold_ : angular_dist_threshold_;
231  if (abs(angular_distance_to_heading) > angular_thresh) {
232  RCLCPP_DEBUG(
233  logger_,
234  "Robot is not within the new path's rough heading, rotating to heading...");
235  in_rotation_ = true;
236  auto cmd_vel = computeRotateToHeadingCommand(angular_distance_to_heading, pose, velocity);
237  last_angular_vel_ = cmd_vel.twist.angular.z;
238  return cmd_vel;
239  } else {
240  RCLCPP_DEBUG(
241  logger_,
242  "Robot is at the new path's rough heading, passing to controller");
243  path_updated_ = false;
244  }
245  } catch (const std::runtime_error & e) {
246  RCLCPP_DEBUG(
247  logger_,
248  "Rotation Shim Controller was unable to find a sampling point,"
249  " a rotational collision was detected, or TF failed to transform"
250  " into base frame! what(): %s", e.what());
251  path_updated_ = false;
252  }
253  }
254 
255  // If at this point, use the primary controller to path track
256  in_rotation_ = false;
257  auto cmd_vel = primary_controller_->computeVelocityCommands(pose, velocity, goal_checker);
258  last_angular_vel_ = cmd_vel.twist.angular.z;
259  return cmd_vel;
260 }
261 
262 geometry_msgs::msg::PoseStamped RotationShimController::getSampledPathPt()
263 {
264  if (current_path_.poses.size() < 2) {
266  "Path is too short to find a valid sampled path point for rotation.");
267  }
268 
269  geometry_msgs::msg::Pose start = current_path_.poses.front().pose;
270  double dx, dy;
271 
272  // Find the first point at least sampling distance away
273  for (unsigned int i = 1; i != current_path_.poses.size(); i++) {
274  dx = current_path_.poses[i].pose.position.x - start.position.x;
275  dy = current_path_.poses[i].pose.position.y - start.position.y;
276  if (hypot(dx, dy) >= forward_sampling_distance_) {
277  current_path_.poses[i].header.frame_id = current_path_.header.frame_id;
278  current_path_.poses[i].header.stamp = clock_->now(); // Get current time transformation
279  return current_path_.poses[i];
280  }
281  }
282 
283  auto goal = current_path_.poses.back();
284  goal.header.frame_id = current_path_.header.frame_id;
285  goal.header.stamp = clock_->now();
286  return goal;
287 }
288 
289 geometry_msgs::msg::PoseStamped RotationShimController::getSampledPathGoal()
290 {
291  if (current_path_.poses.empty()) {
292  throw nav2_core::InvalidPath("Path is empty - cannot find a goal point");
293  }
294 
295  auto goal = current_path_.poses.back();
296  goal.header.frame_id = current_path_.header.frame_id;
297  goal.header.stamp = clock_->now();
298  return goal;
299 }
300 
301 geometry_msgs::msg::Pose
302 RotationShimController::transformPoseToBaseFrame(const geometry_msgs::msg::PoseStamped & pt)
303 {
304  geometry_msgs::msg::PoseStamped pt_base;
305  if (!nav2_util::transformPoseInTargetFrame(pt, pt_base, *tf_, costmap_ros_->getBaseFrameID())) {
306  throw nav2_core::ControllerTFError("Failed to transform pose to base frame!");
307  }
308  return pt_base.pose;
309 }
310 
311 geometry_msgs::msg::TwistStamped
313  const double & angular_distance_to_heading,
314  const geometry_msgs::msg::PoseStamped & pose,
315  const geometry_msgs::msg::Twist & velocity)
316 {
317  auto current = closed_loop_ ? velocity.angular.z : last_angular_vel_;
318  if (current == std::numeric_limits<double>::max()) {
319  current = 0.0;
320  }
321 
322  geometry_msgs::msg::TwistStamped cmd_vel;
323  cmd_vel.header = pose.header;
324  const double sign = angular_distance_to_heading > 0.0 ? 1.0 : -1.0;
325  const double angular_vel = sign * rotate_to_heading_angular_vel_;
326  const double & dt = control_duration_;
327  const double min_feasible_angular_speed = current - max_angular_accel_ * dt;
328  const double max_feasible_angular_speed = current + max_angular_accel_ * dt;
329  cmd_vel.twist.angular.z =
330  std::clamp(angular_vel, min_feasible_angular_speed, max_feasible_angular_speed);
331 
332  // Check if we need to slow down to avoid overshooting
333  double max_vel_to_stop = std::sqrt(2 * max_angular_accel_ * fabs(angular_distance_to_heading));
334  if (fabs(cmd_vel.twist.angular.z) > max_vel_to_stop) {
335  cmd_vel.twist.angular.z = sign * max_vel_to_stop;
336  }
337 
338  isCollisionFree(cmd_vel, angular_distance_to_heading, pose);
339  return cmd_vel;
340 }
341 
343  const geometry_msgs::msg::TwistStamped & cmd_vel,
344  const double & angular_distance_to_heading,
345  const geometry_msgs::msg::PoseStamped & pose)
346 {
347  // Simulate rotation ahead by time in control frequency increments
348  double simulated_time = 0.0;
349  double initial_yaw = tf2::getYaw(pose.pose.orientation);
350  double yaw = 0.0;
351  double footprint_cost = 0.0;
352  double remaining_rotation_before_thresh =
353  fabs(angular_distance_to_heading) - angular_dist_threshold_;
354 
355  while (simulated_time < simulate_ahead_time_) {
356  simulated_time += control_duration_;
357  yaw = initial_yaw + cmd_vel.twist.angular.z * simulated_time;
358 
359  // Stop simulating past the point it would be passed onto the primary controller
360  if (angles::shortest_angular_distance(yaw, initial_yaw) >= remaining_rotation_before_thresh) {
361  break;
362  }
363 
364  using namespace nav2_costmap_2d; // NOLINT
365  footprint_cost = collision_checker_->footprintCostAtPose(
366  pose.pose.position.x, pose.pose.position.y,
367  yaw, costmap_ros_->getRobotFootprint());
368 
369  if (footprint_cost == static_cast<double>(NO_INFORMATION) &&
370  costmap_ros_->getLayeredCostmap()->isTrackingUnknown())
371  {
373  "RotationShimController detected a potential collision ahead!");
374  }
375 
376  if (footprint_cost >= max_cost_threshold_) {
377  throw nav2_core::NoValidControl("RotationShimController detected collision ahead!");
378  }
379  }
380 }
381 
382 bool RotationShimController::isGoalChanged(const nav_msgs::msg::Path & path)
383 {
384  // Return true if rotating or if the current path is empty
385  if (in_rotation_ || current_path_.poses.empty()) {
386  return true;
387  }
388 
389  // Check if the last pose of the current and new paths differ
390  return current_path_.poses.back().pose != path.poses.back().pose;
391 }
392 
393 void RotationShimController::setPlan(const nav_msgs::msg::Path & path)
394 {
395  path_updated_ = rotate_to_heading_once_ ? isGoalChanged(path) : true;
396  current_path_ = path;
397  primary_controller_->setPlan(path);
398  position_goal_checker_->reset();
399 }
400 
401 void RotationShimController::setSpeedLimit(const double & speed_limit, const bool & percentage)
402 {
403  primary_controller_->setSpeedLimit(speed_limit, percentage);
404 }
405 
407 {
408  last_angular_vel_ = std::numeric_limits<double>::max();
409  primary_controller_->reset();
410  position_goal_checker_->reset();
411 }
412 
413 rcl_interfaces::msg::SetParametersResult
414 RotationShimController::dynamicParametersCallback(std::vector<rclcpp::Parameter> parameters)
415 {
416  rcl_interfaces::msg::SetParametersResult result;
417  std::lock_guard<std::mutex> lock_reinit(mutex_);
418 
419  for (auto parameter : parameters) {
420  const auto & type = parameter.get_type();
421  const auto & name = parameter.get_name();
422 
423  if (type == ParameterType::PARAMETER_DOUBLE) {
424  if (name == plugin_name_ + ".angular_dist_threshold") {
425  angular_dist_threshold_ = parameter.as_double();
426  } else if (name == plugin_name_ + ".forward_sampling_distance") {
427  forward_sampling_distance_ = parameter.as_double();
428  } else if (name == plugin_name_ + ".rotate_to_heading_angular_vel") {
429  rotate_to_heading_angular_vel_ = parameter.as_double();
430  } else if (name == plugin_name_ + ".max_angular_accel") {
431  max_angular_accel_ = parameter.as_double();
432  } else if (name == plugin_name_ + ".simulate_ahead_time") {
433  simulate_ahead_time_ = parameter.as_double();
434  } else if (name == plugin_name_ + ".max_cost_threshold") {
435  max_cost_threshold_ = parameter.as_double();
436  }
437  } else if (type == ParameterType::PARAMETER_BOOL) {
438  if (name == plugin_name_ + ".rotate_to_goal_heading") {
439  rotate_to_goal_heading_ = parameter.as_bool();
440  } else if (name == plugin_name_ + ".rotate_to_heading_once") {
441  rotate_to_heading_once_ = parameter.as_bool();
442  } else if (name == plugin_name_ + ".closed_loop") {
443  closed_loop_ = parameter.as_bool();
444  } else if (name == plugin_name_ + ".use_path_orientations") {
445  use_path_orientations_ = parameter.as_bool();
446  }
447  }
448  }
449 
450  result.successful = true;
451  return result;
452 }
453 
454 } // namespace nav2_rotation_shim_controller
455 
456 // Register this controller as a nav2_core plugin
457 PLUGINLIB_EXPORT_CLASS(
controller interface that acts as a virtual base class for all controller plugins
Definition: controller.hpp:61
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...
A 2D costmap provides a mapping between points in the world and their associated "costs".
Definition: costmap_2d.hpp:68
Rotate to rough path heading controller shim plugin.
void deactivate() override
Deactivate controller state machine.
void configure(const rclcpp_lifecycle::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::PoseStamped getSampledPathPt()
Finds the point on the path that is roughly the sampling point distance away from the robot for use....
geometry_msgs::msg::TwistStamped computeVelocityCommands(const geometry_msgs::msg::PoseStamped &pose, const geometry_msgs::msg::Twist &velocity, nav2_core::GoalChecker *) override
Compute the best command given the current pose and velocity.
geometry_msgs::msg::Pose transformPoseToBaseFrame(const geometry_msgs::msg::PoseStamped &pt)
Uses TF to find the location of the sampled path point in base frame.
void cleanup() override
Cleanup controller state machine.
geometry_msgs::msg::PoseStamped getSampledPathGoal()
Find the goal point in path May throw exception if the path is empty.
bool isGoalChanged(const nav_msgs::msg::Path &path)
Checks if the goal has changed based on the given path.
geometry_msgs::msg::TwistStamped computeRotateToHeadingCommand(const double &angular_distance, const geometry_msgs::msg::PoseStamped &pose, const geometry_msgs::msg::Twist &velocity)
Rotates the robot to the rough heading.
void activate() override
Activate controller state machine.
void setPlan(const nav_msgs::msg::Path &path) override
nav2_core setPlan - Sets the global plan
void setSpeedLimit(const double &speed_limit, const bool &percentage) override
Limits the maximum linear speed of the robot.
RotationShimController()
Constructor for nav2_rotation_shim_controller::RotationShimController.
rcl_interfaces::msg::SetParametersResult dynamicParametersCallback(std::vector< rclcpp::Parameter > parameters)
Callback executed when a parameter change is detected.
void isCollisionFree(const geometry_msgs::msg::TwistStamped &cmd_vel, const double &angular_distance_to_heading, const geometry_msgs::msg::PoseStamped &pose)
Checks if rotation is safe.
void reset() override
Reset the state of the controller.