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