Nav2 Navigation Stack - rolling  main
ROS 2 Navigation Stack
simple_charging_dock.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 <cmath>
16 #include <chrono>
17 
18 #include "nav2_ros_common/node_utils.hpp"
19 #include "opennav_docking/simple_charging_dock.hpp"
20 #include "opennav_docking/utils.hpp"
21 
22 using namespace std::chrono_literals;
23 
24 namespace opennav_docking
25 {
26 
27 void SimpleChargingDock::configure(
28  const nav2::LifecycleNode::WeakPtr & parent,
29  const std::string & name, std::shared_ptr<tf2_ros::Buffer> tf)
30 {
31  name_ = name;
32  tf2_buffer_ = tf;
33  is_charging_ = false;
34  node_ = parent.lock();
35  if (!node_) {
36  throw std::runtime_error{"Failed to lock node"};
37  }
38 
39  // Optionally use battery info to check when charging, else say charging if docked
40  nav2::declare_parameter_if_not_declared(
41  node_, name + ".use_battery_status", rclcpp::ParameterValue(true));
42 
43  // Parameters for optional detector control
44  nav2::declare_parameter_if_not_declared(
45  node_, name + ".detector_service_name", rclcpp::ParameterValue(""));
46  nav2::declare_parameter_if_not_declared(
47  node_, name + ".detector_service_timeout", rclcpp::ParameterValue(5.0));
48  nav2::declare_parameter_if_not_declared(
49  node_, name + ".subscribe_toggle", rclcpp::ParameterValue(false));
50 
51  // Parameters for optional external detection of dock pose
52  nav2::declare_parameter_if_not_declared(
53  node_, name + ".use_external_detection_pose", rclcpp::ParameterValue(false));
54  nav2::declare_parameter_if_not_declared(
55  node_, name + ".external_detection_timeout", rclcpp::ParameterValue(1.0));
56  nav2::declare_parameter_if_not_declared(
57  node_, name + ".external_detection_translation_x", rclcpp::ParameterValue(-0.20));
58  nav2::declare_parameter_if_not_declared(
59  node_, name + ".external_detection_translation_y", rclcpp::ParameterValue(0.0));
60  nav2::declare_parameter_if_not_declared(
61  node_, name + ".external_detection_rotation_yaw", rclcpp::ParameterValue(0.0));
62  nav2::declare_parameter_if_not_declared(
63  node_, name + ".external_detection_rotation_pitch", rclcpp::ParameterValue(1.57));
64  nav2::declare_parameter_if_not_declared(
65  node_, name + ".external_detection_rotation_roll", rclcpp::ParameterValue(-1.57));
66  nav2::declare_parameter_if_not_declared(
67  node_, name + ".filter_coef", rclcpp::ParameterValue(0.1));
68 
69  // Charging threshold from BatteryState message
70  nav2::declare_parameter_if_not_declared(
71  node_, name + ".charging_threshold", rclcpp::ParameterValue(0.5));
72 
73  // Optionally determine if docked via stall detection using joint_states
74  nav2::declare_parameter_if_not_declared(
75  node_, name + ".use_stall_detection", rclcpp::ParameterValue(false));
76  nav2::declare_parameter_if_not_declared(
77  node_, name + ".stall_joint_names", rclcpp::PARAMETER_STRING_ARRAY);
78  nav2::declare_parameter_if_not_declared(
79  node_, name + ".stall_velocity_threshold", rclcpp::ParameterValue(1.0));
80  nav2::declare_parameter_if_not_declared(
81  node_, name + ".stall_effort_threshold", rclcpp::ParameterValue(1.0));
82 
83  // If not using stall detection, this is how close robot should get to pose
84  nav2::declare_parameter_if_not_declared(
85  node_, name + ".docking_threshold", rclcpp::ParameterValue(0.05));
86 
87  // Staging pose configuration
88  nav2::declare_parameter_if_not_declared(
89  node_, name + ".staging_x_offset", rclcpp::ParameterValue(-0.7));
90  nav2::declare_parameter_if_not_declared(
91  node_, name + ".staging_yaw_offset", rclcpp::ParameterValue(0.0));
92 
93  // Direction of docking and if we should rotate to dock
94  nav2::declare_parameter_if_not_declared(
95  node_, name + ".dock_direction", rclcpp::ParameterValue(std::string("forward")));
96  nav2::declare_parameter_if_not_declared(
97  node_, name + ".rotate_to_dock", rclcpp::ParameterValue(false));
98 
99  node_->get_parameter(name + ".use_battery_status", use_battery_status_);
100  node_->get_parameter(name + ".use_external_detection_pose", use_external_detection_pose_);
101  node_->get_parameter(name + ".external_detection_timeout", external_detection_timeout_);
102  node_->get_parameter(
103  name + ".external_detection_translation_x", external_detection_translation_x_);
104  node_->get_parameter(
105  name + ".external_detection_translation_y", external_detection_translation_y_);
106  double yaw, pitch, roll;
107  node_->get_parameter(name + ".external_detection_rotation_yaw", yaw);
108  node_->get_parameter(name + ".external_detection_rotation_pitch", pitch);
109  node_->get_parameter(name + ".external_detection_rotation_roll", roll);
110  external_detection_rotation_.setEuler(pitch, roll, yaw);
111  node_->get_parameter(name + ".charging_threshold", charging_threshold_);
112  node_->get_parameter(name + ".stall_velocity_threshold", stall_velocity_threshold_);
113  node_->get_parameter(name + ".stall_effort_threshold", stall_effort_threshold_);
114  node_->get_parameter(name + ".docking_threshold", docking_threshold_);
115  node_->get_parameter("base_frame", base_frame_id_); // Get server base frame ID
116  node_->get_parameter(name + ".staging_x_offset", staging_x_offset_);
117  node_->get_parameter(name + ".staging_yaw_offset", staging_yaw_offset_);
118 
119  node_->get_parameter(name + ".detector_service_name", detector_service_name_);
120  node_->get_parameter(name + ".detector_service_timeout", detector_service_timeout_);
121  node_->get_parameter(name + ".subscribe_toggle", subscribe_toggle_);
122 
123  // Initialize detection state
124  detection_active_ = false;
125  initial_pose_received_ = false;
126 
127  // Create persistent subscription if toggling is disabled.
128  if (use_external_detection_pose_ && !subscribe_toggle_) {
129  dock_pose_.header.stamp = rclcpp::Time(0);
130  dock_pose_sub_ = node_->create_subscription<geometry_msgs::msg::PoseStamped>(
131  "detected_dock_pose",
132  [this](const geometry_msgs::msg::PoseStamped::SharedPtr pose) {
133  detected_dock_pose_ = *pose;
134  initial_pose_received_ = true;
135  },
137  }
138 
139  std::string dock_direction;
140  node_->get_parameter(name + ".dock_direction", dock_direction);
141  dock_direction_ = utils::getDockDirectionFromString(dock_direction);
142  if (dock_direction_ == opennav_docking_core::DockDirection::UNKNOWN) {
143  throw std::runtime_error{"Dock direction is not valid. Valid options are: forward or backward"};
144  }
145 
146  node_->get_parameter(name + ".rotate_to_dock", rotate_to_dock_);
147  if (rotate_to_dock_ && dock_direction_ != opennav_docking_core::DockDirection::BACKWARD) {
148  throw std::runtime_error{"Parameter rotate_to_dock is enabled but dock direction is not "
149  "backward. Please set dock direction to backward."};
150  }
151 
152  // Setup filter
153  double filter_coef;
154  node_->get_parameter(name + ".filter_coef", filter_coef);
155  filter_ = std::make_unique<PoseFilter>(filter_coef, external_detection_timeout_);
156 
157  if (!detector_service_name_.empty()) {
158  detector_client_ = node_->create_client<std_srvs::srv::Trigger>(
159  detector_service_name_, false);
160  }
161 
162  if (use_battery_status_) {
163  battery_sub_ = node_->create_subscription<sensor_msgs::msg::BatteryState>(
164  "battery_state",
165  [this](const sensor_msgs::msg::BatteryState::SharedPtr state) {
166  is_charging_ = state->current > charging_threshold_;
167  });
168  }
169 
170  bool use_stall_detection;
171  node_->get_parameter(name + ".use_stall_detection", use_stall_detection);
172  if (use_stall_detection) {
173  is_stalled_ = false;
174  node_->get_parameter(name + ".stall_joint_names", stall_joint_names_);
175  if (stall_joint_names_.size() < 1) {
176  RCLCPP_ERROR(node_->get_logger(), "stall_joint_names cannot be empty!");
177  }
178  joint_state_sub_ = node_->create_subscription<sensor_msgs::msg::JointState>(
179  "joint_states",
180  std::bind(&SimpleChargingDock::jointStateCallback, this, std::placeholders::_1),
182  }
183 
184  dock_pose_pub_ = node_->create_publisher<geometry_msgs::msg::PoseStamped>("dock_pose");
185  filtered_dock_pose_pub_ = node_->create_publisher<geometry_msgs::msg::PoseStamped>(
186  "filtered_dock_pose");
187  staging_pose_pub_ = node_->create_publisher<geometry_msgs::msg::PoseStamped>("staging_pose");
188 }
189 
190 geometry_msgs::msg::PoseStamped SimpleChargingDock::getStagingPose(
191  const geometry_msgs::msg::Pose & pose, const std::string & frame)
192 {
193  // If not using detection, set the dock pose as the given dock pose estimate
194  if (!use_external_detection_pose_) {
195  // This gets called at the start of docking
196  // Reset our internally tracked dock pose
197  dock_pose_.header.frame_id = frame;
198  dock_pose_.pose = pose;
199  }
200 
201  // Compute the staging pose with given offsets
202  const double yaw = tf2::getYaw(pose.orientation);
203  geometry_msgs::msg::PoseStamped staging_pose;
204  staging_pose.header.frame_id = frame;
205  staging_pose.header.stamp = node_->now();
206  staging_pose.pose = pose;
207  staging_pose.pose.position.x += cos(yaw) * staging_x_offset_;
208  staging_pose.pose.position.y += sin(yaw) * staging_x_offset_;
209  tf2::Quaternion orientation;
210  orientation.setEuler(0.0, 0.0, yaw + staging_yaw_offset_);
211  staging_pose.pose.orientation = tf2::toMsg(orientation);
212 
213  // Publish staging pose for debugging purposes
214  staging_pose_pub_->publish(staging_pose);
215  return staging_pose;
216 }
217 
218 bool SimpleChargingDock::getRefinedPose(geometry_msgs::msg::PoseStamped & pose, std::string /*id*/)
219 {
220  // If using not detection, set the dock pose to the static fixed-frame version
221  if (!use_external_detection_pose_) {
222  dock_pose_pub_->publish(pose);
223  dock_pose_ = pose;
224  return true;
225  }
226 
227  // Guard against using pose data before the first detection has arrived.
228  if (!initial_pose_received_) {
229  RCLCPP_WARN(node_->get_logger(), "Waiting for first detected_dock_pose; none received yet");
230  return false;
231  }
232 
233  // If using detections, get current detections, transform to frame, and apply offsets
234  geometry_msgs::msg::PoseStamped detected = detected_dock_pose_;
235 
236  // Validate that external pose is new enough
237  auto timeout = rclcpp::Duration::from_seconds(external_detection_timeout_);
238  if (node_->now() - detected.header.stamp > timeout) {
239  RCLCPP_WARN(node_->get_logger(), "Lost detection or did not detect: timeout exceeded");
240  return false;
241  }
242 
243  // Transform detected pose into fixed frame. Note that the argument pose
244  // is the output of detection, but also acts as the initial estimate
245  // and contains the frame_id of docking
246  if (detected.header.frame_id != pose.header.frame_id) {
247  try {
248  if (!tf2_buffer_->canTransform(
249  pose.header.frame_id, detected.header.frame_id,
250  detected.header.stamp, rclcpp::Duration::from_seconds(0.2)))
251  {
252  RCLCPP_WARN(node_->get_logger(), "Failed to transform detected dock pose");
253  return false;
254  }
255  tf2_buffer_->transform(detected, detected, pose.header.frame_id);
256  } catch (const tf2::TransformException & ex) {
257  RCLCPP_WARN(node_->get_logger(), "Failed to transform detected dock pose");
258  return false;
259  }
260  }
261 
262  // Filter the detected pose
263  detected = filter_->update(detected);
264  filtered_dock_pose_pub_->publish(detected);
265 
266  // Rotate the just the orientation, then remove roll/pitch
267  geometry_msgs::msg::PoseStamped just_orientation;
268  just_orientation.pose.orientation = tf2::toMsg(external_detection_rotation_);
269  geometry_msgs::msg::TransformStamped transform;
270  transform.transform.rotation = detected.pose.orientation;
271  tf2::doTransform(just_orientation, just_orientation, transform);
272 
273  tf2::Quaternion orientation;
274  orientation.setEuler(0.0, 0.0, tf2::getYaw(just_orientation.pose.orientation));
275  dock_pose_.pose.orientation = tf2::toMsg(orientation);
276 
277  // Construct dock_pose_ by applying translation/rotation
278  dock_pose_.header = detected.header;
279  dock_pose_.pose.position = detected.pose.position;
280  const double yaw = tf2::getYaw(dock_pose_.pose.orientation);
281  dock_pose_.pose.position.x += cos(yaw) * external_detection_translation_x_ -
282  sin(yaw) * external_detection_translation_y_;
283  dock_pose_.pose.position.y += sin(yaw) * external_detection_translation_x_ +
284  cos(yaw) * external_detection_translation_y_;
285  dock_pose_.pose.position.z = 0.0;
286 
287  // Publish & return dock pose for debugging purposes
288  dock_pose_pub_->publish(dock_pose_);
289  pose = dock_pose_;
290  return true;
291 }
292 
293 bool SimpleChargingDock::isDocked()
294 {
295  if (joint_state_sub_) {
296  // Using stall detection
297  return is_stalled_;
298  }
299 
300  if (dock_pose_.header.frame_id.empty()) {
301  // Dock pose is not yet valid
302  return false;
303  }
304 
305  // Find base pose in target frame
306  geometry_msgs::msg::PoseStamped base_pose;
307  base_pose.header.stamp = rclcpp::Time(0);
308  base_pose.header.frame_id = base_frame_id_;
309  base_pose.pose.orientation.w = 1.0;
310  try {
311  tf2_buffer_->transform(base_pose, base_pose, dock_pose_.header.frame_id);
312  } catch (const tf2::TransformException & ex) {
313  return false;
314  }
315 
316  // If we are close enough, pretend we are charging
317  double d = std::hypot(
318  base_pose.pose.position.x - dock_pose_.pose.position.x,
319  base_pose.pose.position.y - dock_pose_.pose.position.y);
320  return d < docking_threshold_;
321 }
322 
323 bool SimpleChargingDock::isCharging()
324 {
325  return use_battery_status_ ? is_charging_ : isDocked();
326 }
327 
328 bool SimpleChargingDock::disableCharging()
329 {
330  return true;
331 }
332 
333 bool SimpleChargingDock::hasStoppedCharging()
334 {
335  return !isCharging();
336 }
337 
338 void SimpleChargingDock::jointStateCallback(const sensor_msgs::msg::JointState::SharedPtr state)
339 {
340  double velocity = 0.0;
341  double effort = 0.0;
342  for (size_t i = 0; i < state->name.size(); ++i) {
343  for (auto & name : stall_joint_names_) {
344  if (state->name[i] == name) {
345  // Tracking this joint
346  velocity += abs(state->velocity[i]);
347  effort += abs(state->effort[i]);
348  }
349  }
350  }
351 
352  // Take average
353  effort /= stall_joint_names_.size();
354  velocity /= stall_joint_names_.size();
355 
356  is_stalled_ = (velocity < stall_velocity_threshold_) && (effort > stall_effort_threshold_);
357 }
358 
359 bool SimpleChargingDock::startDetectionProcess()
360 {
361  // Skip if already active
362  if (detection_active_) {
363  return true;
364  }
365 
366  // 1. Service START request
367  if (detector_client_) {
368  auto req = std::make_shared<std_srvs::srv::Trigger::Request>();
369  try {
370  auto future = detector_client_->invoke(
371  req,
372  std::chrono::duration_cast<std::chrono::nanoseconds>(
373  std::chrono::duration<double>(detector_service_timeout_)));
374 
375  if (!future || !future->success) {
376  RCLCPP_ERROR(
377  node_->get_logger(), "Detector service '%s' failed to start.",
378  detector_service_name_.c_str());
379  return false;
380  }
381  } catch (const std::exception & e) {
382  RCLCPP_ERROR(
383  node_->get_logger(), "Calling detector service '%s' failed: %s",
384  detector_service_name_.c_str(), e.what());
385  return false;
386  }
387  }
388 
389  // 2. Subscription toggle
390  // Only subscribe once; will set state to ON on first message
391  if (subscribe_toggle_ && !dock_pose_sub_) {
392  dock_pose_sub_ = node_->create_subscription<geometry_msgs::msg::PoseStamped>(
393  "detected_dock_pose",
394  [this](const geometry_msgs::msg::PoseStamped::SharedPtr pose) {
395  detected_dock_pose_ = *pose;
396  initial_pose_received_ = true;
397  },
399  }
400 
401  detection_active_ = true;
402  RCLCPP_INFO(node_->get_logger(), "External detector activation requested.");
403  return true;
404 }
405 
406 bool SimpleChargingDock::stopDetectionProcess()
407 {
408  // Skip if already OFF
409  if (!detection_active_) {
410  return true;
411  }
412 
413  // 1. Service STOP request
414  if (detector_client_) {
415  auto req = std::make_shared<std_srvs::srv::Trigger::Request>();
416  try {
417  auto future = detector_client_->invoke(
418  req,
419  std::chrono::duration_cast<std::chrono::nanoseconds>(
420  std::chrono::duration<double>(detector_service_timeout_)));
421 
422  if (!future || !future->success) {
423  RCLCPP_ERROR(
424  node_->get_logger(), "Detector service '%s' failed to stop.",
425  detector_service_name_.c_str());
426  return false;
427  }
428  } catch (const std::exception & e) {
429  RCLCPP_ERROR(
430  node_->get_logger(), "Calling detector service '%s' failed: %s",
431  detector_service_name_.c_str(), e.what());
432  return false;
433  }
434  }
435 
436  // 2. Unsubscribe to release resources
437  // reset() will tear down the topic subscription immediately
438  if (subscribe_toggle_ && dock_pose_sub_) {
439  dock_pose_sub_.reset();
440  }
441 
442  detection_active_ = false;
443  initial_pose_received_ = false;
444  RCLCPP_INFO(node_->get_logger(), "External detector deactivation requested.");
445  return true;
446 }
447 
448 void SimpleChargingDock::activate()
449 {
450  dock_pose_pub_->on_activate();
451  filtered_dock_pose_pub_->on_activate();
452  staging_pose_pub_->on_activate();
453 }
454 
455 void SimpleChargingDock::deactivate()
456 {
457  stopDetectionProcess();
458  dock_pose_pub_->on_deactivate();
459  filtered_dock_pose_pub_->on_deactivate();
460  staging_pose_pub_->on_deactivate();
461  RCLCPP_DEBUG(node_->get_logger(), "SimpleChargingDock deactivated");
462 }
463 
464 void SimpleChargingDock::cleanup()
465 {
466  detector_client_.reset();
467  dock_pose_sub_.reset();
468  detection_active_ = false;
469  initial_pose_received_ = false;
470  RCLCPP_DEBUG(node_->get_logger(), "SimpleChargingDock cleaned up");
471 }
472 
473 } // namespace opennav_docking
474 
475 #include "pluginlib/class_list_macros.hpp"
A QoS profile for standard reliable topics with a history of 10 messages.
Abstract interface for a charging dock for the docking framework.