Nav2 Navigation Stack - rolling  main
ROS 2 Navigation Stack
docking_server.cpp
1 // Copyright (c) 2024 Open Navigation LLC
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 "angles/angles.h"
16 #include "opennav_docking/docking_server.hpp"
17 #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
18 #include "tf2/utils.hpp"
19 
20 using namespace std::chrono_literals;
21 using rcl_interfaces::msg::ParameterType;
22 using std::placeholders::_1;
23 
24 namespace opennav_docking
25 {
26 
27 DockingServer::DockingServer(const rclcpp::NodeOptions & options)
28 : nav2::LifecycleNode("docking_server", "", options)
29 {
30  RCLCPP_INFO(get_logger(), "Creating %s", get_name());
31 
32  declare_parameter("controller_frequency", 50.0);
33  declare_parameter("initial_perception_timeout", 5.0);
34  declare_parameter("wait_charge_timeout", 5.0);
35  declare_parameter("dock_approach_timeout", 30.0);
36  declare_parameter("rotate_to_dock_timeout", 10.0);
37  declare_parameter("undock_linear_tolerance", 0.05);
38  declare_parameter("undock_angular_tolerance", 0.05);
39  declare_parameter("max_retries", 3);
40  declare_parameter("base_frame", "base_link");
41  declare_parameter("fixed_frame", "odom");
42  declare_parameter("dock_backwards", rclcpp::PARAMETER_BOOL);
43  declare_parameter("dock_prestaging_tolerance", 0.5);
44  declare_parameter("odom_topic", "odom");
45  declare_parameter("odom_duration", 0.3);
46  declare_parameter("rotation_angular_tolerance", 0.05);
47 }
48 
49 nav2::CallbackReturn
50 DockingServer::on_configure(const rclcpp_lifecycle::State & state)
51 {
52  RCLCPP_INFO(get_logger(), "Configuring %s", get_name());
53  auto node = shared_from_this();
54 
55  get_parameter("controller_frequency", controller_frequency_);
56  get_parameter("initial_perception_timeout", initial_perception_timeout_);
57  get_parameter("wait_charge_timeout", wait_charge_timeout_);
58  get_parameter("dock_approach_timeout", dock_approach_timeout_);
59  get_parameter("rotate_to_dock_timeout", rotate_to_dock_timeout_);
60  get_parameter("undock_linear_tolerance", undock_linear_tolerance_);
61  get_parameter("undock_angular_tolerance", undock_angular_tolerance_);
62  get_parameter("max_retries", max_retries_);
63  get_parameter("base_frame", base_frame_);
64  get_parameter("fixed_frame", fixed_frame_);
65  get_parameter("dock_prestaging_tolerance", dock_prestaging_tolerance_);
66  get_parameter("rotation_angular_tolerance", rotation_angular_tolerance_);
67 
68  RCLCPP_INFO(get_logger(), "Controller frequency set to %.4fHz", controller_frequency_);
69 
70  // Check the dock_backwards deprecated parameter
71  bool dock_backwards = false;
72  try {
73  if (get_parameter("dock_backwards", dock_backwards)) {
74  dock_backwards_ = dock_backwards;
75  RCLCPP_WARN(get_logger(), "Parameter dock_backwards is deprecated. "
76  "Please use the dock_direction parameter in your dock plugin instead.");
77  }
78  } catch (rclcpp::exceptions::ParameterUninitializedException & ex) {
79  }
80 
81  vel_publisher_ = std::make_unique<nav2_util::TwistPublisher>(node, "cmd_vel");
82  tf2_buffer_ = std::make_shared<tf2_ros::Buffer>(node->get_clock());
83 
84  // Create odom subscriber for backward blind docking
85  std::string odom_topic;
86  get_parameter("odom_topic", odom_topic);
87  double odom_duration;
88  get_parameter("odom_duration", odom_duration);
89  odom_sub_ = std::make_unique<nav2_util::OdomSmoother>(node, odom_duration, odom_topic);
90 
91  // Create the action servers for dock / undock
92  docking_action_server_ = node->create_action_server<DockRobot>(
93  "dock_robot",
94  std::bind(&DockingServer::dockRobot, this),
95  nullptr, std::chrono::milliseconds(500),
96  true);
97 
98  undocking_action_server_ = node->create_action_server<UndockRobot>(
99  "undock_robot",
100  std::bind(&DockingServer::undockRobot, this),
101  nullptr, std::chrono::milliseconds(500),
102  true);
103 
104  // Create composed utilities
105  mutex_ = std::make_shared<std::mutex>();
106  controller_ = std::make_unique<Controller>(node, tf2_buffer_, fixed_frame_, base_frame_);
107  navigator_ = std::make_unique<Navigator>(node);
108  dock_db_ = std::make_unique<DockDatabase>(mutex_);
109  if (!dock_db_->initialize(node, tf2_buffer_)) {
110  on_cleanup(state);
111  return nav2::CallbackReturn::FAILURE;
112  }
113 
114  return nav2::CallbackReturn::SUCCESS;
115 }
116 
117 nav2::CallbackReturn
118 DockingServer::on_activate(const rclcpp_lifecycle::State & /*state*/)
119 {
120  RCLCPP_INFO(get_logger(), "Activating %s", get_name());
121 
122  auto node = shared_from_this();
123 
124  tf2_listener_ = std::make_unique<tf2_ros::TransformListener>(*tf2_buffer_, this, true);
125  dock_db_->activate();
126  navigator_->activate();
127  vel_publisher_->on_activate();
128  docking_action_server_->activate();
129  undocking_action_server_->activate();
130  curr_dock_type_.clear();
131 
132  // Add callback for dynamic parameters
133  dyn_params_handler_ = node->add_on_set_parameters_callback(
134  std::bind(&DockingServer::dynamicParametersCallback, this, _1));
135 
136  // Create bond connection
137  createBond();
138 
139  return nav2::CallbackReturn::SUCCESS;
140 }
141 
142 nav2::CallbackReturn
143 DockingServer::on_deactivate(const rclcpp_lifecycle::State & /*state*/)
144 {
145  RCLCPP_INFO(get_logger(), "Deactivating %s", get_name());
146 
147  docking_action_server_->deactivate();
148  undocking_action_server_->deactivate();
149  dock_db_->deactivate();
150  navigator_->deactivate();
151  vel_publisher_->on_deactivate();
152 
153  remove_on_set_parameters_callback(dyn_params_handler_.get());
154  dyn_params_handler_.reset();
155  tf2_listener_.reset();
156 
157  // Destroy bond connection
158  destroyBond();
159 
160  return nav2::CallbackReturn::SUCCESS;
161 }
162 
163 nav2::CallbackReturn
164 DockingServer::on_cleanup(const rclcpp_lifecycle::State & /*state*/)
165 {
166  RCLCPP_INFO(get_logger(), "Cleaning up %s", get_name());
167  tf2_buffer_.reset();
168  docking_action_server_.reset();
169  undocking_action_server_.reset();
170  dock_db_.reset();
171  navigator_.reset();
172  curr_dock_type_.clear();
173  controller_.reset();
174  vel_publisher_.reset();
175  dock_backwards_.reset();
176  odom_sub_.reset();
177  return nav2::CallbackReturn::SUCCESS;
178 }
179 
180 nav2::CallbackReturn
181 DockingServer::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::lock_guard<std::mutex> lock(*mutex_);
224  action_start_time_ = this->now();
225  rclcpp::Rate loop_rate(controller_frequency_);
226 
227  auto goal = docking_action_server_->get_current_goal();
228  auto result = std::make_shared<DockRobot::Result>();
229  result->success = false;
230 
231  if (!docking_action_server_ || !docking_action_server_->is_server_active()) {
232  RCLCPP_DEBUG(get_logger(), "Action server unavailable or inactive. Stopping.");
233  return;
234  }
235 
236  if (checkAndWarnIfCancelled<DockRobot>(docking_action_server_, "dock_robot")) {
237  docking_action_server_->terminate_all();
238  return;
239  }
240 
241  getPreemptedGoalIfRequested<DockRobot>(goal, docking_action_server_);
242  Dock * dock{nullptr};
243  num_retries_ = 0;
244 
245  try {
246  // Get dock (instance and plugin information) from request
247  if (goal->use_dock_id) {
248  RCLCPP_INFO(
249  get_logger(),
250  "Attempting to dock robot at %s.", goal->dock_id.c_str());
251  dock = dock_db_->findDock(goal->dock_id);
252  } else {
253  RCLCPP_INFO(
254  get_logger(),
255  "Attempting to dock robot at position (%0.2f, %0.2f).",
256  goal->dock_pose.pose.position.x, goal->dock_pose.pose.position.y);
257  dock = generateGoalDock(goal);
258  }
259 
260  // Check if robot is docked or charging before proceeding, only applicable to charging docks
261  if (dock->plugin->isCharger() && (dock->plugin->isDocked() || dock->plugin->isCharging())) {
262  RCLCPP_INFO(
263  get_logger(), "Robot is already docked and/or charging (if applicable), no need to dock");
264  result->success = true;
265  docking_action_server_->succeeded_current(result);
266  return;
267  }
268 
269  // Send robot to its staging pose
270  publishDockingFeedback(DockRobot::Feedback::NAV_TO_STAGING_POSE);
271  const auto initial_staging_pose = dock->getStagingPose();
272  const auto robot_pose = getRobotPoseInFrame(initial_staging_pose.header.frame_id);
273  if (!goal->navigate_to_staging_pose ||
274  utils::l2Norm(robot_pose.pose, initial_staging_pose.pose) < dock_prestaging_tolerance_)
275  {
276  RCLCPP_INFO(get_logger(), "Robot already within pre-staging pose tolerance for dock");
277  } else {
278  std::function<bool()> isPreempted = [this]() {
279  return checkAndWarnIfCancelled<DockRobot>(docking_action_server_, "dock_robot") ||
280  checkAndWarnIfPreempted<DockRobot>(docking_action_server_, "dock_robot");
281  };
282 
283  navigator_->goToPose(
284  initial_staging_pose,
285  rclcpp::Duration::from_seconds(goal->max_staging_time),
286  isPreempted);
287  RCLCPP_INFO(get_logger(), "Successful navigation to staging pose");
288  }
289 
290  // Construct initial estimate of where the dock is located in fixed_frame
291  auto dock_pose = utils::getDockPoseStamped(dock, rclcpp::Time(0));
292  tf2_buffer_->transform(dock_pose, dock_pose, fixed_frame_);
293 
294  // Get initial detection of dock before proceeding to move
295  doInitialPerception(dock, dock_pose);
296  RCLCPP_INFO(get_logger(), "Successful initial dock detection");
297 
298  // Get the direction of the movement
299  bool dock_backward = dock_backwards_.has_value() ?
300  dock_backwards_.value() :
301  (dock->plugin->getDockDirection() == opennav_docking_core::DockDirection::BACKWARD);
302 
303  // If we performed a rotation before docking backward, we must rotate the staging pose
304  // to match the robot orientation
305  auto staging_pose = dock->getStagingPose();
306  if (dock->plugin->shouldRotateToDock()) {
307  staging_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(
308  tf2::getYaw(staging_pose.pose.orientation) + M_PI);
309  }
310 
311  // Docking control loop: while not docked, run controller
312  rclcpp::Time dock_contact_time;
313  while (rclcpp::ok()) {
314  try {
315  // Perform a 180º to face away from the dock if needed
316  if (dock->plugin->shouldRotateToDock()) {
317  rotateToDock(dock_pose);
318  }
319  // Approach the dock using control law
320  if (approachDock(dock, dock_pose, dock_backward)) {
321  // We are docked, wait for charging to begin
322  RCLCPP_INFO(
323  get_logger(), "Made contact with dock, waiting for charge to start (if applicable).");
325  if (waitForCharge(dock)) {
326  if (dock->plugin->isCharger()) {
327  RCLCPP_INFO(get_logger(), "Robot is charging!");
328  } else {
329  RCLCPP_INFO(get_logger(), "Docking was successful!");
330  }
331  result->success = true;
332  result->num_retries = num_retries_;
333  stashDockData(goal->use_dock_id, dock, true);
335  dock->plugin->stopDetectionProcess();
336  docking_action_server_->succeeded_current(result);
337  return;
338  }
339  }
340 
341  // Cancelled, preempted, or shutting down (recoverable errors throw DockingException)
342  stashDockData(goal->use_dock_id, dock, false);
344  dock->plugin->stopDetectionProcess();
345  docking_action_server_->terminate_all(result);
346  return;
348  if (++num_retries_ > max_retries_) {
349  RCLCPP_ERROR(get_logger(), "Failed to dock, all retries have been used");
350  throw;
351  }
352  RCLCPP_WARN(get_logger(), "Docking failed, will retry: %s", e.what());
353  }
354 
355  // Reset to staging pose to try again
356  if (!resetApproach(staging_pose, dock_backward)) {
357  // Cancelled, preempted, or shutting down
358  stashDockData(goal->use_dock_id, dock, false);
360  dock->plugin->stopDetectionProcess();
361  docking_action_server_->terminate_all(result);
362  return;
363  }
364  RCLCPP_INFO(get_logger(), "Returned to staging pose, attempting docking again");
365  }
366  } catch (const tf2::TransformException & e) {
367  result->error_msg = std::string("Transform error: ") + e.what();
368  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
369  result->error_code = DockRobot::Result::UNKNOWN;
370  } catch (opennav_docking_core::DockNotInDB & e) {
371  result->error_msg = e.what();
372  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
373  result->error_code = DockRobot::Result::DOCK_NOT_IN_DB;
374  } catch (opennav_docking_core::DockNotValid & e) {
375  result->error_msg = e.what();
376  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
377  result->error_code = DockRobot::Result::DOCK_NOT_VALID;
379  result->error_msg = e.what();
380  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
381  result->error_code = DockRobot::Result::FAILED_TO_STAGE;
383  result->error_msg = e.what();
384  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
385  result->error_code = DockRobot::Result::FAILED_TO_DETECT_DOCK;
387  result->error_msg = e.what();
388  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
389  result->error_code = DockRobot::Result::FAILED_TO_CONTROL;
391  result->error_msg = e.what();
392  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
393  result->error_code = DockRobot::Result::FAILED_TO_CHARGE;
395  result->error_msg = e.what();
396  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
397  result->error_code = DockRobot::Result::UNKNOWN;
398  } catch (std::exception & e) {
399  result->error_code = DockRobot::Result::UNKNOWN;
400  result->error_msg = e.what();
401  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
402  }
403 
404  // Store dock state for later undocking and delete temp dock, if applicable
405  stashDockData(goal->use_dock_id, dock, false);
406  result->num_retries = num_retries_;
408  dock->plugin->stopDetectionProcess();
409  docking_action_server_->terminate_current(result);
410 }
411 
412 void DockingServer::stashDockData(bool use_dock_id, Dock * dock, bool successful)
413 {
414  if (dock && successful) {
415  curr_dock_type_ = dock->type;
416  }
417 
418  if (!use_dock_id && dock) {
419  delete dock;
420  dock = nullptr;
421  }
422 }
423 
424 Dock * DockingServer::generateGoalDock(std::shared_ptr<const DockRobot::Goal> goal)
425 {
426  auto dock = new Dock();
427  dock->frame = goal->dock_pose.header.frame_id;
428  dock->pose = goal->dock_pose.pose;
429  dock->type = goal->dock_type;
430  dock->plugin = dock_db_->findDockPlugin(dock->type);
431  return dock;
432 }
433 
434 void DockingServer::doInitialPerception(Dock * dock, geometry_msgs::msg::PoseStamped & dock_pose)
435 {
436  publishDockingFeedback(DockRobot::Feedback::INITIAL_PERCEPTION);
437 
438  if (!dock->plugin->startDetectionProcess()) {
439  throw opennav_docking_core::FailedToDetectDock("Failed to start the detection process.");
440  }
441 
442  rclcpp::Rate loop_rate(controller_frequency_);
443  auto start = this->now();
444  auto timeout = rclcpp::Duration::from_seconds(initial_perception_timeout_);
445  while (!dock->plugin->getRefinedPose(dock_pose, dock->id)) {
446  if (this->now() - start > timeout) {
448  "Failed initial dock detection: Timeout exceeded");
449  }
450 
451  if (checkAndWarnIfCancelled<DockRobot>(docking_action_server_, "dock_robot") ||
452  checkAndWarnIfPreempted<DockRobot>(docking_action_server_, "dock_robot"))
453  {
454  return;
455  }
456 
457  loop_rate.sleep();
458  }
459 }
460 
461 void DockingServer::rotateToDock(const geometry_msgs::msg::PoseStamped & dock_pose)
462 {
463  const double dt = 1.0 / controller_frequency_;
464  auto target_pose = dock_pose;
465  target_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(
466  tf2::getYaw(target_pose.pose.orientation) + M_PI);
467 
468  rclcpp::Rate loop_rate(controller_frequency_);
469  auto start = this->now();
470  auto timeout = rclcpp::Duration::from_seconds(rotate_to_dock_timeout_);
471 
472  while (rclcpp::ok()) {
473  auto robot_pose = getRobotPoseInFrame(dock_pose.header.frame_id);
474  auto angular_distance_to_heading = angles::shortest_angular_distance(
475  tf2::getYaw(robot_pose.pose.orientation), tf2::getYaw(target_pose.pose.orientation));
476  if (fabs(angular_distance_to_heading) < rotation_angular_tolerance_) {
477  break;
478  }
479 
480  auto current_vel = std::make_unique<geometry_msgs::msg::TwistStamped>();
481  current_vel->twist.angular.z = odom_sub_->getRawTwist().angular.z;
482 
483  auto command = std::make_unique<geometry_msgs::msg::TwistStamped>();
484  command->header = robot_pose.header;
485  command->twist = controller_->computeRotateToHeadingCommand(
486  angular_distance_to_heading, current_vel->twist, dt);
487 
488  vel_publisher_->publish(std::move(command));
489 
490  if (this->now() - start > timeout) {
491  throw opennav_docking_core::FailedToControl("Timed out rotating to dock");
492  }
493 
494  loop_rate.sleep();
495  }
496 }
497 
499  Dock * dock, geometry_msgs::msg::PoseStamped & dock_pose, bool backward)
500 {
501  rclcpp::Rate loop_rate(controller_frequency_);
502  auto start = this->now();
503  auto timeout = rclcpp::Duration::from_seconds(dock_approach_timeout_);
504 
505  while (rclcpp::ok()) {
506  publishDockingFeedback(DockRobot::Feedback::CONTROLLING);
507 
508  // Stop and report success if connected to dock
509  if (dock->plugin->isDocked() || (dock->plugin->isCharger() && dock->plugin->isCharging())) {
510  return true;
511  }
512 
513  // Stop if cancelled/preempted
514  if (checkAndWarnIfCancelled<DockRobot>(docking_action_server_, "dock_robot") ||
515  checkAndWarnIfPreempted<DockRobot>(docking_action_server_, "dock_robot"))
516  {
517  return false;
518  }
519 
520  // Update perception
521  if (!dock->plugin->getRefinedPose(dock_pose, dock->id) && !dock->plugin->shouldRotateToDock()) {
522  throw opennav_docking_core::FailedToDetectDock("Failed dock detection");
523  }
524 
525  // Transform target_pose into base_link frame
526  geometry_msgs::msg::PoseStamped target_pose = dock_pose;
527  target_pose.header.stamp = rclcpp::Time(0);
528 
529  // The control law can get jittery when close to the end when atan2's can explode.
530  // Thus, we backward project the controller's target pose a little bit after the
531  // dock so that the robot never gets to the end of the spiral before its in contact
532  // with the dock to stop the docking procedure.
533  const double backward_projection = 0.25;
534  const double yaw = tf2::getYaw(target_pose.pose.orientation);
535  target_pose.pose.position.x += cos(yaw) * backward_projection;
536  target_pose.pose.position.y += sin(yaw) * backward_projection;
537  tf2_buffer_->transform(target_pose, target_pose, base_frame_);
538 
539  // Make sure that the target pose is pointing at the robot when moving backwards
540  // This is to ensure that the robot doesn't try to dock from the wrong side
541  if (backward) {
542  target_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(
543  tf2::getYaw(target_pose.pose.orientation) + M_PI);
544  }
545 
546  // Compute and publish controls
547  auto command = std::make_unique<geometry_msgs::msg::TwistStamped>();
548  command->header.stamp = now();
549  if (!controller_->computeVelocityCommand(target_pose.pose, command->twist, true, backward)) {
550  throw opennav_docking_core::FailedToControl("Failed to get control");
551  }
552  vel_publisher_->publish(std::move(command));
553 
554  if (this->now() - start > timeout) {
556  "Timed out approaching dock; dock nor charging (if applicable) detected");
557  }
558 
559  loop_rate.sleep();
560  }
561  return false;
562 }
563 
565 {
566  // This is a non-charger docking request
567  if (!dock->plugin->isCharger()) {
568  return true;
569  }
570 
571  rclcpp::Rate loop_rate(controller_frequency_);
572  auto start = this->now();
573  auto timeout = rclcpp::Duration::from_seconds(wait_charge_timeout_);
574  while (rclcpp::ok()) {
575  publishDockingFeedback(DockRobot::Feedback::WAIT_FOR_CHARGE);
576 
577  if (dock->plugin->isCharging()) {
578  return true;
579  }
580 
581  if (checkAndWarnIfCancelled<DockRobot>(docking_action_server_, "dock_robot") ||
582  checkAndWarnIfPreempted<DockRobot>(docking_action_server_, "dock_robot"))
583  {
584  return false;
585  }
586 
587  if (this->now() - start > timeout) {
588  throw opennav_docking_core::FailedToCharge("Timed out waiting for charge to start");
589  }
590 
591  loop_rate.sleep();
592  }
593  return false;
594 }
595 
597  const geometry_msgs::msg::PoseStamped & staging_pose, bool backward)
598 {
599  rclcpp::Rate loop_rate(controller_frequency_);
600  auto start = this->now();
601  auto timeout = rclcpp::Duration::from_seconds(dock_approach_timeout_);
602  while (rclcpp::ok()) {
603  publishDockingFeedback(DockRobot::Feedback::INITIAL_PERCEPTION);
604 
605  // Stop if cancelled/preempted
606  if (checkAndWarnIfCancelled<DockRobot>(docking_action_server_, "dock_robot") ||
607  checkAndWarnIfPreempted<DockRobot>(docking_action_server_, "dock_robot"))
608  {
609  return false;
610  }
611 
612  // Compute and publish command
613  auto command = std::make_unique<geometry_msgs::msg::TwistStamped>();
614  command->header.stamp = now();
615  if (getCommandToPose(
616  command->twist, staging_pose, undock_linear_tolerance_, undock_angular_tolerance_, false,
617  !backward))
618  {
619  return true;
620  }
621  vel_publisher_->publish(std::move(command));
622 
623  if (this->now() - start > timeout) {
624  throw opennav_docking_core::FailedToControl("Timed out resetting dock approach");
625  }
626 
627  loop_rate.sleep();
628  }
629  return false;
630 }
631 
633  geometry_msgs::msg::Twist & cmd, const geometry_msgs::msg::PoseStamped & pose,
634  double linear_tolerance, double angular_tolerance, bool is_docking, bool backward)
635 {
636  // Reset command to zero velocity
637  cmd.linear.x = 0;
638  cmd.angular.z = 0;
639 
640  // Determine if we have reached pose yet & stop
641  geometry_msgs::msg::PoseStamped robot_pose = getRobotPoseInFrame(pose.header.frame_id);
642  const double dist = std::hypot(
643  robot_pose.pose.position.x - pose.pose.position.x,
644  robot_pose.pose.position.y - pose.pose.position.y);
645  const double yaw = angles::shortest_angular_distance(
646  tf2::getYaw(robot_pose.pose.orientation), tf2::getYaw(pose.pose.orientation));
647  if (dist < linear_tolerance && abs(yaw) < angular_tolerance) {
648  return true;
649  }
650 
651  // Transform target_pose into base_link frame
652  geometry_msgs::msg::PoseStamped target_pose = pose;
653  target_pose.header.stamp = rclcpp::Time(0);
654  tf2_buffer_->transform(target_pose, target_pose, base_frame_);
655 
656  // Compute velocity command
657  if (!controller_->computeVelocityCommand(target_pose.pose, cmd, is_docking, backward)) {
658  throw opennav_docking_core::FailedToControl("Failed to get control");
659  }
660 
661  // Command is valid, but target is not reached
662  return false;
663 }
664 
666 {
667  std::lock_guard<std::mutex> lock(*mutex_);
668  action_start_time_ = this->now();
669  rclcpp::Rate loop_rate(controller_frequency_);
670 
671  auto goal = undocking_action_server_->get_current_goal();
672  auto result = std::make_shared<UndockRobot::Result>();
673  result->success = false;
674 
675  if (!undocking_action_server_ || !undocking_action_server_->is_server_active()) {
676  RCLCPP_DEBUG(get_logger(), "Action server unavailable or inactive. Stopping.");
677  return;
678  }
679 
680  if (checkAndWarnIfCancelled<UndockRobot>(undocking_action_server_, "undock_robot")) {
681  undocking_action_server_->terminate_all(result);
682  return;
683  }
684 
685  getPreemptedGoalIfRequested<UndockRobot>(goal, undocking_action_server_);
686  auto max_duration = rclcpp::Duration::from_seconds(goal->max_undocking_time);
687 
688  try {
689  // Get dock plugin information from request or docked state, reset state.
690  std::string dock_type = curr_dock_type_;
691  if (!goal->dock_type.empty()) {
692  dock_type = goal->dock_type;
693  }
694 
695  ChargingDock::Ptr dock = dock_db_->findDockPlugin(dock_type);
696  if (!dock) {
697  throw opennav_docking_core::DockNotValid("No dock information to undock from!");
698  }
699  RCLCPP_INFO(
700  get_logger(),
701  "Attempting to undock robot of dock type %s.", dock->getName().c_str());
702 
703  // Check if the robot is docked before proceeding
704  if (dock->isCharger() && (!dock->isDocked() && !dock->isCharging())) {
705  RCLCPP_INFO(get_logger(), "Robot is not in the dock, no need to undock");
706  return;
707  }
708 
709  bool dock_backward = dock_backwards_.has_value() ?
710  dock_backwards_.value() :
711  (dock->getDockDirection() == opennav_docking_core::DockDirection::BACKWARD);
712 
713  // Get "dock pose" by finding the robot pose
714  geometry_msgs::msg::PoseStamped dock_pose = getRobotPoseInFrame(fixed_frame_);
715 
716  // Make sure that the staging pose is pointing in the same direction when moving backwards
717  if (dock_backward) {
718  dock_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(
719  tf2::getYaw(dock_pose.pose.orientation) + M_PI);
720  }
721 
722  // Get staging pose (in fixed frame)
723  geometry_msgs::msg::PoseStamped staging_pose =
724  dock->getStagingPose(dock_pose.pose, dock_pose.header.frame_id);
725 
726  // If we performed a rotation before docking backward, we must rotate the staging pose
727  // to match the robot orientation
728  if (dock->shouldRotateToDock()) {
729  staging_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(
730  tf2::getYaw(staging_pose.pose.orientation) + M_PI);
731  }
732 
733  // Control robot to staging pose
734  rclcpp::Time loop_start = this->now();
735  while (rclcpp::ok()) {
736  // Stop if we exceed max duration
737  auto timeout = rclcpp::Duration::from_seconds(goal->max_undocking_time);
738  if (this->now() - loop_start > timeout) {
739  throw opennav_docking_core::FailedToControl("Undocking timed out");
740  }
741 
742  // Stop if cancelled/preempted
743  if (checkAndWarnIfCancelled<UndockRobot>(undocking_action_server_, "undock_robot") ||
744  checkAndWarnIfPreempted<UndockRobot>(undocking_action_server_, "undock_robot"))
745  {
747  undocking_action_server_->terminate_all(result);
748  return;
749  }
750 
751  // Don't control the robot until charging is disabled
752  if (dock->isCharger() && !dock->disableCharging()) {
753  loop_rate.sleep();
754  continue;
755  }
756 
757  // Get command to approach staging pose
758  auto command = std::make_unique<geometry_msgs::msg::TwistStamped>();
759  command->header.stamp = now();
760 
761  if (getCommandToPose(
762  command->twist, staging_pose, undock_linear_tolerance_, undock_angular_tolerance_, false,
763  !dock_backward))
764  {
765  // Perform a 180º to the original staging pose
766  if (dock->shouldRotateToDock()) {
767  rotateToDock(staging_pose);
768  }
769 
770  // Have reached staging_pose
771  RCLCPP_INFO(get_logger(), "Robot has reached staging pose");
772  vel_publisher_->publish(std::move(command));
773  if (!dock->isCharger() || dock->hasStoppedCharging()) {
774  RCLCPP_INFO(get_logger(), "Robot has undocked!");
775  result->success = true;
776  curr_dock_type_.clear();
778  undocking_action_server_->succeeded_current(result);
779  return;
780  }
781  // Haven't stopped charging?
782  throw opennav_docking_core::FailedToControl("Failed to control off dock");
783  }
784 
785  // Publish command and sleep
786  vel_publisher_->publish(std::move(command));
787  loop_rate.sleep();
788  }
789  } catch (const tf2::TransformException & e) {
790  result->error_msg = std::string("Transform error: ") + e.what();
791  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
792  result->error_code = DockRobot::Result::UNKNOWN;
793  } catch (opennav_docking_core::DockNotValid & e) {
794  result->error_msg = e.what();
795  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
796  result->error_code = DockRobot::Result::DOCK_NOT_VALID;
798  result->error_msg = e.what();
799  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
800  result->error_code = DockRobot::Result::FAILED_TO_CONTROL;
802  result->error_msg = e.what();
803  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
804  result->error_code = DockRobot::Result::UNKNOWN;
805  } catch (std::exception & e) {
806  result->error_msg = std::string("Internal error: ") + e.what();
807  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
808  result->error_code = DockRobot::Result::UNKNOWN;
809  }
810 
812  undocking_action_server_->terminate_current(result);
813 }
814 
815 geometry_msgs::msg::PoseStamped DockingServer::getRobotPoseInFrame(const std::string & frame)
816 {
817  geometry_msgs::msg::PoseStamped robot_pose;
818  robot_pose.header.frame_id = base_frame_;
819  robot_pose.header.stamp = rclcpp::Time(0);
820  tf2_buffer_->transform(robot_pose, robot_pose, frame);
821  return robot_pose;
822 }
823 
825 {
826  auto cmd_vel = std::make_unique<geometry_msgs::msg::TwistStamped>();
827  cmd_vel->header.stamp = now();
828  vel_publisher_->publish(std::move(cmd_vel));
829 }
830 
832 {
833  auto feedback = std::make_shared<DockRobot::Feedback>();
834  feedback->state = state;
835  feedback->docking_time = this->now() - action_start_time_;
836  feedback->num_retries = num_retries_;
837  docking_action_server_->publish_feedback(feedback);
838 }
839 
840 rcl_interfaces::msg::SetParametersResult
841 DockingServer::dynamicParametersCallback(std::vector<rclcpp::Parameter> parameters)
842 {
843  std::lock_guard<std::mutex> lock(*mutex_);
844 
845  rcl_interfaces::msg::SetParametersResult result;
846  for (auto parameter : parameters) {
847  const auto & param_type = parameter.get_type();
848  const auto & param_name = parameter.get_name();
849  if (param_name.find('.') != std::string::npos) {
850  continue;
851  }
852 
853  if (param_type == ParameterType::PARAMETER_DOUBLE) {
854  if (param_name == "controller_frequency") {
855  controller_frequency_ = parameter.as_double();
856  } else if (param_name == "initial_perception_timeout") {
857  initial_perception_timeout_ = parameter.as_double();
858  } else if (param_name == "wait_charge_timeout") {
859  wait_charge_timeout_ = parameter.as_double();
860  } else if (param_name == "undock_linear_tolerance") {
861  undock_linear_tolerance_ = parameter.as_double();
862  } else if (param_name == "undock_angular_tolerance") {
863  undock_angular_tolerance_ = parameter.as_double();
864  } else if (param_name == "rotation_angular_tolerance") {
865  rotation_angular_tolerance_ = parameter.as_double();
866  }
867  } else if (param_type == ParameterType::PARAMETER_STRING) {
868  if (param_name == "base_frame") {
869  base_frame_ = parameter.as_string();
870  } else if (param_name == "fixed_frame") {
871  fixed_frame_ = parameter.as_string();
872  }
873  } else if (param_type == ParameterType::PARAMETER_INTEGER) {
874  if (param_name == "max_retries") {
875  max_retries_ = parameter.as_int();
876  }
877  }
878  }
879 
880  result.successful = true;
881  return result;
882 }
883 
884 } // namespace opennav_docking
885 
886 #include "rclcpp_components/register_node_macro.hpp"
887 
888 // Register the component with class_loader.
889 // This acts as a sort of entry point, allowing the component to be discoverable when its library
890 // is being loaded into a running process.
891 RCLCPP_COMPONENTS_REGISTER_NODE(opennav_docking::DockingServer)
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.
An action server which implements charger docking node for AMRs.
rcl_interfaces::msg::SetParametersResult dynamicParametersCallback(std::vector< rclcpp::Parameter > parameters)
Callback executed when a parameter change is detected.
virtual geometry_msgs::msg::PoseStamped getRobotPoseInFrame(const std::string &frame)
Get the robot pose (aka base_frame pose) in another frame.
bool resetApproach(const geometry_msgs::msg::PoseStamped &staging_pose, bool backward)
Reset the robot for another approach by controlling back to staging pose.
void dockRobot()
Main action callback method to complete docking request.
nav2::CallbackReturn on_deactivate(const rclcpp_lifecycle::State &state) override
Deactivate member variables.
nav2::CallbackReturn on_cleanup(const rclcpp_lifecycle::State &state) override
Reset member variables.
bool approachDock(Dock *dock, geometry_msgs::msg::PoseStamped &dock_pose, bool backward)
Use control law and dock perception to approach the charge dock.
void doInitialPerception(Dock *dock, geometry_msgs::msg::PoseStamped &dock_pose)
Do initial perception, up to a timeout.
void publishDockingFeedback(uint16_t state)
Publish feedback from a docking action.
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.
bool checkAndWarnIfPreempted(typename nav2::SimpleActionServer< ActionT >::SharedPtr &action_server, const std::string &name)
Checks and logs warning if action preempted.
bool getCommandToPose(geometry_msgs::msg::Twist &cmd, const geometry_msgs::msg::PoseStamped &pose, double linear_tolerance, double angular_tolerance, bool is_docking, bool backward)
Run a single iteration of the control loop to approach a pose.
bool waitForCharge(Dock *dock)
Wait for charging to begin.
Dock * generateGoalDock(std::shared_ptr< const DockRobot::Goal > goal)
Generate a dock from action goal.
nav2::CallbackReturn on_activate(const rclcpp_lifecycle::State &state) override
Activate member variables.
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.
nav2::CallbackReturn on_configure(const rclcpp_lifecycle::State &state) override
Configure member variables.
void publishZeroVelocity()
Publish zero velocity at terminal condition.
void rotateToDock(const geometry_msgs::msg::PoseStamped &dock_pose)
Perform a pure rotation to dock orientation.
void undockRobot()
Main action callback method to complete undocking request.
void stashDockData(bool use_dock_id, Dock *dock, bool successful)
Called at the conclusion of docking actions. Saves relevant docking data for later undocking action.
Dock was not found in the provided dock database.
Dock plugin provided in the database or action was invalid.
Failed to control into or out of the dock.
Failed to detect the charging dock.
Failed to navigate to the staging pose.
Definition: types.hpp:33