Nav2 Navigation Stack - rolling  main
ROS 2 Navigation Stack
following_server.cpp
1 // Copyright (c) 2024 Open Navigation LLC
2 // Copyright (c) 2024 Alberto J. Tudela Roldán
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 
16 #include "angles/angles.h"
17 #include "opennav_docking_core/docking_exceptions.hpp"
18 #include "opennav_following/following_server.hpp"
19 #include "nav2_util/geometry_utils.hpp"
20 #include "nav2_util/robot_utils.hpp"
21 #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
22 #include "tf2/utils.hpp"
23 
24 using namespace std::chrono_literals;
25 using rcl_interfaces::msg::ParameterType;
26 using std::placeholders::_1;
27 
28 namespace opennav_following
29 {
30 
31 FollowingServer::FollowingServer(const rclcpp::NodeOptions & options)
32 : nav2::LifecycleNode("following_server", "", options)
33 {
34  RCLCPP_INFO(get_logger(), "Creating %s", get_name());
35 
36  declare_parameter("controller_frequency", 50.0);
37  declare_parameter("detection_timeout", 2.0);
38  declare_parameter("rotate_to_object_timeout", 10.0);
39  declare_parameter("static_object_timeout", -1.0);
40  declare_parameter("linear_tolerance", 0.15);
41  declare_parameter("angular_tolerance", 0.15);
42  declare_parameter("max_retries", 3);
43  declare_parameter("base_frame", "base_link");
44  declare_parameter("fixed_frame", "odom");
45  declare_parameter("filter_coef", 0.1);
46  declare_parameter("desired_distance", 1.0);
47  declare_parameter("skip_orientation", true);
48  declare_parameter("search_by_rotating", false);
49  declare_parameter("search_angle", M_PI_2);
50  declare_parameter("odom_topic", "odom");
51  declare_parameter("odom_duration", 0.3);
52  declare_parameter("transform_tolerance", 0.1);
53 }
54 
55 nav2::CallbackReturn
56 FollowingServer::on_configure(const rclcpp_lifecycle::State & /*state*/)
57 {
58  RCLCPP_INFO(get_logger(), "Configuring %s", get_name());
59  auto node = shared_from_this();
60 
61  get_parameter("controller_frequency", controller_frequency_);
62  get_parameter("detection_timeout", detection_timeout_);
63  get_parameter("rotate_to_object_timeout", rotate_to_object_timeout_);
64  get_parameter("static_object_timeout", static_object_timeout_);
65  get_parameter("linear_tolerance", linear_tolerance_);
66  get_parameter("angular_tolerance", angular_tolerance_);
67  get_parameter("max_retries", max_retries_);
68  get_parameter("base_frame", base_frame_);
69  get_parameter("fixed_frame", fixed_frame_);
70  get_parameter("desired_distance", desired_distance_);
71  get_parameter("skip_orientation", skip_orientation_);
72  get_parameter("search_by_rotating", search_by_rotating_);
73  get_parameter("search_angle", search_angle_);
74  get_parameter("transform_tolerance", transform_tolerance_);
75  RCLCPP_INFO(get_logger(), "Controller frequency set to %.4fHz", controller_frequency_);
76 
77  vel_publisher_ = std::make_unique<nav2_util::TwistPublisher>(node, "cmd_vel");
78  tf2_buffer_ = std::make_shared<tf2_ros::Buffer>(node->get_clock());
79 
80  // Create odom subscriber for backward blind docking
81  std::string odom_topic;
82  get_parameter("odom_topic", odom_topic);
83  double odom_duration;
84  get_parameter("odom_duration", odom_duration);
85  odom_sub_ = std::make_unique<nav2_util::OdomSmoother>(node, odom_duration, odom_topic);
86 
87  // Create the action server for dynamic following
88  following_action_server_ = node->create_action_server<FollowObject>(
89  "follow_object",
90  std::bind(&FollowingServer::followObject, this),
91  nullptr, std::chrono::milliseconds(500),
92  true);
93 
94  // Create the controller
95  // Note: Collision detection is not supported in following server so we force it off
96  // and warn if the user has it enabled (from launch file or parameter file)
97  declare_parameter("controller.use_collision_detection", false);
98  controller_ =
99  std::make_unique<opennav_docking::Controller>(node, tf2_buffer_, fixed_frame_, base_frame_);
100 
101  auto get_use_collision_detection = false;
102  get_parameter("controller.use_collision_detection", get_use_collision_detection);
103  if (get_use_collision_detection) {
104  RCLCPP_ERROR(get_logger(),
105  "Collision detection is not supported in the following server. Please disable "
106  "the controller.use_collision_detection parameter.");
107  return nav2::CallbackReturn::FAILURE;
108  }
109 
110  // Setup filter
111  double filter_coef;
112  get_parameter("filter_coef", filter_coef);
113  filter_ = std::make_unique<opennav_docking::PoseFilter>(filter_coef, detection_timeout_);
114 
115  // And publish the filtered pose
116  filtered_dynamic_pose_pub_ =
117  create_publisher<geometry_msgs::msg::PoseStamped>("filtered_dynamic_pose");
118 
119  // Initialize static object detection variables
120  static_timer_initialized_ = false;
121  static_object_start_time_ = rclcpp::Time(0);
122 
123  return nav2::CallbackReturn::SUCCESS;
124 }
125 
126 nav2::CallbackReturn
127 FollowingServer::on_activate(const rclcpp_lifecycle::State & /*state*/)
128 {
129  RCLCPP_INFO(get_logger(), "Activating %s", get_name());
130 
131  auto node = shared_from_this();
132 
133  tf2_listener_ = std::make_unique<tf2_ros::TransformListener>(*tf2_buffer_, this, true);
134  vel_publisher_->on_activate();
135  filtered_dynamic_pose_pub_->on_activate();
136  following_action_server_->activate();
137 
138  // Add callback for dynamic parameters
139  dyn_params_handler_ = node->add_on_set_parameters_callback(
140  std::bind(&FollowingServer::dynamicParametersCallback, this, _1));
141 
142  // Create bond connection
143  createBond();
144 
145  return nav2::CallbackReturn::SUCCESS;
146 }
147 
148 nav2::CallbackReturn
149 FollowingServer::on_deactivate(const rclcpp_lifecycle::State & /*state*/)
150 {
151  RCLCPP_INFO(get_logger(), "Deactivating %s", get_name());
152 
153  following_action_server_->deactivate();
154  vel_publisher_->on_deactivate();
155  filtered_dynamic_pose_pub_->on_deactivate();
156 
157  remove_on_set_parameters_callback(dyn_params_handler_.get());
158  dyn_params_handler_.reset();
159  tf2_listener_.reset();
160 
161  // Destroy bond connection
162  destroyBond();
163 
164  return nav2::CallbackReturn::SUCCESS;
165 }
166 
167 nav2::CallbackReturn
168 FollowingServer::on_cleanup(const rclcpp_lifecycle::State & /*state*/)
169 {
170  RCLCPP_INFO(get_logger(), "Cleaning up %s", get_name());
171  tf2_buffer_.reset();
172  following_action_server_.reset();
173  controller_.reset();
174  vel_publisher_.reset();
175  filtered_dynamic_pose_pub_.reset();
176  odom_sub_.reset();
177  return nav2::CallbackReturn::SUCCESS;
178 }
179 
180 nav2::CallbackReturn
181 FollowingServer::on_shutdown(const rclcpp_lifecycle::State &)
182 {
183  RCLCPP_INFO(get_logger(), "Shutting down %s", get_name());
184  return nav2::CallbackReturn::SUCCESS;
185 }
186 
187 template<typename ActionT>
189  typename std::shared_ptr<const typename ActionT::Goal> goal,
190  const typename nav2::SimpleActionServer<ActionT>::SharedPtr & action_server)
191 {
192  if (action_server->is_preempt_requested()) {
193  goal = action_server->accept_pending_goal();
194  }
195 }
196 
197 template<typename ActionT>
199  typename nav2::SimpleActionServer<ActionT>::SharedPtr & action_server,
200  const std::string & name)
201 {
202  if (action_server->is_cancel_requested()) {
203  RCLCPP_WARN(get_logger(), "Goal was cancelled. Cancelling %s action", name.c_str());
204  return true;
205  }
206  return false;
207 }
208 
209 template<typename ActionT>
211  typename nav2::SimpleActionServer<ActionT>::SharedPtr & action_server,
212  const std::string & name)
213 {
214  if (action_server->is_preempt_requested()) {
215  RCLCPP_WARN(get_logger(), "Goal was preempted. Cancelling %s action", name.c_str());
216  return true;
217  }
218  return false;
219 }
220 
222 {
223  std::unique_lock<std::mutex> lock(dynamic_params_lock_);
224  action_start_time_ = this->now();
225  rclcpp::Rate loop_rate(controller_frequency_);
226 
227  auto goal = following_action_server_->get_current_goal();
228  auto result = std::make_shared<FollowObject::Result>();
229 
230  if (!following_action_server_ || !following_action_server_->is_server_active()) {
231  RCLCPP_DEBUG(get_logger(), "Action server unavailable or inactive. Stopping.");
232  return;
233  }
234 
235  if (checkAndWarnIfCancelled<FollowObject>(following_action_server_, "follow_object")) {
236  following_action_server_->terminate_all();
237  return;
238  }
239 
240  getPreemptedGoalIfRequested<FollowObject>(goal, following_action_server_);
241  num_retries_ = 0;
242  static_timer_initialized_ = false;
243 
244  // Reset the last detected dynamic pose timestamp so we start fresh for this action
245  detected_dynamic_pose_.header.stamp = rclcpp::Time(0);
246 
247  try {
248  auto pose_topic = goal->pose_topic;
249  auto target_frame = goal->tracked_frame;
250  if (target_frame.empty()) {
251  if (pose_topic.empty()) {
252  RCLCPP_ERROR(get_logger(),
253  "Both pose topic and target frame are empty. Cannot follow object.");
254  result->error_code = FollowObject::Result::FAILED_TO_DETECT_OBJECT;
255  result->error_msg = "No pose topic or target frame provided.";
256  following_action_server_->terminate_all(result);
257  return;
258  } else {
259  lock.unlock();
260  RCLCPP_INFO(get_logger(), "Subscribing to pose topic: %s", pose_topic.c_str());
261  dynamic_pose_sub_ = create_subscription<geometry_msgs::msg::PoseStamped>(
262  pose_topic,
263  [this](const geometry_msgs::msg::PoseStamped::SharedPtr pose) {
264  detected_dynamic_pose_ = *pose;
265  },
266  nav2::qos::StandardTopicQoS(1)); // Only want the most recent pose
267  lock.lock();
268  }
269  } else {
270  RCLCPP_INFO(get_logger(), "Following frame: %s instead of pose", target_frame.c_str());
271  }
272 
273  // Following control loop: while not timeout, run controller
274  geometry_msgs::msg::PoseStamped object_pose;
275  rclcpp::Duration max_duration = goal->max_duration;
276  while (rclcpp::ok()) {
277  try {
278  // Check if we have run out of time
279  if (this->now() - action_start_time_ > max_duration && max_duration.seconds() > 0.0) {
280  RCLCPP_INFO(get_logger(), "Exceeded max duration. Stopping.");
281  result->total_elapsed_time = this->now() - action_start_time_;
282  result->num_retries = num_retries_;
284  following_action_server_->succeeded_current(result);
285  dynamic_pose_sub_.reset();
286  return;
287  }
288 
289  // Approach the object using control law
290  if (approachObject(object_pose, target_frame)) {
291  // Initialize static timer on first entry
292  if (!static_timer_initialized_) {
293  static_object_start_time_ = this->now();
294  static_timer_initialized_ = true;
295  }
296 
297  // We have reached the object, maintain position
298  RCLCPP_INFO_THROTTLE(
299  get_logger(), *get_clock(), 1000,
300  "Reached object. Stopping until goal is moved again.");
301  publishFollowingFeedback(FollowObject::Feedback::STOPPING);
303 
304  // Stop if the object has been static for some time
305  if (static_object_timeout_ > 0.0) {
306  auto static_duration = this->now() - static_object_start_time_;
307  if (static_duration.seconds() > static_object_timeout_) {
308  RCLCPP_INFO(get_logger(),
309  "Object has been static for %.2f seconds (timeout: %.2f), stopping.",
310  static_duration.seconds(), static_object_timeout_);
311  result->total_elapsed_time = this->now() - action_start_time_;
312  result->num_retries = num_retries_;
314  following_action_server_->succeeded_current(result);
315  return;
316  }
317  }
318  } else {
319  // Cancelled, preempted, or shutting down (recoverable errors throw DockingException)
320  static_timer_initialized_ = false;
321  result->total_elapsed_time = this->now() - action_start_time_;
323  following_action_server_->terminate_all(result);
324  dynamic_pose_sub_.reset();
325  return;
326  }
328  if (++num_retries_ > max_retries_) {
329  RCLCPP_ERROR(get_logger(), "Failed to follow, all retries have been used");
330  throw;
331  }
332  RCLCPP_WARN(get_logger(), "Following failed, will retry: %s", e.what());
333 
334  // Perform an in-place rotation to find the object again
335  if (search_by_rotating_) {
336  RCLCPP_INFO(get_logger(), "Rotating to find object again");
337  if (!rotateToObject(object_pose, target_frame)) {
338  // Cancelled, preempted, or shutting down
340  following_action_server_->terminate_all(result);
341  return;
342  }
343  } else {
344  RCLCPP_INFO(get_logger(), "Using last known heading to find object again");
345  }
346  }
347  loop_rate.sleep();
348  }
349  } catch (const tf2::TransformException & e) {
350  result->error_msg = std::string("Transform error: ") + e.what();
351  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
352  result->error_code = FollowObject::Result::TF_ERROR;
354  result->error_msg = e.what();
355  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
356  result->error_code = FollowObject::Result::FAILED_TO_DETECT_OBJECT;
358  result->error_msg = e.what();
359  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
360  result->error_code = FollowObject::Result::FAILED_TO_CONTROL;
362  result->error_msg = e.what();
363  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
364  result->error_code = FollowObject::Result::UNKNOWN;
365  } catch (std::exception & e) {
366  result->error_msg = e.what();
367  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
368  result->error_code = FollowObject::Result::UNKNOWN;
369  }
370 
371  // Stop the robot and report
372  result->total_elapsed_time = this->now() - action_start_time_;
373  result->num_retries = num_retries_;
375  following_action_server_->terminate_current(result);
376  dynamic_pose_sub_.reset();
377 }
378 
380  geometry_msgs::msg::PoseStamped & object_pose, const std::string & target_frame)
381 {
382  rclcpp::Rate loop_rate(controller_frequency_);
383  while (rclcpp::ok()) {
384  // Update the iteration start time, used for get robot position, transformation and control
385  iteration_start_time_ = this->now();
386 
387  publishFollowingFeedback(FollowObject::Feedback::CONTROLLING);
388 
389  // Stop if cancelled/preempted
390  if (checkAndWarnIfCancelled<FollowObject>(following_action_server_, "follow_object") ||
391  checkAndWarnIfPreempted<FollowObject>(following_action_server_, "follow_object"))
392  {
393  return false;
394  }
395 
396  // Get the tracking pose from topic or frame
397  getTrackingPose(object_pose, target_frame);
398 
399  // Get the pose at the distance we want to maintain from the object
400  // Stop and report success if goal is reached
401  auto target_pose = getPoseAtDistance(object_pose, desired_distance_);
402  if (isGoalReached(target_pose)) {
403  return true;
404  }
405 
406  // The control law can get jittery when close to the end when atan2's can explode.
407  // Thus, we reduce the desired distance by a small amount so that the robot never
408  // gets to the end of the spiral before its at the desired distance to stop the
409  // following procedure.
410  const double backward_projection = 0.25;
411  const double effective_distance = desired_distance_ - backward_projection;
412  target_pose = getPoseAtDistance(object_pose, effective_distance);
413 
414  // ... and transform the target_pose into base_frame
415  try {
416  tf2_buffer_->transform(
417  target_pose, target_pose, base_frame_, tf2::durationFromSec(transform_tolerance_));
418  } catch (const tf2::TransformException & ex) {
419  RCLCPP_WARN(get_logger(), "Failed to transform target pose: %s", ex.what());
420  return false;
421  }
422 
423  // If the object is behind the robot, we reverse the control
424  geometry_msgs::msg::PoseStamped robot_pose;
425  if (!nav2_util::getCurrentPose(
426  robot_pose, *tf2_buffer_, target_pose.header.frame_id, base_frame_, transform_tolerance_,
427  iteration_start_time_))
428  {
429  RCLCPP_WARN(get_logger(), "Failed to get current robot pose");
430  return false;
431  }
432 
433  // Compute and publish controls
434  auto command = std::make_unique<geometry_msgs::msg::TwistStamped>();
435  command->header.stamp = now();
436  if (!controller_->computeVelocityCommand(target_pose.pose, command->twist, true, false)) {
437  throw opennav_docking_core::FailedToControl("Failed to get control");
438  }
439  vel_publisher_->publish(std::move(command));
440 
441  loop_rate.sleep();
442  }
443  return false;
444 }
445 
447  geometry_msgs::msg::PoseStamped & object_pose, const std::string & target_frame)
448 {
449  const double dt = 1.0 / controller_frequency_;
450 
451  // Compute initial robot heading
452  geometry_msgs::msg::PoseStamped robot_pose;
453  if (!nav2_util::getCurrentPose(
454  robot_pose, *tf2_buffer_, object_pose.header.frame_id, base_frame_, transform_tolerance_,
455  iteration_start_time_))
456  {
457  RCLCPP_WARN(get_logger(), "Failed to get current robot pose");
458  return false;
459  }
460  double initial_yaw = tf2::getYaw(robot_pose.pose.orientation);
461 
462  // Search angles: left offset, then right offset from initial heading
463  std::vector<double> angles = {initial_yaw + search_angle_, initial_yaw - search_angle_};
464 
465  rclcpp::Rate loop_rate(controller_frequency_);
466  auto start = this->now();
467  auto timeout = rclcpp::Duration::from_seconds(rotate_to_object_timeout_);
468 
469  // Iterate over target angles
470  for (const double & target_angle : angles) {
471  // Create a target pose oriented at target_angle
472  auto target_pose = object_pose;
473  target_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(target_angle);
474 
475  // Rotate towards target_angle while checking for detection
476  while (rclcpp::ok()) {
477  // Update the iteration start time, used for get robot position, transformation and control
478  iteration_start_time_ = this->now();
479 
480  publishFollowingFeedback(FollowObject::Feedback::RETRY);
481 
482  // Stop if cancelled/preempted
483  if (checkAndWarnIfCancelled<FollowObject>(following_action_server_, "follow_object") ||
484  checkAndWarnIfPreempted<FollowObject>(following_action_server_, "follow_object"))
485  {
486  return false;
487  }
488 
489  // Get current robot pose
490  if (!nav2_util::getCurrentPose(
491  robot_pose, *tf2_buffer_, object_pose.header.frame_id, base_frame_, transform_tolerance_,
492  iteration_start_time_))
493  {
494  RCLCPP_WARN(get_logger(), "Failed to get current robot pose");
495  return false;
496  }
497 
498  double angular_distance_to_heading = angles::shortest_angular_distance(
499  tf2::getYaw(robot_pose.pose.orientation), target_angle);
500 
501  // If we are close enough to the target orientation, break and try next angle
502  if (fabs(angular_distance_to_heading) < angular_tolerance_) {
503  break;
504  }
505 
506  // While rotating, check if we can get the tracking pose (object detected)
507  try {
508  if (getTrackingPose(object_pose, target_frame)) {
509  return true;
510  }
512  // No detection yet, continue rotating
513  }
514 
515  geometry_msgs::msg::Twist current_vel;
516  current_vel.angular.z = odom_sub_->getRawTwist().angular.z;
517 
518  auto command = std::make_unique<geometry_msgs::msg::TwistStamped>();
519  command->header = robot_pose.header;
520  command->twist = controller_->computeRotateToHeadingCommand(
521  angular_distance_to_heading, current_vel, dt);
522 
523  vel_publisher_->publish(std::move(command));
524 
525  if (this->now() - start > timeout) {
526  throw opennav_docking_core::FailedToControl("Timed out rotating to object");
527  }
528 
529  loop_rate.sleep();
530  }
531  }
532 
533  // If we exhausted all search angles and did not detect the object, fail
534  throw opennav_docking_core::FailedToControl("Failed to rotate to object");
535 }
536 
538 {
539  auto cmd_vel = std::make_unique<geometry_msgs::msg::TwistStamped>();
540  cmd_vel->header.frame_id = base_frame_;
541  cmd_vel->header.stamp = now();
542  vel_publisher_->publish(std::move(cmd_vel));
543 }
544 
546 {
547  auto feedback = std::make_shared<FollowObject::Feedback>();
548  feedback->state = state;
549  feedback->following_time = iteration_start_time_ - action_start_time_;
550  feedback->num_retries = num_retries_;
551  following_action_server_->publish_feedback(feedback);
552 }
553 
554 bool FollowingServer::getRefinedPose(geometry_msgs::msg::PoseStamped & pose)
555 {
556  // Get current detections and transform to frame
557  geometry_msgs::msg::PoseStamped detected = detected_dynamic_pose_;
558 
559  // If we haven't received any detection yet, wait up to detection_timeout_ for one to arrive.
560  if (detected.header.stamp == rclcpp::Time(0)) {
561  auto start = this->now();
562  auto timeout = rclcpp::Duration::from_seconds(detection_timeout_);
563  rclcpp::Rate wait_rate(controller_frequency_);
564  while (this->now() - start < timeout) {
565  // Check if a new detection arrived
566  if (detected_dynamic_pose_.header.stamp != rclcpp::Time(0)) {
567  detected = detected_dynamic_pose_;
568  break;
569  }
570  wait_rate.sleep();
571  }
572  if (detected.header.stamp == rclcpp::Time(0)) {
573  RCLCPP_WARN(this->get_logger(), "No detection received within timeout period");
574  return false;
575  }
576  }
577 
578  // Validate that external pose is new enough
579  auto timeout = rclcpp::Duration::from_seconds(detection_timeout_);
580  if (this->now() - detected.header.stamp > timeout) {
581  RCLCPP_WARN(this->get_logger(), "Lost detection or did not detect: timeout exceeded");
582  return false;
583  }
584 
585  // Transform detected pose into fixed frame
586  if (detected.header.frame_id != fixed_frame_) {
587  try {
588  tf2_buffer_->transform(
589  detected, detected, fixed_frame_, tf2::durationFromSec(transform_tolerance_));
590  } catch (const tf2::TransformException & ex) {
591  RCLCPP_WARN(this->get_logger(), "Failed to transform detected object pose");
592  return false;
593  }
594  }
595 
596  // The control law can oscillate if the orientation in the perception
597  // is not set correctly or has a lot of noise.
598  // Then, we skip the target orientation by pointing it
599  // in the same orientation than the vector from the robot to the object.
600  if (skip_orientation_) {
601  geometry_msgs::msg::PoseStamped robot_pose;
602  if (!nav2_util::getCurrentPose(
603  robot_pose, *tf2_buffer_, detected.header.frame_id, base_frame_, transform_tolerance_,
604  iteration_start_time_))
605  {
606  RCLCPP_WARN(get_logger(), "Failed to get current robot pose");
607  return false;
608  }
609  double dx = detected.pose.position.x - robot_pose.pose.position.x;
610  double dy = detected.pose.position.y - robot_pose.pose.position.y;
611  double angle_to_target = std::atan2(dy, dx);
612  detected.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(angle_to_target);
613  }
614 
615  // Filter the detected pose
616  auto pose_filtered = filter_->update(detected);
617  filtered_dynamic_pose_pub_->publish(pose_filtered);
618 
619  pose = pose_filtered;
620  return true;
621 }
622 
624  geometry_msgs::msg::PoseStamped & pose, const std::string & frame_id)
625 {
626  try {
627  // Get the transform from the target frame to the fixed frame
628  auto transform = tf2_buffer_->lookupTransform(
629  fixed_frame_, frame_id, iteration_start_time_, tf2::durationFromSec(transform_tolerance_));
630 
631  // Convert transform to pose
632  pose.header.frame_id = fixed_frame_;
633  pose.header.stamp = transform.header.stamp;
634  pose.pose.position.x = transform.transform.translation.x;
635  pose.pose.position.y = transform.transform.translation.y;
636  pose.pose.position.z = transform.transform.translation.z;
637  pose.pose.orientation = transform.transform.rotation;
638  } catch (const tf2::TransformException & ex) {
639  RCLCPP_WARN(get_logger(),
640  "Failed to get transform for frame %s: %s", frame_id.c_str(), ex.what());
641  return false;
642  }
643 
644  // Filter the detected pose
645  auto filtered_pose = filter_->update(pose);
646  filtered_dynamic_pose_pub_->publish(filtered_pose);
647 
648  pose = filtered_pose;
649  return true;
650 }
651 
653  geometry_msgs::msg::PoseStamped & pose, const std::string & frame_id)
654 {
655  // Use frame tracking if we have a target frame, otherwise use topic tracking
656  if (!frame_id.empty()) {
657  if (!getFramePose(pose, frame_id)) {
659  "Failed to get pose in target frame: " + frame_id);
660  }
661  } else {
662  // Use the traditional pose detection from topic
663  if (!getRefinedPose(pose)) {
664  throw opennav_docking_core::FailedToDetectDock("Failed object detection");
665  }
666  }
667  return true;
668 }
669 
670 geometry_msgs::msg::PoseStamped FollowingServer::getPoseAtDistance(
671  const geometry_msgs::msg::PoseStamped & pose, double distance)
672 {
673  geometry_msgs::msg::PoseStamped robot_pose;
674  if (!nav2_util::getCurrentPose(
675  robot_pose, *tf2_buffer_, pose.header.frame_id, base_frame_, transform_tolerance_,
676  iteration_start_time_))
677  {
678  RCLCPP_WARN(get_logger(), "Failed to get current robot pose");
679  // Return original pose as fallback
680  return pose;
681  }
682  double dx = pose.pose.position.x - robot_pose.pose.position.x;
683  double dy = pose.pose.position.y - robot_pose.pose.position.y;
684  const double dist = std::hypot(dx, dy);
685  geometry_msgs::msg::PoseStamped forward_pose = pose;
686  forward_pose.pose.position.x -= distance * (dx / dist);
687  forward_pose.pose.position.y -= distance * (dy / dist);
688  return forward_pose;
689 }
690 
691 bool FollowingServer::isGoalReached(const geometry_msgs::msg::PoseStamped & goal_pose)
692 {
693  geometry_msgs::msg::PoseStamped robot_pose;
694  if (!nav2_util::getCurrentPose(
695  robot_pose, *tf2_buffer_, goal_pose.header.frame_id, base_frame_, transform_tolerance_,
696  iteration_start_time_))
697  {
698  RCLCPP_WARN(get_logger(), "Failed to get current robot pose");
699  return false;
700  }
701  const double dist = std::hypot(
702  robot_pose.pose.position.x - goal_pose.pose.position.x,
703  robot_pose.pose.position.y - goal_pose.pose.position.y);
704  const double yaw = angles::shortest_angular_distance(
705  tf2::getYaw(robot_pose.pose.orientation), tf2::getYaw(goal_pose.pose.orientation));
706  return dist < linear_tolerance_ && abs(yaw) < angular_tolerance_;
707 }
708 
709 rcl_interfaces::msg::SetParametersResult
710 FollowingServer::dynamicParametersCallback(std::vector<rclcpp::Parameter> parameters)
711 {
712  std::lock_guard<std::mutex> lock(dynamic_params_lock_);
713 
714  rcl_interfaces::msg::SetParametersResult result;
715  for (auto parameter : parameters) {
716  const auto & type = parameter.get_type();
717  const auto & name = parameter.get_name();
718 
719  if (type == ParameterType::PARAMETER_DOUBLE) {
720  if (name == "controller_frequency") {
721  controller_frequency_ = parameter.as_double();
722  } else if (name == "detection_timeout") {
723  detection_timeout_ = parameter.as_double();
724  } else if (name == "rotate_to_object_timeout") {
725  rotate_to_object_timeout_ = parameter.as_double();
726  } else if (name == "static_object_timeout") {
727  static_object_timeout_ = parameter.as_double();
728  } else if (name == "linear_tolerance") {
729  linear_tolerance_ = parameter.as_double();
730  } else if (name == "angular_tolerance") {
731  angular_tolerance_ = parameter.as_double();
732  } else if (name == "desired_distance") {
733  desired_distance_ = parameter.as_double();
734  } else if (name == "transform_tolerance") {
735  transform_tolerance_ = parameter.as_double();
736  } else if (name == "search_angle") {
737  search_angle_ = parameter.as_double();
738  }
739  } else if (type == ParameterType::PARAMETER_STRING) {
740  if (name == "base_frame") {
741  base_frame_ = parameter.as_string();
742  } else if (name == "fixed_frame") {
743  fixed_frame_ = parameter.as_string();
744  }
745  } else if (type == ParameterType::PARAMETER_BOOL) {
746  if (name == "skip_orientation") {
747  skip_orientation_ = parameter.as_bool();
748  } else if (name == "search_by_rotating") {
749  search_by_rotating_ = parameter.as_bool();
750  }
751  }
752  }
753 
754  result.successful = true;
755  return result;
756 }
757 
758 } // namespace opennav_following
759 
760 #include "rclcpp_components/register_node_macro.hpp"
761 
762 // Register the component with class_loader.
763 // This acts as a sort of entry point, allowing the component to be discoverable when its library
764 // is being loaded into a running process.
765 RCLCPP_COMPONENTS_REGISTER_NODE(opennav_following::FollowingServer)
void destroyBond()
Destroy bond connection to lifecycle manager.
nav2::LifecycleNode::SharedPtr shared_from_this()
Get a shared pointer of this.
void createBond()
Create bond connection to lifecycle manager.
bool is_cancel_requested() const
Whether or not a cancel command has come in.
bool is_preempt_requested() const
Whether the action server has been asked to be preempted with a new goal.
const std::shared_ptr< const typename ActionT::Goal > accept_pending_goal()
Accept pending goals.
A QoS profile for standard reliable topics with a history of 10 messages.
Failed to control into or out of the dock.
Failed to detect the charging dock.
An action server which implements a dynamic following behavior.
bool checkAndWarnIfCancelled(typename nav2::SimpleActionServer< ActionT >::SharedPtr &action_server, const std::string &name)
Checks and logs warning if action canceled.
nav2::CallbackReturn on_shutdown(const rclcpp_lifecycle::State &state) override
Called when in shutdown state.
void followObject()
Main action callback method to complete following request.
virtual bool getFramePose(geometry_msgs::msg::PoseStamped &pose, const std::string &frame_id)
Get the pose of a specific frame in the fixed frame.
virtual bool approachObject(geometry_msgs::msg::PoseStamped &object_pose, const std::string &target_frame=std::string(""))
Use control law and perception to approach the object.
virtual bool getTrackingPose(geometry_msgs::msg::PoseStamped &pose, const std::string &frame_id)
Get the tracking pose based on the current tracking mode.
void publishFollowingFeedback(uint16_t state)
Publish feedback from a following action.
geometry_msgs::msg::PoseStamped getPoseAtDistance(const geometry_msgs::msg::PoseStamped &pose, double distance)
Get the pose at a distance in front of the input pose.
virtual bool getRefinedPose(geometry_msgs::msg::PoseStamped &pose)
Method to obtain the refined dynamic pose.
nav2::CallbackReturn on_deactivate(const rclcpp_lifecycle::State &state) override
Deactivate member variables.
void publishZeroVelocity()
Publish zero velocity at terminal condition.
void getPreemptedGoalIfRequested(typename std::shared_ptr< const typename ActionT::Goal > goal, const typename nav2::SimpleActionServer< ActionT >::SharedPtr &action_server)
Gets a preempted goal if immediately requested.
bool isGoalReached(const geometry_msgs::msg::PoseStamped &goal_pose)
Check if the goal has been reached.
rcl_interfaces::msg::SetParametersResult dynamicParametersCallback(std::vector< rclcpp::Parameter > parameters)
Callback executed when a parameter change is detected.
virtual bool rotateToObject(geometry_msgs::msg::PoseStamped &object_pose, const std::string &target_frame=std::string(""))
Rotate the robot to find the object again.
bool checkAndWarnIfPreempted(typename nav2::SimpleActionServer< ActionT >::SharedPtr &action_server, const std::string &name)
Checks and logs warning if action preempted.
nav2::CallbackReturn on_activate(const rclcpp_lifecycle::State &state) override
Activate member variables.
nav2::CallbackReturn on_configure(const rclcpp_lifecycle::State &state) override
Configure member variables.
nav2::CallbackReturn on_cleanup(const rclcpp_lifecycle::State &state) override
Reset member variables.