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).");
324  if (waitForCharge(dock)) {
325  if (dock->plugin->isCharger()) {
326  RCLCPP_INFO(get_logger(), "Robot is charging!");
327  } else {
328  RCLCPP_INFO(get_logger(), "Docking was successful!");
329  }
330  result->success = true;
331  result->num_retries = num_retries_;
332  stashDockData(goal->use_dock_id, dock, true);
334  docking_action_server_->succeeded_current(result);
335  return;
336  }
337  }
338 
339  // Cancelled, preempted, or shutting down (recoverable errors throw DockingException)
340  stashDockData(goal->use_dock_id, dock, false);
342  docking_action_server_->terminate_all(result);
343  return;
345  if (++num_retries_ > max_retries_) {
346  RCLCPP_ERROR(get_logger(), "Failed to dock, all retries have been used");
347  throw;
348  }
349  RCLCPP_WARN(get_logger(), "Docking failed, will retry: %s", e.what());
350  }
351 
352  // Reset to staging pose to try again
353  if (!resetApproach(staging_pose, dock_backward)) {
354  // Cancelled, preempted, or shutting down
355  stashDockData(goal->use_dock_id, dock, false);
357  docking_action_server_->terminate_all(result);
358  return;
359  }
360  RCLCPP_INFO(get_logger(), "Returned to staging pose, attempting docking again");
361  }
362  } catch (const tf2::TransformException & e) {
363  result->error_msg = std::string("Transform error: ") + e.what();
364  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
365  result->error_code = DockRobot::Result::UNKNOWN;
366  } catch (opennav_docking_core::DockNotInDB & e) {
367  result->error_msg = e.what();
368  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
369  result->error_code = DockRobot::Result::DOCK_NOT_IN_DB;
370  } catch (opennav_docking_core::DockNotValid & 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_VALID;
375  result->error_msg = e.what();
376  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
377  result->error_code = DockRobot::Result::FAILED_TO_STAGE;
379  result->error_msg = e.what();
380  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
381  result->error_code = DockRobot::Result::FAILED_TO_DETECT_DOCK;
383  result->error_msg = e.what();
384  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
385  result->error_code = DockRobot::Result::FAILED_TO_CONTROL;
387  result->error_msg = e.what();
388  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
389  result->error_code = DockRobot::Result::FAILED_TO_CHARGE;
391  result->error_msg = e.what();
392  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
393  result->error_code = DockRobot::Result::UNKNOWN;
394  } catch (std::exception & e) {
395  result->error_code = DockRobot::Result::UNKNOWN;
396  result->error_msg = e.what();
397  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
398  }
399 
400  // Store dock state for later undocking and delete temp dock, if applicable
401  stashDockData(goal->use_dock_id, dock, false);
402  result->num_retries = num_retries_;
404  docking_action_server_->terminate_current(result);
405 }
406 
407 void DockingServer::stashDockData(bool use_dock_id, Dock * dock, bool successful)
408 {
409  if (dock && successful) {
410  curr_dock_type_ = dock->type;
411  }
412 
413  if (!use_dock_id && dock) {
414  delete dock;
415  dock = nullptr;
416  }
417 }
418 
419 Dock * DockingServer::generateGoalDock(std::shared_ptr<const DockRobot::Goal> goal)
420 {
421  auto dock = new Dock();
422  dock->frame = goal->dock_pose.header.frame_id;
423  dock->pose = goal->dock_pose.pose;
424  dock->type = goal->dock_type;
425  dock->plugin = dock_db_->findDockPlugin(dock->type);
426  return dock;
427 }
428 
429 void DockingServer::doInitialPerception(Dock * dock, geometry_msgs::msg::PoseStamped & dock_pose)
430 {
431  publishDockingFeedback(DockRobot::Feedback::INITIAL_PERCEPTION);
432  rclcpp::Rate loop_rate(controller_frequency_);
433  auto start = this->now();
434  auto timeout = rclcpp::Duration::from_seconds(initial_perception_timeout_);
435  while (!dock->plugin->getRefinedPose(dock_pose, dock->id)) {
436  if (this->now() - start > timeout) {
437  throw opennav_docking_core::FailedToDetectDock("Failed initial dock detection");
438  }
439 
440  if (checkAndWarnIfCancelled<DockRobot>(docking_action_server_, "dock_robot") ||
441  checkAndWarnIfPreempted<DockRobot>(docking_action_server_, "dock_robot"))
442  {
443  return;
444  }
445 
446  loop_rate.sleep();
447  }
448 }
449 
450 void DockingServer::rotateToDock(const geometry_msgs::msg::PoseStamped & dock_pose)
451 {
452  const double dt = 1.0 / controller_frequency_;
453  auto target_pose = dock_pose;
454  target_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(
455  tf2::getYaw(target_pose.pose.orientation) + M_PI);
456 
457  rclcpp::Rate loop_rate(controller_frequency_);
458  auto start = this->now();
459  auto timeout = rclcpp::Duration::from_seconds(rotate_to_dock_timeout_);
460 
461  while (rclcpp::ok()) {
462  auto robot_pose = getRobotPoseInFrame(dock_pose.header.frame_id);
463  auto angular_distance_to_heading = angles::shortest_angular_distance(
464  tf2::getYaw(robot_pose.pose.orientation), tf2::getYaw(target_pose.pose.orientation));
465  if (fabs(angular_distance_to_heading) < rotation_angular_tolerance_) {
466  break;
467  }
468 
469  auto current_vel = std::make_unique<geometry_msgs::msg::TwistStamped>();
470  current_vel->twist.angular.z = odom_sub_->getRawTwist().angular.z;
471 
472  auto command = std::make_unique<geometry_msgs::msg::TwistStamped>();
473  command->header = robot_pose.header;
474  command->twist = controller_->computeRotateToHeadingCommand(
475  angular_distance_to_heading, current_vel->twist, dt);
476 
477  vel_publisher_->publish(std::move(command));
478 
479  if (this->now() - start > timeout) {
480  throw opennav_docking_core::FailedToControl("Timed out rotating to dock");
481  }
482 
483  loop_rate.sleep();
484  }
485 }
486 
488  Dock * dock, geometry_msgs::msg::PoseStamped & dock_pose, bool backward)
489 {
490  rclcpp::Rate loop_rate(controller_frequency_);
491  auto start = this->now();
492  auto timeout = rclcpp::Duration::from_seconds(dock_approach_timeout_);
493 
494  while (rclcpp::ok()) {
495  publishDockingFeedback(DockRobot::Feedback::CONTROLLING);
496 
497  // Stop and report success if connected to dock
498  if (dock->plugin->isDocked() || (dock->plugin->isCharger() && dock->plugin->isCharging())) {
499  return true;
500  }
501 
502  // Stop if cancelled/preempted
503  if (checkAndWarnIfCancelled<DockRobot>(docking_action_server_, "dock_robot") ||
504  checkAndWarnIfPreempted<DockRobot>(docking_action_server_, "dock_robot"))
505  {
506  return false;
507  }
508 
509  // Update perception
510  if (!dock->plugin->getRefinedPose(dock_pose, dock->id) && !dock->plugin->shouldRotateToDock()) {
511  throw opennav_docking_core::FailedToDetectDock("Failed dock detection");
512  }
513 
514  // Transform target_pose into base_link frame
515  geometry_msgs::msg::PoseStamped target_pose = dock_pose;
516  target_pose.header.stamp = rclcpp::Time(0);
517 
518  // The control law can get jittery when close to the end when atan2's can explode.
519  // Thus, we backward project the controller's target pose a little bit after the
520  // dock so that the robot never gets to the end of the spiral before its in contact
521  // with the dock to stop the docking procedure.
522  const double backward_projection = 0.25;
523  const double yaw = tf2::getYaw(target_pose.pose.orientation);
524  target_pose.pose.position.x += cos(yaw) * backward_projection;
525  target_pose.pose.position.y += sin(yaw) * backward_projection;
526  tf2_buffer_->transform(target_pose, target_pose, base_frame_);
527 
528  // Make sure that the target pose is pointing at the robot when moving backwards
529  // This is to ensure that the robot doesn't try to dock from the wrong side
530  if (backward) {
531  target_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(
532  tf2::getYaw(target_pose.pose.orientation) + M_PI);
533  }
534 
535  // Compute and publish controls
536  auto command = std::make_unique<geometry_msgs::msg::TwistStamped>();
537  command->header.stamp = now();
538  if (!controller_->computeVelocityCommand(target_pose.pose, command->twist, true, backward)) {
539  throw opennav_docking_core::FailedToControl("Failed to get control");
540  }
541  vel_publisher_->publish(std::move(command));
542 
543  if (this->now() - start > timeout) {
545  "Timed out approaching dock; dock nor charging (if applicable) detected");
546  }
547 
548  loop_rate.sleep();
549  }
550  return false;
551 }
552 
554 {
555  // This is a non-charger docking request
556  if (!dock->plugin->isCharger()) {
557  return true;
558  }
559 
560  rclcpp::Rate loop_rate(controller_frequency_);
561  auto start = this->now();
562  auto timeout = rclcpp::Duration::from_seconds(wait_charge_timeout_);
563  while (rclcpp::ok()) {
564  publishDockingFeedback(DockRobot::Feedback::WAIT_FOR_CHARGE);
565 
566  if (dock->plugin->isCharging()) {
567  return true;
568  }
569 
570  if (checkAndWarnIfCancelled<DockRobot>(docking_action_server_, "dock_robot") ||
571  checkAndWarnIfPreempted<DockRobot>(docking_action_server_, "dock_robot"))
572  {
573  return false;
574  }
575 
576  if (this->now() - start > timeout) {
577  throw opennav_docking_core::FailedToCharge("Timed out waiting for charge to start");
578  }
579 
580  loop_rate.sleep();
581  }
582  return false;
583 }
584 
586  const geometry_msgs::msg::PoseStamped & staging_pose, bool backward)
587 {
588  rclcpp::Rate loop_rate(controller_frequency_);
589  auto start = this->now();
590  auto timeout = rclcpp::Duration::from_seconds(dock_approach_timeout_);
591  while (rclcpp::ok()) {
592  publishDockingFeedback(DockRobot::Feedback::INITIAL_PERCEPTION);
593 
594  // Stop if cancelled/preempted
595  if (checkAndWarnIfCancelled<DockRobot>(docking_action_server_, "dock_robot") ||
596  checkAndWarnIfPreempted<DockRobot>(docking_action_server_, "dock_robot"))
597  {
598  return false;
599  }
600 
601  // Compute and publish command
602  auto command = std::make_unique<geometry_msgs::msg::TwistStamped>();
603  command->header.stamp = now();
604  if (getCommandToPose(
605  command->twist, staging_pose, undock_linear_tolerance_, undock_angular_tolerance_, false,
606  !backward))
607  {
608  return true;
609  }
610  vel_publisher_->publish(std::move(command));
611 
612  if (this->now() - start > timeout) {
613  throw opennav_docking_core::FailedToControl("Timed out resetting dock approach");
614  }
615 
616  loop_rate.sleep();
617  }
618  return false;
619 }
620 
622  geometry_msgs::msg::Twist & cmd, const geometry_msgs::msg::PoseStamped & pose,
623  double linear_tolerance, double angular_tolerance, bool is_docking, bool backward)
624 {
625  // Reset command to zero velocity
626  cmd.linear.x = 0;
627  cmd.angular.z = 0;
628 
629  // Determine if we have reached pose yet & stop
630  geometry_msgs::msg::PoseStamped robot_pose = getRobotPoseInFrame(pose.header.frame_id);
631  const double dist = std::hypot(
632  robot_pose.pose.position.x - pose.pose.position.x,
633  robot_pose.pose.position.y - pose.pose.position.y);
634  const double yaw = angles::shortest_angular_distance(
635  tf2::getYaw(robot_pose.pose.orientation), tf2::getYaw(pose.pose.orientation));
636  if (dist < linear_tolerance && abs(yaw) < angular_tolerance) {
637  return true;
638  }
639 
640  // Transform target_pose into base_link frame
641  geometry_msgs::msg::PoseStamped target_pose = pose;
642  target_pose.header.stamp = rclcpp::Time(0);
643  tf2_buffer_->transform(target_pose, target_pose, base_frame_);
644 
645  // Compute velocity command
646  if (!controller_->computeVelocityCommand(target_pose.pose, cmd, is_docking, backward)) {
647  throw opennav_docking_core::FailedToControl("Failed to get control");
648  }
649 
650  // Command is valid, but target is not reached
651  return false;
652 }
653 
655 {
656  std::lock_guard<std::mutex> lock(*mutex_);
657  action_start_time_ = this->now();
658  rclcpp::Rate loop_rate(controller_frequency_);
659 
660  auto goal = undocking_action_server_->get_current_goal();
661  auto result = std::make_shared<UndockRobot::Result>();
662  result->success = false;
663 
664  if (!undocking_action_server_ || !undocking_action_server_->is_server_active()) {
665  RCLCPP_DEBUG(get_logger(), "Action server unavailable or inactive. Stopping.");
666  return;
667  }
668 
669  if (checkAndWarnIfCancelled<UndockRobot>(undocking_action_server_, "undock_robot")) {
670  undocking_action_server_->terminate_all(result);
671  return;
672  }
673 
674  getPreemptedGoalIfRequested<UndockRobot>(goal, undocking_action_server_);
675  auto max_duration = rclcpp::Duration::from_seconds(goal->max_undocking_time);
676 
677  try {
678  // Get dock plugin information from request or docked state, reset state.
679  std::string dock_type = curr_dock_type_;
680  if (!goal->dock_type.empty()) {
681  dock_type = goal->dock_type;
682  }
683 
684  ChargingDock::Ptr dock = dock_db_->findDockPlugin(dock_type);
685  if (!dock) {
686  throw opennav_docking_core::DockNotValid("No dock information to undock from!");
687  }
688  RCLCPP_INFO(
689  get_logger(),
690  "Attempting to undock robot of dock type %s.", dock->getName().c_str());
691 
692  // Check if the robot is docked before proceeding
693  if (dock->isCharger() && (!dock->isDocked() && !dock->isCharging())) {
694  RCLCPP_INFO(get_logger(), "Robot is not in the dock, no need to undock");
695  return;
696  }
697 
698  bool dock_backward = dock_backwards_.has_value() ?
699  dock_backwards_.value() :
700  (dock->getDockDirection() == opennav_docking_core::DockDirection::BACKWARD);
701 
702  // Get "dock pose" by finding the robot pose
703  geometry_msgs::msg::PoseStamped dock_pose = getRobotPoseInFrame(fixed_frame_);
704 
705  // Make sure that the staging pose is pointing in the same direction when moving backwards
706  if (dock_backward) {
707  dock_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(
708  tf2::getYaw(dock_pose.pose.orientation) + M_PI);
709  }
710 
711  // Get staging pose (in fixed frame)
712  geometry_msgs::msg::PoseStamped staging_pose =
713  dock->getStagingPose(dock_pose.pose, dock_pose.header.frame_id);
714 
715  // If we performed a rotation before docking backward, we must rotate the staging pose
716  // to match the robot orientation
717  if (dock->shouldRotateToDock()) {
718  staging_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(
719  tf2::getYaw(staging_pose.pose.orientation) + M_PI);
720  }
721 
722  // Control robot to staging pose
723  rclcpp::Time loop_start = this->now();
724  while (rclcpp::ok()) {
725  // Stop if we exceed max duration
726  auto timeout = rclcpp::Duration::from_seconds(goal->max_undocking_time);
727  if (this->now() - loop_start > timeout) {
728  throw opennav_docking_core::FailedToControl("Undocking timed out");
729  }
730 
731  // Stop if cancelled/preempted
732  if (checkAndWarnIfCancelled<UndockRobot>(undocking_action_server_, "undock_robot") ||
733  checkAndWarnIfPreempted<UndockRobot>(undocking_action_server_, "undock_robot"))
734  {
736  undocking_action_server_->terminate_all(result);
737  return;
738  }
739 
740  // Don't control the robot until charging is disabled
741  if (dock->isCharger() && !dock->disableCharging()) {
742  loop_rate.sleep();
743  continue;
744  }
745 
746  // Get command to approach staging pose
747  auto command = std::make_unique<geometry_msgs::msg::TwistStamped>();
748  command->header.stamp = now();
749 
750  if (getCommandToPose(
751  command->twist, staging_pose, undock_linear_tolerance_, undock_angular_tolerance_, false,
752  !dock_backward))
753  {
754  // Perform a 180º to the original staging pose
755  if (dock->shouldRotateToDock()) {
756  rotateToDock(staging_pose);
757  }
758 
759  // Have reached staging_pose
760  RCLCPP_INFO(get_logger(), "Robot has reached staging pose");
761  vel_publisher_->publish(std::move(command));
762  if (!dock->isCharger() || dock->hasStoppedCharging()) {
763  RCLCPP_INFO(get_logger(), "Robot has undocked!");
764  result->success = true;
765  curr_dock_type_.clear();
767  undocking_action_server_->succeeded_current(result);
768  return;
769  }
770  // Haven't stopped charging?
771  throw opennav_docking_core::FailedToControl("Failed to control off dock");
772  }
773 
774  // Publish command and sleep
775  vel_publisher_->publish(std::move(command));
776  loop_rate.sleep();
777  }
778  } catch (const tf2::TransformException & e) {
779  result->error_msg = std::string("Transform error: ") + e.what();
780  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
781  result->error_code = DockRobot::Result::UNKNOWN;
782  } catch (opennav_docking_core::DockNotValid & e) {
783  result->error_msg = e.what();
784  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
785  result->error_code = DockRobot::Result::DOCK_NOT_VALID;
787  result->error_msg = e.what();
788  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
789  result->error_code = DockRobot::Result::FAILED_TO_CONTROL;
791  result->error_msg = e.what();
792  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
793  result->error_code = DockRobot::Result::UNKNOWN;
794  } catch (std::exception & e) {
795  result->error_msg = std::string("Internal error: ") + e.what();
796  RCLCPP_ERROR(get_logger(), result->error_msg.c_str());
797  result->error_code = DockRobot::Result::UNKNOWN;
798  }
799 
801  undocking_action_server_->terminate_current(result);
802 }
803 
804 geometry_msgs::msg::PoseStamped DockingServer::getRobotPoseInFrame(const std::string & frame)
805 {
806  geometry_msgs::msg::PoseStamped robot_pose;
807  robot_pose.header.frame_id = base_frame_;
808  robot_pose.header.stamp = rclcpp::Time(0);
809  tf2_buffer_->transform(robot_pose, robot_pose, frame);
810  return robot_pose;
811 }
812 
814 {
815  auto cmd_vel = std::make_unique<geometry_msgs::msg::TwistStamped>();
816  cmd_vel->header.stamp = now();
817  vel_publisher_->publish(std::move(cmd_vel));
818 }
819 
821 {
822  auto feedback = std::make_shared<DockRobot::Feedback>();
823  feedback->state = state;
824  feedback->docking_time = this->now() - action_start_time_;
825  feedback->num_retries = num_retries_;
826  docking_action_server_->publish_feedback(feedback);
827 }
828 
829 rcl_interfaces::msg::SetParametersResult
830 DockingServer::dynamicParametersCallback(std::vector<rclcpp::Parameter> parameters)
831 {
832  std::lock_guard<std::mutex> lock(*mutex_);
833 
834  rcl_interfaces::msg::SetParametersResult result;
835  for (auto parameter : parameters) {
836  const auto & param_type = parameter.get_type();
837  const auto & param_name = parameter.get_name();
838  if (param_name.find('.') != std::string::npos) {
839  continue;
840  }
841 
842  if (param_type == ParameterType::PARAMETER_DOUBLE) {
843  if (param_name == "controller_frequency") {
844  controller_frequency_ = parameter.as_double();
845  } else if (param_name == "initial_perception_timeout") {
846  initial_perception_timeout_ = parameter.as_double();
847  } else if (param_name == "wait_charge_timeout") {
848  wait_charge_timeout_ = parameter.as_double();
849  } else if (param_name == "undock_linear_tolerance") {
850  undock_linear_tolerance_ = parameter.as_double();
851  } else if (param_name == "undock_angular_tolerance") {
852  undock_angular_tolerance_ = parameter.as_double();
853  } else if (param_name == "rotation_angular_tolerance") {
854  rotation_angular_tolerance_ = parameter.as_double();
855  }
856  } else if (param_type == ParameterType::PARAMETER_STRING) {
857  if (param_name == "base_frame") {
858  base_frame_ = parameter.as_string();
859  } else if (param_name == "fixed_frame") {
860  fixed_frame_ = parameter.as_string();
861  }
862  } else if (param_type == ParameterType::PARAMETER_INTEGER) {
863  if (param_name == "max_retries") {
864  max_retries_ = parameter.as_int();
865  }
866  }
867  }
868 
869  result.successful = true;
870  return result;
871 }
872 
873 } // namespace opennav_docking
874 
875 #include "rclcpp_components/register_node_macro.hpp"
876 
877 // Register the component with class_loader.
878 // This acts as a sort of entry point, allowing the component to be discoverable when its library
879 // is being loaded into a running process.
880 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