Nav2 Navigation Stack - humble  humble
ROS 2 Navigation Stack
spin_behavior_tester.cpp
1 // Copyright (c) 2020 Sarthak Mittal
2 // Copyright (c) 2018 Intel Corporation
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License. Reserved.
15 
16 #include <string>
17 #include <random>
18 #include <tuple>
19 #include <memory>
20 #include <iostream>
21 #include <chrono>
22 #include <sstream>
23 #include <iomanip>
24 
25 #include "spin_behavior_tester.hpp"
26 
27 using namespace std::chrono_literals;
28 using namespace std::chrono; // NOLINT
29 
30 namespace nav2_system_tests
31 {
32 
33 SpinBehaviorTester::SpinBehaviorTester()
34 : is_active_(false),
35  initial_pose_received_(false)
36 {
37  node_ = rclcpp::Node::make_shared("spin_behavior_test");
38 
39 
40  tf_buffer_ = std::make_shared<tf2_ros::Buffer>(node_->get_clock());
41  tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);
42  tf_broadcaster_ = std::make_shared<tf2_ros::TransformBroadcaster>(node_);
43  if (std::getenv("MAKE_FAKE_COSTMAP") != NULL) {
44  // if this variable is set, make a fake costmap
45  make_fake_costmap_ = true;
46  } else {
47  make_fake_costmap_ = false;
48  }
49 
50  client_ptr_ = rclcpp_action::create_client<Spin>(
51  node_->get_node_base_interface(),
52  node_->get_node_graph_interface(),
53  node_->get_node_logging_interface(),
54  node_->get_node_waitables_interface(),
55  "spin");
56 
57  publisher_ =
58  node_->create_publisher<geometry_msgs::msg::PoseWithCovarianceStamped>("initialpose", 10);
59  fake_costmap_publisher_ =
60  node_->create_publisher<nav2_msgs::msg::Costmap>(
61  "local_costmap/costmap_raw",
62  rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable());
63  fake_footprint_publisher_ = node_->create_publisher<geometry_msgs::msg::PolygonStamped>(
64  "local_costmap/published_footprint", rclcpp::SystemDefaultsQoS());
65 
66  subscription_ = node_->create_subscription<geometry_msgs::msg::PoseWithCovarianceStamped>(
67  "amcl_pose", rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable(),
68  std::bind(&SpinBehaviorTester::amclPoseCallback, this, std::placeholders::_1));
69 
70  stamp_ = node_->now();
71 }
72 
73 SpinBehaviorTester::~SpinBehaviorTester()
74 {
75  if (is_active_) {
76  deactivate();
77  }
78 }
79 
80 void SpinBehaviorTester::activate()
81 {
82  if (is_active_) {
83  throw std::runtime_error("Trying to activate while already active");
84  return;
85  }
86  if (!make_fake_costmap_) {
87  while (!initial_pose_received_) {
88  RCLCPP_WARN(node_->get_logger(), "Initial pose not received");
89  sendInitialPose();
90  std::this_thread::sleep_for(100ms);
91  rclcpp::spin_some(node_);
92  }
93  } else {
94  sendFakeOdom(0.0);
95  }
96 
97  // Wait for lifecycle_manager_navigation to activate behavior_server
98  std::this_thread::sleep_for(10s);
99 
100  if (!client_ptr_) {
101  RCLCPP_ERROR(node_->get_logger(), "Action client not initialized");
102  is_active_ = false;
103  return;
104  }
105 
106  if (!client_ptr_->wait_for_action_server(10s)) {
107  RCLCPP_ERROR(node_->get_logger(), "Action server not available after waiting");
108  is_active_ = false;
109  return;
110  }
111 
112  RCLCPP_INFO(this->node_->get_logger(), "Spin action server is ready");
113  is_active_ = true;
114 }
115 
116 void SpinBehaviorTester::deactivate()
117 {
118  if (!is_active_) {
119  throw std::runtime_error("Trying to deactivate while already inactive");
120  }
121  is_active_ = false;
122 }
123 
124 bool SpinBehaviorTester::defaultSpinBehaviorTest(
125  const float target_yaw,
126  const double tolerance)
127 {
128  if (!is_active_) {
129  RCLCPP_ERROR(node_->get_logger(), "Not activated");
130  return false;
131  }
132 
133  // Sleep to let behavior server be ready for serving in multiple runs
134  std::this_thread::sleep_for(5s);
135 
136  if (make_fake_costmap_) {
137  sendFakeOdom(0.0);
138  }
139 
140  auto goal_msg = Spin::Goal();
141  goal_msg.target_yaw = target_yaw;
142 
143  // Initialize fake costmap
144  if (make_fake_costmap_) {
145  sendFakeCostmap(target_yaw);
146  sendFakeOdom(0.0);
147  }
148 
149  geometry_msgs::msg::PoseStamped initial_pose;
150  if (!nav2_util::getCurrentPose(initial_pose, *tf_buffer_, "odom")) {
151  RCLCPP_ERROR(node_->get_logger(), "Current robot pose is not available.");
152  return false;
153  }
154  RCLCPP_INFO(node_->get_logger(), "Found current robot pose");
155  RCLCPP_INFO(
156  node_->get_logger(),
157  "Init Yaw is %lf",
158  fabs(tf2::getYaw(initial_pose.pose.orientation)));
159  RCLCPP_INFO(node_->get_logger(), "Before sending goal");
160 
161  // Initialize fake costmap
162  if (make_fake_costmap_) {
163  sendFakeCostmap(target_yaw);
164  sendFakeOdom(0.0);
165  }
166 
167  rclcpp::sleep_for(std::chrono::milliseconds(100));
168 
169  auto goal_handle_future = client_ptr_->async_send_goal(goal_msg);
170 
171  if (rclcpp::spin_until_future_complete(node_, goal_handle_future) !=
172  rclcpp::FutureReturnCode::SUCCESS)
173  {
174  RCLCPP_ERROR(node_->get_logger(), "send goal call failed :(");
175  return false;
176  }
177 
178  rclcpp_action::ClientGoalHandle<Spin>::SharedPtr goal_handle = goal_handle_future.get();
179  if (!goal_handle) {
180  RCLCPP_ERROR(node_->get_logger(), "Goal was rejected by server");
181  return false;
182  }
183 
184  // Wait for the server to be done with the goal
185  auto result_future = client_ptr_->async_get_result(goal_handle);
186 
187  RCLCPP_INFO(node_->get_logger(), "Waiting for result");
188  rclcpp::sleep_for(std::chrono::milliseconds(1000));
189 
190  if (make_fake_costmap_) { // if we are faking the costmap, we will fake success.
191  sendFakeOdom(0.0);
192  sendFakeCostmap(target_yaw);
193  RCLCPP_INFO(node_->get_logger(), "target_yaw %lf", target_yaw);
194  // Slowly increment command yaw by increment to simulate the robot slowly spinning into place
195  float step_size = tolerance / 4.0;
196  for (float command_yaw = 0.0;
197  abs(command_yaw) < abs(target_yaw);
198  command_yaw = command_yaw + step_size)
199  {
200  sendFakeOdom(command_yaw);
201  sendFakeCostmap(target_yaw);
202  rclcpp::sleep_for(std::chrono::milliseconds(1));
203  }
204  sendFakeOdom(target_yaw);
205  sendFakeCostmap(target_yaw);
206  RCLCPP_INFO(node_->get_logger(), "After sending goal");
207  }
208  if (rclcpp::spin_until_future_complete(node_, result_future) !=
209  rclcpp::FutureReturnCode::SUCCESS)
210  {
211  RCLCPP_ERROR(node_->get_logger(), "get result call failed :(");
212  return false;
213  }
214 
215  rclcpp_action::ClientGoalHandle<Spin>::WrappedResult wrapped_result = result_future.get();
216 
217  switch (wrapped_result.code) {
218  case rclcpp_action::ResultCode::SUCCEEDED: break;
219  case rclcpp_action::ResultCode::ABORTED: RCLCPP_ERROR(
220  node_->get_logger(),
221  "Goal was aborted");
222  return false;
223  case rclcpp_action::ResultCode::CANCELED: RCLCPP_ERROR(
224  node_->get_logger(),
225  "Goal was canceled");
226  return false;
227  default: RCLCPP_ERROR(node_->get_logger(), "Unknown result code");
228  return false;
229  }
230 
231  RCLCPP_INFO(node_->get_logger(), "result received");
232 
233  geometry_msgs::msg::PoseStamped current_pose;
234  if (!nav2_util::getCurrentPose(current_pose, *tf_buffer_, "odom")) {
235  RCLCPP_ERROR(node_->get_logger(), "Current robot pose is not available.");
236  return false;
237  }
238 
239  double goal_yaw = angles::normalize_angle(
240  tf2::getYaw(initial_pose.pose.orientation) + target_yaw);
241  double dyaw = angles::shortest_angular_distance(
242  goal_yaw, tf2::getYaw(current_pose.pose.orientation));
243 
244  if (fabs(dyaw) > tolerance) {
245  RCLCPP_ERROR(
246  node_->get_logger(),
247  "Init Yaw is %lf (tolerance %lf)",
248  fabs(tf2::getYaw(initial_pose.pose.orientation)), tolerance);
249  RCLCPP_ERROR(
250  node_->get_logger(),
251  "Current Yaw is %lf (tolerance %lf)",
252  fabs(tf2::getYaw(current_pose.pose.orientation)), tolerance);
253  RCLCPP_ERROR(
254  node_->get_logger(),
255  "Angular distance from goal is %lf (tolerance %lf)",
256  fabs(dyaw), tolerance);
257  return false;
258  }
259 
260  return true;
261 }
262 
263 void SpinBehaviorTester::sendFakeCostmap(float angle)
264 {
265  nav2_msgs::msg::Costmap fake_costmap;
266 
267  fake_costmap.header.frame_id = "odom";
268  fake_costmap.header.stamp = stamp_;
269  fake_costmap.metadata.layer = "master";
270  fake_costmap.metadata.resolution = .1;
271  fake_costmap.metadata.size_x = 100;
272  fake_costmap.metadata.size_y = 100;
273  fake_costmap.metadata.origin.position.x = 0;
274  fake_costmap.metadata.origin.position.y = 0;
275  fake_costmap.metadata.origin.orientation.w = 1.0;
276  float costmap_val = 0;
277  for (int ix = 0; ix < 100; ix++) {
278  for (int iy = 0; iy < 100; iy++) {
279  if (abs(angle) > M_PI_2f32) {
280  // fake obstacles in the way so we get failure due to potential collision
281  costmap_val = 100;
282  }
283  fake_costmap.data.push_back(costmap_val);
284  }
285  }
286  fake_costmap_publisher_->publish(fake_costmap);
287 }
288 
289 void SpinBehaviorTester::sendInitialPose()
290 {
291  geometry_msgs::msg::PoseWithCovarianceStamped pose;
292  pose.header.frame_id = "map";
293  pose.header.stamp = stamp_;
294  pose.pose.pose.position.x = -2.0;
295  pose.pose.pose.position.y = -0.5;
296  pose.pose.pose.position.z = 0.0;
297  pose.pose.pose.orientation.x = 0.0;
298  pose.pose.pose.orientation.y = 0.0;
299  pose.pose.pose.orientation.z = 0.0;
300  pose.pose.pose.orientation.w = 1.0;
301  for (int i = 0; i < 35; i++) {
302  pose.pose.covariance[i] = 0.0;
303  }
304  pose.pose.covariance[0] = 0.08;
305  pose.pose.covariance[7] = 0.08;
306  pose.pose.covariance[35] = 0.05;
307 
308  publisher_->publish(pose);
309  RCLCPP_INFO(node_->get_logger(), "Sent initial pose");
310 }
311 
312 void SpinBehaviorTester::sendFakeOdom(float angle)
313 {
314  geometry_msgs::msg::TransformStamped transformStamped;
315 
316  transformStamped.header.stamp = stamp_;
317  transformStamped.header.frame_id = "odom";
318  transformStamped.child_frame_id = "base_link";
319  transformStamped.transform.translation.x = 0.0;
320  transformStamped.transform.translation.y = 0.0;
321  transformStamped.transform.translation.z = 0.0;
322  tf2::Quaternion q;
323  q.setRPY(0, 0, angle);
324  transformStamped.transform.rotation.x = q.x();
325  transformStamped.transform.rotation.y = q.y();
326  transformStamped.transform.rotation.z = q.z();
327  transformStamped.transform.rotation.w = q.w();
328 
329  tf_broadcaster_->sendTransform(transformStamped);
330 
331  geometry_msgs::msg::PolygonStamped footprint;
332  footprint.header.frame_id = "odom";
333  footprint.header.stamp = stamp_;
334  footprint.polygon.points.resize(4);
335  footprint.polygon.points[0].x = 0.22;
336  footprint.polygon.points[0].y = 0.22;
337  footprint.polygon.points[1].x = 0.22;
338  footprint.polygon.points[1].y = -0.22;
339  footprint.polygon.points[2].x = -0.22;
340  footprint.polygon.points[2].y = -0.22;
341  footprint.polygon.points[3].x = -0.22;
342  footprint.polygon.points[3].y = 0.22;
343  fake_footprint_publisher_->publish(footprint);
344 }
345 void SpinBehaviorTester::amclPoseCallback(
346  const geometry_msgs::msg::PoseWithCovarianceStamped::SharedPtr)
347 {
348  initial_pose_received_ = true;
349 }
350 
351 } // namespace nav2_system_tests