Nav2 Navigation Stack - lyrical  lyrical
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 "angles/angles.h"
22 #include "nav2_util/geometry_utils.hpp"
23 #include "nav2_ros_common/node_utils.hpp"
24 #include "nav2_util/robot_utils.hpp"
25 
26 #include "nav2_rotation_shim_controller/nav2_rotation_shim_controller.hpp"
27 #include "nav2_ros_common/tf2_factories.hpp"
28 
29 using rcl_interfaces::msg::ParameterType;
30 
31 namespace nav2_rotation_shim_controller
32 {
33 
35 : lp_loader_("nav2_core", "nav2_core::Controller"),
36  primary_controller_(nullptr),
37  path_updated_(false),
38  in_rotation_(false)
39 {
40 }
41 
43  const nav2::LifecycleNode::WeakPtr & parent,
44  std::string name, nav2::TransformBuffer::SharedPtr tf,
45  std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros)
46 {
47  plugin_name_ = name;
48  node_ = parent;
49  auto node = parent.lock();
50 
51  tf_ = tf;
52  costmap_ros_ = costmap_ros;
53  logger_ = node->get_logger();
54  clock_ = node->get_clock();
55 
56  // Handles storage and dynamic configuration of parameters.
57  // Returns pointer to data current param settings.
58  param_handler_ = std::make_unique<ParameterHandler>(
59  node, plugin_name_, logger_);
60  params_ = param_handler_->getParams();
61 
62  try {
63  primary_controller_ = lp_loader_.createUniqueInstance(params_->primary_controller);
64  RCLCPP_INFO(
65  logger_, "Created internal controller for rotation shimming: %s of type %s",
66  plugin_name_.c_str(), params_->primary_controller.c_str());
67  } catch (const pluginlib::PluginlibException & ex) {
68  RCLCPP_FATAL(
69  logger_,
70  "Failed to create internal controller for rotation shimming. Exception: %s", ex.what());
71  return;
72  }
73 
74  primary_controller_->configure(parent, name + ".primary_controller", tf, costmap_ros);
75 
76  // initialize collision checker and set costmap
77  collision_checker_ = std::make_unique<nav2_costmap_2d::
78  FootprintCollisionChecker<nav2_costmap_2d::Costmap2D *>>(costmap_ros->getCostmap());
79 }
80 
82 {
83  RCLCPP_INFO(
84  logger_,
85  "Activating controller: %s of type "
86  "nav2_rotation_shim_controller::RotationShimController",
87  plugin_name_.c_str());
88 
89  primary_controller_->activate();
90  in_rotation_ = false;
91  last_angular_vel_ = std::numeric_limits<double>::max();
92  param_handler_->activate();
93 }
94 
96 {
97  RCLCPP_INFO(
98  logger_,
99  "Deactivating controller: %s of type "
100  "nav2_rotation_shim_controller::RotationShimController",
101  plugin_name_.c_str());
102 
103  primary_controller_->deactivate();
104  param_handler_->deactivate();
105 }
106 
108 {
109  RCLCPP_INFO(
110  logger_,
111  "Cleaning up controller: %s of type "
112  "nav2_rotation_shim_controller::RotationShimController",
113  plugin_name_.c_str());
114 
115  primary_controller_->cleanup();
116  primary_controller_.reset();
117 }
118 
119 geometry_msgs::msg::TwistStamped RotationShimController::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  current_path_ = transformed_global_plan;
127  path_updated_ = params_->rotate_to_heading_once ? isGoalChanged(global_goal) : true;
128  current_goal_ = global_goal;
129  // Rotate to goal heading when in goal xy tolerance
130  if (params_->rotate_to_goal_heading) {
131  std::lock_guard<std::mutex> lock_reinit(param_handler_->getMutex());
132 
133  try {
134  if (goal_checker->isGoalXYReached(pose.pose, global_goal.pose, velocity,
135  transformed_global_plan))
136  {
137  double pose_yaw = tf2::getYaw(pose.pose.orientation);
138  double goal_yaw = tf2::getYaw(global_goal.pose.orientation);
139 
140  double angular_distance_to_heading = angles::shortest_angular_distance(pose_yaw, goal_yaw);
141 
142  auto cmd_vel = computeRotateToHeadingCommand(angular_distance_to_heading, pose, velocity);
143  last_angular_vel_ = cmd_vel.twist.angular.z;
144  return cmd_vel;
145  }
146  } catch (const std::runtime_error & e) {
147  RCLCPP_INFO(
148  logger_,
149  "Rotation Shim Controller was unable to find a goal point,"
150  " a rotational collision was detected, or TF failed to transform"
151  " into base frame! what(): %s", e.what());
152  }
153  }
154 
155  if (path_updated_) {
156  nav2_costmap_2d::Costmap2D * costmap = costmap_ros_->getCostmap();
157  std::unique_lock<nav2_costmap_2d::Costmap2D::mutex_t> lock(*(costmap->getMutex()));
158 
159  std::lock_guard<std::mutex> lock_reinit(param_handler_->getMutex());
160  try {
161  auto sampled_pt = getSampledPathPt(global_goal);
162  double angular_distance_to_heading;
163  if (params_->use_path_orientations) {
164  angular_distance_to_heading = angles::shortest_angular_distance(
165  tf2::getYaw(pose.pose.orientation),
166  tf2::getYaw(sampled_pt.pose.orientation));
167  } else {
168  geometry_msgs::msg::Pose sampled_pt_base = transformPoseToBaseFrame(sampled_pt);
169  angular_distance_to_heading = std::atan2(
170  sampled_pt_base.position.y,
171  sampled_pt_base.position.x);
172  }
173 
174  double angular_thresh =
175  in_rotation_ ? params_->angular_disengage_threshold : params_->angular_dist_threshold;
176  if (abs(angular_distance_to_heading) > angular_thresh) {
177  RCLCPP_DEBUG(
178  logger_,
179  "Robot is not within the new path's rough heading, rotating to heading...");
180  in_rotation_ = true;
181  auto cmd_vel = computeRotateToHeadingCommand(angular_distance_to_heading, pose, velocity);
182  last_angular_vel_ = cmd_vel.twist.angular.z;
183  return cmd_vel;
184  } else {
185  RCLCPP_DEBUG(
186  logger_,
187  "Robot is at the new path's rough heading, passing to controller");
188  path_updated_ = false;
189  }
190  } catch (const std::runtime_error & e) {
191  RCLCPP_DEBUG(
192  logger_,
193  "Rotation Shim Controller was unable to find a sampling point,"
194  " a rotational collision was detected, or TF failed to transform"
195  " into base frame! what(): %s", e.what());
196  path_updated_ = false;
197  }
198  }
199 
200  // If at this point, use the primary controller to path track
201  in_rotation_ = false;
202  auto cmd_vel = primary_controller_->computeVelocityCommands(pose, velocity, goal_checker,
203  transformed_global_plan, global_goal);
204  last_angular_vel_ = cmd_vel.twist.angular.z;
205  return cmd_vel;
206 }
207 
208 geometry_msgs::msg::PoseStamped RotationShimController::getSampledPathPt(
209  const geometry_msgs::msg::PoseStamped & global_goal)
210 {
211  if (current_path_.poses.size() < 2) {
213  "Path is too short to find a valid sampled path point for rotation.");
214  }
215 
216  geometry_msgs::msg::Pose start = current_path_.poses.front().pose;
217  double dx, dy;
218 
219  // Find the first point at least sampling distance away
220  for (unsigned int i = 1; i != current_path_.poses.size(); i++) {
221  dx = current_path_.poses[i].pose.position.x - start.position.x;
222  dy = current_path_.poses[i].pose.position.y - start.position.y;
223  if (hypot(dx, dy) >= params_->forward_sampling_distance) {
224  current_path_.poses[i].header.frame_id = current_path_.header.frame_id;
225  // Get current time transformation
226  current_path_.poses[i].header.stamp = clock_->now();
227  return current_path_.poses[i];
228  }
229  }
230 
231  auto goal = current_path_.poses.back();
232  goal.header.frame_id = current_path_.header.frame_id;
233  goal.header.stamp = clock_->now();
234  double gx = global_goal.pose.position.x - goal.pose.position.x;
235  double gy = global_goal.pose.position.y - goal.pose.position.y;
236  double distance = hypot(gx, gy);
237  if (distance > 1e-4) {
238  RCLCPP_WARN(logger_,
239  "The last pose of the local plan is %.2f m away from the global goal", hypot(gx, gy));
240  }
241  return goal;
242 }
243 
244 geometry_msgs::msg::Pose
245 RotationShimController::transformPoseToBaseFrame(const geometry_msgs::msg::PoseStamped & pt)
246 {
247  geometry_msgs::msg::PoseStamped pt_base;
248  if (!nav2_util::transformPoseInTargetFrame(pt, pt_base, *tf_, costmap_ros_->getBaseFrameID(),
249  costmap_ros_->getTransformTolerance()))
250  {
251  throw nav2_core::ControllerTFError("Failed to transform pose to base frame!");
252  }
253  return pt_base.pose;
254 }
255 
256 geometry_msgs::msg::TwistStamped
258  const double & angular_distance_to_heading,
259  const geometry_msgs::msg::PoseStamped & pose,
260  const geometry_msgs::msg::Twist & velocity)
261 {
262  auto current = params_->closed_loop ? velocity.angular.z : last_angular_vel_;
263  if (current == std::numeric_limits<double>::max()) {
264  current = 0.0;
265  }
266 
267  geometry_msgs::msg::TwistStamped cmd_vel;
268  cmd_vel.header = pose.header;
269  const double sign = angular_distance_to_heading > 0.0 ? 1.0 : -1.0;
270  const double angular_vel = sign * params_->rotate_to_heading_angular_vel;
271  const double & dt = params_->control_duration;
272  const double min_feasible_angular_speed = current - params_->max_angular_accel * dt;
273  const double max_feasible_angular_speed = current + params_->max_angular_accel * dt;
274  cmd_vel.twist.angular.z =
275  std::clamp(angular_vel, min_feasible_angular_speed, max_feasible_angular_speed);
276 
277  // Check if we need to slow down to avoid overshooting
278  double max_vel_to_stop = std::sqrt(2 * params_->max_angular_accel *
279  fabs(angular_distance_to_heading));
280  if (fabs(cmd_vel.twist.angular.z) > max_vel_to_stop) {
281  cmd_vel.twist.angular.z = sign * max_vel_to_stop;
282  }
283 
284  isCollisionFree(cmd_vel, angular_distance_to_heading, pose);
285  return cmd_vel;
286 }
287 
289  const geometry_msgs::msg::TwistStamped & cmd_vel,
290  const double & angular_distance_to_heading,
291  const geometry_msgs::msg::PoseStamped & pose)
292 {
293  // Simulate rotation ahead by time in control frequency increments
294  double simulated_time = 0.0;
295  double initial_yaw = tf2::getYaw(pose.pose.orientation);
296  double yaw = 0.0;
297  double footprint_cost = 0.0;
298  double remaining_rotation_before_thresh =
299  fabs(angular_distance_to_heading) - params_->angular_dist_threshold;
300 
301  while (simulated_time < params_->simulate_ahead_time) {
302  simulated_time += params_->control_duration;
303  yaw = initial_yaw + cmd_vel.twist.angular.z * simulated_time;
304 
305  // Stop simulating past the point it would be passed onto the primary controller
306  if (angles::shortest_angular_distance(yaw, initial_yaw) >= remaining_rotation_before_thresh) {
307  break;
308  }
309 
310  using namespace nav2_costmap_2d; // NOLINT
311  footprint_cost = collision_checker_->footprintCostAtPose(
312  pose.pose.position.x, pose.pose.position.y,
313  yaw, costmap_ros_->getRobotFootprint());
314 
315  if (footprint_cost == static_cast<double>(NO_INFORMATION) &&
316  costmap_ros_->getLayeredCostmap()->isTrackingUnknown())
317  {
319  "RotationShimController detected a potential collision ahead!");
320  }
321 
322  if (footprint_cost >= params_->max_cost_threshold) {
323  throw nav2_core::NoValidControl("RotationShimController detected collision ahead!");
324  }
325  }
326 }
327 
328 bool RotationShimController::isGoalChanged(const geometry_msgs::msg::PoseStamped & goal)
329 {
330  // Return true if rotating or if the goal pose is empty
331  if (in_rotation_ || current_goal_ == geometry_msgs::msg::PoseStamped()) {
332  return true;
333  }
334 
335  // Check if the last goal pose and the new goal pose differ
336  return current_goal_ != goal;
337 }
338 
339 void RotationShimController::newPathReceived(const nav_msgs::msg::Path & raw_global_path)
340 {
341  primary_controller_->newPathReceived(raw_global_path);
342 }
343 
344 void RotationShimController::setSpeedLimit(const double & speed_limit, const bool & percentage)
345 {
346  primary_controller_->setSpeedLimit(speed_limit, percentage);
347 }
348 
350 {
351  last_angular_vel_ = std::numeric_limits<double>::max();
352  primary_controller_->reset();
353 }
354 
355 } // namespace nav2_rotation_shim_controller
356 
357 // Register this controller as a nav2_core plugin
358 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...
A 2D costmap provides a mapping between points in the world and their associated "costs".
Definition: costmap_2d.hpp:69
Rotate to rough path heading controller shim plugin.
void deactivate() override
Deactivate controller state machine.
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.
geometry_msgs::msg::PoseStamped getSampledPathPt(const geometry_msgs::msg::PoseStamped &global_goal)
Finds the point on the path that is roughly the sampling point distance away from the robot for use....
void cleanup() override
Cleanup controller state machine.
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 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.
bool isGoalChanged(const geometry_msgs::msg::PoseStamped &goal)
Checks if the goal has changed based on the given path.
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 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.
geometry_msgs::msg::TwistStamped computeVelocityCommands(const geometry_msgs::msg::PoseStamped &pose, const geometry_msgs::msg::Twist &velocity, nav2_core::GoalChecker *, 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 newPathReceived(const nav_msgs::msg::Path &raw_global_path) override
nav2_core newPathReceived - Receives a new plan from the Planner Server
void reset() override
Reset the state of the controller.