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 
17 #include "nav2_ros_common/node_utils.hpp"
18 #include "opennav_docking/simple_charging_dock.hpp"
19 #include "opennav_docking/utils.hpp"
20 
21 namespace opennav_docking
22 {
23 
25  const nav2::LifecycleNode::WeakPtr & parent,
26  const std::string & name, std::shared_ptr<tf2_ros::Buffer> tf)
27 {
28  name_ = name;
29  tf2_buffer_ = tf;
30  is_charging_ = false;
31  node_ = parent.lock();
32  if (!node_) {
33  throw std::runtime_error{"Failed to lock node"};
34  }
35 
36  // Optionally use battery info to check when charging, else say charging if docked
37  nav2::declare_parameter_if_not_declared(
38  node_, name + ".use_battery_status", rclcpp::ParameterValue(true));
39 
40  // Parameters for optional external detection of dock pose
41  nav2::declare_parameter_if_not_declared(
42  node_, name + ".use_external_detection_pose", rclcpp::ParameterValue(false));
43  nav2::declare_parameter_if_not_declared(
44  node_, name + ".external_detection_timeout", rclcpp::ParameterValue(1.0));
45  nav2::declare_parameter_if_not_declared(
46  node_, name + ".external_detection_translation_x", rclcpp::ParameterValue(-0.20));
47  nav2::declare_parameter_if_not_declared(
48  node_, name + ".external_detection_translation_y", rclcpp::ParameterValue(0.0));
49  nav2::declare_parameter_if_not_declared(
50  node_, name + ".external_detection_rotation_yaw", rclcpp::ParameterValue(0.0));
51  nav2::declare_parameter_if_not_declared(
52  node_, name + ".external_detection_rotation_pitch", rclcpp::ParameterValue(1.57));
53  nav2::declare_parameter_if_not_declared(
54  node_, name + ".external_detection_rotation_roll", rclcpp::ParameterValue(-1.57));
55  nav2::declare_parameter_if_not_declared(
56  node_, name + ".filter_coef", rclcpp::ParameterValue(0.1));
57 
58  // Charging threshold from BatteryState message
59  nav2::declare_parameter_if_not_declared(
60  node_, name + ".charging_threshold", rclcpp::ParameterValue(0.5));
61 
62  // Optionally determine if docked via stall detection using joint_states
63  nav2::declare_parameter_if_not_declared(
64  node_, name + ".use_stall_detection", rclcpp::ParameterValue(false));
65  nav2::declare_parameter_if_not_declared(
66  node_, name + ".stall_joint_names", rclcpp::PARAMETER_STRING_ARRAY);
67  nav2::declare_parameter_if_not_declared(
68  node_, name + ".stall_velocity_threshold", rclcpp::ParameterValue(1.0));
69  nav2::declare_parameter_if_not_declared(
70  node_, name + ".stall_effort_threshold", rclcpp::ParameterValue(1.0));
71 
72  // If not using stall detection, this is how close robot should get to pose
73  nav2::declare_parameter_if_not_declared(
74  node_, name + ".docking_threshold", rclcpp::ParameterValue(0.05));
75 
76  // Staging pose configuration
77  nav2::declare_parameter_if_not_declared(
78  node_, name + ".staging_x_offset", rclcpp::ParameterValue(-0.7));
79  nav2::declare_parameter_if_not_declared(
80  node_, name + ".staging_yaw_offset", rclcpp::ParameterValue(0.0));
81 
82  // Direction of docking and if we should rotate to dock
83  nav2::declare_parameter_if_not_declared(
84  node_, name + ".dock_direction", rclcpp::ParameterValue(std::string("forward")));
85  nav2::declare_parameter_if_not_declared(
86  node_, name + ".rotate_to_dock", rclcpp::ParameterValue(false));
87 
88  node_->get_parameter(name + ".use_battery_status", use_battery_status_);
89  node_->get_parameter(name + ".use_external_detection_pose", use_external_detection_pose_);
90  node_->get_parameter(name + ".external_detection_timeout", external_detection_timeout_);
91  node_->get_parameter(
92  name + ".external_detection_translation_x", external_detection_translation_x_);
93  node_->get_parameter(
94  name + ".external_detection_translation_y", external_detection_translation_y_);
95  double yaw, pitch, roll;
96  node_->get_parameter(name + ".external_detection_rotation_yaw", yaw);
97  node_->get_parameter(name + ".external_detection_rotation_pitch", pitch);
98  node_->get_parameter(name + ".external_detection_rotation_roll", roll);
99  external_detection_rotation_.setEuler(pitch, roll, yaw);
100  node_->get_parameter(name + ".charging_threshold", charging_threshold_);
101  node_->get_parameter(name + ".stall_velocity_threshold", stall_velocity_threshold_);
102  node_->get_parameter(name + ".stall_effort_threshold", stall_effort_threshold_);
103  node_->get_parameter(name + ".docking_threshold", docking_threshold_);
104  node_->get_parameter("base_frame", base_frame_id_); // Get server base frame ID
105  node_->get_parameter(name + ".staging_x_offset", staging_x_offset_);
106  node_->get_parameter(name + ".staging_yaw_offset", staging_yaw_offset_);
107 
108  std::string dock_direction;
109  node_->get_parameter(name + ".dock_direction", dock_direction);
110  dock_direction_ = utils::getDockDirectionFromString(dock_direction);
111  if (dock_direction_ == opennav_docking_core::DockDirection::UNKNOWN) {
112  throw std::runtime_error{"Dock direction is not valid. Valid options are: forward or backward"};
113  }
114 
115  node_->get_parameter(name + ".rotate_to_dock", rotate_to_dock_);
116  if (rotate_to_dock_ && dock_direction_ != opennav_docking_core::DockDirection::BACKWARD) {
117  throw std::runtime_error{"Parameter rotate_to_dock is enabled but dock direction is not "
118  "backward. Please set dock direction to backward."};
119  }
120 
121  // Setup filter
122  double filter_coef;
123  node_->get_parameter(name + ".filter_coef", filter_coef);
124  filter_ = std::make_unique<PoseFilter>(filter_coef, external_detection_timeout_);
125 
126  if (use_battery_status_) {
127  battery_sub_ = node_->create_subscription<sensor_msgs::msg::BatteryState>(
128  "battery_state",
129  [this](const sensor_msgs::msg::BatteryState::SharedPtr state) {
130  is_charging_ = state->current > charging_threshold_;
132  }
133 
134  if (use_external_detection_pose_) {
135  dock_pose_.header.stamp = rclcpp::Time(0);
136  dock_pose_sub_ = node_->create_subscription<geometry_msgs::msg::PoseStamped>(
137  "detected_dock_pose",
138  [this](const geometry_msgs::msg::PoseStamped::SharedPtr pose) {
139  detected_dock_pose_ = *pose;
140  }, nav2::qos::StandardTopicQoS(1)); // Only want the most recent pose
141  }
142 
143  bool use_stall_detection;
144  node_->get_parameter(name + ".use_stall_detection", use_stall_detection);
145  if (use_stall_detection) {
146  is_stalled_ = false;
147  node_->get_parameter(name + ".stall_joint_names", stall_joint_names_);
148  if (stall_joint_names_.size() < 1) {
149  RCLCPP_ERROR(node_->get_logger(), "stall_joint_names cannot be empty!");
150  }
151  joint_state_sub_ = node_->create_subscription<sensor_msgs::msg::JointState>(
152  "joint_states",
153  std::bind(&SimpleChargingDock::jointStateCallback, this, std::placeholders::_1),
155  }
156 
157  dock_pose_pub_ = node_->create_publisher<geometry_msgs::msg::PoseStamped>("dock_pose");
158  filtered_dock_pose_pub_ = node_->create_publisher<geometry_msgs::msg::PoseStamped>(
159  "filtered_dock_pose");
160  staging_pose_pub_ = node_->create_publisher<geometry_msgs::msg::PoseStamped>("staging_pose");
161 }
162 
163 geometry_msgs::msg::PoseStamped SimpleChargingDock::getStagingPose(
164  const geometry_msgs::msg::Pose & pose, const std::string & frame)
165 {
166  // If not using detection, set the dock pose as the given dock pose estimate
167  if (!use_external_detection_pose_) {
168  // This gets called at the start of docking
169  // Reset our internally tracked dock pose
170  dock_pose_.header.frame_id = frame;
171  dock_pose_.pose = pose;
172  }
173 
174  // Compute the staging pose with given offsets
175  const double yaw = tf2::getYaw(pose.orientation);
176  geometry_msgs::msg::PoseStamped staging_pose;
177  staging_pose.header.frame_id = frame;
178  staging_pose.header.stamp = node_->now();
179  staging_pose.pose = pose;
180  staging_pose.pose.position.x += cos(yaw) * staging_x_offset_;
181  staging_pose.pose.position.y += sin(yaw) * staging_x_offset_;
182  tf2::Quaternion orientation;
183  orientation.setEuler(0.0, 0.0, yaw + staging_yaw_offset_);
184  staging_pose.pose.orientation = tf2::toMsg(orientation);
185 
186  // Publish staging pose for debugging purposes
187  staging_pose_pub_->publish(staging_pose);
188  return staging_pose;
189 }
190 
191 bool SimpleChargingDock::getRefinedPose(geometry_msgs::msg::PoseStamped & pose, std::string /*id*/)
192 {
193  // If using not detection, set the dock pose to the static fixed-frame version
194  if (!use_external_detection_pose_) {
195  dock_pose_pub_->publish(pose);
196  dock_pose_ = pose;
197  return true;
198  }
199 
200  // If using detections, get current detections, transform to frame, and apply offsets
201  geometry_msgs::msg::PoseStamped detected = detected_dock_pose_;
202 
203  // Validate that external pose is new enough
204  auto timeout = rclcpp::Duration::from_seconds(external_detection_timeout_);
205  if (node_->now() - detected.header.stamp > timeout) {
206  RCLCPP_WARN(node_->get_logger(), "Lost detection or did not detect: timeout exceeded");
207  return false;
208  }
209 
210  // Transform detected pose into fixed frame. Note that the argument pose
211  // is the output of detection, but also acts as the initial estimate
212  // and contains the frame_id of docking
213  if (detected.header.frame_id != pose.header.frame_id) {
214  try {
215  if (!tf2_buffer_->canTransform(
216  pose.header.frame_id, detected.header.frame_id,
217  detected.header.stamp, rclcpp::Duration::from_seconds(0.2)))
218  {
219  RCLCPP_WARN(node_->get_logger(), "Failed to transform detected dock pose");
220  return false;
221  }
222  tf2_buffer_->transform(detected, detected, pose.header.frame_id);
223  } catch (const tf2::TransformException & ex) {
224  RCLCPP_WARN(node_->get_logger(), "Failed to transform detected dock pose");
225  return false;
226  }
227  }
228 
229  // Filter the detected pose
230  detected = filter_->update(detected);
231  filtered_dock_pose_pub_->publish(detected);
232 
233  // Rotate the just the orientation, then remove roll/pitch
234  geometry_msgs::msg::PoseStamped just_orientation;
235  just_orientation.pose.orientation = tf2::toMsg(external_detection_rotation_);
236  geometry_msgs::msg::TransformStamped transform;
237  transform.transform.rotation = detected.pose.orientation;
238  tf2::doTransform(just_orientation, just_orientation, transform);
239 
240  tf2::Quaternion orientation;
241  orientation.setEuler(0.0, 0.0, tf2::getYaw(just_orientation.pose.orientation));
242  dock_pose_.pose.orientation = tf2::toMsg(orientation);
243 
244  // Construct dock_pose_ by applying translation/rotation
245  dock_pose_.header = detected.header;
246  dock_pose_.pose.position = detected.pose.position;
247  const double yaw = tf2::getYaw(dock_pose_.pose.orientation);
248  dock_pose_.pose.position.x += cos(yaw) * external_detection_translation_x_ -
249  sin(yaw) * external_detection_translation_y_;
250  dock_pose_.pose.position.y += sin(yaw) * external_detection_translation_x_ +
251  cos(yaw) * external_detection_translation_y_;
252  dock_pose_.pose.position.z = 0.0;
253 
254  // Publish & return dock pose for debugging purposes
255  dock_pose_pub_->publish(dock_pose_);
256  pose = dock_pose_;
257  return true;
258 }
259 
261 {
262  if (joint_state_sub_) {
263  // Using stall detection
264  return is_stalled_;
265  }
266 
267  if (dock_pose_.header.frame_id.empty()) {
268  // Dock pose is not yet valid
269  return false;
270  }
271 
272  // Find base pose in target frame
273  geometry_msgs::msg::PoseStamped base_pose;
274  base_pose.header.stamp = rclcpp::Time(0);
275  base_pose.header.frame_id = base_frame_id_;
276  base_pose.pose.orientation.w = 1.0;
277  try {
278  tf2_buffer_->transform(base_pose, base_pose, dock_pose_.header.frame_id);
279  } catch (const tf2::TransformException & ex) {
280  return false;
281  }
282 
283  // If we are close enough, pretend we are charging
284  double d = std::hypot(
285  base_pose.pose.position.x - dock_pose_.pose.position.x,
286  base_pose.pose.position.y - dock_pose_.pose.position.y);
287  return d < docking_threshold_;
288 }
289 
291 {
292  return use_battery_status_ ? is_charging_ : isDocked();
293 }
294 
296 {
297  return true;
298 }
299 
301 {
302  return !isCharging();
303 }
304 
305 void SimpleChargingDock::jointStateCallback(const sensor_msgs::msg::JointState::SharedPtr state)
306 {
307  double velocity = 0.0;
308  double effort = 0.0;
309  for (size_t i = 0; i < state->name.size(); ++i) {
310  for (auto & name : stall_joint_names_) {
311  if (state->name[i] == name) {
312  // Tracking this joint
313  velocity += abs(state->velocity[i]);
314  effort += abs(state->effort[i]);
315  }
316  }
317  }
318 
319  // Take average
320  effort /= stall_joint_names_.size();
321  velocity /= stall_joint_names_.size();
322 
323  is_stalled_ = (velocity < stall_velocity_threshold_) && (effort > stall_effort_threshold_);
324 }
325 
326 } // namespace opennav_docking
327 
328 #include "pluginlib/class_list_macros.hpp"
A QoS profile for standard reliable topics with a history of 10 messages.
virtual geometry_msgs::msg::PoseStamped getStagingPose(const geometry_msgs::msg::Pose &pose, const std::string &frame)
Method to obtain the dock's staging pose. This method should likely be using TF and the dock's pose i...
virtual bool disableCharging()
Undocking while current is still flowing can damage a charge dock so some charge docks provide the ab...
virtual void configure(const nav2::LifecycleNode::WeakPtr &parent, const std::string &name, std::shared_ptr< tf2_ros::Buffer > tf)
virtual bool isCharging()
Are we charging? If a charge dock requires any sort of negotiation to begin charging,...
virtual bool isDocked()
Have we made contact with dock? This can be implemented in a variety of ways: by establishing communi...
virtual bool hasStoppedCharging()
Similar to isCharging() but called when undocking.
virtual bool getRefinedPose(geometry_msgs::msg::PoseStamped &pose, std::string id)
Method to obtain the refined pose of the dock, usually based on sensors.
Abstract interface for a charging dock for the docking framework.