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