add humble-navigation2
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
find_package(Boost COMPONENTS system filesystem REQUIRED)
|
||||
|
||||
ament_add_gtest(test_behavior_tree_node
|
||||
test_behavior_tree_node.cpp
|
||||
server_handler.cpp
|
||||
)
|
||||
|
||||
ament_target_dependencies(test_behavior_tree_node
|
||||
${dependencies}
|
||||
)
|
||||
|
||||
target_include_directories(test_behavior_tree_node PUBLIC ${Boost_INCLUDE_DIRS})
|
||||
target_link_libraries(test_behavior_tree_node
|
||||
${Boost_FILESYSTEM_LIBRARY}
|
||||
${Boost_SYSTEM_LIBRARY}
|
||||
)
|
||||
@@ -0,0 +1,207 @@
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#ifndef BEHAVIOR_TREE__DUMMY_SERVERS_HPP_
|
||||
#define BEHAVIOR_TREE__DUMMY_SERVERS_HPP_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
|
||||
#include "rclcpp_action/rclcpp_action.hpp"
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
|
||||
using namespace std::chrono_literals; // NOLINT
|
||||
using namespace std::chrono; // NOLINT
|
||||
using namespace std::placeholders; // NOLINT
|
||||
|
||||
template<class ServiceT>
|
||||
class DummyService
|
||||
{
|
||||
public:
|
||||
explicit DummyService(
|
||||
const rclcpp::Node::SharedPtr & node,
|
||||
std::string service_name)
|
||||
: node_(node),
|
||||
service_name_(service_name),
|
||||
request_count_(0),
|
||||
disabled_(false)
|
||||
{
|
||||
server_ = node->create_service<ServiceT>(
|
||||
service_name,
|
||||
std::bind(&DummyService::handle_service, this, _1, _2, _3));
|
||||
}
|
||||
|
||||
void disable()
|
||||
{
|
||||
server_.reset();
|
||||
disabled_ = true;
|
||||
}
|
||||
|
||||
void enable()
|
||||
{
|
||||
if (disabled_) {
|
||||
server_ = node_->create_service<ServiceT>(
|
||||
service_name_,
|
||||
std::bind(&DummyService::handle_service, this, _1, _2, _3));
|
||||
disabled_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
enable();
|
||||
request_count_ = 0;
|
||||
}
|
||||
|
||||
int getRequestCount() const
|
||||
{
|
||||
return request_count_;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void fillResponse(
|
||||
const std::shared_ptr<typename ServiceT::Request>/*request*/,
|
||||
const std::shared_ptr<typename ServiceT::Response>/*response*/) {}
|
||||
|
||||
void handle_service(
|
||||
const std::shared_ptr<rmw_request_id_t>/*request_header*/,
|
||||
const std::shared_ptr<typename ServiceT::Request> request,
|
||||
const std::shared_ptr<typename ServiceT::Response> response)
|
||||
{
|
||||
request_count_++;
|
||||
fillResponse(request, response);
|
||||
}
|
||||
|
||||
private:
|
||||
rclcpp::Node::SharedPtr node_;
|
||||
typename rclcpp::Service<ServiceT>::SharedPtr server_;
|
||||
std::string service_name_;
|
||||
int request_count_;
|
||||
bool disabled_;
|
||||
};
|
||||
|
||||
template<class ActionT>
|
||||
class DummyActionServer
|
||||
{
|
||||
public:
|
||||
explicit DummyActionServer(
|
||||
const rclcpp::Node::SharedPtr & node,
|
||||
std::string action_name)
|
||||
: action_name_(action_name),
|
||||
goal_count_(0)
|
||||
{
|
||||
this->action_server_ = rclcpp_action::create_server<ActionT>(
|
||||
node->get_node_base_interface(),
|
||||
node->get_node_clock_interface(),
|
||||
node->get_node_logging_interface(),
|
||||
node->get_node_waitables_interface(),
|
||||
action_name,
|
||||
std::bind(&DummyActionServer::handle_goal, this, _1, _2),
|
||||
std::bind(&DummyActionServer::handle_cancel, this, _1),
|
||||
std::bind(&DummyActionServer::handle_accepted, this, _1));
|
||||
}
|
||||
|
||||
void setFailureRanges(const std::vector<std::pair<int, int>> & failureRanges)
|
||||
{
|
||||
failure_ranges_ = failureRanges;
|
||||
}
|
||||
|
||||
void setRunningRanges(const std::vector<std::pair<int, int>> & runningRanges)
|
||||
{
|
||||
running_ranges_ = runningRanges;
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
failure_ranges_.clear();
|
||||
running_ranges_.clear();
|
||||
goal_count_ = 0;
|
||||
}
|
||||
|
||||
int getGoalCount() const
|
||||
{
|
||||
return goal_count_;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual std::shared_ptr<typename ActionT::Result> fillResult()
|
||||
{
|
||||
return std::make_shared<typename ActionT::Result>();
|
||||
}
|
||||
|
||||
virtual rclcpp_action::GoalResponse handle_goal(
|
||||
const rclcpp_action::GoalUUID &,
|
||||
std::shared_ptr<const typename ActionT::Goal>/*goal*/)
|
||||
{
|
||||
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
|
||||
}
|
||||
|
||||
virtual rclcpp_action::CancelResponse handle_cancel(
|
||||
const typename std::shared_ptr<rclcpp_action::ServerGoalHandle<ActionT>>)
|
||||
{
|
||||
return rclcpp_action::CancelResponse::ACCEPT;
|
||||
}
|
||||
|
||||
void execute(
|
||||
const typename std::shared_ptr<rclcpp_action::ServerGoalHandle<ActionT>> goal_handle)
|
||||
{
|
||||
goal_count_++;
|
||||
auto result = fillResult();
|
||||
|
||||
// if current goal index exists in running range, the thread sleeps for 1 second
|
||||
// to simulate a long running action
|
||||
for (auto & index : running_ranges_) {
|
||||
if (goal_count_ >= index.first && goal_count_ <= index.second) {
|
||||
std::this_thread::sleep_for(1s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if current goal index exists in failure range, the goal will be aborted
|
||||
for (auto & index : failure_ranges_) {
|
||||
if (goal_count_ >= index.first && goal_count_ <= index.second) {
|
||||
goal_handle->abort(result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// goal succeeds for all other indices
|
||||
goal_handle->succeed(result);
|
||||
}
|
||||
|
||||
void handle_accepted(
|
||||
const std::shared_ptr<rclcpp_action::ServerGoalHandle<ActionT>> goal_handle)
|
||||
{
|
||||
using namespace std::placeholders; // NOLINT
|
||||
// this needs to return quickly to avoid blocking the executor, so spin up a new thread
|
||||
std::thread{std::bind(&DummyActionServer::execute, this, _1), goal_handle}.detach();
|
||||
}
|
||||
|
||||
protected:
|
||||
typename rclcpp_action::Server<ActionT>::SharedPtr action_server_;
|
||||
std::string action_name_;
|
||||
|
||||
// contains pairs of indices which define a range for which the
|
||||
// requested action goal will return running for 1s or be aborted
|
||||
// for all other indices, the action server will return success
|
||||
std::vector<std::pair<int, int>> failure_ranges_;
|
||||
std::vector<std::pair<int, int>> running_ranges_;
|
||||
|
||||
int goal_count_;
|
||||
};
|
||||
|
||||
#endif // BEHAVIOR_TREE__DUMMY_SERVERS_HPP_
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2020 Vinny Ruia
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
#include "server_handler.hpp"
|
||||
|
||||
using namespace std::chrono_literals; // NOLINT
|
||||
using namespace std::chrono; // NOLINT
|
||||
|
||||
ServerHandler::ServerHandler()
|
||||
: is_active_(false)
|
||||
{
|
||||
node_ = rclcpp::Node::make_shared("behavior_tree_tester");
|
||||
|
||||
clear_local_costmap_server = std::make_unique<DummyService<nav2_msgs::srv::ClearEntireCostmap>>(
|
||||
node_, "local_costmap/clear_entirely_local_costmap");
|
||||
clear_global_costmap_server = std::make_unique<DummyService<nav2_msgs::srv::ClearEntireCostmap>>(
|
||||
node_, "global_costmap/clear_entirely_global_costmap");
|
||||
compute_path_to_pose_server = std::make_unique<ComputePathToPoseActionServer>(node_);
|
||||
follow_path_server = std::make_unique<DummyActionServer<nav2_msgs::action::FollowPath>>(
|
||||
node_, "follow_path");
|
||||
spin_server = std::make_unique<DummyActionServer<nav2_msgs::action::Spin>>(
|
||||
node_, "spin");
|
||||
wait_server = std::make_unique<DummyActionServer<nav2_msgs::action::Wait>>(
|
||||
node_, "wait");
|
||||
backup_server = std::make_unique<DummyActionServer<nav2_msgs::action::BackUp>>(
|
||||
node_, "backup");
|
||||
drive_on_heading_server = std::make_unique<DummyActionServer<nav2_msgs::action::DriveOnHeading>>(
|
||||
node_, "drive_on_heading");
|
||||
ntp_server = std::make_unique<DummyActionServer<nav2_msgs::action::ComputePathThroughPoses>>(
|
||||
node_, "compute_path_through_poses");
|
||||
}
|
||||
|
||||
ServerHandler::~ServerHandler()
|
||||
{
|
||||
if (is_active_) {
|
||||
deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
void ServerHandler::activate()
|
||||
{
|
||||
if (is_active_) {
|
||||
throw std::runtime_error("Trying to activate while already activated");
|
||||
}
|
||||
|
||||
is_active_ = true;
|
||||
server_thread_ =
|
||||
std::make_shared<std::thread>(std::bind(&ServerHandler::spinThread, this));
|
||||
|
||||
std::cout << "Server handler is active!" << std::endl;
|
||||
}
|
||||
|
||||
void ServerHandler::deactivate()
|
||||
{
|
||||
if (!is_active_) {
|
||||
throw std::runtime_error("Trying to deactivate while already inactive");
|
||||
}
|
||||
|
||||
is_active_ = false;
|
||||
server_thread_->join();
|
||||
|
||||
std::cout << "Server handler has been deactivated!" << std::endl;
|
||||
}
|
||||
|
||||
void ServerHandler::reset() const
|
||||
{
|
||||
clear_global_costmap_server->reset();
|
||||
clear_local_costmap_server->reset();
|
||||
compute_path_to_pose_server->reset();
|
||||
follow_path_server->reset();
|
||||
spin_server->reset();
|
||||
wait_server->reset();
|
||||
backup_server->reset();
|
||||
drive_on_heading_server->reset();
|
||||
}
|
||||
|
||||
void ServerHandler::spinThread()
|
||||
{
|
||||
rclcpp::spin(node_);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2020 Vinny Ruia
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#ifndef BEHAVIOR_TREE__SERVER_HANDLER_HPP_
|
||||
#define BEHAVIOR_TREE__SERVER_HANDLER_HPP_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "nav2_msgs/srv/clear_entire_costmap.hpp"
|
||||
#include "nav2_msgs/action/compute_path_to_pose.hpp"
|
||||
#include "nav2_msgs/action/follow_path.hpp"
|
||||
#include "nav2_msgs/action/spin.hpp"
|
||||
#include "nav2_msgs/action/back_up.hpp"
|
||||
#include "nav2_msgs/action/wait.hpp"
|
||||
#include "nav2_msgs/action/drive_on_heading.hpp"
|
||||
#include "nav2_msgs/action/compute_path_through_poses.hpp"
|
||||
|
||||
#include "geometry_msgs/msg/point_stamped.hpp"
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
|
||||
#include "dummy_servers.hpp"
|
||||
|
||||
class ComputePathToPoseActionServer
|
||||
: public DummyActionServer<nav2_msgs::action::ComputePathToPose>
|
||||
{
|
||||
public:
|
||||
explicit ComputePathToPoseActionServer(const rclcpp::Node::SharedPtr & node)
|
||||
: DummyActionServer(node, "compute_path_to_pose")
|
||||
{
|
||||
result_ = std::make_shared<nav2_msgs::action::ComputePathToPose::Result>();
|
||||
geometry_msgs::msg::PoseStamped pose;
|
||||
pose.header = result_->path.header;
|
||||
pose.pose.position.x = 0.0;
|
||||
pose.pose.position.y = 0.0;
|
||||
pose.pose.position.z = 0.0;
|
||||
pose.pose.orientation.x = 0.0;
|
||||
pose.pose.orientation.y = 0.0;
|
||||
pose.pose.orientation.z = 0.0;
|
||||
pose.pose.orientation.w = 1.0;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
result_->path.poses.push_back(pose);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<nav2_msgs::action::ComputePathToPose::Result> fillResult() override
|
||||
{
|
||||
return result_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<nav2_msgs::action::ComputePathToPose::Result> result_;
|
||||
};
|
||||
|
||||
class ServerHandler
|
||||
{
|
||||
public:
|
||||
ServerHandler();
|
||||
~ServerHandler();
|
||||
|
||||
void activate();
|
||||
|
||||
void deactivate();
|
||||
|
||||
bool isActive() const
|
||||
{
|
||||
return is_active_;
|
||||
}
|
||||
|
||||
void reset() const;
|
||||
|
||||
public:
|
||||
std::unique_ptr<DummyService<nav2_msgs::srv::ClearEntireCostmap>> clear_local_costmap_server;
|
||||
std::unique_ptr<DummyService<nav2_msgs::srv::ClearEntireCostmap>> clear_global_costmap_server;
|
||||
std::unique_ptr<ComputePathToPoseActionServer> compute_path_to_pose_server;
|
||||
std::unique_ptr<DummyActionServer<nav2_msgs::action::FollowPath>> follow_path_server;
|
||||
std::unique_ptr<DummyActionServer<nav2_msgs::action::Spin>> spin_server;
|
||||
std::unique_ptr<DummyActionServer<nav2_msgs::action::Wait>> wait_server;
|
||||
std::unique_ptr<DummyActionServer<nav2_msgs::action::BackUp>> backup_server;
|
||||
std::unique_ptr<DummyActionServer<nav2_msgs::action::DriveOnHeading>> drive_on_heading_server;
|
||||
std::unique_ptr<DummyActionServer<nav2_msgs::action::ComputePathThroughPoses>> ntp_server;
|
||||
|
||||
private:
|
||||
void spinThread();
|
||||
|
||||
bool is_active_;
|
||||
rclcpp::Node::SharedPtr node_;
|
||||
std::shared_ptr<std::thread> server_thread_;
|
||||
};
|
||||
|
||||
#endif // BEHAVIOR_TREE__SERVER_HANDLER_HPP_
|
||||
@@ -0,0 +1,656 @@
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "behaviortree_cpp_v3/behavior_tree.h"
|
||||
#include "behaviortree_cpp_v3/bt_factory.h"
|
||||
#include "behaviortree_cpp_v3/utils/shared_library.h"
|
||||
|
||||
#include "tf2_ros/buffer.h"
|
||||
#include "tf2_ros/transform_listener.h"
|
||||
#include "tf2_ros/create_timer_ros.h"
|
||||
|
||||
#include "nav2_util/odometry_utils.hpp"
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "ament_index_cpp/get_package_share_directory.hpp"
|
||||
|
||||
#include "server_handler.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
class BehaviorTreeHandler
|
||||
{
|
||||
public:
|
||||
BehaviorTreeHandler()
|
||||
{
|
||||
node_ = rclcpp::Node::make_shared("behavior_tree_handler");
|
||||
|
||||
tf_ = std::make_shared<tf2_ros::Buffer>(node_->get_clock());
|
||||
auto timer_interface = std::make_shared<tf2_ros::CreateTimerROS>(
|
||||
node_->get_node_base_interface(), node_->get_node_timers_interface());
|
||||
tf_->setCreateTimerInterface(timer_interface);
|
||||
tf_->setUsingDedicatedThread(true);
|
||||
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_, node_, false);
|
||||
|
||||
odom_smoother_ = std::make_shared<nav2_util::OdomSmoother>(node_);
|
||||
|
||||
const std::vector<std::string> plugin_libs = {
|
||||
"nav2_compute_path_to_pose_action_bt_node",
|
||||
"nav2_compute_path_through_poses_action_bt_node",
|
||||
"nav2_smooth_path_action_bt_node",
|
||||
"nav2_follow_path_action_bt_node",
|
||||
"nav2_spin_action_bt_node",
|
||||
"nav2_wait_action_bt_node",
|
||||
"nav2_assisted_teleop_action_bt_node",
|
||||
"nav2_back_up_action_bt_node",
|
||||
"nav2_drive_on_heading_bt_node",
|
||||
"nav2_clear_costmap_service_bt_node",
|
||||
"nav2_is_stuck_condition_bt_node",
|
||||
"nav2_goal_reached_condition_bt_node",
|
||||
"nav2_initial_pose_received_condition_bt_node",
|
||||
"nav2_goal_updated_condition_bt_node",
|
||||
"nav2_globally_updated_goal_condition_bt_node",
|
||||
"nav2_is_path_valid_condition_bt_node",
|
||||
"nav2_reinitialize_global_localization_service_bt_node",
|
||||
"nav2_rate_controller_bt_node",
|
||||
"nav2_distance_controller_bt_node",
|
||||
"nav2_speed_controller_bt_node",
|
||||
"nav2_truncate_path_action_bt_node",
|
||||
"nav2_truncate_path_local_action_bt_node",
|
||||
"nav2_goal_updater_node_bt_node",
|
||||
"nav2_recovery_node_bt_node",
|
||||
"nav2_pipeline_sequence_bt_node",
|
||||
"nav2_round_robin_node_bt_node",
|
||||
"nav2_transform_available_condition_bt_node",
|
||||
"nav2_time_expired_condition_bt_node",
|
||||
"nav2_path_expiring_timer_condition",
|
||||
"nav2_distance_traveled_condition_bt_node",
|
||||
"nav2_single_trigger_bt_node",
|
||||
"nav2_is_battery_low_condition_bt_node",
|
||||
"nav2_navigate_through_poses_action_bt_node",
|
||||
"nav2_navigate_to_pose_action_bt_node",
|
||||
"nav2_remove_passed_goals_action_bt_node",
|
||||
"nav2_planner_selector_bt_node",
|
||||
"nav2_controller_selector_bt_node",
|
||||
"nav2_goal_checker_selector_bt_node",
|
||||
"nav2_controller_cancel_bt_node",
|
||||
"nav2_path_longer_on_approach_bt_node",
|
||||
"nav2_assisted_teleop_cancel_bt_node",
|
||||
"nav2_wait_cancel_bt_node",
|
||||
"nav2_spin_cancel_bt_node",
|
||||
"nav2_back_up_cancel_bt_node",
|
||||
"nav2_drive_on_heading_cancel_bt_node",
|
||||
"nav2_goal_updated_controller_bt_node"
|
||||
};
|
||||
for (const auto & p : plugin_libs) {
|
||||
factory_.registerFromPlugin(BT::SharedLibrary::getOSName(p));
|
||||
}
|
||||
}
|
||||
|
||||
bool loadBehaviorTree(const std::string & filename)
|
||||
{
|
||||
// Read the input BT XML from the specified file into a string
|
||||
std::ifstream xml_file(filename);
|
||||
|
||||
if (!xml_file.good()) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Couldn't open input XML file: %s", filename.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto xml_string = std::string(
|
||||
std::istreambuf_iterator<char>(xml_file),
|
||||
std::istreambuf_iterator<char>());
|
||||
|
||||
// Create the blackboard that will be shared by all of the nodes in the tree
|
||||
blackboard = BT::Blackboard::create();
|
||||
|
||||
// Put items on the blackboard
|
||||
blackboard->set<rclcpp::Node::SharedPtr>("node", node_); // NOLINT
|
||||
blackboard->set<std::chrono::milliseconds>(
|
||||
"server_timeout", std::chrono::milliseconds(20)); // NOLINT
|
||||
blackboard->set<std::chrono::milliseconds>(
|
||||
"bt_loop_duration", std::chrono::milliseconds(10)); // NOLINT
|
||||
blackboard->set<std::chrono::milliseconds>(
|
||||
"wait_for_service_timeout", std::chrono::milliseconds(1000)); // NOLINT
|
||||
blackboard->set<std::shared_ptr<tf2_ros::Buffer>>("tf_buffer", tf_); // NOLINT
|
||||
blackboard->set<bool>("initial_pose_received", false); // NOLINT
|
||||
blackboard->set<int>("number_recoveries", 0); // NOLINT
|
||||
blackboard->set<std::shared_ptr<nav2_util::OdomSmoother>>("odom_smoother", odom_smoother_); // NOLINT
|
||||
|
||||
// set dummy goal on blackboard
|
||||
geometry_msgs::msg::PoseStamped goal;
|
||||
goal.header.stamp = node_->now();
|
||||
goal.header.frame_id = "map";
|
||||
goal.pose.position.x = 0.0;
|
||||
goal.pose.position.y = 0.0;
|
||||
goal.pose.position.z = 0.0;
|
||||
goal.pose.orientation.x = 0.0;
|
||||
goal.pose.orientation.y = 0.0;
|
||||
goal.pose.orientation.z = 0.0;
|
||||
goal.pose.orientation.w = 1.0;
|
||||
|
||||
blackboard->set<geometry_msgs::msg::PoseStamped>("goal", goal); // NOLINT
|
||||
|
||||
// Create the Behavior Tree from the XML input
|
||||
try {
|
||||
tree = factory_.createTreeFromText(xml_string, blackboard);
|
||||
} catch (BT::RuntimeError & exp) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "%s: %s", filename.c_str(), exp.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public:
|
||||
BT::Blackboard::Ptr blackboard;
|
||||
BT::Tree tree;
|
||||
|
||||
private:
|
||||
rclcpp::Node::SharedPtr node_;
|
||||
|
||||
std::shared_ptr<tf2_ros::Buffer> tf_;
|
||||
std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
|
||||
|
||||
std::shared_ptr<nav2_util::OdomSmoother> odom_smoother_;
|
||||
|
||||
BT::BehaviorTreeFactory factory_;
|
||||
};
|
||||
|
||||
class BehaviorTreeTestFixture : public ::testing::Test
|
||||
{
|
||||
public:
|
||||
static void SetUpTestCase()
|
||||
{
|
||||
// initialize ROS
|
||||
rclcpp::init(0, nullptr);
|
||||
|
||||
server_handler = std::make_shared<ServerHandler>();
|
||||
if (!server_handler->isActive()) {
|
||||
server_handler->activate();
|
||||
}
|
||||
}
|
||||
|
||||
static void TearDownTestCase()
|
||||
{
|
||||
// shutdown ROS
|
||||
rclcpp::shutdown();
|
||||
|
||||
server_handler.reset();
|
||||
bt_handler.reset();
|
||||
}
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
server_handler->reset();
|
||||
bt_handler = std::make_shared<BehaviorTreeHandler>();
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
bt_handler.reset();
|
||||
}
|
||||
|
||||
protected:
|
||||
static std::shared_ptr<ServerHandler> server_handler;
|
||||
static std::shared_ptr<BehaviorTreeHandler> bt_handler;
|
||||
};
|
||||
|
||||
std::shared_ptr<ServerHandler> BehaviorTreeTestFixture::server_handler = nullptr;
|
||||
std::shared_ptr<BehaviorTreeHandler> BehaviorTreeTestFixture::bt_handler = nullptr;
|
||||
|
||||
TEST_F(BehaviorTreeTestFixture, TestBTXMLFiles)
|
||||
{
|
||||
fs::path root = ament_index_cpp::get_package_share_directory("nav2_bt_navigator");
|
||||
root /= "behavior_trees/";
|
||||
|
||||
if (boost::filesystem::exists(root) && boost::filesystem::is_directory(root)) {
|
||||
for (auto const & entry : boost::filesystem::recursive_directory_iterator(root)) {
|
||||
if (boost::filesystem::is_regular_file(entry) && entry.path().extension() == ".xml") {
|
||||
std::cout << entry.path().string() << std::endl;
|
||||
EXPECT_EQ(bt_handler->loadBehaviorTree(entry.path().string()), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test scenario:
|
||||
*
|
||||
* ComputePathToPose and FollowPath return SUCCESS
|
||||
* The behavior tree should execute correctly and return SUCCESS
|
||||
*/
|
||||
TEST_F(BehaviorTreeTestFixture, TestAllSuccess)
|
||||
{
|
||||
// Load behavior tree from file
|
||||
fs::path bt_file = ament_index_cpp::get_package_share_directory("nav2_bt_navigator");
|
||||
bt_file /= "behavior_trees/";
|
||||
bt_file /= "navigate_to_pose_w_replanning_and_recovery.xml";
|
||||
EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string()), true);
|
||||
|
||||
BT::NodeStatus result = BT::NodeStatus::RUNNING;
|
||||
|
||||
while (result == BT::NodeStatus::RUNNING) {
|
||||
result = bt_handler->tree.tickRoot();
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
|
||||
// The final result should be success since all action servers returned success
|
||||
EXPECT_EQ(result, BT::NodeStatus::SUCCESS);
|
||||
|
||||
// Goal count should be 1 since only one goal is sent to ComputePathToPose and FollowPath servers
|
||||
EXPECT_EQ(server_handler->compute_path_to_pose_server->getGoalCount(), 1);
|
||||
EXPECT_EQ(server_handler->follow_path_server->getGoalCount(), 1);
|
||||
|
||||
// Goal count should be 0 since no goal is sent to all other servers
|
||||
EXPECT_EQ(server_handler->spin_server->getGoalCount(), 0);
|
||||
EXPECT_EQ(server_handler->wait_server->getGoalCount(), 0);
|
||||
EXPECT_EQ(server_handler->backup_server->getGoalCount(), 0);
|
||||
EXPECT_EQ(server_handler->clear_local_costmap_server->getRequestCount(), 0);
|
||||
EXPECT_EQ(server_handler->clear_global_costmap_server->getRequestCount(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test scenario:
|
||||
*
|
||||
* ComputePathToPose returns FAILURE and ClearGlobalCostmap-Context returns FAILURE
|
||||
* PipelineSequence returns FAILURE and NavigateRecovery triggers RecoveryFallback
|
||||
* GoalUpdated returns FAILURE and RoundRobin is triggered
|
||||
* RoundRobin triggers ClearingActions Sequence which returns FAILURE
|
||||
* RoundRobin triggers Spin, Wait, and BackUp which return FAILURE
|
||||
* RoundRobin returns FAILURE hence RecoveryCallbackk returns FAILURE
|
||||
* Finally NavigateRecovery returns FAILURE
|
||||
* The behavior tree should also return FAILURE
|
||||
*/
|
||||
TEST_F(BehaviorTreeTestFixture, TestAllFailure)
|
||||
{
|
||||
// Load behavior tree from file
|
||||
fs::path bt_file = ament_index_cpp::get_package_share_directory("nav2_bt_navigator");
|
||||
bt_file /= "behavior_trees/";
|
||||
bt_file /= "navigate_to_pose_w_replanning_and_recovery.xml";
|
||||
EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string()), true);
|
||||
|
||||
// Set all action server to fail the first 100 times
|
||||
std::vector<std::pair<int, int>> failureRange;
|
||||
failureRange.emplace_back(std::pair<int, int>(0, 100));
|
||||
server_handler->compute_path_to_pose_server->setFailureRanges(failureRange);
|
||||
server_handler->follow_path_server->setFailureRanges(failureRange);
|
||||
server_handler->spin_server->setFailureRanges(failureRange);
|
||||
server_handler->wait_server->setFailureRanges(failureRange);
|
||||
server_handler->backup_server->setFailureRanges(failureRange);
|
||||
|
||||
// Disable services
|
||||
server_handler->clear_global_costmap_server->disable();
|
||||
server_handler->clear_local_costmap_server->disable();
|
||||
|
||||
BT::NodeStatus result = BT::NodeStatus::RUNNING;
|
||||
|
||||
while (result == BT::NodeStatus::RUNNING) {
|
||||
result = bt_handler->tree.tickRoot();
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
|
||||
// The final result should be failure
|
||||
EXPECT_EQ(result, BT::NodeStatus::FAILURE);
|
||||
|
||||
// Goal count should be 1 since only one goal is sent to ComputePathToPose
|
||||
EXPECT_EQ(server_handler->compute_path_to_pose_server->getGoalCount(), 1);
|
||||
|
||||
// Goal count should be 0 since no goal is sent to FollowPath action server
|
||||
EXPECT_EQ(server_handler->follow_path_server->getGoalCount(), 0);
|
||||
|
||||
// All recovery action servers were sent 1 goal
|
||||
EXPECT_EQ(server_handler->spin_server->getGoalCount(), 1);
|
||||
EXPECT_EQ(server_handler->wait_server->getGoalCount(), 1);
|
||||
EXPECT_EQ(server_handler->backup_server->getGoalCount(), 1);
|
||||
|
||||
// Service count is 0 since the server was disabled
|
||||
EXPECT_EQ(server_handler->clear_local_costmap_server->getRequestCount(), 0);
|
||||
EXPECT_EQ(server_handler->clear_global_costmap_server->getRequestCount(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test scenario:
|
||||
*
|
||||
* ComputePathToPose returns FAILURE on the first try triggering the planner recovery
|
||||
* ClearGlobalCostmap-Context returns SUCCESS and ComputePathToPose returns SUCCESS when retried
|
||||
* FollowPath returns FAILURE on the first try triggering the controller recovery
|
||||
* ClearLocalCostmap-Context returns SUCCESS and FollowPath returns SUCCESS when retried
|
||||
* The behavior tree should return SUCCESS
|
||||
*/
|
||||
TEST_F(BehaviorTreeTestFixture, TestNavigateSubtreeRecoveries)
|
||||
{
|
||||
// Load behavior tree from file
|
||||
fs::path bt_file = ament_index_cpp::get_package_share_directory("nav2_bt_navigator");
|
||||
bt_file /= "behavior_trees/";
|
||||
bt_file /= "navigate_to_pose_w_replanning_and_recovery.xml";
|
||||
EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string()), true);
|
||||
|
||||
// Set ComputePathToPose and FollowPath action servers to fail for the first action
|
||||
std::vector<std::pair<int, int>> failureRange;
|
||||
failureRange.emplace_back(std::pair<int, int>(0, 1));
|
||||
server_handler->compute_path_to_pose_server->setFailureRanges(failureRange);
|
||||
server_handler->follow_path_server->setFailureRanges(failureRange);
|
||||
|
||||
BT::NodeStatus result = BT::NodeStatus::RUNNING;
|
||||
|
||||
while (result == BT::NodeStatus::RUNNING) {
|
||||
result = bt_handler->tree.tickRoot();
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
|
||||
// The final result should be success
|
||||
EXPECT_EQ(result, BT::NodeStatus::SUCCESS);
|
||||
|
||||
// Goal count should be 2 since only two goals were sent to ComputePathToPose and FollowPath
|
||||
EXPECT_EQ(server_handler->compute_path_to_pose_server->getGoalCount(), 2);
|
||||
EXPECT_EQ(server_handler->follow_path_server->getGoalCount(), 2);
|
||||
|
||||
// Navigate subtree recovery services are called once each
|
||||
EXPECT_EQ(server_handler->clear_local_costmap_server->getRequestCount(), 1);
|
||||
EXPECT_EQ(server_handler->clear_global_costmap_server->getRequestCount(), 1);
|
||||
|
||||
// Goal count should be 0 since no goal is sent to all other servers
|
||||
EXPECT_EQ(server_handler->spin_server->getGoalCount(), 0);
|
||||
EXPECT_EQ(server_handler->wait_server->getGoalCount(), 0);
|
||||
EXPECT_EQ(server_handler->backup_server->getGoalCount(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test scenario:
|
||||
*
|
||||
* ComputePathToPose returns FAILURE on the first try triggering the planner recovery
|
||||
* ClearGlobalCostmap-Context returns SUCCESS and ComputePathToPose returns SUCCESS when retried
|
||||
* FollowPath returns FAILURE on the first try triggering the controller recovery
|
||||
* ClearLocalCostmap-Context returns SUCCESS and FollowPath is retried
|
||||
* FollowPath returns FAILURE again and PipelineSequence returns FAILURE
|
||||
* NavigateRecovery triggers RecoveryFallback and GoalUpdated returns FAILURE
|
||||
* RoundRobin triggers ClearingActions Sequence which returns SUCCESS
|
||||
* RoundRobin returns SUCCESS and RecoveryFallback returns SUCCESS
|
||||
* PipelineSequence is triggered again and ComputePathToPose returns SUCCESS
|
||||
* FollowPath returns FAILURE on the third try triggering the controller recovery
|
||||
* ClearLocalCostmap-Context returns SUCCESS and FollowPath returns SUCCESS on the fourth try
|
||||
* The behavior tree should return SUCCESS
|
||||
*/
|
||||
TEST_F(BehaviorTreeTestFixture, TestNavigateRecoverySimple)
|
||||
{
|
||||
// Load behavior tree from file
|
||||
fs::path bt_file = ament_index_cpp::get_package_share_directory("nav2_bt_navigator");
|
||||
bt_file /= "behavior_trees/";
|
||||
bt_file /= "navigate_to_pose_w_replanning_and_recovery.xml";
|
||||
EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string()), true);
|
||||
|
||||
// Set ComputePathToPose action server to fail for the first action
|
||||
std::vector<std::pair<int, int>> plannerFailureRange;
|
||||
plannerFailureRange.emplace_back(std::pair<int, int>(0, 1));
|
||||
server_handler->compute_path_to_pose_server->setFailureRanges(plannerFailureRange);
|
||||
|
||||
// Set FollowPath action server to fail for the first 3 actions
|
||||
std::vector<std::pair<int, int>> controllerFailureRange;
|
||||
controllerFailureRange.emplace_back(std::pair<int, int>(0, 3));
|
||||
server_handler->follow_path_server->setFailureRanges(controllerFailureRange);
|
||||
|
||||
BT::NodeStatus result = BT::NodeStatus::RUNNING;
|
||||
|
||||
while (result == BT::NodeStatus::RUNNING) {
|
||||
result = bt_handler->tree.tickRoot();
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
|
||||
// The final result should be success
|
||||
EXPECT_EQ(result, BT::NodeStatus::SUCCESS);
|
||||
|
||||
// FollowPath is called 4 times
|
||||
EXPECT_EQ(server_handler->follow_path_server->getGoalCount(), 4);
|
||||
|
||||
// ComputePathToPose is called 3 times
|
||||
EXPECT_EQ(server_handler->compute_path_to_pose_server->getGoalCount(), 3);
|
||||
|
||||
// Local costmap is cleared 3 times
|
||||
EXPECT_EQ(server_handler->clear_local_costmap_server->getRequestCount(), 3);
|
||||
|
||||
// Global costmap is cleared 2 times
|
||||
EXPECT_EQ(server_handler->clear_global_costmap_server->getRequestCount(), 2);
|
||||
|
||||
// Goal count should be 0 since only no goal is sent to all other servers
|
||||
EXPECT_EQ(server_handler->spin_server->getGoalCount(), 0);
|
||||
EXPECT_EQ(server_handler->wait_server->getGoalCount(), 0);
|
||||
EXPECT_EQ(server_handler->backup_server->getGoalCount(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test scenario:
|
||||
*
|
||||
* ComputePathToPose returns FAILURE on the first try triggering the planner recovery
|
||||
* ClearGlobalCostmap-Context returns SUCCESS and ComputePathToPose returns FAILURE when retried
|
||||
* PipelineSequence returns FAILURE and NavigateRecovery triggers RecoveryFallback
|
||||
* GoalUpdated returns FAILURE, RoundRobin triggers ClearingActions Sequence which returns SUCCESS
|
||||
* RoundRobin returns SUCCESS and RecoveryFallback returns SUCCESS
|
||||
*
|
||||
* PipelineSequence is triggered again and ComputePathToPose returns SUCCESS (retry #1)
|
||||
* FollowPath returns FAILURE on the first try triggering the controller recovery
|
||||
* ClearLocalCostmap-Context returns SUCCESS and FollowPath is retried
|
||||
* FollowPath returns FAILURE again and PipelineSequence returns FAILURE
|
||||
* NavigateRecovery triggers RecoveryFallback and GoalUpdated returns FAILURE
|
||||
* RoundRobin triggers Spin which returns FAILURE
|
||||
* RoundRobin triggers Wait which returns SUCCESS
|
||||
* RoundRobin returns SUCCESS and RecoveryFallback returns SUCCESS
|
||||
*
|
||||
* PipelineSequence is triggered again and ComputePathToPose returns FAILURE (retry #2)
|
||||
* ClearGlobalCostmap-Context returns SUCCESS and ComputePathToPose returns FAILURE when retried
|
||||
* PipelineSequence returns FAILURE NavigateRecovery triggers RecoveryFallback
|
||||
* GoalUpdated returns FAILURE and RoundRobin triggers BackUp which returns FAILURE
|
||||
* RoundRobin triggers ClearingActions Sequence which returns SUCCESS
|
||||
* RoundRobin returns SUCCESS and RecoveryFallback returns SUCCESS
|
||||
*
|
||||
* PipelineSequence is triggered again and ComputePathToPose returns FAILURE (retry #3)
|
||||
* ClearGlobalCostmap-Context returns SUCCESS and ComputePathToPose returns FAILURE when retried
|
||||
* PipelineSequence returns FAILURE NavigateRecovery triggers RecoveryFallback
|
||||
* GoalUpdated returns FAILURE and RoundRobin triggers Spin which returns SUCCESS
|
||||
* RoundRobin returns SUCCESS and RecoveryFallback returns SUCCESS
|
||||
*
|
||||
* PipelineSequence is triggered again and ComputePathToPose returns FAILURE (retry #4)
|
||||
* ClearGlobalCostmap-Context returns SUCCESS and ComputePathToPose returns FAILURE when retried
|
||||
* PipelineSequence returns FAILURE NavigateRecovery triggers RecoveryFallback
|
||||
* GoalUpdated returns FAILURE and RoundRobin triggers Wait which returns FAILURE
|
||||
* RoundRobin triggers BackUp which returns SUCCESS
|
||||
* RoundRobin returns SUCCESS and RecoveryFallback returns SUCCESS
|
||||
*
|
||||
* PipelineSequence is triggered again and ComputePathToPose returns SUCCESS (retry #5)
|
||||
* FollowPath returns FAILURE on the first try triggering the controller recovery
|
||||
* ClearLocalCostmap-Context returns SUCCESS and FollowPath is retried
|
||||
* FollowPath returns FAILURE again and PipelineSequence returns FAILURE
|
||||
* NavigateRecovery triggers RecoveryFallback and GoalUpdated returns FAILURE
|
||||
* RoundRobin triggers ClearingActions Sequence which returns SUCCESS
|
||||
* RoundRobin returns SUCCESS and RecoveryFallback returns SUCCESS
|
||||
*
|
||||
* PipelineSequence is triggered again and ComputePathToPose returns FAILURE (retry #6)
|
||||
* ClearGlobalCostmap-Context returns SUCCESS and ComputePathToPose returns FAILURE when retried
|
||||
* PipelineSequence returns FAILURE and NavigateRecovery finally also returns FAILURE
|
||||
*
|
||||
* The behavior tree should return FAILURE
|
||||
*/
|
||||
TEST_F(BehaviorTreeTestFixture, TestNavigateRecoveryComplex)
|
||||
{
|
||||
// Load behavior tree from file
|
||||
fs::path bt_file = ament_index_cpp::get_package_share_directory("nav2_bt_navigator");
|
||||
bt_file /= "behavior_trees/";
|
||||
bt_file /= "navigate_to_pose_w_replanning_and_recovery.xml";
|
||||
EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string()), true);
|
||||
|
||||
// Set ComputePathToPose action server to fail for the first 2 actions
|
||||
std::vector<std::pair<int, int>> plannerFailureRange;
|
||||
plannerFailureRange.emplace_back(std::pair<int, int>(0, 2));
|
||||
plannerFailureRange.emplace_back(std::pair<int, int>(4, 9));
|
||||
plannerFailureRange.emplace_back(std::pair<int, int>(11, 12));
|
||||
server_handler->compute_path_to_pose_server->setFailureRanges(plannerFailureRange);
|
||||
|
||||
// Set FollowPath action server to fail for the first 2 actions
|
||||
std::vector<std::pair<int, int>> controllerFailureRange;
|
||||
controllerFailureRange.emplace_back(std::pair<int, int>(0, 4));
|
||||
server_handler->follow_path_server->setFailureRanges(controllerFailureRange);
|
||||
|
||||
// Set Spin action server to fail for the first action
|
||||
std::vector<std::pair<int, int>> spinFailureRange;
|
||||
spinFailureRange.emplace_back(std::pair<int, int>(0, 1));
|
||||
server_handler->spin_server->setFailureRanges(spinFailureRange);
|
||||
|
||||
// Set Wait action server to fail for the first action
|
||||
std::vector<std::pair<int, int>> waitFailureRange;
|
||||
waitFailureRange.emplace_back(std::pair<int, int>(2, 2));
|
||||
server_handler->wait_server->setFailureRanges(waitFailureRange);
|
||||
|
||||
// Set BackUp action server to fail for the first action
|
||||
std::vector<std::pair<int, int>> backupFailureRange;
|
||||
backupFailureRange.emplace_back(std::pair<int, int>(0, 1));
|
||||
server_handler->backup_server->setFailureRanges(backupFailureRange);
|
||||
|
||||
BT::NodeStatus result = BT::NodeStatus::RUNNING;
|
||||
|
||||
while (result == BT::NodeStatus::RUNNING) {
|
||||
result = bt_handler->tree.tickRoot();
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
|
||||
// The final result should be success
|
||||
EXPECT_EQ(result, BT::NodeStatus::FAILURE);
|
||||
|
||||
// ComputePathToPose is called 12 times
|
||||
EXPECT_EQ(server_handler->compute_path_to_pose_server->getGoalCount(), 12);
|
||||
|
||||
// FollowPath is called 4 times
|
||||
EXPECT_EQ(server_handler->follow_path_server->getGoalCount(), 4);
|
||||
|
||||
// Local costmap is cleared 5 times
|
||||
EXPECT_EQ(server_handler->clear_local_costmap_server->getRequestCount(), 5);
|
||||
|
||||
// Global costmap is cleared 8 times
|
||||
EXPECT_EQ(server_handler->clear_global_costmap_server->getRequestCount(), 8);
|
||||
|
||||
// All recovery action servers receive 2 goals
|
||||
EXPECT_EQ(server_handler->spin_server->getGoalCount(), 2);
|
||||
EXPECT_EQ(server_handler->wait_server->getGoalCount(), 2);
|
||||
EXPECT_EQ(server_handler->backup_server->getGoalCount(), 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test scenario:
|
||||
*
|
||||
* ComputePathToPose returns FAILURE on the first try triggering the planner recovery
|
||||
* ClearGlobalCostmap-Context returns SUCCESS and ComputePathToPose returns FAILURE when retried
|
||||
* PipelineSequence returns FAILURE and NavigateRecovery triggers RecoveryFallback
|
||||
* GoalUpdated returns FAILURE, RoundRobin triggers ClearingActions Sequence which returns SUCCESS
|
||||
* RoundRobin returns SUCCESS and RecoveryFallback returns SUCCESS
|
||||
* PipelineSequence is triggered again and ComputePathToPose returns SUCCESS
|
||||
* FollowPath returns FAILURE on the first try triggering the controller recovery
|
||||
* ClearLocalCostmap-Context returns SUCCESS and FollowPath is retried
|
||||
* FollowPath returns FAILURE and PipelineSequence returns FAILURE
|
||||
* NavigateRecovery triggers RecoveryFallback which triggers GoalUpdated
|
||||
* GoalUpdated returns FAILURE and RecoveryFallback triggers RoundRobin
|
||||
* RoundRobin triggers Spin which returns RUNNING
|
||||
*
|
||||
* At this point a new goal is updated on the blackboard
|
||||
*
|
||||
* RecoveryFallback triggers GoalUpdated which returns SUCCESS this time
|
||||
* Since GoalUpdated returned SUCCESS, RoundRobin and hence Spin is halted
|
||||
* RecoveryFallback also returns SUCCESS and PipelineSequence is retried
|
||||
* PipelineSequence triggers ComputePathToPose which returns SUCCESS
|
||||
* FollowPath returns SUCCESS and NavigateRecovery finally also returns SUCCESS
|
||||
*
|
||||
* The behavior tree should return SUCCESS
|
||||
*/
|
||||
TEST_F(BehaviorTreeTestFixture, TestRecoverySubtreeGoalUpdated)
|
||||
{
|
||||
// Load behavior tree from file
|
||||
fs::path bt_file = ament_index_cpp::get_package_share_directory("nav2_bt_navigator");
|
||||
bt_file /= "behavior_trees/";
|
||||
bt_file /= "navigate_to_pose_w_replanning_and_recovery.xml";
|
||||
EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string()), true);
|
||||
|
||||
// Set ComputePathToPose action server to fail for the first 2 actions
|
||||
std::vector<std::pair<int, int>> plannerFailureRange;
|
||||
plannerFailureRange.emplace_back(std::pair<int, int>(0, 2));
|
||||
server_handler->compute_path_to_pose_server->setFailureRanges(plannerFailureRange);
|
||||
|
||||
// Set FollowPath action server to fail for the first 2 actions
|
||||
std::vector<std::pair<int, int>> controllerFailureRange;
|
||||
controllerFailureRange.emplace_back(std::pair<int, int>(0, 2));
|
||||
server_handler->follow_path_server->setFailureRanges(controllerFailureRange);
|
||||
|
||||
// Set Spin action server to return running for the first action
|
||||
std::vector<std::pair<int, int>> spinRunningRange;
|
||||
spinRunningRange.emplace_back(std::pair<int, int>(1, 1));
|
||||
server_handler->spin_server->setRunningRanges(spinRunningRange);
|
||||
|
||||
BT::NodeStatus result = BT::NodeStatus::RUNNING;
|
||||
|
||||
while (result == BT::NodeStatus::RUNNING) {
|
||||
result = bt_handler->tree.tickRoot();
|
||||
|
||||
// Update goal on blackboard after Spin has been triggered once
|
||||
// to simulate a goal update during a recovery action
|
||||
if (server_handler->spin_server->getGoalCount() > 0) {
|
||||
geometry_msgs::msg::PoseStamped goal;
|
||||
goal.pose.position.x = 1.0;
|
||||
goal.pose.position.y = 1.0;
|
||||
goal.pose.position.z = 1.0;
|
||||
goal.pose.orientation.x = 0.0;
|
||||
goal.pose.orientation.y = 0.0;
|
||||
goal.pose.orientation.z = 0.0;
|
||||
goal.pose.orientation.w = 1.0;
|
||||
bt_handler->blackboard->set<geometry_msgs::msg::PoseStamped>("goal", goal); // NOLINT
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
|
||||
// The final result should be success
|
||||
EXPECT_EQ(result, BT::NodeStatus::SUCCESS);
|
||||
|
||||
// ComputePathToPose is called 4 times
|
||||
EXPECT_EQ(server_handler->compute_path_to_pose_server->getGoalCount(), 4);
|
||||
|
||||
// FollowPath is called 3 times
|
||||
EXPECT_EQ(server_handler->follow_path_server->getGoalCount(), 3);
|
||||
|
||||
// Local costmap is cleared 2 times
|
||||
EXPECT_EQ(server_handler->clear_local_costmap_server->getRequestCount(), 2);
|
||||
|
||||
// Global costmap is cleared 2 times
|
||||
EXPECT_EQ(server_handler->clear_global_costmap_server->getRequestCount(), 2);
|
||||
|
||||
// Spin server receives 1 action
|
||||
EXPECT_EQ(server_handler->spin_server->getGoalCount(), 1);
|
||||
|
||||
// All recovery action servers receive 0 goals
|
||||
EXPECT_EQ(server_handler->wait_server->getGoalCount(), 0);
|
||||
EXPECT_EQ(server_handler->backup_server->getGoalCount(), 0);
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
bool all_successful = RUN_ALL_TESTS();
|
||||
return all_successful;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Behavior Test
|
||||
|
||||
Provides some simple tests for behavior plugins.
|
||||
It creates an instance of the stack, with the behavior server loading different behavior plugins, and checks for successful behavior behaviors.
|
||||
@@ -0,0 +1,23 @@
|
||||
set(test_assisted_teleop_behavior test_assisted_teleop_behavior_node)
|
||||
|
||||
ament_add_gtest_executable(${test_assisted_teleop_behavior}
|
||||
test_assisted_teleop_behavior_node.cpp
|
||||
assisted_teleop_behavior_tester.cpp
|
||||
)
|
||||
|
||||
ament_target_dependencies(${test_assisted_teleop_behavior}
|
||||
${dependencies}
|
||||
)
|
||||
|
||||
ament_add_test(test_assisted_teleop_behavior
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_assisted_teleop_behavior_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_EXECUTABLE=$<TARGET_FILE:${test_assisted_teleop_behavior}>
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
)
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <string>
|
||||
#include <random>
|
||||
#include <tuple>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include "assisted_teleop_behavior_tester.hpp"
|
||||
#include "nav2_util/geometry_utils.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace std::chrono; // NOLINT
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
AssistedTeleopBehaviorTester::AssistedTeleopBehaviorTester()
|
||||
: is_active_(false),
|
||||
initial_pose_received_(false)
|
||||
{
|
||||
node_ = rclcpp::Node::make_shared("assisted_teleop_behavior_test");
|
||||
|
||||
tf_buffer_ = std::make_shared<tf2_ros::Buffer>(node_->get_clock());
|
||||
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);
|
||||
|
||||
client_ptr_ = rclcpp_action::create_client<AssistedTeleop>(
|
||||
node_->get_node_base_interface(),
|
||||
node_->get_node_graph_interface(),
|
||||
node_->get_node_logging_interface(),
|
||||
node_->get_node_waitables_interface(),
|
||||
"assisted_teleop");
|
||||
|
||||
initial_pose_pub_ =
|
||||
node_->create_publisher<geometry_msgs::msg::PoseWithCovarianceStamped>("initialpose", 10);
|
||||
|
||||
preempt_pub_ =
|
||||
node_->create_publisher<std_msgs::msg::Empty>("preempt_teleop", 10);
|
||||
|
||||
cmd_vel_pub_ =
|
||||
node_->create_publisher<geometry_msgs::msg::Twist>("cmd_vel_teleop", 10);
|
||||
|
||||
subscription_ = node_->create_subscription<geometry_msgs::msg::PoseWithCovarianceStamped>(
|
||||
"amcl_pose", rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable(),
|
||||
std::bind(&AssistedTeleopBehaviorTester::amclPoseCallback, this, std::placeholders::_1));
|
||||
|
||||
filtered_vel_sub_ = node_->create_subscription<geometry_msgs::msg::Twist>(
|
||||
"cmd_vel",
|
||||
rclcpp::SystemDefaultsQoS(),
|
||||
std::bind(&AssistedTeleopBehaviorTester::filteredVelCallback, this, std::placeholders::_1));
|
||||
|
||||
std::string costmap_topic = "/local_costmap/costmap_raw";
|
||||
std::string footprint_topic = "/local_costmap/published_footprint";
|
||||
|
||||
costmap_sub_ = std::make_shared<nav2_costmap_2d::CostmapSubscriber>(
|
||||
node_,
|
||||
costmap_topic);
|
||||
|
||||
footprint_sub_ = std::make_shared<nav2_costmap_2d::FootprintSubscriber>(
|
||||
node_,
|
||||
footprint_topic,
|
||||
*tf_buffer_);
|
||||
|
||||
collision_checker_ = std::make_unique<nav2_costmap_2d::CostmapTopicCollisionChecker>(
|
||||
*costmap_sub_,
|
||||
*footprint_sub_
|
||||
);
|
||||
|
||||
stamp_ = node_->now();
|
||||
}
|
||||
|
||||
AssistedTeleopBehaviorTester::~AssistedTeleopBehaviorTester()
|
||||
{
|
||||
if (is_active_) {
|
||||
deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
void AssistedTeleopBehaviorTester::activate()
|
||||
{
|
||||
if (is_active_) {
|
||||
throw std::runtime_error("Trying to activate while already active");
|
||||
return;
|
||||
}
|
||||
|
||||
while (!initial_pose_received_) {
|
||||
RCLCPP_WARN(node_->get_logger(), "Initial pose not received");
|
||||
sendInitialPose();
|
||||
std::this_thread::sleep_for(100ms);
|
||||
rclcpp::spin_some(node_);
|
||||
}
|
||||
|
||||
// Wait for lifecycle_manager_navigation to activate behavior_server
|
||||
std::this_thread::sleep_for(10s);
|
||||
|
||||
if (!client_ptr_) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Action client not initialized");
|
||||
is_active_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client_ptr_->wait_for_action_server(10s)) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Action server not available after waiting");
|
||||
is_active_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
RCLCPP_INFO(this->node_->get_logger(), "Assisted Teleop action server is ready");
|
||||
is_active_ = true;
|
||||
}
|
||||
|
||||
void AssistedTeleopBehaviorTester::deactivate()
|
||||
{
|
||||
if (!is_active_) {
|
||||
throw std::runtime_error("Trying to deactivate while already inactive");
|
||||
}
|
||||
is_active_ = false;
|
||||
}
|
||||
|
||||
bool AssistedTeleopBehaviorTester::defaultAssistedTeleopTest(
|
||||
const float lin_vel,
|
||||
const float ang_vel)
|
||||
{
|
||||
if (!is_active_) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Not activated");
|
||||
return false;
|
||||
}
|
||||
|
||||
RCLCPP_INFO(node_->get_logger(), "Sending goal");
|
||||
|
||||
auto goal_handle_future = client_ptr_->async_send_goal(nav2_msgs::action::AssistedTeleop::Goal());
|
||||
|
||||
if (rclcpp::spin_until_future_complete(node_, goal_handle_future) !=
|
||||
rclcpp::FutureReturnCode::SUCCESS)
|
||||
{
|
||||
RCLCPP_ERROR(node_->get_logger(), "send goal call failed :(");
|
||||
return false;
|
||||
}
|
||||
|
||||
rclcpp_action::ClientGoalHandle<AssistedTeleop>::SharedPtr goal_handle = goal_handle_future.get();
|
||||
if (!goal_handle) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Goal was rejected by server");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for the server to be done with the goal
|
||||
auto result_future = client_ptr_->async_get_result(goal_handle);
|
||||
|
||||
rclcpp::Rate r(1);
|
||||
|
||||
counter_ = 0;
|
||||
auto start_time = std::chrono::system_clock::now();
|
||||
while (rclcpp::ok()) {
|
||||
geometry_msgs::msg::Twist cmd_vel = geometry_msgs::msg::Twist();
|
||||
cmd_vel.linear.x = lin_vel;
|
||||
cmd_vel.angular.z = ang_vel;
|
||||
cmd_vel_pub_->publish(cmd_vel);
|
||||
|
||||
if (counter_ > 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto current_time = std::chrono::system_clock::now();
|
||||
if (current_time - start_time > 25s) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Exceeded Timeout");
|
||||
return false;
|
||||
}
|
||||
|
||||
rclcpp::spin_some(node_);
|
||||
r.sleep();
|
||||
}
|
||||
|
||||
auto preempt_msg = std_msgs::msg::Empty();
|
||||
preempt_pub_->publish(preempt_msg);
|
||||
|
||||
RCLCPP_INFO(node_->get_logger(), "Waiting for result");
|
||||
if (rclcpp::spin_until_future_complete(node_, result_future) !=
|
||||
rclcpp::FutureReturnCode::SUCCESS)
|
||||
{
|
||||
RCLCPP_ERROR(node_->get_logger(), "get result call failed :(");
|
||||
return false;
|
||||
}
|
||||
|
||||
rclcpp_action::ClientGoalHandle<AssistedTeleop>::WrappedResult
|
||||
wrapped_result = result_future.get();
|
||||
|
||||
switch (wrapped_result.code) {
|
||||
case rclcpp_action::ResultCode::SUCCEEDED: break;
|
||||
case rclcpp_action::ResultCode::ABORTED: RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Goal was aborted");
|
||||
return false;
|
||||
case rclcpp_action::ResultCode::CANCELED: RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Goal was canceled");
|
||||
return false;
|
||||
default: RCLCPP_ERROR(node_->get_logger(), "Unknown result code");
|
||||
return false;
|
||||
}
|
||||
|
||||
RCLCPP_INFO(node_->get_logger(), "result received");
|
||||
|
||||
geometry_msgs::msg::PoseStamped current_pose;
|
||||
if (!nav2_util::getCurrentPose(current_pose, *tf_buffer_, "odom")) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Current robot pose is not available.");
|
||||
return false;
|
||||
}
|
||||
|
||||
geometry_msgs::msg::Pose2D pose_2d;
|
||||
pose_2d.x = current_pose.pose.position.x;
|
||||
pose_2d.y = current_pose.pose.position.y;
|
||||
pose_2d.theta = tf2::getYaw(current_pose.pose.orientation);
|
||||
|
||||
if (!collision_checker_->isCollisionFree(pose_2d)) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Ended in collision");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AssistedTeleopBehaviorTester::sendInitialPose()
|
||||
{
|
||||
geometry_msgs::msg::PoseWithCovarianceStamped pose;
|
||||
pose.header.frame_id = "map";
|
||||
pose.header.stamp = stamp_;
|
||||
pose.pose.pose.position.x = -2.0;
|
||||
pose.pose.pose.position.y = -0.5;
|
||||
pose.pose.pose.position.z = 0.0;
|
||||
pose.pose.pose.orientation.x = 0.0;
|
||||
pose.pose.pose.orientation.y = 0.0;
|
||||
pose.pose.pose.orientation.z = 0.0;
|
||||
pose.pose.pose.orientation.w = 1.0;
|
||||
for (int i = 0; i < 35; i++) {
|
||||
pose.pose.covariance[i] = 0.0;
|
||||
}
|
||||
pose.pose.covariance[0] = 0.08;
|
||||
pose.pose.covariance[7] = 0.08;
|
||||
pose.pose.covariance[35] = 0.05;
|
||||
|
||||
initial_pose_pub_->publish(pose);
|
||||
RCLCPP_INFO(node_->get_logger(), "Sent initial pose");
|
||||
}
|
||||
|
||||
void AssistedTeleopBehaviorTester::amclPoseCallback(
|
||||
const geometry_msgs::msg::PoseWithCovarianceStamped::SharedPtr)
|
||||
{
|
||||
initial_pose_received_ = true;
|
||||
}
|
||||
|
||||
void AssistedTeleopBehaviorTester::filteredVelCallback(
|
||||
geometry_msgs::msg::Twist::SharedPtr msg)
|
||||
{
|
||||
if (msg->linear.x == 0.0f) {
|
||||
counter_++;
|
||||
} else {
|
||||
counter_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2020 Samsung Research
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#ifndef BEHAVIORS__ASSISTED_TELEOP__ASSISTED_TELEOP_BEHAVIOR_TESTER_HPP_
|
||||
#define BEHAVIORS__ASSISTED_TELEOP__ASSISTED_TELEOP_BEHAVIOR_TESTER_HPP_
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <algorithm>
|
||||
|
||||
#include "angles/angles.h"
|
||||
#include "geometry_msgs/msg/pose_stamped.hpp"
|
||||
#include "geometry_msgs/msg/pose_with_covariance_stamped.hpp"
|
||||
#include "geometry_msgs/msg/twist.hpp"
|
||||
#include "geometry_msgs/msg/pose2_d.hpp"
|
||||
#include "nav2_costmap_2d/costmap_topic_collision_checker.hpp"
|
||||
#include "nav2_msgs/action/assisted_teleop.hpp"
|
||||
#include "nav2_util/node_thread.hpp"
|
||||
#include "nav2_util/robot_utils.hpp"
|
||||
#include "rclcpp_action/rclcpp_action.hpp"
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "std_msgs/msg/empty.hpp"
|
||||
|
||||
#include "tf2/utils.h"
|
||||
#include "tf2_ros/buffer.h"
|
||||
#include "tf2_ros/transform_listener.h"
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
class AssistedTeleopBehaviorTester
|
||||
{
|
||||
public:
|
||||
using AssistedTeleop = nav2_msgs::action::AssistedTeleop;
|
||||
|
||||
AssistedTeleopBehaviorTester();
|
||||
~AssistedTeleopBehaviorTester();
|
||||
|
||||
// Runs a single test with given target yaw
|
||||
bool defaultAssistedTeleopTest(
|
||||
const float lin_vel,
|
||||
const float ang_vel);
|
||||
|
||||
void activate();
|
||||
|
||||
void deactivate();
|
||||
|
||||
bool isActive() const
|
||||
{
|
||||
return is_active_;
|
||||
}
|
||||
|
||||
private:
|
||||
void sendInitialPose();
|
||||
|
||||
void amclPoseCallback(geometry_msgs::msg::PoseWithCovarianceStamped::SharedPtr);
|
||||
|
||||
void filteredVelCallback(geometry_msgs::msg::Twist::SharedPtr msg);
|
||||
|
||||
unsigned int counter_;
|
||||
bool is_active_;
|
||||
bool initial_pose_received_;
|
||||
rclcpp::Time stamp_;
|
||||
|
||||
std::shared_ptr<tf2_ros::Buffer> tf_buffer_;
|
||||
std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
|
||||
|
||||
rclcpp::Node::SharedPtr node_;
|
||||
|
||||
// Publishers
|
||||
rclcpp::Publisher<geometry_msgs::msg::PoseWithCovarianceStamped>::SharedPtr initial_pose_pub_;
|
||||
rclcpp::Publisher<std_msgs::msg::Empty>::SharedPtr preempt_pub_;
|
||||
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_vel_pub_;
|
||||
|
||||
// Subscribers
|
||||
rclcpp::Subscription<geometry_msgs::msg::PoseWithCovarianceStamped>::SharedPtr subscription_;
|
||||
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr filtered_vel_sub_;
|
||||
|
||||
// Action client to call AssistedTeleop action
|
||||
rclcpp_action::Client<AssistedTeleop>::SharedPtr client_ptr_;
|
||||
|
||||
// collision checking
|
||||
std::shared_ptr<nav2_costmap_2d::CostmapSubscriber> costmap_sub_;
|
||||
std::shared_ptr<nav2_costmap_2d::FootprintSubscriber> footprint_sub_;
|
||||
std::unique_ptr<nav2_costmap_2d::CostmapTopicCollisionChecker> collision_checker_;
|
||||
};
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
|
||||
#endif // BEHAVIORS__ASSISTED_TELEOP__ASSISTED_TELEOP_BEHAVIOR_TESTER_HPP_
|
||||
Executable
+103
@@ -0,0 +1,103 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright (c) 2012 Samsung Research America
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
from launch.actions import ExecuteProcess, IncludeLaunchDescription, SetEnvironmentVariable
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch_ros.actions import Node
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
from nav2_common.launch import RewrittenYaml
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
map_yaml_file = os.getenv('TEST_MAP')
|
||||
world = os.getenv('TEST_WORLD')
|
||||
|
||||
bt_navigator_xml = os.path.join(get_package_share_directory('nav2_bt_navigator'),
|
||||
'behavior_trees',
|
||||
os.getenv('BT_NAVIGATOR_XML'))
|
||||
|
||||
bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
params_file = os.path.join(bringup_dir, 'params/nav2_params.yaml')
|
||||
|
||||
# Replace the `use_astar` setting on the params file
|
||||
configured_params = RewrittenYaml(
|
||||
source_file=params_file,
|
||||
root_key='',
|
||||
param_rewrites='',
|
||||
convert_types=True)
|
||||
|
||||
return LaunchDescription([
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM', '1'),
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_USE_STDOUT', '1'),
|
||||
|
||||
# Launch gazebo server for simulation
|
||||
ExecuteProcess(
|
||||
cmd=['gzserver', '-s', 'libgazebo_ros_init.so',
|
||||
'--minimal_comms', world],
|
||||
output='screen'),
|
||||
|
||||
# TODO(orduno) Launch the robot state publisher instead
|
||||
# using a local copy of TB3 urdf file
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_footprint', 'base_link']),
|
||||
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_scan']),
|
||||
|
||||
IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(bringup_dir, 'launch', 'bringup_launch.py')),
|
||||
launch_arguments={
|
||||
'map': map_yaml_file,
|
||||
'use_sim_time': 'True',
|
||||
'params_file': configured_params,
|
||||
'bt_xml_file': bt_navigator_xml,
|
||||
'autostart': 'True'}.items()),
|
||||
])
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
ld = generate_launch_description()
|
||||
|
||||
testExecutable = os.getenv('TEST_EXECUTABLE')
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[testExecutable],
|
||||
name='test_assisted_teleop_behavior_node',
|
||||
output='screen')
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2020 Samsung Research
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
// Copyright (c) 2022 Joshua Wallace
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <tuple>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
|
||||
#include "assisted_teleop_behavior_tester.hpp"
|
||||
#include "nav2_msgs/action/back_up.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using nav2_system_tests::AssistedTeleopBehaviorTester;
|
||||
|
||||
struct TestParameters
|
||||
{
|
||||
float lin_vel;
|
||||
float ang_vel;
|
||||
};
|
||||
|
||||
|
||||
std::string testNameGenerator(const testing::TestParamInfo<TestParameters> &)
|
||||
{
|
||||
static int test_index = 0;
|
||||
std::string name = "AssistedTeleopTest" + std::to_string(test_index);
|
||||
++test_index;
|
||||
return name;
|
||||
}
|
||||
|
||||
class AssistedTeleopBehaviorTestFixture
|
||||
: public ::testing::TestWithParam<TestParameters>
|
||||
{
|
||||
public:
|
||||
static void SetUpTestCase()
|
||||
{
|
||||
assisted_teleop_behavior_tester = new AssistedTeleopBehaviorTester();
|
||||
if (!assisted_teleop_behavior_tester->isActive()) {
|
||||
assisted_teleop_behavior_tester->activate();
|
||||
}
|
||||
}
|
||||
|
||||
static void TearDownTestCase()
|
||||
{
|
||||
delete assisted_teleop_behavior_tester;
|
||||
assisted_teleop_behavior_tester = nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
static AssistedTeleopBehaviorTester * assisted_teleop_behavior_tester;
|
||||
};
|
||||
|
||||
AssistedTeleopBehaviorTester *
|
||||
AssistedTeleopBehaviorTestFixture::assisted_teleop_behavior_tester = nullptr;
|
||||
|
||||
TEST_P(AssistedTeleopBehaviorTestFixture, testAssistedTeleopBehavior)
|
||||
{
|
||||
auto test_params = GetParam();
|
||||
|
||||
if (!assisted_teleop_behavior_tester->isActive()) {
|
||||
assisted_teleop_behavior_tester->activate();
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
success = assisted_teleop_behavior_tester->defaultAssistedTeleopTest(
|
||||
test_params.lin_vel,
|
||||
test_params.ang_vel);
|
||||
|
||||
EXPECT_TRUE(success);
|
||||
}
|
||||
|
||||
std::vector<TestParameters> test_params = {TestParameters{-0.1, 0.0},
|
||||
TestParameters{0.35, 0.05}};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
TestAssistedTeleopBehavior,
|
||||
AssistedTeleopBehaviorTestFixture,
|
||||
::testing::Values(
|
||||
test_params[0],
|
||||
test_params[1]),
|
||||
testNameGenerator
|
||||
);
|
||||
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
||||
// initialize ROS
|
||||
rclcpp::init(argc, argv);
|
||||
|
||||
bool all_successful = RUN_ALL_TESTS();
|
||||
|
||||
// shutdown ROS
|
||||
rclcpp::shutdown();
|
||||
|
||||
return all_successful;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
set(test_backup_behavior test_backup_behavior_node)
|
||||
|
||||
ament_add_gtest_executable(${test_backup_behavior}
|
||||
test_backup_behavior_node.cpp
|
||||
backup_behavior_tester.cpp
|
||||
)
|
||||
|
||||
ament_target_dependencies(${test_backup_behavior}
|
||||
${dependencies}
|
||||
)
|
||||
|
||||
ament_add_test(test_backup_recovery
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_backup_behavior_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_EXECUTABLE=$<TARGET_FILE:${test_backup_behavior}>
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
)
|
||||
@@ -0,0 +1,223 @@
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <string>
|
||||
#include <random>
|
||||
#include <tuple>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include "backup_behavior_tester.hpp"
|
||||
#include "nav2_util/geometry_utils.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace std::chrono; // NOLINT
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
BackupBehaviorTester::BackupBehaviorTester()
|
||||
: is_active_(false),
|
||||
initial_pose_received_(false)
|
||||
{
|
||||
rclcpp::NodeOptions options;
|
||||
options.parameter_overrides({{"use_sim_time", true}});
|
||||
node_ = rclcpp::Node::make_shared("backup_behavior_test", options);
|
||||
|
||||
tf_buffer_ = std::make_shared<tf2_ros::Buffer>(node_->get_clock());
|
||||
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);
|
||||
|
||||
client_ptr_ = rclcpp_action::create_client<BackUp>(
|
||||
node_->get_node_base_interface(),
|
||||
node_->get_node_graph_interface(),
|
||||
node_->get_node_logging_interface(),
|
||||
node_->get_node_waitables_interface(),
|
||||
"backup");
|
||||
|
||||
publisher_ =
|
||||
node_->create_publisher<geometry_msgs::msg::PoseWithCovarianceStamped>("initialpose", 10);
|
||||
|
||||
subscription_ = node_->create_subscription<geometry_msgs::msg::PoseWithCovarianceStamped>(
|
||||
"amcl_pose", rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable(),
|
||||
std::bind(&BackupBehaviorTester::amclPoseCallback, this, std::placeholders::_1));
|
||||
|
||||
stamp_ = node_->now();
|
||||
}
|
||||
|
||||
BackupBehaviorTester::~BackupBehaviorTester()
|
||||
{
|
||||
if (is_active_) {
|
||||
deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
void BackupBehaviorTester::activate()
|
||||
{
|
||||
if (is_active_) {
|
||||
throw std::runtime_error("Trying to activate while already active");
|
||||
return;
|
||||
}
|
||||
|
||||
while (!initial_pose_received_) {
|
||||
RCLCPP_WARN(node_->get_logger(), "Initial pose not received");
|
||||
sendInitialPose();
|
||||
std::this_thread::sleep_for(100ms);
|
||||
rclcpp::spin_some(node_);
|
||||
}
|
||||
|
||||
// Wait for lifecycle_manager_navigation to activate behavior_server
|
||||
std::this_thread::sleep_for(10s);
|
||||
|
||||
if (!client_ptr_) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Action client not initialized");
|
||||
is_active_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client_ptr_->wait_for_action_server(10s)) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Action server not available after waiting");
|
||||
is_active_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
RCLCPP_INFO(this->node_->get_logger(), "Backup action server is ready");
|
||||
is_active_ = true;
|
||||
}
|
||||
|
||||
void BackupBehaviorTester::deactivate()
|
||||
{
|
||||
if (!is_active_) {
|
||||
throw std::runtime_error("Trying to deactivate while already inactive");
|
||||
}
|
||||
is_active_ = false;
|
||||
}
|
||||
|
||||
bool BackupBehaviorTester::defaultBackupBehaviorTest(
|
||||
const BackUp::Goal goal_msg,
|
||||
const double tolerance)
|
||||
{
|
||||
if (!is_active_) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Not activated");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sleep to let behavior server be ready for serving in multiple runs
|
||||
std::this_thread::sleep_for(5s);
|
||||
|
||||
RCLCPP_INFO(this->node_->get_logger(), "Sending goal");
|
||||
|
||||
geometry_msgs::msg::PoseStamped initial_pose;
|
||||
if (!nav2_util::getCurrentPose(initial_pose, *tf_buffer_, "odom")) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Current robot pose is not available.");
|
||||
return false;
|
||||
}
|
||||
RCLCPP_INFO(node_->get_logger(), "Found current robot pose");
|
||||
|
||||
auto goal_handle_future = client_ptr_->async_send_goal(goal_msg);
|
||||
|
||||
if (rclcpp::spin_until_future_complete(node_, goal_handle_future) !=
|
||||
rclcpp::FutureReturnCode::SUCCESS)
|
||||
{
|
||||
RCLCPP_ERROR(node_->get_logger(), "send goal call failed :(");
|
||||
return false;
|
||||
}
|
||||
|
||||
rclcpp_action::ClientGoalHandle<BackUp>::SharedPtr goal_handle = goal_handle_future.get();
|
||||
if (!goal_handle) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Goal was rejected by server");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for the server to be done with the goal
|
||||
auto result_future = client_ptr_->async_get_result(goal_handle);
|
||||
|
||||
RCLCPP_INFO(node_->get_logger(), "Waiting for result");
|
||||
if (rclcpp::spin_until_future_complete(node_, result_future) !=
|
||||
rclcpp::FutureReturnCode::SUCCESS)
|
||||
{
|
||||
RCLCPP_ERROR(node_->get_logger(), "get result call failed :(");
|
||||
return false;
|
||||
}
|
||||
|
||||
rclcpp_action::ClientGoalHandle<BackUp>::WrappedResult wrapped_result = result_future.get();
|
||||
|
||||
switch (wrapped_result.code) {
|
||||
case rclcpp_action::ResultCode::SUCCEEDED: break;
|
||||
case rclcpp_action::ResultCode::ABORTED: RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Goal was aborted");
|
||||
return false;
|
||||
case rclcpp_action::ResultCode::CANCELED: RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Goal was canceled");
|
||||
return false;
|
||||
default: RCLCPP_ERROR(node_->get_logger(), "Unknown result code");
|
||||
return false;
|
||||
}
|
||||
|
||||
RCLCPP_INFO(node_->get_logger(), "result received");
|
||||
|
||||
geometry_msgs::msg::PoseStamped current_pose;
|
||||
if (!nav2_util::getCurrentPose(current_pose, *tf_buffer_, "odom")) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Current robot pose is not available.");
|
||||
return false;
|
||||
}
|
||||
|
||||
double dist = nav2_util::geometry_utils::euclidean_distance(initial_pose, current_pose);
|
||||
|
||||
if (fabs(dist) > fabs(goal_msg.target.x) + tolerance) {
|
||||
RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Distance from goal is %lf (tolerance %lf)",
|
||||
fabs(dist - goal_msg.target.x), tolerance);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BackupBehaviorTester::sendInitialPose()
|
||||
{
|
||||
geometry_msgs::msg::PoseWithCovarianceStamped pose;
|
||||
pose.header.frame_id = "map";
|
||||
pose.header.stamp = stamp_;
|
||||
pose.pose.pose.position.x = -2.0;
|
||||
pose.pose.pose.position.y = -0.5;
|
||||
pose.pose.pose.position.z = 0.0;
|
||||
pose.pose.pose.orientation.x = 0.0;
|
||||
pose.pose.pose.orientation.y = 0.0;
|
||||
pose.pose.pose.orientation.z = 0.0;
|
||||
pose.pose.pose.orientation.w = 1.0;
|
||||
for (int i = 0; i < 35; i++) {
|
||||
pose.pose.covariance[i] = 0.0;
|
||||
}
|
||||
pose.pose.covariance[0] = 0.08;
|
||||
pose.pose.covariance[7] = 0.08;
|
||||
pose.pose.covariance[35] = 0.05;
|
||||
|
||||
publisher_->publish(pose);
|
||||
RCLCPP_INFO(node_->get_logger(), "Sent initial pose");
|
||||
}
|
||||
|
||||
void BackupBehaviorTester::amclPoseCallback(
|
||||
const geometry_msgs::msg::PoseWithCovarianceStamped::SharedPtr)
|
||||
{
|
||||
initial_pose_received_ = true;
|
||||
}
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2020 Samsung Research
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#ifndef BEHAVIORS__BACKUP__BACKUP_BEHAVIOR_TESTER_HPP_
|
||||
#define BEHAVIORS__BACKUP__BACKUP_BEHAVIOR_TESTER_HPP_
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <algorithm>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "rclcpp_action/rclcpp_action.hpp"
|
||||
#include "angles/angles.h"
|
||||
#include "nav2_msgs/action/back_up.hpp"
|
||||
#include "nav2_util/robot_utils.hpp"
|
||||
#include "nav2_util/node_thread.hpp"
|
||||
#include "geometry_msgs/msg/pose_stamped.hpp"
|
||||
#include "geometry_msgs/msg/pose_with_covariance_stamped.hpp"
|
||||
|
||||
#include "tf2/utils.h"
|
||||
#include "tf2_ros/buffer.h"
|
||||
#include "tf2_ros/transform_listener.h"
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
class BackupBehaviorTester
|
||||
{
|
||||
public:
|
||||
using BackUp = nav2_msgs::action::BackUp;
|
||||
using GoalHandleBackup = rclcpp_action::ClientGoalHandle<BackUp>;
|
||||
|
||||
BackupBehaviorTester();
|
||||
~BackupBehaviorTester();
|
||||
|
||||
// Runs a single test with given target yaw
|
||||
bool defaultBackupBehaviorTest(
|
||||
const BackUp::Goal goal_msg,
|
||||
const double tolerance);
|
||||
|
||||
void activate();
|
||||
|
||||
void deactivate();
|
||||
|
||||
bool isActive() const
|
||||
{
|
||||
return is_active_;
|
||||
}
|
||||
|
||||
private:
|
||||
void sendInitialPose();
|
||||
|
||||
void amclPoseCallback(geometry_msgs::msg::PoseWithCovarianceStamped::SharedPtr);
|
||||
|
||||
bool is_active_;
|
||||
bool initial_pose_received_;
|
||||
rclcpp::Time stamp_;
|
||||
|
||||
std::shared_ptr<tf2_ros::Buffer> tf_buffer_;
|
||||
std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
|
||||
|
||||
rclcpp::Node::SharedPtr node_;
|
||||
|
||||
// Publisher to publish initial pose
|
||||
rclcpp::Publisher<geometry_msgs::msg::PoseWithCovarianceStamped>::SharedPtr publisher_;
|
||||
|
||||
// Subscriber for amcl pose
|
||||
rclcpp::Subscription<geometry_msgs::msg::PoseWithCovarianceStamped>::SharedPtr subscription_;
|
||||
|
||||
// Action client to call Backup action
|
||||
rclcpp_action::Client<BackUp>::SharedPtr client_ptr_;
|
||||
};
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
|
||||
#endif // BEHAVIORS__BACKUP__BACKUP_BEHAVIOR_TESTER_HPP_
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright (c) 2012 Samsung Research America
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
from launch.actions import ExecuteProcess, IncludeLaunchDescription, SetEnvironmentVariable
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch_ros.actions import Node
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
from nav2_common.launch import RewrittenYaml
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
map_yaml_file = os.getenv('TEST_MAP')
|
||||
world = os.getenv('TEST_WORLD')
|
||||
|
||||
bt_navigator_xml = os.path.join(get_package_share_directory('nav2_bt_navigator'),
|
||||
'behavior_trees',
|
||||
os.getenv('BT_NAVIGATOR_XML'))
|
||||
|
||||
bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
params_file = os.path.join(bringup_dir, 'params/nav2_params.yaml')
|
||||
|
||||
# Replace the `use_astar` setting on the params file
|
||||
configured_params = RewrittenYaml(
|
||||
source_file=params_file,
|
||||
root_key='',
|
||||
param_rewrites='',
|
||||
convert_types=True)
|
||||
|
||||
return LaunchDescription([
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM', '1'),
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_USE_STDOUT', '1'),
|
||||
|
||||
# Launch gazebo server for simulation
|
||||
ExecuteProcess(
|
||||
cmd=['gzserver', '-s', 'libgazebo_ros_init.so',
|
||||
'--minimal_comms', world],
|
||||
output='screen'),
|
||||
|
||||
# TODO(orduno) Launch the robot state publisher instead
|
||||
# using a local copy of TB3 urdf file
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_footprint', 'base_link']),
|
||||
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_scan']),
|
||||
|
||||
IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(bringup_dir, 'launch', 'bringup_launch.py')),
|
||||
launch_arguments={
|
||||
'map': map_yaml_file,
|
||||
'use_sim_time': 'True',
|
||||
'params_file': configured_params,
|
||||
'bt_xml_file': bt_navigator_xml,
|
||||
'autostart': 'True'}.items()),
|
||||
])
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
ld = generate_launch_description()
|
||||
|
||||
testExecutable = os.getenv('TEST_EXECUTABLE')
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[testExecutable], name='test_backup_behavior_node', output='screen',
|
||||
)
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2020 Samsung Research
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
// Copyright (c) 2022 Joshua Wallace
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <tuple>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
|
||||
#include "backup_behavior_tester.hpp"
|
||||
#include "nav2_msgs/action/back_up.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using nav2_system_tests::BackupBehaviorTester;
|
||||
|
||||
struct TestParameters
|
||||
{
|
||||
float x;
|
||||
float y;
|
||||
float speed;
|
||||
float tolerance;
|
||||
};
|
||||
|
||||
|
||||
std::string testNameGenerator(const testing::TestParamInfo<TestParameters> &)
|
||||
{
|
||||
static int test_index = 0;
|
||||
std::string name = "BackUpTest" + std::to_string(test_index);
|
||||
++test_index;
|
||||
return name;
|
||||
}
|
||||
|
||||
class BackupBehaviorTestFixture
|
||||
: public ::testing::TestWithParam<TestParameters>
|
||||
{
|
||||
public:
|
||||
static void SetUpTestCase()
|
||||
{
|
||||
backup_behavior_tester = new BackupBehaviorTester();
|
||||
if (!backup_behavior_tester->isActive()) {
|
||||
backup_behavior_tester->activate();
|
||||
}
|
||||
}
|
||||
|
||||
static void TearDownTestCase()
|
||||
{
|
||||
delete backup_behavior_tester;
|
||||
backup_behavior_tester = nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
static BackupBehaviorTester * backup_behavior_tester;
|
||||
};
|
||||
|
||||
BackupBehaviorTester * BackupBehaviorTestFixture::backup_behavior_tester = nullptr;
|
||||
|
||||
TEST_P(BackupBehaviorTestFixture, testBackupBehavior)
|
||||
{
|
||||
auto test_params = GetParam();
|
||||
auto goal = nav2_msgs::action::BackUp::Goal();
|
||||
goal.target.x = test_params.x;
|
||||
goal.target.y = test_params.y;
|
||||
goal.speed = test_params.speed;
|
||||
float tolerance = test_params.tolerance;
|
||||
|
||||
if (!backup_behavior_tester->isActive()) {
|
||||
backup_behavior_tester->activate();
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
success = backup_behavior_tester->defaultBackupBehaviorTest(goal, tolerance);
|
||||
|
||||
float dist_to_obstacle = 2.0f;
|
||||
|
||||
if ( ((dist_to_obstacle - std::fabs(test_params.x)) < std::fabs(goal.speed)) ||
|
||||
std::fabs(goal.target.y) > 0)
|
||||
{
|
||||
EXPECT_FALSE(success);
|
||||
} else {
|
||||
EXPECT_TRUE(success);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<TestParameters> test_params = {TestParameters{-0.05, 0.0, -0.2, 0.01},
|
||||
TestParameters{-0.05, 0.1, -0.2, 0.01},
|
||||
TestParameters{-2.0, 0.0, -0.2, 0.1}};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
BackupBehaviorTests,
|
||||
BackupBehaviorTestFixture,
|
||||
::testing::Values(
|
||||
test_params[0],
|
||||
test_params[1],
|
||||
test_params[2]),
|
||||
testNameGenerator
|
||||
);
|
||||
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
||||
// initialize ROS
|
||||
rclcpp::init(argc, argv);
|
||||
|
||||
bool all_successful = RUN_ALL_TESTS();
|
||||
|
||||
// shutdown ROS
|
||||
rclcpp::shutdown();
|
||||
|
||||
return all_successful;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
set(test_drive_on_heading_behavior test_drive_on_heading_behavior_node)
|
||||
|
||||
ament_add_gtest_executable(${test_drive_on_heading_behavior}
|
||||
test_drive_on_heading_behavior_node.cpp
|
||||
drive_on_heading_behavior_tester.cpp
|
||||
)
|
||||
|
||||
ament_target_dependencies(${test_drive_on_heading_behavior}
|
||||
${dependencies}
|
||||
)
|
||||
|
||||
ament_add_test(test_drive_on_heading_recovery
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_drive_on_heading_behavior_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_EXECUTABLE=$<TARGET_FILE:${test_drive_on_heading_behavior}>
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
)
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <string>
|
||||
#include <random>
|
||||
#include <tuple>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include "drive_on_heading_behavior_tester.hpp"
|
||||
#include "nav2_util/geometry_utils.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace std::chrono; // NOLINT
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
DriveOnHeadingBehaviorTester::DriveOnHeadingBehaviorTester()
|
||||
: is_active_(false),
|
||||
initial_pose_received_(false)
|
||||
{
|
||||
rclcpp::NodeOptions options;
|
||||
options.parameter_overrides({{"use_sim_time", true}});
|
||||
node_ = rclcpp::Node::make_shared("DriveOnHeading_behavior_test", options);
|
||||
|
||||
tf_buffer_ = std::make_shared<tf2_ros::Buffer>(node_->get_clock());
|
||||
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);
|
||||
|
||||
client_ptr_ = rclcpp_action::create_client<DriveOnHeading>(
|
||||
node_->get_node_base_interface(),
|
||||
node_->get_node_graph_interface(),
|
||||
node_->get_node_logging_interface(),
|
||||
node_->get_node_waitables_interface(),
|
||||
"drive_on_heading");
|
||||
|
||||
publisher_ =
|
||||
node_->create_publisher<geometry_msgs::msg::PoseWithCovarianceStamped>("initialpose", 10);
|
||||
|
||||
subscription_ = node_->create_subscription<geometry_msgs::msg::PoseWithCovarianceStamped>(
|
||||
"amcl_pose", rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable(),
|
||||
std::bind(&DriveOnHeadingBehaviorTester::amclPoseCallback, this, std::placeholders::_1));
|
||||
|
||||
stamp_ = node_->now();
|
||||
}
|
||||
|
||||
DriveOnHeadingBehaviorTester::~DriveOnHeadingBehaviorTester()
|
||||
{
|
||||
if (is_active_) {
|
||||
deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
void DriveOnHeadingBehaviorTester::activate()
|
||||
{
|
||||
if (is_active_) {
|
||||
throw std::runtime_error("Trying to activate while already active");
|
||||
return;
|
||||
}
|
||||
|
||||
while (!initial_pose_received_) {
|
||||
RCLCPP_WARN(node_->get_logger(), "Initial pose not received");
|
||||
sendInitialPose();
|
||||
std::this_thread::sleep_for(100ms);
|
||||
rclcpp::spin_some(node_);
|
||||
}
|
||||
|
||||
// Wait for lifecycle_manager_navigation to activate behavior_server
|
||||
std::this_thread::sleep_for(10s);
|
||||
|
||||
if (!client_ptr_) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Action client not initialized");
|
||||
is_active_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client_ptr_->wait_for_action_server(10s)) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Action server not available after waiting");
|
||||
is_active_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
RCLCPP_INFO(this->node_->get_logger(), "DriveOnHeading action server is ready");
|
||||
is_active_ = true;
|
||||
}
|
||||
|
||||
void DriveOnHeadingBehaviorTester::deactivate()
|
||||
{
|
||||
if (!is_active_) {
|
||||
throw std::runtime_error("Trying to deactivate while already inactive");
|
||||
}
|
||||
is_active_ = false;
|
||||
}
|
||||
|
||||
bool DriveOnHeadingBehaviorTester::defaultDriveOnHeadingBehaviorTest(
|
||||
const DriveOnHeading::Goal goal_msg,
|
||||
const double tolerance)
|
||||
{
|
||||
if (!is_active_) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Not activated");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sleep to let behavior server be ready for serving in multiple runs
|
||||
std::this_thread::sleep_for(5s);
|
||||
|
||||
RCLCPP_INFO(this->node_->get_logger(), "Sending goal");
|
||||
|
||||
geometry_msgs::msg::PoseStamped initial_pose;
|
||||
if (!nav2_util::getCurrentPose(initial_pose, *tf_buffer_, "odom")) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Current robot pose is not available.");
|
||||
return false;
|
||||
}
|
||||
RCLCPP_INFO(node_->get_logger(), "Found current robot pose");
|
||||
|
||||
auto goal_handle_future = client_ptr_->async_send_goal(goal_msg);
|
||||
|
||||
if (rclcpp::spin_until_future_complete(node_, goal_handle_future) !=
|
||||
rclcpp::FutureReturnCode::SUCCESS)
|
||||
{
|
||||
RCLCPP_ERROR(node_->get_logger(), "send goal call failed :(");
|
||||
return false;
|
||||
}
|
||||
|
||||
rclcpp_action::ClientGoalHandle<DriveOnHeading>::SharedPtr goal_handle = goal_handle_future.get();
|
||||
if (!goal_handle) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Goal was rejected by server");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for the server to be done with the goal
|
||||
auto result_future = client_ptr_->async_get_result(goal_handle);
|
||||
|
||||
RCLCPP_INFO(node_->get_logger(), "Waiting for result");
|
||||
if (rclcpp::spin_until_future_complete(node_, result_future) !=
|
||||
rclcpp::FutureReturnCode::SUCCESS)
|
||||
{
|
||||
RCLCPP_ERROR(node_->get_logger(), "get result call failed :(");
|
||||
return false;
|
||||
}
|
||||
|
||||
rclcpp_action::ClientGoalHandle<DriveOnHeading>::WrappedResult wrapped_result =
|
||||
result_future.get();
|
||||
|
||||
switch (wrapped_result.code) {
|
||||
case rclcpp_action::ResultCode::SUCCEEDED: break;
|
||||
case rclcpp_action::ResultCode::ABORTED: RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Goal was aborted");
|
||||
return false;
|
||||
case rclcpp_action::ResultCode::CANCELED: RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Goal was canceled");
|
||||
return false;
|
||||
default: RCLCPP_ERROR(node_->get_logger(), "Unknown result code");
|
||||
return false;
|
||||
}
|
||||
|
||||
RCLCPP_INFO(node_->get_logger(), "result received");
|
||||
|
||||
geometry_msgs::msg::PoseStamped current_pose;
|
||||
if (!nav2_util::getCurrentPose(current_pose, *tf_buffer_, "odom")) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Current robot pose is not available.");
|
||||
return false;
|
||||
}
|
||||
|
||||
double dist = nav2_util::geometry_utils::euclidean_distance(initial_pose, current_pose);
|
||||
|
||||
if (fabs(dist) > fabs(goal_msg.target.x) + tolerance) {
|
||||
RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Distance from goal is %lf (tolerance %lf)",
|
||||
fabs(dist - goal_msg.target.x), tolerance);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DriveOnHeadingBehaviorTester::sendInitialPose()
|
||||
{
|
||||
geometry_msgs::msg::PoseWithCovarianceStamped pose;
|
||||
pose.header.frame_id = "map";
|
||||
pose.header.stamp = stamp_;
|
||||
pose.pose.pose.position.x = -2.0;
|
||||
pose.pose.pose.position.y = -0.5;
|
||||
pose.pose.pose.position.z = 0.0;
|
||||
pose.pose.pose.orientation.x = 0.0;
|
||||
pose.pose.pose.orientation.y = 0.0;
|
||||
pose.pose.pose.orientation.z = 0.0;
|
||||
pose.pose.pose.orientation.w = 1.0;
|
||||
for (int i = 0; i < 35; i++) {
|
||||
pose.pose.covariance[i] = 0.0;
|
||||
}
|
||||
pose.pose.covariance[0] = 0.08;
|
||||
pose.pose.covariance[7] = 0.08;
|
||||
pose.pose.covariance[35] = 0.05;
|
||||
|
||||
publisher_->publish(pose);
|
||||
RCLCPP_INFO(node_->get_logger(), "Sent initial pose");
|
||||
}
|
||||
|
||||
void DriveOnHeadingBehaviorTester::amclPoseCallback(
|
||||
const geometry_msgs::msg::PoseWithCovarianceStamped::SharedPtr)
|
||||
{
|
||||
initial_pose_received_ = true;
|
||||
}
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2020 Samsung Research
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#ifndef BEHAVIORS__DRIVE_ON_HEADING__DRIVE_ON_HEADING_BEHAVIOR_TESTER_HPP_
|
||||
#define BEHAVIORS__DRIVE_ON_HEADING__DRIVE_ON_HEADING_BEHAVIOR_TESTER_HPP_
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <algorithm>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "rclcpp_action/rclcpp_action.hpp"
|
||||
#include "angles/angles.h"
|
||||
#include "nav2_msgs/action/drive_on_heading.hpp"
|
||||
#include "nav2_util/robot_utils.hpp"
|
||||
#include "nav2_util/node_thread.hpp"
|
||||
#include "geometry_msgs/msg/pose_stamped.hpp"
|
||||
#include "geometry_msgs/msg/pose_with_covariance_stamped.hpp"
|
||||
|
||||
#include "tf2/utils.h"
|
||||
#include "tf2_ros/buffer.h"
|
||||
#include "tf2_ros/transform_listener.h"
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
class DriveOnHeadingBehaviorTester
|
||||
{
|
||||
public:
|
||||
using DriveOnHeading = nav2_msgs::action::DriveOnHeading;
|
||||
using GoalHandleDriveOnHeading = rclcpp_action::ClientGoalHandle<DriveOnHeading>;
|
||||
|
||||
DriveOnHeadingBehaviorTester();
|
||||
~DriveOnHeadingBehaviorTester();
|
||||
|
||||
// Runs a single test with given target yaw
|
||||
bool defaultDriveOnHeadingBehaviorTest(
|
||||
const DriveOnHeading::Goal goal_msg,
|
||||
double tolerance);
|
||||
|
||||
void activate();
|
||||
|
||||
void deactivate();
|
||||
|
||||
bool isActive() const
|
||||
{
|
||||
return is_active_;
|
||||
}
|
||||
|
||||
private:
|
||||
void sendInitialPose();
|
||||
|
||||
void amclPoseCallback(geometry_msgs::msg::PoseWithCovarianceStamped::SharedPtr);
|
||||
|
||||
bool is_active_;
|
||||
bool initial_pose_received_;
|
||||
rclcpp::Time stamp_;
|
||||
|
||||
std::shared_ptr<tf2_ros::Buffer> tf_buffer_;
|
||||
std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
|
||||
|
||||
rclcpp::Node::SharedPtr node_;
|
||||
|
||||
// Publisher to publish initial pose
|
||||
rclcpp::Publisher<geometry_msgs::msg::PoseWithCovarianceStamped>::SharedPtr publisher_;
|
||||
|
||||
// Subscriber for amcl pose
|
||||
rclcpp::Subscription<geometry_msgs::msg::PoseWithCovarianceStamped>::SharedPtr subscription_;
|
||||
|
||||
// Action client to call DriveOnHeading action
|
||||
rclcpp_action::Client<DriveOnHeading>::SharedPtr client_ptr_;
|
||||
};
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
|
||||
#endif // BEHAVIORS__DRIVE_ON_HEADING__DRIVE_ON_HEADING_BEHAVIOR_TESTER_HPP_
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright (c) 2012 Samsung Research America
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
from launch.actions import ExecuteProcess, IncludeLaunchDescription, SetEnvironmentVariable
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch_ros.actions import Node
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
from nav2_common.launch import RewrittenYaml
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
map_yaml_file = os.getenv('TEST_MAP')
|
||||
world = os.getenv('TEST_WORLD')
|
||||
|
||||
bt_navigator_xml = os.path.join(get_package_share_directory('nav2_bt_navigator'),
|
||||
'behavior_trees',
|
||||
os.getenv('BT_NAVIGATOR_XML'))
|
||||
|
||||
bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
params_file = os.path.join(bringup_dir, 'params/nav2_params.yaml')
|
||||
|
||||
# Replace the `use_astar` setting on the params file
|
||||
configured_params = RewrittenYaml(
|
||||
source_file=params_file,
|
||||
root_key='',
|
||||
param_rewrites='',
|
||||
convert_types=True)
|
||||
|
||||
return LaunchDescription([
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM', '1'),
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_USE_STDOUT', '1'),
|
||||
|
||||
# Launch gazebo server for simulation
|
||||
ExecuteProcess(
|
||||
cmd=['gzserver', '-s', 'libgazebo_ros_init.so',
|
||||
'--minimal_comms', world],
|
||||
output='screen'),
|
||||
|
||||
# TODO(orduno) Launch the robot state publisher instead
|
||||
# using a local copy of TB3 urdf file
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_footprint', 'base_link']),
|
||||
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_scan']),
|
||||
|
||||
IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(bringup_dir, 'launch', 'bringup_launch.py')),
|
||||
launch_arguments={
|
||||
'map': map_yaml_file,
|
||||
'use_sim_time': 'True',
|
||||
'params_file': configured_params,
|
||||
'bt_xml_file': bt_navigator_xml,
|
||||
'autostart': 'True'}.items()),
|
||||
])
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
ld = generate_launch_description()
|
||||
|
||||
testExecutable = os.getenv('TEST_EXECUTABLE')
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[testExecutable],
|
||||
name='test_drive_on_heading_behavior_node',
|
||||
output='screen')
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) 2020 Samsung Research
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <tuple>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
|
||||
#include "drive_on_heading_behavior_tester.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using nav2_system_tests::DriveOnHeadingBehaviorTester;
|
||||
|
||||
struct TestParameters
|
||||
{
|
||||
float x;
|
||||
float y;
|
||||
float speed;
|
||||
float time_allowance;
|
||||
float tolerance;
|
||||
};
|
||||
|
||||
std::string testNameGenerator(const testing::TestParamInfo<TestParameters> &)
|
||||
{
|
||||
static int test_index = 0;
|
||||
std::string name = "DriveOnHeadingTest" + std::to_string(test_index);
|
||||
++test_index;
|
||||
return name;
|
||||
}
|
||||
|
||||
class DriveOnHeadingBehaviorTestFixture
|
||||
: public ::testing::TestWithParam<TestParameters>
|
||||
{
|
||||
public:
|
||||
static void SetUpTestCase()
|
||||
{
|
||||
drive_on_heading_behavior_tester = new DriveOnHeadingBehaviorTester();
|
||||
if (!drive_on_heading_behavior_tester->isActive()) {
|
||||
drive_on_heading_behavior_tester->activate();
|
||||
}
|
||||
}
|
||||
|
||||
static void TearDownTestCase()
|
||||
{
|
||||
delete drive_on_heading_behavior_tester;
|
||||
drive_on_heading_behavior_tester = nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
static DriveOnHeadingBehaviorTester * drive_on_heading_behavior_tester;
|
||||
};
|
||||
|
||||
DriveOnHeadingBehaviorTester * DriveOnHeadingBehaviorTestFixture::drive_on_heading_behavior_tester =
|
||||
nullptr;
|
||||
|
||||
TEST_P(DriveOnHeadingBehaviorTestFixture, testBackupBehavior)
|
||||
{
|
||||
auto test_params = GetParam();
|
||||
auto goal = nav2_msgs::action::DriveOnHeading::Goal();
|
||||
goal.target.x = test_params.x;
|
||||
goal.target.y = test_params.y;
|
||||
goal.speed = test_params.speed;
|
||||
goal.time_allowance.sec = test_params.time_allowance;
|
||||
float tolerance = test_params.tolerance;
|
||||
|
||||
if (!drive_on_heading_behavior_tester->isActive()) {
|
||||
drive_on_heading_behavior_tester->activate();
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
success = drive_on_heading_behavior_tester->defaultDriveOnHeadingBehaviorTest(
|
||||
goal,
|
||||
tolerance);
|
||||
|
||||
float dist_to_obstacle = 2.0f;
|
||||
if ( ((dist_to_obstacle - std::fabs(test_params.x)) < std::fabs(goal.speed)) ||
|
||||
std::fabs(goal.target.y) > 0 ||
|
||||
goal.time_allowance.sec < 2.0 ||
|
||||
!((goal.target.x > 0.0) == (goal.speed > 0.0)))
|
||||
{
|
||||
EXPECT_FALSE(success);
|
||||
} else {
|
||||
EXPECT_TRUE(success);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<TestParameters> test_params = {TestParameters{-0.05, 0.0, -0.2, 10.0, 0.01},
|
||||
TestParameters{-0.05, 0.1, -0.2, 10.0, 0.01},
|
||||
TestParameters{-2.0, 0.0, -0.2, 10.0, 0.1},
|
||||
TestParameters{-0.05, 0.0, -0.01, 1.0, 0.01},
|
||||
TestParameters{0.05, 0.0, -0.2, 10.0, 0.01}};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
DriveOnHeadingBehaviorTests,
|
||||
DriveOnHeadingBehaviorTestFixture,
|
||||
::testing::Values(
|
||||
test_params[0],
|
||||
test_params[1],
|
||||
test_params[2],
|
||||
test_params[3],
|
||||
test_params[4]),
|
||||
testNameGenerator);
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
||||
// initialize ROS
|
||||
rclcpp::init(argc, argv);
|
||||
|
||||
bool all_successful = RUN_ALL_TESTS();
|
||||
|
||||
// shutdown ROS
|
||||
rclcpp::shutdown();
|
||||
|
||||
return all_successful;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
set(test_spin_behavior_exec test_spin_behavior_node)
|
||||
|
||||
ament_add_gtest_executable(${test_spin_behavior_exec}
|
||||
test_spin_behavior_node.cpp
|
||||
spin_behavior_tester.cpp
|
||||
)
|
||||
|
||||
ament_target_dependencies(${test_spin_behavior_exec}
|
||||
${dependencies}
|
||||
)
|
||||
|
||||
ament_add_test(test_spin_behavior
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_spin_behavior_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_EXECUTABLE=$<TARGET_FILE:${test_spin_behavior_exec}>
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
)
|
||||
|
||||
ament_add_test(test_spin_behavior_fake
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_spin_behavior_fake_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_EXECUTABLE=$<TARGET_FILE:${test_spin_behavior_exec}>
|
||||
MAKE_FAKE_COSTMAP=true
|
||||
)
|
||||
@@ -0,0 +1,351 @@
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <string>
|
||||
#include <random>
|
||||
#include <tuple>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include "spin_behavior_tester.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace std::chrono; // NOLINT
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
SpinBehaviorTester::SpinBehaviorTester()
|
||||
: is_active_(false),
|
||||
initial_pose_received_(false)
|
||||
{
|
||||
node_ = rclcpp::Node::make_shared("spin_behavior_test");
|
||||
|
||||
|
||||
tf_buffer_ = std::make_shared<tf2_ros::Buffer>(node_->get_clock());
|
||||
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);
|
||||
tf_broadcaster_ = std::make_shared<tf2_ros::TransformBroadcaster>(node_);
|
||||
if (std::getenv("MAKE_FAKE_COSTMAP") != NULL) {
|
||||
// if this variable is set, make a fake costmap
|
||||
make_fake_costmap_ = true;
|
||||
} else {
|
||||
make_fake_costmap_ = false;
|
||||
}
|
||||
|
||||
client_ptr_ = rclcpp_action::create_client<Spin>(
|
||||
node_->get_node_base_interface(),
|
||||
node_->get_node_graph_interface(),
|
||||
node_->get_node_logging_interface(),
|
||||
node_->get_node_waitables_interface(),
|
||||
"spin");
|
||||
|
||||
publisher_ =
|
||||
node_->create_publisher<geometry_msgs::msg::PoseWithCovarianceStamped>("initialpose", 10);
|
||||
fake_costmap_publisher_ =
|
||||
node_->create_publisher<nav2_msgs::msg::Costmap>(
|
||||
"local_costmap/costmap_raw",
|
||||
rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable());
|
||||
fake_footprint_publisher_ = node_->create_publisher<geometry_msgs::msg::PolygonStamped>(
|
||||
"local_costmap/published_footprint", rclcpp::SystemDefaultsQoS());
|
||||
|
||||
subscription_ = node_->create_subscription<geometry_msgs::msg::PoseWithCovarianceStamped>(
|
||||
"amcl_pose", rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable(),
|
||||
std::bind(&SpinBehaviorTester::amclPoseCallback, this, std::placeholders::_1));
|
||||
|
||||
stamp_ = node_->now();
|
||||
}
|
||||
|
||||
SpinBehaviorTester::~SpinBehaviorTester()
|
||||
{
|
||||
if (is_active_) {
|
||||
deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
void SpinBehaviorTester::activate()
|
||||
{
|
||||
if (is_active_) {
|
||||
throw std::runtime_error("Trying to activate while already active");
|
||||
return;
|
||||
}
|
||||
if (!make_fake_costmap_) {
|
||||
while (!initial_pose_received_) {
|
||||
RCLCPP_WARN(node_->get_logger(), "Initial pose not received");
|
||||
sendInitialPose();
|
||||
std::this_thread::sleep_for(100ms);
|
||||
rclcpp::spin_some(node_);
|
||||
}
|
||||
} else {
|
||||
sendFakeOdom(0.0);
|
||||
}
|
||||
|
||||
// Wait for lifecycle_manager_navigation to activate behavior_server
|
||||
std::this_thread::sleep_for(10s);
|
||||
|
||||
if (!client_ptr_) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Action client not initialized");
|
||||
is_active_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client_ptr_->wait_for_action_server(10s)) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Action server not available after waiting");
|
||||
is_active_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
RCLCPP_INFO(this->node_->get_logger(), "Spin action server is ready");
|
||||
is_active_ = true;
|
||||
}
|
||||
|
||||
void SpinBehaviorTester::deactivate()
|
||||
{
|
||||
if (!is_active_) {
|
||||
throw std::runtime_error("Trying to deactivate while already inactive");
|
||||
}
|
||||
is_active_ = false;
|
||||
}
|
||||
|
||||
bool SpinBehaviorTester::defaultSpinBehaviorTest(
|
||||
const float target_yaw,
|
||||
const double tolerance)
|
||||
{
|
||||
if (!is_active_) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Not activated");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sleep to let behavior server be ready for serving in multiple runs
|
||||
std::this_thread::sleep_for(5s);
|
||||
|
||||
if (make_fake_costmap_) {
|
||||
sendFakeOdom(0.0);
|
||||
}
|
||||
|
||||
auto goal_msg = Spin::Goal();
|
||||
goal_msg.target_yaw = target_yaw;
|
||||
|
||||
// Initialize fake costmap
|
||||
if (make_fake_costmap_) {
|
||||
sendFakeCostmap(target_yaw);
|
||||
sendFakeOdom(0.0);
|
||||
}
|
||||
|
||||
geometry_msgs::msg::PoseStamped initial_pose;
|
||||
if (!nav2_util::getCurrentPose(initial_pose, *tf_buffer_, "odom")) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Current robot pose is not available.");
|
||||
return false;
|
||||
}
|
||||
RCLCPP_INFO(node_->get_logger(), "Found current robot pose");
|
||||
RCLCPP_INFO(
|
||||
node_->get_logger(),
|
||||
"Init Yaw is %lf",
|
||||
fabs(tf2::getYaw(initial_pose.pose.orientation)));
|
||||
RCLCPP_INFO(node_->get_logger(), "Before sending goal");
|
||||
|
||||
// Initialize fake costmap
|
||||
if (make_fake_costmap_) {
|
||||
sendFakeCostmap(target_yaw);
|
||||
sendFakeOdom(0.0);
|
||||
}
|
||||
|
||||
rclcpp::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
auto goal_handle_future = client_ptr_->async_send_goal(goal_msg);
|
||||
|
||||
if (rclcpp::spin_until_future_complete(node_, goal_handle_future) !=
|
||||
rclcpp::FutureReturnCode::SUCCESS)
|
||||
{
|
||||
RCLCPP_ERROR(node_->get_logger(), "send goal call failed :(");
|
||||
return false;
|
||||
}
|
||||
|
||||
rclcpp_action::ClientGoalHandle<Spin>::SharedPtr goal_handle = goal_handle_future.get();
|
||||
if (!goal_handle) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Goal was rejected by server");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for the server to be done with the goal
|
||||
auto result_future = client_ptr_->async_get_result(goal_handle);
|
||||
|
||||
RCLCPP_INFO(node_->get_logger(), "Waiting for result");
|
||||
rclcpp::sleep_for(std::chrono::milliseconds(1000));
|
||||
|
||||
if (make_fake_costmap_) { // if we are faking the costmap, we will fake success.
|
||||
sendFakeOdom(0.0);
|
||||
sendFakeCostmap(target_yaw);
|
||||
RCLCPP_INFO(node_->get_logger(), "target_yaw %lf", target_yaw);
|
||||
// Slowly increment command yaw by increment to simulate the robot slowly spinning into place
|
||||
float step_size = tolerance / 4.0;
|
||||
for (float command_yaw = 0.0;
|
||||
abs(command_yaw) < abs(target_yaw);
|
||||
command_yaw = command_yaw + step_size)
|
||||
{
|
||||
sendFakeOdom(command_yaw);
|
||||
sendFakeCostmap(target_yaw);
|
||||
rclcpp::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
sendFakeOdom(target_yaw);
|
||||
sendFakeCostmap(target_yaw);
|
||||
RCLCPP_INFO(node_->get_logger(), "After sending goal");
|
||||
}
|
||||
if (rclcpp::spin_until_future_complete(node_, result_future) !=
|
||||
rclcpp::FutureReturnCode::SUCCESS)
|
||||
{
|
||||
RCLCPP_ERROR(node_->get_logger(), "get result call failed :(");
|
||||
return false;
|
||||
}
|
||||
|
||||
rclcpp_action::ClientGoalHandle<Spin>::WrappedResult wrapped_result = result_future.get();
|
||||
|
||||
switch (wrapped_result.code) {
|
||||
case rclcpp_action::ResultCode::SUCCEEDED: break;
|
||||
case rclcpp_action::ResultCode::ABORTED: RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Goal was aborted");
|
||||
return false;
|
||||
case rclcpp_action::ResultCode::CANCELED: RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Goal was canceled");
|
||||
return false;
|
||||
default: RCLCPP_ERROR(node_->get_logger(), "Unknown result code");
|
||||
return false;
|
||||
}
|
||||
|
||||
RCLCPP_INFO(node_->get_logger(), "result received");
|
||||
|
||||
geometry_msgs::msg::PoseStamped current_pose;
|
||||
if (!nav2_util::getCurrentPose(current_pose, *tf_buffer_, "odom")) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Current robot pose is not available.");
|
||||
return false;
|
||||
}
|
||||
|
||||
double goal_yaw = angles::normalize_angle(
|
||||
tf2::getYaw(initial_pose.pose.orientation) + target_yaw);
|
||||
double dyaw = angles::shortest_angular_distance(
|
||||
goal_yaw, tf2::getYaw(current_pose.pose.orientation));
|
||||
|
||||
if (fabs(dyaw) > tolerance) {
|
||||
RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Init Yaw is %lf (tolerance %lf)",
|
||||
fabs(tf2::getYaw(initial_pose.pose.orientation)), tolerance);
|
||||
RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Current Yaw is %lf (tolerance %lf)",
|
||||
fabs(tf2::getYaw(current_pose.pose.orientation)), tolerance);
|
||||
RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Angular distance from goal is %lf (tolerance %lf)",
|
||||
fabs(dyaw), tolerance);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SpinBehaviorTester::sendFakeCostmap(float angle)
|
||||
{
|
||||
nav2_msgs::msg::Costmap fake_costmap;
|
||||
|
||||
fake_costmap.header.frame_id = "odom";
|
||||
fake_costmap.header.stamp = stamp_;
|
||||
fake_costmap.metadata.layer = "master";
|
||||
fake_costmap.metadata.resolution = .1;
|
||||
fake_costmap.metadata.size_x = 100;
|
||||
fake_costmap.metadata.size_y = 100;
|
||||
fake_costmap.metadata.origin.position.x = 0;
|
||||
fake_costmap.metadata.origin.position.y = 0;
|
||||
fake_costmap.metadata.origin.orientation.w = 1.0;
|
||||
float costmap_val = 0;
|
||||
for (int ix = 0; ix < 100; ix++) {
|
||||
for (int iy = 0; iy < 100; iy++) {
|
||||
if (abs(angle) > M_PI_2f32) {
|
||||
// fake obstacles in the way so we get failure due to potential collision
|
||||
costmap_val = 100;
|
||||
}
|
||||
fake_costmap.data.push_back(costmap_val);
|
||||
}
|
||||
}
|
||||
fake_costmap_publisher_->publish(fake_costmap);
|
||||
}
|
||||
|
||||
void SpinBehaviorTester::sendInitialPose()
|
||||
{
|
||||
geometry_msgs::msg::PoseWithCovarianceStamped pose;
|
||||
pose.header.frame_id = "map";
|
||||
pose.header.stamp = stamp_;
|
||||
pose.pose.pose.position.x = -2.0;
|
||||
pose.pose.pose.position.y = -0.5;
|
||||
pose.pose.pose.position.z = 0.0;
|
||||
pose.pose.pose.orientation.x = 0.0;
|
||||
pose.pose.pose.orientation.y = 0.0;
|
||||
pose.pose.pose.orientation.z = 0.0;
|
||||
pose.pose.pose.orientation.w = 1.0;
|
||||
for (int i = 0; i < 35; i++) {
|
||||
pose.pose.covariance[i] = 0.0;
|
||||
}
|
||||
pose.pose.covariance[0] = 0.08;
|
||||
pose.pose.covariance[7] = 0.08;
|
||||
pose.pose.covariance[35] = 0.05;
|
||||
|
||||
publisher_->publish(pose);
|
||||
RCLCPP_INFO(node_->get_logger(), "Sent initial pose");
|
||||
}
|
||||
|
||||
void SpinBehaviorTester::sendFakeOdom(float angle)
|
||||
{
|
||||
geometry_msgs::msg::TransformStamped transformStamped;
|
||||
|
||||
transformStamped.header.stamp = stamp_;
|
||||
transformStamped.header.frame_id = "odom";
|
||||
transformStamped.child_frame_id = "base_link";
|
||||
transformStamped.transform.translation.x = 0.0;
|
||||
transformStamped.transform.translation.y = 0.0;
|
||||
transformStamped.transform.translation.z = 0.0;
|
||||
tf2::Quaternion q;
|
||||
q.setRPY(0, 0, angle);
|
||||
transformStamped.transform.rotation.x = q.x();
|
||||
transformStamped.transform.rotation.y = q.y();
|
||||
transformStamped.transform.rotation.z = q.z();
|
||||
transformStamped.transform.rotation.w = q.w();
|
||||
|
||||
tf_broadcaster_->sendTransform(transformStamped);
|
||||
|
||||
geometry_msgs::msg::PolygonStamped footprint;
|
||||
footprint.header.frame_id = "odom";
|
||||
footprint.header.stamp = stamp_;
|
||||
footprint.polygon.points.resize(4);
|
||||
footprint.polygon.points[0].x = 0.22;
|
||||
footprint.polygon.points[0].y = 0.22;
|
||||
footprint.polygon.points[1].x = 0.22;
|
||||
footprint.polygon.points[1].y = -0.22;
|
||||
footprint.polygon.points[2].x = -0.22;
|
||||
footprint.polygon.points[2].y = -0.22;
|
||||
footprint.polygon.points[3].x = -0.22;
|
||||
footprint.polygon.points[3].y = 0.22;
|
||||
fake_footprint_publisher_->publish(footprint);
|
||||
}
|
||||
void SpinBehaviorTester::amclPoseCallback(
|
||||
const geometry_msgs::msg::PoseWithCovarianceStamped::SharedPtr)
|
||||
{
|
||||
initial_pose_received_ = true;
|
||||
}
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#ifndef BEHAVIORS__SPIN__SPIN_BEHAVIOR_TESTER_HPP_
|
||||
#define BEHAVIORS__SPIN__SPIN_BEHAVIOR_TESTER_HPP_
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <algorithm>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "rclcpp_action/rclcpp_action.hpp"
|
||||
#include "angles/angles.h"
|
||||
#include "nav2_msgs/action/spin.hpp"
|
||||
#include "nav2_msgs/msg/costmap.hpp"
|
||||
#include "nav2_util/robot_utils.hpp"
|
||||
#include "nav2_util/node_thread.hpp"
|
||||
#include "geometry_msgs/msg/point32.hpp"
|
||||
#include "geometry_msgs/msg/polygon_stamped.hpp"
|
||||
#include "geometry_msgs/msg/pose_stamped.hpp"
|
||||
#include "geometry_msgs/msg/pose_with_covariance_stamped.hpp"
|
||||
#include "geometry_msgs/msg/transform_stamped.hpp"
|
||||
#include "geometry_msgs/msg/quaternion.hpp"
|
||||
|
||||
#include "tf2/utils.h"
|
||||
#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
|
||||
#include "tf2_ros/buffer.h"
|
||||
#include "tf2_ros/transform_broadcaster.h"
|
||||
#include "tf2_ros/transform_listener.h"
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
class SpinBehaviorTester
|
||||
{
|
||||
public:
|
||||
using Spin = nav2_msgs::action::Spin;
|
||||
using GoalHandleSpin = rclcpp_action::ClientGoalHandle<Spin>;
|
||||
|
||||
SpinBehaviorTester();
|
||||
~SpinBehaviorTester();
|
||||
|
||||
// Runs a single test with given target yaw
|
||||
bool defaultSpinBehaviorTest(
|
||||
float target_yaw,
|
||||
double tolerance = 0.1);
|
||||
|
||||
void activate();
|
||||
|
||||
void deactivate();
|
||||
|
||||
bool isActive() const
|
||||
{
|
||||
return is_active_;
|
||||
}
|
||||
|
||||
private:
|
||||
void sendInitialPose();
|
||||
|
||||
void sendFakeCostmap(float angle);
|
||||
void sendFakeOdom(float angle);
|
||||
|
||||
void amclPoseCallback(geometry_msgs::msg::PoseWithCovarianceStamped::SharedPtr);
|
||||
|
||||
bool is_active_;
|
||||
bool initial_pose_received_;
|
||||
bool make_fake_costmap_;
|
||||
rclcpp::Time stamp_;
|
||||
|
||||
std::shared_ptr<tf2_ros::Buffer> tf_buffer_;
|
||||
std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
|
||||
std::shared_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
|
||||
|
||||
rclcpp::Node::SharedPtr node_;
|
||||
|
||||
// Publisher to publish initial pose
|
||||
rclcpp::Publisher<geometry_msgs::msg::PoseWithCovarianceStamped>::SharedPtr publisher_;
|
||||
|
||||
// Publisher to publish fake costmap raw
|
||||
rclcpp::Publisher<nav2_msgs::msg::Costmap>::SharedPtr fake_costmap_publisher_;
|
||||
|
||||
// Publisher to publish fake costmap footprint
|
||||
rclcpp::Publisher<geometry_msgs::msg::PolygonStamped>::SharedPtr fake_footprint_publisher_;
|
||||
|
||||
// Subscriber for amcl pose
|
||||
rclcpp::Subscription<geometry_msgs::msg::PoseWithCovarianceStamped>::SharedPtr subscription_;
|
||||
|
||||
// Action client to call spin action
|
||||
rclcpp_action::Client<Spin>::SharedPtr client_ptr_;
|
||||
};
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
|
||||
#endif // BEHAVIORS__SPIN__SPIN_BEHAVIOR_TESTER_HPP_
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright (c) 2019 Samsung Research America
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
from launch.actions import DeclareLaunchArgument, ExecuteProcess, SetEnvironmentVariable
|
||||
from launch.substitutions import LaunchConfiguration
|
||||
from launch_ros.actions import Node
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
from nav2_common.launch import RewrittenYaml
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
|
||||
bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
|
||||
namespace = LaunchConfiguration('namespace')
|
||||
use_sim_time = LaunchConfiguration('use_sim_time')
|
||||
autostart = LaunchConfiguration('autostart')
|
||||
params_file = LaunchConfiguration('params_file')
|
||||
default_nav_through_poses_bt_xml = LaunchConfiguration('default_nav_through_poses_bt_xml')
|
||||
default_nav_to_pose_bt_xml = LaunchConfiguration('default_nav_to_pose_bt_xml')
|
||||
map_subscribe_transient_local = LaunchConfiguration('map_subscribe_transient_local')
|
||||
|
||||
# Create our own temporary YAML files that include substitutions
|
||||
param_substitutions = {
|
||||
'use_sim_time': use_sim_time,
|
||||
'default_nav_through_poses_bt_xml': default_nav_through_poses_bt_xml,
|
||||
'default_nav_to_pose_bt_xml': default_nav_to_pose_bt_xml,
|
||||
'autostart': autostart,
|
||||
'map_subscribe_transient_local': map_subscribe_transient_local}
|
||||
|
||||
configured_params = RewrittenYaml(
|
||||
source_file=params_file,
|
||||
root_key=namespace,
|
||||
param_rewrites=param_substitutions,
|
||||
convert_types=True)
|
||||
|
||||
lifecycle_nodes = ['behavior_server']
|
||||
|
||||
# Map fully qualified names to relative ones so the node's namespace can be prepended.
|
||||
# In case of the transforms (tf), currently, there doesn't seem to be a better alternative
|
||||
# https://github.com/ros/geometry2/issues/32
|
||||
# https://github.com/ros/robot_state_publisher/pull/30
|
||||
# TODO(orduno) Substitute with `PushNodeRemapping`
|
||||
# https://github.com/ros2/launch_ros/issues/56
|
||||
remappings = [('/tf', 'tf'),
|
||||
('/tf_static', 'tf_static')]
|
||||
|
||||
return LaunchDescription([
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM', '1'),
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_USE_STDOUT', '1'),
|
||||
|
||||
DeclareLaunchArgument(
|
||||
'namespace', default_value='',
|
||||
description='Top-level namespace'),
|
||||
|
||||
DeclareLaunchArgument(
|
||||
'use_sim_time', default_value='false',
|
||||
description='Use simulation (Gazebo) clock if true'),
|
||||
|
||||
DeclareLaunchArgument(
|
||||
'autostart', default_value='true',
|
||||
description='Automatically startup the nav2 stack'),
|
||||
|
||||
DeclareLaunchArgument(
|
||||
'params_file',
|
||||
default_value=os.path.join(bringup_dir, 'params', 'nav2_params.yaml'),
|
||||
description='Full path to the ROS2 parameters file to use'),
|
||||
|
||||
DeclareLaunchArgument(
|
||||
'default_nav_through_poses_bt_xml',
|
||||
default_value=os.path.join(
|
||||
get_package_share_directory('nav2_bt_navigator'),
|
||||
'behavior_trees', 'navigate_through_poses_w_replanning_and_recovery.xml'),
|
||||
description='Full path to the behavior tree xml file to use'),
|
||||
|
||||
DeclareLaunchArgument(
|
||||
'default_nav_to_pose_bt_xml',
|
||||
default_value=os.path.join(
|
||||
get_package_share_directory('nav2_bt_navigator'),
|
||||
'behavior_trees', 'navigate_to_pose_w_replanning_and_recovery.xml'),
|
||||
description='Full path to the behavior tree xml file to use'),
|
||||
|
||||
DeclareLaunchArgument(
|
||||
'map_subscribe_transient_local', default_value='false',
|
||||
description='Whether to set the map subscriber QoS to transient local'),
|
||||
|
||||
# TODO(orduno) Launch the robot state publisher instead
|
||||
# using a local copy of TB3 urdf file
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'map', 'odom']),
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_footprint', 'base_link']),
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_scan']),
|
||||
|
||||
Node(
|
||||
package='nav2_behaviors',
|
||||
executable='behavior_server',
|
||||
name='behavior_server',
|
||||
output='screen',
|
||||
parameters=[configured_params],
|
||||
remappings=remappings),
|
||||
|
||||
Node(
|
||||
package='nav2_lifecycle_manager',
|
||||
executable='lifecycle_manager',
|
||||
name='lifecycle_manager_navigation',
|
||||
output='screen',
|
||||
parameters=[{'use_sim_time': use_sim_time},
|
||||
{'autostart': autostart},
|
||||
{'node_names': lifecycle_nodes}]),
|
||||
])
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
ld = generate_launch_description()
|
||||
|
||||
testExecutable = os.getenv('TEST_EXECUTABLE')
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[testExecutable],
|
||||
name='test_spin_behavior_fake_node',
|
||||
output='screen')
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright (c) 2019 Samsung Research America
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
from launch.actions import ExecuteProcess, IncludeLaunchDescription, SetEnvironmentVariable
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch_ros.actions import Node
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
from nav2_common.launch import RewrittenYaml
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
map_yaml_file = os.getenv('TEST_MAP')
|
||||
world = os.getenv('TEST_WORLD')
|
||||
|
||||
bt_navigator_xml = os.path.join(get_package_share_directory('nav2_bt_navigator'),
|
||||
'behavior_trees',
|
||||
os.getenv('BT_NAVIGATOR_XML'))
|
||||
|
||||
bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
params_file = os.path.join(bringup_dir, 'params/nav2_params.yaml')
|
||||
|
||||
# Replace the `use_astar` setting on the params file
|
||||
configured_params = RewrittenYaml(
|
||||
source_file=params_file,
|
||||
root_key='',
|
||||
param_rewrites='',
|
||||
convert_types=True)
|
||||
|
||||
return LaunchDescription([
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM', '1'),
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_USE_STDOUT', '1'),
|
||||
|
||||
# Launch gazebo server for simulation
|
||||
ExecuteProcess(
|
||||
cmd=['gzserver', '-s', 'libgazebo_ros_init.so',
|
||||
'--minimal_comms', world],
|
||||
output='screen'),
|
||||
|
||||
# TODO(orduno) Launch the robot state publisher instead
|
||||
# using a local copy of TB3 urdf file
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_footprint', 'base_link']),
|
||||
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_scan']),
|
||||
|
||||
IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(bringup_dir, 'launch', 'bringup_launch.py')),
|
||||
launch_arguments={
|
||||
'map': map_yaml_file,
|
||||
'use_sim_time': 'True',
|
||||
'params_file': configured_params,
|
||||
'bt_xml_file': bt_navigator_xml,
|
||||
'autostart': 'True'}.items()),
|
||||
])
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
ld = generate_launch_description()
|
||||
|
||||
testExecutable = os.getenv('TEST_EXECUTABLE')
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[testExecutable], name='test_spin_behavior_node', output='screen',
|
||||
)
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2020 Samsung Research
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <cmath>
|
||||
#include <tuple>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
|
||||
#include "spin_behavior_tester.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using nav2_system_tests::SpinBehaviorTester;
|
||||
|
||||
std::string testNameGenerator(const testing::TestParamInfo<std::tuple<float, float>> & param)
|
||||
{
|
||||
std::string name = std::to_string(std::abs(std::get<0>(param.param))) + "_" + std::to_string(
|
||||
std::get<1>(param.param));
|
||||
name.erase(std::remove(name.begin(), name.end(), '.'), name.end());
|
||||
return name;
|
||||
}
|
||||
|
||||
class SpinBehaviorTestFixture
|
||||
: public ::testing::TestWithParam<std::tuple<float, float>>
|
||||
{
|
||||
public:
|
||||
static void SetUpTestCase()
|
||||
{
|
||||
spin_recovery_tester = new SpinBehaviorTester();
|
||||
if (!spin_recovery_tester->isActive()) {
|
||||
spin_recovery_tester->activate();
|
||||
}
|
||||
}
|
||||
|
||||
static void TearDownTestCase()
|
||||
{
|
||||
delete spin_recovery_tester;
|
||||
spin_recovery_tester = nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
static SpinBehaviorTester * spin_recovery_tester;
|
||||
};
|
||||
|
||||
SpinBehaviorTester * SpinBehaviorTestFixture::spin_recovery_tester = nullptr;
|
||||
|
||||
TEST_P(SpinBehaviorTestFixture, testSpinRecovery)
|
||||
{
|
||||
float target_yaw = std::get<0>(GetParam());
|
||||
float tolerance = std::get<1>(GetParam());
|
||||
|
||||
bool success = false;
|
||||
int num_tries = 3;
|
||||
for (int i = 0; i != num_tries; i++) {
|
||||
success = success || spin_recovery_tester->defaultSpinBehaviorTest(target_yaw, tolerance);
|
||||
if (success) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (std::getenv("MAKE_FAKE_COSTMAP") != NULL && abs(target_yaw) > M_PI_2f32) {
|
||||
// if this variable is set, make a fake costmap
|
||||
// in the fake spin test, we expect a collision for angles > M_PI_2
|
||||
EXPECT_EQ(false, success);
|
||||
} else {
|
||||
EXPECT_EQ(true, success);
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
SpinRecoveryTests,
|
||||
SpinBehaviorTestFixture,
|
||||
::testing::Values(
|
||||
std::make_tuple(-M_PIf32 / 6.0, 0.1),
|
||||
std::make_tuple(M_PI_4f32, 0.1),
|
||||
std::make_tuple(-M_PI_2f32, 0.1),
|
||||
std::make_tuple(M_PIf32, 0.1),
|
||||
std::make_tuple(3.0 * M_PIf32 / 2.0, 0.15),
|
||||
std::make_tuple(-2.0 * M_PIf32, 0.1),
|
||||
std::make_tuple(4.0 * M_PIf32, 0.15)),
|
||||
testNameGenerator);
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
||||
// initialize ROS
|
||||
rclcpp::init(argc, argv);
|
||||
|
||||
bool all_successful = RUN_ALL_TESTS();
|
||||
|
||||
// shutdown ROS
|
||||
rclcpp::shutdown();
|
||||
|
||||
return all_successful;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
set(test_wait_behavior_exec test_wait_behavior_node)
|
||||
|
||||
ament_add_gtest_executable(${test_wait_behavior_exec}
|
||||
test_wait_behavior_node.cpp
|
||||
wait_behavior_tester.cpp
|
||||
)
|
||||
|
||||
ament_target_dependencies(${test_wait_behavior_exec}
|
||||
${dependencies}
|
||||
)
|
||||
|
||||
ament_add_test(test_wait_behavior
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_wait_behavior_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_EXECUTABLE=$<TARGET_FILE:${test_wait_behavior_exec}>
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_behavior.xml
|
||||
)
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright (c) 2019 Samsung Research America
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
from launch.actions import ExecuteProcess, IncludeLaunchDescription, SetEnvironmentVariable
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch_ros.actions import Node
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
from nav2_common.launch import RewrittenYaml
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
map_yaml_file = os.getenv('TEST_MAP')
|
||||
world = os.getenv('TEST_WORLD')
|
||||
|
||||
bt_navigator_xml = os.path.join(get_package_share_directory('nav2_bt_navigator'),
|
||||
'behavior_trees',
|
||||
os.getenv('BT_NAVIGATOR_XML'))
|
||||
|
||||
bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
params_file = os.path.join(bringup_dir, 'params/nav2_params.yaml')
|
||||
|
||||
# Replace the `use_astar` setting on the params file
|
||||
configured_params = RewrittenYaml(
|
||||
source_file=params_file,
|
||||
root_key='',
|
||||
param_rewrites='',
|
||||
convert_types=True)
|
||||
|
||||
return LaunchDescription([
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM', '1'),
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_USE_STDOUT', '1'),
|
||||
|
||||
# Launch gazebo server for simulation
|
||||
ExecuteProcess(
|
||||
cmd=['gzserver', '-s', 'libgazebo_ros_init.so',
|
||||
'--minimal_comms', world],
|
||||
output='screen'),
|
||||
|
||||
# TODO(orduno) Launch the robot state publisher instead
|
||||
# using a local copy of TB3 urdf file
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_footprint', 'base_link']),
|
||||
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_scan']),
|
||||
|
||||
IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(bringup_dir, 'launch', 'bringup_launch.py')),
|
||||
launch_arguments={
|
||||
'map': map_yaml_file,
|
||||
'use_sim_time': 'True',
|
||||
'params_file': configured_params,
|
||||
'bt_xml_file': bt_navigator_xml,
|
||||
'autostart': 'True'}.items()),
|
||||
])
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
ld = generate_launch_description()
|
||||
|
||||
testExecutable = os.getenv('TEST_EXECUTABLE')
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[testExecutable], name='test_wait_behavior_node', output='screen',
|
||||
)
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) 2020 Samsung Research
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <tuple>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
|
||||
#include "wait_behavior_tester.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using nav2_system_tests::WaitBehaviorTester;
|
||||
|
||||
std::string testNameGenerator(const testing::TestParamInfo<std::tuple<float, float>> & param)
|
||||
{
|
||||
std::string name = std::to_string(std::abs(std::get<0>(param.param))) + "_" + std::to_string(
|
||||
std::get<1>(param.param));
|
||||
name.erase(std::remove(name.begin(), name.end(), '.'), name.end());
|
||||
return name;
|
||||
}
|
||||
|
||||
class WaitBehaviorTestFixture
|
||||
: public ::testing::TestWithParam<std::tuple<float, float>>
|
||||
{
|
||||
public:
|
||||
static void SetUpTestCase()
|
||||
{
|
||||
wait_behavior_tester = new WaitBehaviorTester();
|
||||
if (!wait_behavior_tester->isActive()) {
|
||||
wait_behavior_tester->activate();
|
||||
}
|
||||
}
|
||||
|
||||
static void TearDownTestCase()
|
||||
{
|
||||
delete wait_behavior_tester;
|
||||
wait_behavior_tester = nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
static WaitBehaviorTester * wait_behavior_tester;
|
||||
};
|
||||
|
||||
WaitBehaviorTester * WaitBehaviorTestFixture::wait_behavior_tester = nullptr;
|
||||
|
||||
TEST_P(WaitBehaviorTestFixture, testSWaitBehavior)
|
||||
{
|
||||
float wait_time = std::get<0>(GetParam());
|
||||
float cancel = std::get<1>(GetParam());
|
||||
|
||||
bool success = false;
|
||||
int num_tries = 3;
|
||||
for (int i = 0; i != num_tries; i++) {
|
||||
if (cancel == 1.0) {
|
||||
success = success || wait_behavior_tester->behaviorTestCancel(wait_time);
|
||||
} else {
|
||||
success = success || wait_behavior_tester->behaviorTest(wait_time);
|
||||
}
|
||||
if (success) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_EQ(true, success);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
WaitBehaviorTests,
|
||||
WaitBehaviorTestFixture,
|
||||
::testing::Values(
|
||||
std::make_tuple(1.0, 0.0),
|
||||
std::make_tuple(2.0, 0.0),
|
||||
std::make_tuple(5.0, 0.0),
|
||||
std::make_tuple(10.0, 1.0)),
|
||||
testNameGenerator);
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
||||
// initialize ROS
|
||||
rclcpp::init(argc, argv);
|
||||
|
||||
bool all_successful = RUN_ALL_TESTS();
|
||||
|
||||
// shutdown ROS
|
||||
rclcpp::shutdown();
|
||||
|
||||
return all_successful;
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
// Copyright (c) 2020 Samsung Research
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <string>
|
||||
#include <random>
|
||||
#include <tuple>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include "wait_behavior_tester.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace std::chrono; // NOLINT
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
WaitBehaviorTester::WaitBehaviorTester()
|
||||
: is_active_(false),
|
||||
initial_pose_received_(false)
|
||||
{
|
||||
node_ = rclcpp::Node::make_shared("wait_behavior_test");
|
||||
|
||||
tf_buffer_ = std::make_shared<tf2_ros::Buffer>(node_->get_clock());
|
||||
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);
|
||||
|
||||
client_ptr_ = rclcpp_action::create_client<Wait>(
|
||||
node_->get_node_base_interface(),
|
||||
node_->get_node_graph_interface(),
|
||||
node_->get_node_logging_interface(),
|
||||
node_->get_node_waitables_interface(),
|
||||
"wait");
|
||||
|
||||
publisher_ =
|
||||
node_->create_publisher<geometry_msgs::msg::PoseWithCovarianceStamped>("initialpose", 10);
|
||||
|
||||
subscription_ = node_->create_subscription<geometry_msgs::msg::PoseWithCovarianceStamped>(
|
||||
"amcl_pose", rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable(),
|
||||
std::bind(&WaitBehaviorTester::amclPoseCallback, this, std::placeholders::_1));
|
||||
}
|
||||
|
||||
WaitBehaviorTester::~WaitBehaviorTester()
|
||||
{
|
||||
if (is_active_) {
|
||||
deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
void WaitBehaviorTester::activate()
|
||||
{
|
||||
if (is_active_) {
|
||||
throw std::runtime_error("Trying to activate while already active");
|
||||
return;
|
||||
}
|
||||
|
||||
while (!initial_pose_received_) {
|
||||
RCLCPP_WARN(node_->get_logger(), "Initial pose not received");
|
||||
sendInitialPose();
|
||||
std::this_thread::sleep_for(100ms);
|
||||
rclcpp::spin_some(node_);
|
||||
}
|
||||
|
||||
// Wait for lifecycle_manager_navigation to activate behavior_server
|
||||
std::this_thread::sleep_for(10s);
|
||||
|
||||
if (!client_ptr_) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Action client not initialized");
|
||||
is_active_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client_ptr_->wait_for_action_server(10s)) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Action server not available after waiting");
|
||||
is_active_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
RCLCPP_INFO(this->node_->get_logger(), "Wait action server is ready");
|
||||
is_active_ = true;
|
||||
}
|
||||
|
||||
void WaitBehaviorTester::deactivate()
|
||||
{
|
||||
if (!is_active_) {
|
||||
throw std::runtime_error("Trying to deactivate while already inactive");
|
||||
}
|
||||
is_active_ = false;
|
||||
}
|
||||
|
||||
bool WaitBehaviorTester::behaviorTest(
|
||||
const float wait_time)
|
||||
{
|
||||
if (!is_active_) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Not activated");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sleep to let behavior server be ready for serving in multiple runs
|
||||
std::this_thread::sleep_for(5s);
|
||||
|
||||
auto start_time = node_->now();
|
||||
auto goal_msg = Wait::Goal();
|
||||
goal_msg.time = rclcpp::Duration(wait_time, 0.0);
|
||||
|
||||
RCLCPP_INFO(this->node_->get_logger(), "Sending goal");
|
||||
|
||||
auto goal_handle_future = client_ptr_->async_send_goal(goal_msg);
|
||||
|
||||
if (rclcpp::spin_until_future_complete(node_, goal_handle_future) !=
|
||||
rclcpp::FutureReturnCode::SUCCESS)
|
||||
{
|
||||
RCLCPP_ERROR(node_->get_logger(), "send goal call failed :(");
|
||||
return false;
|
||||
}
|
||||
|
||||
rclcpp_action::ClientGoalHandle<Wait>::SharedPtr goal_handle = goal_handle_future.get();
|
||||
if (!goal_handle) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Goal was rejected by server");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for the server to be done with the goal
|
||||
auto result_future = client_ptr_->async_get_result(goal_handle);
|
||||
|
||||
RCLCPP_INFO(node_->get_logger(), "Waiting for result");
|
||||
if (rclcpp::spin_until_future_complete(node_, result_future) !=
|
||||
rclcpp::FutureReturnCode::SUCCESS)
|
||||
{
|
||||
RCLCPP_ERROR(node_->get_logger(), "get result call failed :(");
|
||||
return false;
|
||||
}
|
||||
|
||||
rclcpp_action::ClientGoalHandle<Wait>::WrappedResult wrapped_result = result_future.get();
|
||||
|
||||
switch (wrapped_result.code) {
|
||||
case rclcpp_action::ResultCode::SUCCEEDED: break;
|
||||
case rclcpp_action::ResultCode::ABORTED: RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Goal was aborted");
|
||||
return false;
|
||||
case rclcpp_action::ResultCode::CANCELED: RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Goal was canceled");
|
||||
return false;
|
||||
default: RCLCPP_ERROR(node_->get_logger(), "Unknown result code");
|
||||
return false;
|
||||
}
|
||||
|
||||
RCLCPP_INFO(node_->get_logger(), "result received");
|
||||
|
||||
if ((node_->now() - start_time).seconds() < static_cast<double>(wait_time)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WaitBehaviorTester::behaviorTestCancel(
|
||||
const float wait_time)
|
||||
{
|
||||
if (!is_active_) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Not activated");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sleep to let behavior server be ready for serving in multiple runs
|
||||
std::this_thread::sleep_for(5s);
|
||||
|
||||
auto start_time = node_->now();
|
||||
auto goal_msg = Wait::Goal();
|
||||
goal_msg.time = rclcpp::Duration(wait_time, 0.0);
|
||||
|
||||
RCLCPP_INFO(this->node_->get_logger(), "Sending goal");
|
||||
|
||||
auto goal_handle_future = client_ptr_->async_send_goal(goal_msg);
|
||||
|
||||
if (rclcpp::spin_until_future_complete(node_, goal_handle_future) !=
|
||||
rclcpp::FutureReturnCode::SUCCESS)
|
||||
{
|
||||
RCLCPP_ERROR(node_->get_logger(), "send goal call failed :(");
|
||||
return false;
|
||||
}
|
||||
|
||||
rclcpp_action::ClientGoalHandle<Wait>::SharedPtr goal_handle = goal_handle_future.get();
|
||||
if (!goal_handle) {
|
||||
RCLCPP_ERROR(node_->get_logger(), "Goal was rejected by server");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for the server to be done with the goal
|
||||
auto result_future = client_ptr_->async_cancel_all_goals();
|
||||
|
||||
RCLCPP_INFO(node_->get_logger(), "Waiting for cancellation");
|
||||
if (rclcpp::spin_until_future_complete(node_, result_future) !=
|
||||
rclcpp::FutureReturnCode::SUCCESS)
|
||||
{
|
||||
RCLCPP_ERROR(node_->get_logger(), "get cancel result call failed :(");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto status = goal_handle_future.get()->get_status();
|
||||
|
||||
switch (status) {
|
||||
case rclcpp_action::GoalStatus::STATUS_SUCCEEDED: RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Goal succeeded");
|
||||
return false;
|
||||
case rclcpp_action::GoalStatus::STATUS_ABORTED: RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Goal was aborted");
|
||||
return false;
|
||||
case rclcpp_action::GoalStatus::STATUS_CANCELED: RCLCPP_INFO(
|
||||
node_->get_logger(),
|
||||
"Goal was canceled");
|
||||
return true;
|
||||
case rclcpp_action::GoalStatus::STATUS_CANCELING: RCLCPP_INFO(
|
||||
node_->get_logger(),
|
||||
"Goal is cancelling");
|
||||
return true;
|
||||
case rclcpp_action::GoalStatus::STATUS_EXECUTING: RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Goal is executing");
|
||||
return false;
|
||||
case rclcpp_action::GoalStatus::STATUS_ACCEPTED: RCLCPP_ERROR(
|
||||
node_->get_logger(),
|
||||
"Goal is processing");
|
||||
return false;
|
||||
default: RCLCPP_ERROR(node_->get_logger(), "Unknown result code");
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void WaitBehaviorTester::sendInitialPose()
|
||||
{
|
||||
geometry_msgs::msg::PoseWithCovarianceStamped pose;
|
||||
pose.header.frame_id = "map";
|
||||
pose.header.stamp = rclcpp::Time();
|
||||
pose.pose.pose.position.x = -2.0;
|
||||
pose.pose.pose.position.y = -0.5;
|
||||
pose.pose.pose.position.z = 0.0;
|
||||
pose.pose.pose.orientation.x = 0.0;
|
||||
pose.pose.pose.orientation.y = 0.0;
|
||||
pose.pose.pose.orientation.z = 0.0;
|
||||
pose.pose.pose.orientation.w = 1.0;
|
||||
for (int i = 0; i < 35; i++) {
|
||||
pose.pose.covariance[i] = 0.0;
|
||||
}
|
||||
pose.pose.covariance[0] = 0.08;
|
||||
pose.pose.covariance[7] = 0.08;
|
||||
pose.pose.covariance[35] = 0.05;
|
||||
|
||||
publisher_->publish(pose);
|
||||
RCLCPP_INFO(node_->get_logger(), "Sent initial pose");
|
||||
}
|
||||
|
||||
void WaitBehaviorTester::amclPoseCallback(
|
||||
const geometry_msgs::msg::PoseWithCovarianceStamped::SharedPtr)
|
||||
{
|
||||
initial_pose_received_ = true;
|
||||
}
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2020 Samsung Research
|
||||
// Copyright (c) 2020 Sarthak Mittal
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#ifndef BEHAVIORS__WAIT__WAIT_BEHAVIOR_TESTER_HPP_
|
||||
#define BEHAVIORS__WAIT__WAIT_BEHAVIOR_TESTER_HPP_
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <algorithm>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "rclcpp_action/rclcpp_action.hpp"
|
||||
#include "angles/angles.h"
|
||||
#include "nav2_msgs/action/wait.hpp"
|
||||
#include "nav2_util/robot_utils.hpp"
|
||||
#include "nav2_util/node_thread.hpp"
|
||||
#include "geometry_msgs/msg/pose_stamped.hpp"
|
||||
#include "geometry_msgs/msg/pose_with_covariance_stamped.hpp"
|
||||
|
||||
#include "tf2/utils.h"
|
||||
#include "tf2_ros/buffer.h"
|
||||
#include "tf2_ros/transform_listener.h"
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
class WaitBehaviorTester
|
||||
{
|
||||
public:
|
||||
using Wait = nav2_msgs::action::Wait;
|
||||
using GoalHandleWait = rclcpp_action::ClientGoalHandle<Wait>;
|
||||
|
||||
WaitBehaviorTester();
|
||||
~WaitBehaviorTester();
|
||||
|
||||
// Runs a single test with given target yaw
|
||||
bool behaviorTest(
|
||||
float time);
|
||||
|
||||
bool behaviorTestCancel(float time);
|
||||
|
||||
void activate();
|
||||
|
||||
void deactivate();
|
||||
|
||||
bool isActive() const
|
||||
{
|
||||
return is_active_;
|
||||
}
|
||||
|
||||
private:
|
||||
void sendInitialPose();
|
||||
|
||||
void amclPoseCallback(geometry_msgs::msg::PoseWithCovarianceStamped::SharedPtr);
|
||||
|
||||
bool is_active_;
|
||||
bool initial_pose_received_;
|
||||
|
||||
std::shared_ptr<tf2_ros::Buffer> tf_buffer_;
|
||||
std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
|
||||
|
||||
rclcpp::Node::SharedPtr node_;
|
||||
|
||||
// Publisher to publish initial pose
|
||||
rclcpp::Publisher<geometry_msgs::msg::PoseWithCovarianceStamped>::SharedPtr publisher_;
|
||||
|
||||
// Subscriber for amcl pose
|
||||
rclcpp::Subscription<geometry_msgs::msg::PoseWithCovarianceStamped>::SharedPtr subscription_;
|
||||
|
||||
// Action client to call wait action
|
||||
rclcpp_action::Client<Wait>::SharedPtr client_ptr_;
|
||||
};
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
|
||||
#endif // BEHAVIORS__WAIT__WAIT_BEHAVIOR_TESTER_HPP_
|
||||
@@ -0,0 +1,47 @@
|
||||
ament_add_test(test_keepout_filter
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_keepout_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_MASK=${PROJECT_SOURCE_DIR}/maps/keepout_mask.yaml
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
PARAMS_FILE=${CMAKE_CURRENT_SOURCE_DIR}/keepout_params.yaml
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
ASTAR=False
|
||||
)
|
||||
|
||||
ament_add_test(test_speed_filter_global
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_speed_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_MASK=${PROJECT_SOURCE_DIR}/maps/speed_mask.yaml
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
PARAMS_FILE=${CMAKE_CURRENT_SOURCE_DIR}/speed_global_params.yaml
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
ASTAR=False
|
||||
)
|
||||
|
||||
ament_add_test(test_speed_filter_local
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_speed_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_MASK=${PROJECT_SOURCE_DIR}/maps/speed_mask.yaml
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
PARAMS_FILE=${CMAKE_CURRENT_SOURCE_DIR}/speed_local_params.yaml
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
ASTAR=False
|
||||
)
|
||||
@@ -0,0 +1,333 @@
|
||||
amcl:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
alpha1: 0.2
|
||||
alpha2: 0.2
|
||||
alpha3: 0.2
|
||||
alpha4: 0.2
|
||||
alpha5: 0.2
|
||||
base_frame_id: "base_footprint"
|
||||
beam_skip_distance: 0.5
|
||||
beam_skip_error_threshold: 0.9
|
||||
beam_skip_threshold: 0.3
|
||||
do_beamskip: true
|
||||
global_frame_id: "map"
|
||||
lambda_short: 0.1
|
||||
laser_likelihood_max_dist: 2.0
|
||||
laser_max_range: 100.0
|
||||
laser_min_range: -1.0
|
||||
laser_model_type: "likelihood_field_prob"
|
||||
max_beams: 60
|
||||
max_particles: 2000
|
||||
min_particles: 500
|
||||
odom_frame_id: "odom"
|
||||
pf_err: 0.05
|
||||
pf_z: 0.99
|
||||
recovery_alpha_fast: 0.0
|
||||
recovery_alpha_slow: 0.0
|
||||
resample_interval: 1
|
||||
robot_model_type: "nav2_amcl::OmniMotionModel"
|
||||
save_pose_rate: 0.5
|
||||
sigma_hit: 0.2
|
||||
tf_broadcast: true
|
||||
transform_tolerance: 1.0
|
||||
update_min_a: 0.2
|
||||
update_min_d: 0.25
|
||||
z_hit: 0.5
|
||||
z_max: 0.05
|
||||
z_rand: 0.5
|
||||
z_short: 0.05
|
||||
scan_topic: scan
|
||||
|
||||
bt_navigator:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
global_frame: map
|
||||
robot_base_frame: base_link
|
||||
odom_topic: /odom
|
||||
# 'default_nav_through_poses_bt_xml' and 'default_nav_to_pose_bt_xml' are set in the launch
|
||||
# files to allow for a commandline change default used is the
|
||||
# nav2_bt_navigator/navigate_to_pose_w_replanning_and_recovery.xml &
|
||||
# nav2_bt_navigator/navigate_through_poses_w_replanning_and_recovery.xml
|
||||
plugin_lib_names:
|
||||
- nav2_compute_path_to_pose_action_bt_node
|
||||
- nav2_compute_path_through_poses_action_bt_node
|
||||
- nav2_smooth_path_action_bt_node
|
||||
- nav2_follow_path_action_bt_node
|
||||
- nav2_spin_action_bt_node
|
||||
- nav2_wait_action_bt_node
|
||||
- nav2_assisted_teleop_action_bt_node
|
||||
- nav2_back_up_action_bt_node
|
||||
- nav2_drive_on_heading_bt_node
|
||||
- nav2_clear_costmap_service_bt_node
|
||||
- nav2_is_stuck_condition_bt_node
|
||||
- nav2_goal_reached_condition_bt_node
|
||||
- nav2_goal_updated_condition_bt_node
|
||||
- nav2_is_path_valid_condition_bt_node
|
||||
- nav2_initial_pose_received_condition_bt_node
|
||||
- nav2_reinitialize_global_localization_service_bt_node
|
||||
- nav2_rate_controller_bt_node
|
||||
- nav2_distance_controller_bt_node
|
||||
- nav2_speed_controller_bt_node
|
||||
- nav2_truncate_path_action_bt_node
|
||||
- nav2_truncate_path_local_action_bt_node
|
||||
- nav2_goal_updater_node_bt_node
|
||||
- nav2_recovery_node_bt_node
|
||||
- nav2_pipeline_sequence_bt_node
|
||||
- nav2_round_robin_node_bt_node
|
||||
- nav2_transform_available_condition_bt_node
|
||||
- nav2_time_expired_condition_bt_node
|
||||
- nav2_distance_traveled_condition_bt_node
|
||||
- nav2_single_trigger_bt_node
|
||||
- nav2_goal_updated_controller_bt_node
|
||||
- nav2_is_battery_low_condition_bt_node
|
||||
- nav2_navigate_through_poses_action_bt_node
|
||||
- nav2_navigate_to_pose_action_bt_node
|
||||
- nav2_remove_passed_goals_action_bt_node
|
||||
- nav2_controller_cancel_bt_node
|
||||
- nav2_path_longer_on_approach_bt_node
|
||||
- nav2_wait_cancel_bt_node
|
||||
- nav2_spin_cancel_bt_node
|
||||
- nav2_back_up_cancel_bt_node
|
||||
- nav2_assisted_teleop_cancel_bt_node
|
||||
- nav2_drive_on_heading_cancel_bt_node
|
||||
|
||||
bt_navigator_navigate_through_poses_rclcpp_node:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
|
||||
bt_navigator_navigate_to_pose_rclcpp_node:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
|
||||
controller_server:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
controller_frequency: 20.0
|
||||
min_x_velocity_threshold: 0.001
|
||||
min_y_velocity_threshold: 0.5
|
||||
min_theta_velocity_threshold: 0.001
|
||||
progress_checker_plugin: "progress_checker"
|
||||
goal_checker_plugins: ["goal_checker"]
|
||||
controller_plugins: ["FollowPath"]
|
||||
|
||||
# Progress checker parameters
|
||||
progress_checker:
|
||||
plugin: "nav2_controller::SimpleProgressChecker"
|
||||
required_movement_radius: 0.5
|
||||
movement_time_allowance: 10.0
|
||||
# Goal checker parameters
|
||||
goal_checker:
|
||||
plugin: "nav2_controller::SimpleGoalChecker"
|
||||
xy_goal_tolerance: 0.25
|
||||
yaw_goal_tolerance: 0.25
|
||||
stateful: True
|
||||
# DWB parameters
|
||||
FollowPath:
|
||||
plugin: "dwb_core::DWBLocalPlanner"
|
||||
debug_trajectory_details: True
|
||||
prune_distance: 1.0
|
||||
forward_prune_distance: 1.0
|
||||
min_vel_x: 0.0
|
||||
min_vel_y: 0.0
|
||||
max_vel_x: 0.26
|
||||
max_vel_y: 0.0
|
||||
max_vel_theta: 1.0
|
||||
min_speed_xy: 0.0
|
||||
max_speed_xy: 0.26
|
||||
min_speed_theta: 0.0
|
||||
# Add high threshold velocity for turtlebot 3 issue.
|
||||
# https://github.com/ROBOTIS-GIT/turtlebot3_simulations/issues/75
|
||||
acc_lim_x: 2.5
|
||||
acc_lim_y: 0.0
|
||||
acc_lim_theta: 3.2
|
||||
decel_lim_x: -2.5
|
||||
decel_lim_y: 0.0
|
||||
decel_lim_theta: -3.2
|
||||
vx_samples: 20
|
||||
vy_samples: 5
|
||||
vtheta_samples: 20
|
||||
sim_time: 1.7
|
||||
linear_granularity: 0.05
|
||||
angular_granularity: 0.025
|
||||
transform_tolerance: 0.2
|
||||
xy_goal_tolerance: 0.25
|
||||
trans_stopped_velocity: 0.25
|
||||
short_circuit_trajectory_evaluation: True
|
||||
stateful: True
|
||||
critics: ["RotateToGoal", "Oscillation", "BaseObstacle", "GoalAlign", "PathAlign", "PathDist", "GoalDist"]
|
||||
BaseObstacle.scale: 0.02
|
||||
PathAlign.scale: 32.0
|
||||
PathAlign.forward_point_distance: 0.1
|
||||
GoalAlign.scale: 24.0
|
||||
GoalAlign.forward_point_distance: 0.1
|
||||
PathDist.scale: 32.0
|
||||
GoalDist.scale: 24.0
|
||||
RotateToGoal.scale: 32.0
|
||||
RotateToGoal.slowing_factor: 5.0
|
||||
RotateToGoal.lookahead_time: -1.0
|
||||
publish_cost_grid_pc: True
|
||||
|
||||
local_costmap:
|
||||
local_costmap:
|
||||
ros__parameters:
|
||||
update_frequency: 5.0
|
||||
publish_frequency: 2.0
|
||||
global_frame: odom
|
||||
robot_base_frame: base_link
|
||||
use_sim_time: True
|
||||
rolling_window: true
|
||||
width: 3
|
||||
height: 3
|
||||
resolution: 0.05
|
||||
robot_radius: 0.22
|
||||
plugins: ["voxel_layer", "inflation_layer"]
|
||||
filters: ["keepout_filter"]
|
||||
inflation_layer:
|
||||
plugin: "nav2_costmap_2d::InflationLayer"
|
||||
cost_scaling_factor: 3.0
|
||||
inflation_radius: 0.55
|
||||
voxel_layer:
|
||||
plugin: "nav2_costmap_2d::VoxelLayer"
|
||||
enabled: True
|
||||
publish_voxel_map: True
|
||||
origin_z: 0.0
|
||||
z_resolution: 0.05
|
||||
z_voxels: 16
|
||||
max_obstacle_height: 2.0
|
||||
mark_threshold: 0
|
||||
observation_sources: scan
|
||||
scan:
|
||||
topic: /scan
|
||||
max_obstacle_height: 2.0
|
||||
clearing: True
|
||||
marking: True
|
||||
data_type: "LaserScan"
|
||||
raytrace_max_range: 3.0
|
||||
raytrace_min_range: 0.0
|
||||
obstacle_max_range: 2.5
|
||||
obstacle_min_range: 0.0
|
||||
static_layer:
|
||||
map_subscribe_transient_local: True
|
||||
keepout_filter:
|
||||
plugin: "nav2_costmap_2d::KeepoutFilter"
|
||||
enabled: True
|
||||
filter_info_topic: "/costmap_filter_info"
|
||||
always_send_full_costmap: True
|
||||
|
||||
global_costmap:
|
||||
global_costmap:
|
||||
ros__parameters:
|
||||
update_frequency: 1.0
|
||||
publish_frequency: 1.0
|
||||
global_frame: map
|
||||
robot_base_frame: base_link
|
||||
use_sim_time: True
|
||||
robot_radius: 0.22
|
||||
resolution: 0.05
|
||||
track_unknown_space: true
|
||||
plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
|
||||
filters: ["keepout_filter"]
|
||||
obstacle_layer:
|
||||
plugin: "nav2_costmap_2d::ObstacleLayer"
|
||||
enabled: True
|
||||
observation_sources: scan
|
||||
scan:
|
||||
topic: /scan
|
||||
max_obstacle_height: 2.0
|
||||
clearing: True
|
||||
marking: True
|
||||
data_type: "LaserScan"
|
||||
static_layer:
|
||||
plugin: "nav2_costmap_2d::StaticLayer"
|
||||
map_subscribe_transient_local: True
|
||||
inflation_layer:
|
||||
plugin: "nav2_costmap_2d::InflationLayer"
|
||||
cost_scaling_factor: 3.0
|
||||
inflation_radius: 0.55
|
||||
keepout_filter:
|
||||
plugin: "nav2_costmap_2d::KeepoutFilter"
|
||||
enabled: True
|
||||
filter_info_topic: "/costmap_filter_info"
|
||||
always_send_full_costmap: True
|
||||
|
||||
map_server:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
yaml_filename: "turtlebot3_world.yaml"
|
||||
|
||||
map_saver:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
save_map_timeout: 5.0
|
||||
free_thresh_default: 0.25
|
||||
occupied_thresh_default: 0.65
|
||||
map_subscribe_transient_local: True
|
||||
|
||||
planner_server:
|
||||
ros__parameters:
|
||||
expected_planner_frequency: 20.0
|
||||
use_sim_time: True
|
||||
planner_plugins: ["GridBased"]
|
||||
GridBased:
|
||||
plugin: "nav2_navfn_planner/NavfnPlanner"
|
||||
tolerance: 0.5
|
||||
use_astar: False
|
||||
allow_unknown: True
|
||||
|
||||
smoother_server:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
|
||||
behavior_server:
|
||||
ros__parameters:
|
||||
costmap_topic: local_costmap/costmap_raw
|
||||
footprint_topic: local_costmap/published_footprint
|
||||
cycle_frequency: 10.0
|
||||
behavior_plugins: ["spin", "backup", "drive_on_heading", "wait"]
|
||||
spin:
|
||||
plugin: "nav2_behaviors/Spin"
|
||||
backup:
|
||||
plugin: "nav2_behaviors/BackUp"
|
||||
drive_on_heading:
|
||||
plugin: "nav2_behaviors/DriveOnHeading"
|
||||
wait:
|
||||
plugin: "nav2_behaviors/Wait"
|
||||
global_frame: odom
|
||||
robot_base_frame: base_link
|
||||
transform_tolerance: 0.1
|
||||
use_sim_time: true
|
||||
simulate_ahead_time: 2.0
|
||||
max_rotational_vel: 1.0
|
||||
min_rotational_vel: 0.4
|
||||
rotational_acc_lim: 3.2
|
||||
|
||||
robot_state_publisher:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
|
||||
waypoint_follower:
|
||||
ros__parameters:
|
||||
loop_rate: 20
|
||||
stop_on_failure: false
|
||||
waypoint_task_executor_plugin: "waypoint_task_executor"
|
||||
waypoint_task_executor:
|
||||
plugin: "nav2_waypoint_follower::WaitAtWaypoint"
|
||||
enabled: True
|
||||
waypoint_pause_duration: 0
|
||||
|
||||
costmap_filter_info_server:
|
||||
ros__parameters:
|
||||
use_sim_time: true
|
||||
type: 0
|
||||
filter_info_topic: "/costmap_filter_info"
|
||||
mask_topic: "/filter_mask"
|
||||
base: 0.0
|
||||
multiplier: 1.0
|
||||
|
||||
filter_mask_server:
|
||||
ros__parameters:
|
||||
use_sim_time: true
|
||||
frame_id: "map"
|
||||
topic_name: "/filter_mask"
|
||||
yaml_filename: "keepout_mask.yaml"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
@@ -0,0 +1,324 @@
|
||||
amcl:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
alpha1: 0.2
|
||||
alpha2: 0.2
|
||||
alpha3: 0.2
|
||||
alpha4: 0.2
|
||||
alpha5: 0.2
|
||||
base_frame_id: "base_footprint"
|
||||
beam_skip_distance: 0.5
|
||||
beam_skip_error_threshold: 0.9
|
||||
beam_skip_threshold: 0.3
|
||||
do_beamskip: false
|
||||
global_frame_id: "map"
|
||||
lambda_short: 0.1
|
||||
laser_likelihood_max_dist: 2.0
|
||||
laser_max_range: 100.0
|
||||
laser_min_range: -1.0
|
||||
laser_model_type: "beam"
|
||||
max_beams: 60
|
||||
max_particles: 2000
|
||||
min_particles: 500
|
||||
odom_frame_id: "odom"
|
||||
pf_err: 0.05
|
||||
pf_z: 0.99
|
||||
recovery_alpha_fast: 0.0
|
||||
recovery_alpha_slow: 0.0
|
||||
resample_interval: 1
|
||||
robot_model_type: "nav2_amcl::DifferentialMotionModel"
|
||||
save_pose_rate: 0.5
|
||||
sigma_hit: 0.2
|
||||
tf_broadcast: true
|
||||
transform_tolerance: 1.0
|
||||
update_min_a: 0.2
|
||||
update_min_d: 0.25
|
||||
z_hit: 0.5
|
||||
z_max: 0.05
|
||||
z_rand: 0.5
|
||||
z_short: 0.05
|
||||
scan_topic: scan
|
||||
|
||||
bt_navigator:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
global_frame: map
|
||||
robot_base_frame: base_link
|
||||
odom_topic: /odom
|
||||
# 'default_nav_through_poses_bt_xml' and 'default_nav_to_pose_bt_xml' are set in the launch
|
||||
# files to allow for a commandline change default used is the
|
||||
# nav2_bt_navigator/navigate_to_pose_w_replanning_and_recovery.xml &
|
||||
# nav2_bt_navigator/navigate_through_poses_w_replanning_and_recovery.xml
|
||||
plugin_lib_names:
|
||||
- nav2_compute_path_to_pose_action_bt_node
|
||||
- nav2_compute_path_through_poses_action_bt_node
|
||||
- nav2_smooth_path_action_bt_node
|
||||
- nav2_follow_path_action_bt_node
|
||||
- nav2_spin_action_bt_node
|
||||
- nav2_wait_action_bt_node
|
||||
- nav2_assisted_teleop_action_bt_node
|
||||
- nav2_back_up_action_bt_node
|
||||
- nav2_drive_on_heading_bt_node
|
||||
- nav2_clear_costmap_service_bt_node
|
||||
- nav2_is_stuck_condition_bt_node
|
||||
- nav2_goal_reached_condition_bt_node
|
||||
- nav2_goal_updated_condition_bt_node
|
||||
- nav2_globally_updated_goal_condition_bt_node
|
||||
- nav2_is_path_valid_condition_bt_node
|
||||
- nav2_initial_pose_received_condition_bt_node
|
||||
- nav2_reinitialize_global_localization_service_bt_node
|
||||
- nav2_rate_controller_bt_node
|
||||
- nav2_distance_controller_bt_node
|
||||
- nav2_speed_controller_bt_node
|
||||
- nav2_truncate_path_action_bt_node
|
||||
- nav2_truncate_path_local_action_bt_node
|
||||
- nav2_goal_updater_node_bt_node
|
||||
- nav2_recovery_node_bt_node
|
||||
- nav2_pipeline_sequence_bt_node
|
||||
- nav2_round_robin_node_bt_node
|
||||
- nav2_transform_available_condition_bt_node
|
||||
- nav2_time_expired_condition_bt_node
|
||||
- nav2_distance_traveled_condition_bt_node
|
||||
- nav2_single_trigger_bt_node
|
||||
- nav2_goal_updated_controller_bt_node
|
||||
- nav2_is_battery_low_condition_bt_node
|
||||
- nav2_navigate_through_poses_action_bt_node
|
||||
- nav2_navigate_to_pose_action_bt_node
|
||||
- nav2_remove_passed_goals_action_bt_node
|
||||
- nav2_controller_cancel_bt_node
|
||||
- nav2_path_longer_on_approach_bt_node
|
||||
- nav2_wait_cancel_bt_node
|
||||
- nav2_spin_cancel_bt_node
|
||||
- nav2_back_up_cancel_bt_node
|
||||
- nav2_assisted_teleop_cancel_bt_node
|
||||
- nav2_drive_on_heading_cancel_bt_node
|
||||
|
||||
bt_navigator_navigate_through_poses_rclcpp_node:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
|
||||
bt_navigator_navigate_to_pose_rclcpp_node:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
|
||||
controller_server:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
controller_frequency: 20.0
|
||||
min_x_velocity_threshold: 0.001
|
||||
min_y_velocity_threshold: 0.5
|
||||
min_theta_velocity_threshold: 0.001
|
||||
speed_limit_topic: "/speed_limit"
|
||||
progress_checker_plugin: "progress_checker"
|
||||
goal_checker_plugins: ["goal_checker"]
|
||||
controller_plugins: ["FollowPath"]
|
||||
|
||||
# Progress checker parameters
|
||||
progress_checker:
|
||||
plugin: "nav2_controller::SimpleProgressChecker"
|
||||
required_movement_radius: 0.5
|
||||
movement_time_allowance: 10.0
|
||||
# Goal checker parameters
|
||||
goal_checker:
|
||||
plugin: "nav2_controller::SimpleGoalChecker"
|
||||
xy_goal_tolerance: 0.25
|
||||
yaw_goal_tolerance: 0.25
|
||||
stateful: True
|
||||
# DWB parameters
|
||||
FollowPath:
|
||||
plugin: "dwb_core::DWBLocalPlanner"
|
||||
debug_trajectory_details: True
|
||||
min_vel_x: 0.0
|
||||
min_vel_y: 0.0
|
||||
max_vel_x: 0.26
|
||||
max_vel_y: 0.0
|
||||
max_vel_theta: 1.0
|
||||
min_speed_xy: 0.0
|
||||
max_speed_xy: 0.26
|
||||
min_speed_theta: 0.0
|
||||
# Add high threshold velocity for turtlebot 3 issue.
|
||||
# https://github.com/ROBOTIS-GIT/turtlebot3_simulations/issues/75
|
||||
acc_lim_x: 2.5
|
||||
acc_lim_y: 0.0
|
||||
acc_lim_theta: 3.2
|
||||
decel_lim_x: -2.5
|
||||
decel_lim_y: 0.0
|
||||
decel_lim_theta: -3.2
|
||||
vx_samples: 20
|
||||
vy_samples: 5
|
||||
vtheta_samples: 20
|
||||
sim_time: 1.7
|
||||
linear_granularity: 0.05
|
||||
angular_granularity: 0.025
|
||||
transform_tolerance: 0.2
|
||||
xy_goal_tolerance: 0.25
|
||||
trans_stopped_velocity: 0.25
|
||||
short_circuit_trajectory_evaluation: True
|
||||
stateful: True
|
||||
critics: ["RotateToGoal", "Oscillation", "BaseObstacle", "GoalAlign", "PathAlign", "PathDist", "GoalDist"]
|
||||
BaseObstacle.scale: 0.02
|
||||
PathAlign.scale: 32.0
|
||||
PathAlign.forward_point_distance: 0.1
|
||||
GoalAlign.scale: 24.0
|
||||
GoalAlign.forward_point_distance: 0.1
|
||||
PathDist.scale: 32.0
|
||||
GoalDist.scale: 24.0
|
||||
RotateToGoal.scale: 32.0
|
||||
RotateToGoal.slowing_factor: 5.0
|
||||
RotateToGoal.lookahead_time: -1.0
|
||||
|
||||
local_costmap:
|
||||
local_costmap:
|
||||
ros__parameters:
|
||||
update_frequency: 5.0
|
||||
publish_frequency: 2.0
|
||||
global_frame: odom
|
||||
robot_base_frame: base_link
|
||||
use_sim_time: True
|
||||
rolling_window: true
|
||||
width: 3
|
||||
height: 3
|
||||
resolution: 0.05
|
||||
robot_radius: 0.22
|
||||
plugins: ["voxel_layer", "inflation_layer"]
|
||||
inflation_layer:
|
||||
plugin: "nav2_costmap_2d::InflationLayer"
|
||||
cost_scaling_factor: 3.0
|
||||
inflation_radius: 0.55
|
||||
voxel_layer:
|
||||
plugin: "nav2_costmap_2d::VoxelLayer"
|
||||
enabled: True
|
||||
publish_voxel_map: True
|
||||
origin_z: 0.0
|
||||
z_resolution: 0.05
|
||||
z_voxels: 16
|
||||
max_obstacle_height: 2.0
|
||||
mark_threshold: 0
|
||||
observation_sources: scan
|
||||
scan:
|
||||
topic: /scan
|
||||
max_obstacle_height: 2.0
|
||||
clearing: True
|
||||
marking: True
|
||||
data_type: "LaserScan"
|
||||
static_layer:
|
||||
map_subscribe_transient_local: True
|
||||
always_send_full_costmap: True
|
||||
|
||||
global_costmap:
|
||||
global_costmap:
|
||||
ros__parameters:
|
||||
update_frequency: 1.0
|
||||
publish_frequency: 1.0
|
||||
global_frame: map
|
||||
robot_base_frame: base_link
|
||||
use_sim_time: True
|
||||
robot_radius: 0.22
|
||||
resolution: 0.05
|
||||
track_unknown_space: true
|
||||
plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
|
||||
filters: ["speed_filter"]
|
||||
obstacle_layer:
|
||||
plugin: "nav2_costmap_2d::ObstacleLayer"
|
||||
enabled: True
|
||||
observation_sources: scan
|
||||
scan:
|
||||
topic: /scan
|
||||
max_obstacle_height: 2.0
|
||||
clearing: True
|
||||
marking: True
|
||||
data_type: "LaserScan"
|
||||
static_layer:
|
||||
plugin: "nav2_costmap_2d::StaticLayer"
|
||||
map_subscribe_transient_local: True
|
||||
inflation_layer:
|
||||
plugin: "nav2_costmap_2d::InflationLayer"
|
||||
cost_scaling_factor: 3.0
|
||||
inflation_radius: 0.55
|
||||
speed_filter:
|
||||
plugin: "nav2_costmap_2d::SpeedFilter"
|
||||
enabled: True
|
||||
filter_info_topic: "/costmap_filter_info"
|
||||
speed_limit_topic: "/speed_limit"
|
||||
always_send_full_costmap: True
|
||||
|
||||
map_server:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
yaml_filename: "turtlebot3_world.yaml"
|
||||
|
||||
map_saver:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
save_map_timeout: 5.0
|
||||
free_thresh_default: 0.25
|
||||
occupied_thresh_default: 0.65
|
||||
map_subscribe_transient_local: True
|
||||
|
||||
planner_server:
|
||||
ros__parameters:
|
||||
expected_planner_frequency: 20.0
|
||||
use_sim_time: True
|
||||
planner_plugins: ["GridBased"]
|
||||
GridBased:
|
||||
plugin: "nav2_navfn_planner/NavfnPlanner"
|
||||
tolerance: 0.5
|
||||
use_astar: False
|
||||
allow_unknown: True
|
||||
|
||||
smoother_server:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
|
||||
behavior_server:
|
||||
ros__parameters:
|
||||
costmap_topic: local_costmap/costmap_raw
|
||||
footprint_topic: local_costmap/published_footprint
|
||||
cycle_frequency: 10.0
|
||||
behavior_plugins: ["spin", "backup", "drive_on_heading", "wait"]
|
||||
spin:
|
||||
plugin: "nav2_behaviors/Spin"
|
||||
backup:
|
||||
plugin: "nav2_behaviors/BackUp"
|
||||
drive_on_heading:
|
||||
plugin: "nav2_behaviors/DriveOnHeading"
|
||||
wait:
|
||||
plugin: "nav2_behaviors/Wait"
|
||||
global_frame: odom
|
||||
robot_base_frame: base_link
|
||||
transform_tolerance: 0.1
|
||||
use_sim_time: true
|
||||
simulate_ahead_time: 2.0
|
||||
max_rotational_vel: 1.0
|
||||
min_rotational_vel: 0.4
|
||||
rotational_acc_lim: 3.2
|
||||
|
||||
robot_state_publisher:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
|
||||
waypoint_follower:
|
||||
ros__parameters:
|
||||
loop_rate: 20
|
||||
stop_on_failure: false
|
||||
waypoint_task_executor_plugin: "waypoint_task_executor"
|
||||
waypoint_task_executor:
|
||||
plugin: "nav2_waypoint_follower::WaitAtWaypoint"
|
||||
enabled: True
|
||||
waypoint_pause_duration: 0
|
||||
|
||||
costmap_filter_info_server:
|
||||
ros__parameters:
|
||||
use_sim_time: true
|
||||
type: 1
|
||||
filter_info_topic: "/costmap_filter_info"
|
||||
mask_topic: "/filter_mask"
|
||||
base: 100.0
|
||||
multiplier: -1.0
|
||||
|
||||
filter_mask_server:
|
||||
ros__parameters:
|
||||
use_sim_time: true
|
||||
frame_id: "map"
|
||||
topic_name: "/filter_mask"
|
||||
yaml_filename: "speed_mask.yaml"
|
||||
@@ -0,0 +1,324 @@
|
||||
amcl:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
alpha1: 0.2
|
||||
alpha2: 0.2
|
||||
alpha3: 0.2
|
||||
alpha4: 0.2
|
||||
alpha5: 0.2
|
||||
base_frame_id: "base_footprint"
|
||||
beam_skip_distance: 0.5
|
||||
beam_skip_error_threshold: 0.9
|
||||
beam_skip_threshold: 0.3
|
||||
do_beamskip: false
|
||||
global_frame_id: "map"
|
||||
lambda_short: 0.1
|
||||
laser_likelihood_max_dist: 2.0
|
||||
laser_max_range: 100.0
|
||||
laser_min_range: -1.0
|
||||
laser_model_type: "beam"
|
||||
max_beams: 60
|
||||
max_particles: 2000
|
||||
min_particles: 500
|
||||
odom_frame_id: "odom"
|
||||
pf_err: 0.05
|
||||
pf_z: 0.99
|
||||
recovery_alpha_fast: 0.0
|
||||
recovery_alpha_slow: 0.0
|
||||
resample_interval: 1
|
||||
robot_model_type: "nav2_amcl::DifferentialMotionModel"
|
||||
save_pose_rate: 0.5
|
||||
sigma_hit: 0.2
|
||||
tf_broadcast: true
|
||||
transform_tolerance: 1.0
|
||||
update_min_a: 0.2
|
||||
update_min_d: 0.25
|
||||
z_hit: 0.5
|
||||
z_max: 0.05
|
||||
z_rand: 0.5
|
||||
z_short: 0.05
|
||||
scan_topic: scan
|
||||
|
||||
bt_navigator:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
global_frame: map
|
||||
robot_base_frame: base_link
|
||||
odom_topic: /odom
|
||||
# 'default_nav_through_poses_bt_xml' and 'default_nav_to_pose_bt_xml' are set in the launch
|
||||
# files to allow for a commandline change default used is the
|
||||
# nav2_bt_navigator/navigate_to_pose_w_replanning_and_recovery.xml &
|
||||
# nav2_bt_navigator/navigate_through_poses_w_replanning_and_recovery.xml
|
||||
plugin_lib_names:
|
||||
- nav2_compute_path_to_pose_action_bt_node
|
||||
- nav2_compute_path_through_poses_action_bt_node
|
||||
- nav2_smooth_path_action_bt_node
|
||||
- nav2_follow_path_action_bt_node
|
||||
- nav2_spin_action_bt_node
|
||||
- nav2_wait_action_bt_node
|
||||
- nav2_assisted_teleop_action_bt_node
|
||||
- nav2_back_up_action_bt_node
|
||||
- nav2_drive_on_heading_bt_node
|
||||
- nav2_clear_costmap_service_bt_node
|
||||
- nav2_is_stuck_condition_bt_node
|
||||
- nav2_goal_reached_condition_bt_node
|
||||
- nav2_goal_updated_condition_bt_node
|
||||
- nav2_globally_updated_goal_condition_bt_node
|
||||
- nav2_is_path_valid_condition_bt_node
|
||||
- nav2_initial_pose_received_condition_bt_node
|
||||
- nav2_reinitialize_global_localization_service_bt_node
|
||||
- nav2_rate_controller_bt_node
|
||||
- nav2_distance_controller_bt_node
|
||||
- nav2_speed_controller_bt_node
|
||||
- nav2_truncate_path_action_bt_node
|
||||
- nav2_truncate_path_local_action_bt_node
|
||||
- nav2_goal_updater_node_bt_node
|
||||
- nav2_recovery_node_bt_node
|
||||
- nav2_pipeline_sequence_bt_node
|
||||
- nav2_round_robin_node_bt_node
|
||||
- nav2_transform_available_condition_bt_node
|
||||
- nav2_time_expired_condition_bt_node
|
||||
- nav2_distance_traveled_condition_bt_node
|
||||
- nav2_single_trigger_bt_node
|
||||
- nav2_goal_updated_controller_bt_node
|
||||
- nav2_is_battery_low_condition_bt_node
|
||||
- nav2_navigate_through_poses_action_bt_node
|
||||
- nav2_navigate_to_pose_action_bt_node
|
||||
- nav2_remove_passed_goals_action_bt_node
|
||||
- nav2_controller_cancel_bt_node
|
||||
- nav2_path_longer_on_approach_bt_node
|
||||
- nav2_wait_cancel_bt_node
|
||||
- nav2_spin_cancel_bt_node
|
||||
- nav2_back_up_cancel_bt_node
|
||||
- nav2_assisted_teleop_cancel_bt_node
|
||||
- nav2_drive_on_heading_cancel_bt_node
|
||||
|
||||
bt_navigator_navigate_through_poses_rclcpp_node:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
|
||||
bt_navigator_navigate_to_pose_rclcpp_node:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
|
||||
controller_server:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
controller_frequency: 20.0
|
||||
min_x_velocity_threshold: 0.001
|
||||
min_y_velocity_threshold: 0.5
|
||||
min_theta_velocity_threshold: 0.001
|
||||
speed_limit_topic: "/speed_limit"
|
||||
progress_checker_plugin: "progress_checker"
|
||||
goal_checker_plugins: ["goal_checker"]
|
||||
controller_plugins: ["FollowPath"]
|
||||
|
||||
# Progress checker parameters
|
||||
progress_checker:
|
||||
plugin: "nav2_controller::SimpleProgressChecker"
|
||||
required_movement_radius: 0.5
|
||||
movement_time_allowance: 10.0
|
||||
# Goal checker parameters
|
||||
goal_checker:
|
||||
plugin: "nav2_controller::SimpleGoalChecker"
|
||||
xy_goal_tolerance: 0.25
|
||||
yaw_goal_tolerance: 0.25
|
||||
stateful: True
|
||||
# DWB parameters
|
||||
FollowPath:
|
||||
plugin: "dwb_core::DWBLocalPlanner"
|
||||
debug_trajectory_details: True
|
||||
min_vel_x: 0.0
|
||||
min_vel_y: 0.0
|
||||
max_vel_x: 0.26
|
||||
max_vel_y: 0.0
|
||||
max_vel_theta: 1.0
|
||||
min_speed_xy: 0.0
|
||||
max_speed_xy: 0.26
|
||||
min_speed_theta: 0.0
|
||||
# Add high threshold velocity for turtlebot 3 issue.
|
||||
# https://github.com/ROBOTIS-GIT/turtlebot3_simulations/issues/75
|
||||
acc_lim_x: 2.5
|
||||
acc_lim_y: 0.0
|
||||
acc_lim_theta: 3.2
|
||||
decel_lim_x: -2.5
|
||||
decel_lim_y: 0.0
|
||||
decel_lim_theta: -3.2
|
||||
vx_samples: 20
|
||||
vy_samples: 5
|
||||
vtheta_samples: 20
|
||||
sim_time: 1.7
|
||||
linear_granularity: 0.05
|
||||
angular_granularity: 0.025
|
||||
transform_tolerance: 0.2
|
||||
xy_goal_tolerance: 0.25
|
||||
trans_stopped_velocity: 0.25
|
||||
short_circuit_trajectory_evaluation: True
|
||||
stateful: True
|
||||
critics: ["RotateToGoal", "Oscillation", "BaseObstacle", "GoalAlign", "PathAlign", "PathDist", "GoalDist"]
|
||||
BaseObstacle.scale: 0.02
|
||||
PathAlign.scale: 32.0
|
||||
PathAlign.forward_point_distance: 0.1
|
||||
GoalAlign.scale: 24.0
|
||||
GoalAlign.forward_point_distance: 0.1
|
||||
PathDist.scale: 32.0
|
||||
GoalDist.scale: 24.0
|
||||
RotateToGoal.scale: 32.0
|
||||
RotateToGoal.slowing_factor: 5.0
|
||||
RotateToGoal.lookahead_time: -1.0
|
||||
|
||||
local_costmap:
|
||||
local_costmap:
|
||||
ros__parameters:
|
||||
update_frequency: 5.0
|
||||
publish_frequency: 2.0
|
||||
global_frame: odom
|
||||
robot_base_frame: base_link
|
||||
use_sim_time: True
|
||||
rolling_window: true
|
||||
width: 3
|
||||
height: 3
|
||||
resolution: 0.05
|
||||
robot_radius: 0.22
|
||||
plugins: ["voxel_layer", "inflation_layer"]
|
||||
filters: ["speed_filter"]
|
||||
inflation_layer:
|
||||
plugin: "nav2_costmap_2d::InflationLayer"
|
||||
cost_scaling_factor: 3.0
|
||||
inflation_radius: 0.55
|
||||
voxel_layer:
|
||||
plugin: "nav2_costmap_2d::VoxelLayer"
|
||||
enabled: True
|
||||
publish_voxel_map: True
|
||||
origin_z: 0.0
|
||||
z_resolution: 0.05
|
||||
z_voxels: 16
|
||||
max_obstacle_height: 2.0
|
||||
mark_threshold: 0
|
||||
observation_sources: scan
|
||||
scan:
|
||||
topic: /scan
|
||||
max_obstacle_height: 2.0
|
||||
clearing: True
|
||||
marking: True
|
||||
data_type: "LaserScan"
|
||||
static_layer:
|
||||
map_subscribe_transient_local: True
|
||||
speed_filter:
|
||||
plugin: "nav2_costmap_2d::SpeedFilter"
|
||||
enabled: True
|
||||
filter_info_topic: "/costmap_filter_info"
|
||||
speed_limit_topic: "/speed_limit"
|
||||
always_send_full_costmap: True
|
||||
|
||||
global_costmap:
|
||||
global_costmap:
|
||||
ros__parameters:
|
||||
update_frequency: 1.0
|
||||
publish_frequency: 1.0
|
||||
global_frame: map
|
||||
robot_base_frame: base_link
|
||||
use_sim_time: True
|
||||
robot_radius: 0.22
|
||||
resolution: 0.05
|
||||
track_unknown_space: true
|
||||
plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
|
||||
obstacle_layer:
|
||||
plugin: "nav2_costmap_2d::ObstacleLayer"
|
||||
enabled: True
|
||||
observation_sources: scan
|
||||
scan:
|
||||
topic: /scan
|
||||
max_obstacle_height: 2.0
|
||||
clearing: True
|
||||
marking: True
|
||||
data_type: "LaserScan"
|
||||
static_layer:
|
||||
plugin: "nav2_costmap_2d::StaticLayer"
|
||||
map_subscribe_transient_local: True
|
||||
inflation_layer:
|
||||
plugin: "nav2_costmap_2d::InflationLayer"
|
||||
cost_scaling_factor: 3.0
|
||||
inflation_radius: 0.55
|
||||
always_send_full_costmap: True
|
||||
|
||||
map_server:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
yaml_filename: "turtlebot3_world.yaml"
|
||||
|
||||
map_saver:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
save_map_timeout: 5.0
|
||||
free_thresh_default: 0.25
|
||||
occupied_thresh_default: 0.65
|
||||
map_subscribe_transient_local: True
|
||||
|
||||
planner_server:
|
||||
ros__parameters:
|
||||
expected_planner_frequency: 20.0
|
||||
use_sim_time: True
|
||||
planner_plugins: ["GridBased"]
|
||||
GridBased:
|
||||
plugin: "nav2_navfn_planner/NavfnPlanner"
|
||||
tolerance: 0.5
|
||||
use_astar: False
|
||||
allow_unknown: True
|
||||
|
||||
smoother_server:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
|
||||
behavior_server:
|
||||
ros__parameters:
|
||||
costmap_topic: local_costmap/costmap_raw
|
||||
footprint_topic: local_costmap/published_footprint
|
||||
cycle_frequency: 10.0
|
||||
behavior_plugins: ["spin", "backup", "drive_on_heading", "wait"]
|
||||
spin:
|
||||
plugin: "nav2_behaviors/Spin"
|
||||
backup:
|
||||
plugin: "nav2_behaviors/BackUp"
|
||||
drive_on_heading:
|
||||
plugin: "nav2_behaviors/DriveOnHeading"
|
||||
wait:
|
||||
plugin: "nav2_behaviors/Wait"
|
||||
global_frame: odom
|
||||
robot_base_frame: base_link
|
||||
transform_tolerance: 0.1
|
||||
use_sim_time: true
|
||||
simulate_ahead_time: 2.0
|
||||
max_rotational_vel: 1.0
|
||||
min_rotational_vel: 0.4
|
||||
rotational_acc_lim: 3.2
|
||||
|
||||
robot_state_publisher:
|
||||
ros__parameters:
|
||||
use_sim_time: True
|
||||
|
||||
waypoint_follower:
|
||||
ros__parameters:
|
||||
loop_rate: 20
|
||||
stop_on_failure: false
|
||||
waypoint_task_executor_plugin: "waypoint_task_executor"
|
||||
waypoint_task_executor:
|
||||
plugin: "nav2_waypoint_follower::WaitAtWaypoint"
|
||||
enabled: True
|
||||
waypoint_pause_duration: 0
|
||||
|
||||
costmap_filter_info_server:
|
||||
ros__parameters:
|
||||
use_sim_time: true
|
||||
type: 1
|
||||
filter_info_topic: "/costmap_filter_info"
|
||||
mask_topic: "/filter_mask"
|
||||
base: 100.0
|
||||
multiplier: -1.0
|
||||
|
||||
filter_mask_server:
|
||||
ros__parameters:
|
||||
use_sim_time: true
|
||||
frame_id: "map"
|
||||
topic_name: "/filter_mask"
|
||||
yaml_filename: "speed_mask.yaml"
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2018 Intel Corporation
|
||||
# Copyright (c) 2020 Samsung Research Russia
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
from launch.actions import ExecuteProcess, IncludeLaunchDescription, SetEnvironmentVariable
|
||||
from launch.launch_context import LaunchContext
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch_ros.actions import Node
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
from nav2_common.launch import RewrittenYaml
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
map_yaml_file = os.getenv('TEST_MAP')
|
||||
filter_mask_file = os.getenv('TEST_MASK')
|
||||
world = os.getenv('TEST_WORLD')
|
||||
|
||||
bt_navigator_xml = os.path.join(get_package_share_directory('nav2_bt_navigator'),
|
||||
'behavior_trees',
|
||||
os.getenv('BT_NAVIGATOR_XML'))
|
||||
|
||||
bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
params_file = os.path.join(script_dir, 'keepout_params.yaml')
|
||||
|
||||
# Replace the `use_astar` setting on the params file
|
||||
param_substitutions = {
|
||||
'planner_server.ros__parameters.GridBased.use_astar': os.getenv('ASTAR'),
|
||||
'filter_mask_server.ros__parameters.yaml_filename': filter_mask_file,
|
||||
'map_server.ros__parameters.yaml_filename': map_yaml_file}
|
||||
configured_params = RewrittenYaml(
|
||||
source_file=params_file,
|
||||
root_key='',
|
||||
param_rewrites=param_substitutions,
|
||||
convert_types=True)
|
||||
|
||||
context = LaunchContext()
|
||||
new_yaml = configured_params.perform(context)
|
||||
return LaunchDescription([
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM', '1'),
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_USE_STDOUT', '1'),
|
||||
|
||||
# Launch gazebo server for simulation
|
||||
ExecuteProcess(
|
||||
cmd=['gzserver', '-s', 'libgazebo_ros_init.so',
|
||||
'--minimal_comms', world],
|
||||
output='screen'),
|
||||
|
||||
# TODO(orduno) Launch the robot state publisher instead
|
||||
# using a local copy of TB3 urdf file
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_footprint', 'base_link']),
|
||||
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_scan']),
|
||||
|
||||
Node(
|
||||
package='nav2_lifecycle_manager',
|
||||
executable='lifecycle_manager',
|
||||
name='lifecycle_manager_filters',
|
||||
output='screen',
|
||||
parameters=[{
|
||||
'node_names':
|
||||
[
|
||||
'filter_mask_server', 'costmap_filter_info_server'
|
||||
]
|
||||
},
|
||||
{'autostart': True}]),
|
||||
|
||||
# Nodes required for Costmap Filters configuration
|
||||
Node(
|
||||
package='nav2_map_server',
|
||||
executable='map_server',
|
||||
name='filter_mask_server',
|
||||
output='screen',
|
||||
parameters=[new_yaml]),
|
||||
|
||||
Node(
|
||||
package='nav2_map_server',
|
||||
executable='costmap_filter_info_server',
|
||||
name='costmap_filter_info_server',
|
||||
output='screen',
|
||||
parameters=[new_yaml]),
|
||||
|
||||
IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(bringup_dir, 'launch', 'bringup_launch.py')),
|
||||
launch_arguments={'namespace': '',
|
||||
'use_namespace': 'False',
|
||||
'map': map_yaml_file,
|
||||
'use_sim_time': 'True',
|
||||
'params_file': new_yaml,
|
||||
'bt_xml_file': bt_navigator_xml,
|
||||
'autostart': 'True'}.items()),
|
||||
|
||||
Node(
|
||||
package='nav2_costmap_2d',
|
||||
executable='nav2_costmap_2d_cloud',
|
||||
name='costmap_2d_cloud',
|
||||
output='screen',
|
||||
remappings=[('voxel_grid', 'local_costmap/voxel_grid')]),
|
||||
])
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
ld = generate_launch_description()
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[os.path.join(os.getenv('TEST_DIR'), 'tester_node.py'),
|
||||
'-t', 'keepout', '-r', '-2.0', '-0.5', '0.0', '-0.5'],
|
||||
name='tester_node',
|
||||
output='screen')
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2018 Intel Corporation
|
||||
# Copyright (c) 2020 Samsung Research Russia
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
from launch.actions import ExecuteProcess, IncludeLaunchDescription, SetEnvironmentVariable
|
||||
from launch.launch_context import LaunchContext
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch_ros.actions import Node
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
from nav2_common.launch import RewrittenYaml
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
map_yaml_file = os.getenv('TEST_MAP')
|
||||
filter_mask_file = os.getenv('TEST_MASK')
|
||||
world = os.getenv('TEST_WORLD')
|
||||
|
||||
bt_navigator_xml = os.path.join(get_package_share_directory('nav2_bt_navigator'),
|
||||
'behavior_trees',
|
||||
os.getenv('BT_NAVIGATOR_XML'))
|
||||
|
||||
bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
params_file = os.getenv('PARAMS_FILE')
|
||||
|
||||
# Replace the `use_astar` setting on the params file
|
||||
param_substitutions = {
|
||||
'planner_server.ros__parameters.GridBased.use_astar': os.getenv('ASTAR'),
|
||||
'filter_mask_server.ros__parameters.yaml_filename': filter_mask_file,
|
||||
'map_server.ros__parameters.yaml_filename': map_yaml_file}
|
||||
configured_params = RewrittenYaml(
|
||||
source_file=params_file,
|
||||
root_key='',
|
||||
param_rewrites=param_substitutions,
|
||||
convert_types=True)
|
||||
context = LaunchContext()
|
||||
new_yaml = configured_params.perform(context)
|
||||
return LaunchDescription([
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM', '1'),
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_USE_STDOUT', '1'),
|
||||
|
||||
# Launch gazebo server for simulation
|
||||
ExecuteProcess(
|
||||
cmd=['gzserver', '-s', 'libgazebo_ros_init.so',
|
||||
'--minimal_comms', world],
|
||||
output='screen'),
|
||||
|
||||
# TODO(orduno) Launch the robot state publisher instead
|
||||
# using a local copy of TB3 urdf file
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_footprint', 'base_link']),
|
||||
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_scan']),
|
||||
|
||||
Node(
|
||||
package='nav2_lifecycle_manager',
|
||||
executable='lifecycle_manager',
|
||||
name='lifecycle_manager_filters',
|
||||
output='screen',
|
||||
parameters=[{
|
||||
'node_names':
|
||||
[
|
||||
'filter_mask_server', 'costmap_filter_info_server'
|
||||
]
|
||||
},
|
||||
{'autostart': True}]),
|
||||
|
||||
# Nodes required for Costmap Filters configuration
|
||||
Node(
|
||||
package='nav2_map_server',
|
||||
executable='map_server',
|
||||
name='filter_mask_server',
|
||||
output='screen',
|
||||
parameters=[new_yaml]),
|
||||
|
||||
Node(
|
||||
package='nav2_map_server',
|
||||
executable='costmap_filter_info_server',
|
||||
name='costmap_filter_info_server',
|
||||
output='screen',
|
||||
parameters=[new_yaml]),
|
||||
|
||||
IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(bringup_dir, 'launch', 'bringup_launch.py')),
|
||||
launch_arguments={'namespace': '',
|
||||
'use_namespace': 'False',
|
||||
'map': map_yaml_file,
|
||||
'use_sim_time': 'True',
|
||||
'params_file': new_yaml,
|
||||
'bt_xml_file': bt_navigator_xml,
|
||||
'autostart': 'True'}.items()),
|
||||
])
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
ld = generate_launch_description()
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[os.path.join(os.getenv('TEST_DIR'), 'tester_node.py'),
|
||||
'-t', 'speed', '-r', '-2.0', '-0.5', '0.0', '-0.5'],
|
||||
name='tester_node',
|
||||
output='screen')
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,537 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2018 Intel Corporation.
|
||||
# Copyright (c) 2020 Samsung Research Russia
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
from enum import Enum
|
||||
import math
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from action_msgs.msg import GoalStatus
|
||||
from geometry_msgs.msg import Pose
|
||||
from geometry_msgs.msg import PoseStamped
|
||||
from geometry_msgs.msg import PoseWithCovarianceStamped
|
||||
from lifecycle_msgs.srv import GetState
|
||||
from nav2_msgs.action import NavigateToPose
|
||||
from nav2_msgs.msg import SpeedLimit
|
||||
from nav2_msgs.srv import ManageLifecycleNodes
|
||||
from nav_msgs.msg import OccupancyGrid
|
||||
from nav_msgs.msg import Path
|
||||
|
||||
import rclpy
|
||||
from rclpy.action import ActionClient
|
||||
from rclpy.node import Node
|
||||
from rclpy.qos import QoSDurabilityPolicy, QoSHistoryPolicy, QoSReliabilityPolicy
|
||||
from rclpy.qos import QoSProfile
|
||||
from sensor_msgs.msg import PointCloud2
|
||||
|
||||
|
||||
class TestType(Enum):
|
||||
KEEPOUT = 0
|
||||
SPEED = 1
|
||||
|
||||
|
||||
class FilterMask():
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
filter_mask: OccupancyGrid
|
||||
):
|
||||
self.filter_mask = filter_mask
|
||||
|
||||
# Converts world coordinates into filter mask map coordinate.
|
||||
# Returns filter mask map coordinates or (-1, -1) in case
|
||||
# if world coordinates are out of mask bounds.
|
||||
def worldToMap(self, wx: float, wy: float):
|
||||
origin_x = self.filter_mask.info.origin.position.x
|
||||
origin_y = self.filter_mask.info.origin.position.y
|
||||
size_x = self.filter_mask.info.width
|
||||
size_y = self.filter_mask.info.height
|
||||
resolution = self.filter_mask.info.resolution
|
||||
|
||||
if wx < origin_x or wy < origin_y:
|
||||
return -1, -1
|
||||
|
||||
mx = int((wx - origin_x) / resolution)
|
||||
my = int((wy - origin_y) / resolution)
|
||||
|
||||
if mx < size_x and my < size_y:
|
||||
return mx, my
|
||||
|
||||
return -1, -1
|
||||
|
||||
# Gets filter_mask[mx, my] value
|
||||
def getValue(self, mx, my):
|
||||
size_x = self.filter_mask.info.width
|
||||
return self.filter_mask.data[mx + my * size_x]
|
||||
|
||||
|
||||
class NavTester(Node):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
test_type: TestType,
|
||||
initial_pose: Pose,
|
||||
goal_pose: Pose,
|
||||
namespace: str = ''
|
||||
):
|
||||
super().__init__(node_name='nav2_tester', namespace=namespace)
|
||||
self.initial_pose_pub = self.create_publisher(PoseWithCovarianceStamped,
|
||||
'initialpose', 10)
|
||||
self.goal_pub = self.create_publisher(PoseStamped, 'goal_pose', 10)
|
||||
|
||||
transient_local_qos = QoSProfile(
|
||||
durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
|
||||
reliability=QoSReliabilityPolicy.RELIABLE,
|
||||
history=QoSHistoryPolicy.KEEP_LAST,
|
||||
depth=1)
|
||||
|
||||
volatile_qos = QoSProfile(
|
||||
durability=QoSDurabilityPolicy.RMW_QOS_POLICY_DURABILITY_VOLATILE,
|
||||
reliability=QoSReliabilityPolicy.RELIABLE,
|
||||
history=QoSHistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
|
||||
depth=1)
|
||||
|
||||
self.model_pose_sub = self.create_subscription(PoseWithCovarianceStamped,
|
||||
'amcl_pose', self.poseCallback,
|
||||
transient_local_qos)
|
||||
self.clearing_ep_sub = self.create_subscription(PointCloud2,
|
||||
'local_costmap/clearing_endpoints',
|
||||
self.clearingEndpointsCallback,
|
||||
transient_local_qos)
|
||||
self.test_type = test_type
|
||||
self.filter_test_result = True
|
||||
self.clearing_endpoints_received = False
|
||||
self.voxel_marked_received = False
|
||||
self.voxel_unknown_received = False
|
||||
self.cost_cloud_received = False
|
||||
|
||||
if self.test_type == TestType.KEEPOUT:
|
||||
self.plan_sub = self.create_subscription(Path, 'plan',
|
||||
self.planCallback, volatile_qos)
|
||||
self.voxel_marked_sub = self.create_subscription(PointCloud2,
|
||||
'voxel_marked_cloud',
|
||||
self.voxelMarkedCallback,
|
||||
1)
|
||||
self.voxel_unknown_sub = self.create_subscription(PointCloud2,
|
||||
'voxel_unknown_cloud',
|
||||
self.voxelUnknownCallback,
|
||||
1)
|
||||
self.cost_cloud_sub = self.create_subscription(PointCloud2,
|
||||
'cost_cloud',
|
||||
self.dwbCostCloudCallback,
|
||||
1)
|
||||
elif self.test_type == TestType.SPEED:
|
||||
self.speed_it = 0
|
||||
# Expected chain of speed limits
|
||||
self.limits = [50.0, 0.0]
|
||||
# Permissive array: all received speed limits must match to "limits" from above
|
||||
self.limit_passed = [False, False]
|
||||
self.plan_sub = self.create_subscription(SpeedLimit, 'speed_limit',
|
||||
self.speedLimitCallback, volatile_qos)
|
||||
|
||||
self.mask_received = False
|
||||
self.mask_sub = self.create_subscription(OccupancyGrid, 'filter_mask',
|
||||
self.maskCallback, transient_local_qos)
|
||||
|
||||
self.initial_pose_received = False
|
||||
self.initial_pose = initial_pose
|
||||
self.goal_pose = goal_pose
|
||||
self.action_client = ActionClient(
|
||||
self, NavigateToPose, 'navigate_to_pose')
|
||||
|
||||
def info_msg(self, msg: str):
|
||||
self.get_logger().info('\033[1;37;44m' + msg + '\033[0m')
|
||||
|
||||
def warn_msg(self, msg: str):
|
||||
self.get_logger().warn('\033[1;37;43m' + msg + '\033[0m')
|
||||
|
||||
def error_msg(self, msg: str):
|
||||
self.get_logger().error('\033[1;37;41m' + msg + '\033[0m')
|
||||
|
||||
def setInitialPose(self):
|
||||
msg = PoseWithCovarianceStamped()
|
||||
msg.pose.pose = self.initial_pose
|
||||
msg.header.frame_id = 'map'
|
||||
self.info_msg('Publishing Initial Pose')
|
||||
self.initial_pose_pub.publish(msg)
|
||||
self.currentPose = self.initial_pose
|
||||
|
||||
def getStampedPoseMsg(self, pose: Pose):
|
||||
msg = PoseStamped()
|
||||
msg.header.frame_id = 'map'
|
||||
msg.pose = pose
|
||||
return msg
|
||||
|
||||
def publishGoalPose(self, goal_pose: Optional[Pose] = None):
|
||||
self.goal_pose = goal_pose if goal_pose is not None else self.goal_pose
|
||||
self.goal_pub.publish(self.getStampedPoseMsg(self.goal_pose))
|
||||
|
||||
def runNavigateAction(self, goal_pose: Optional[Pose] = None):
|
||||
# Sends a `NavToPose` action request and waits for completion
|
||||
self.info_msg("Waiting for 'NavigateToPose' action server")
|
||||
while not self.action_client.wait_for_server(timeout_sec=1.0):
|
||||
self.info_msg(
|
||||
"'NavigateToPose' action server not available, waiting...")
|
||||
|
||||
self.goal_pose = goal_pose if goal_pose is not None else self.goal_pose
|
||||
goal_msg = NavigateToPose.Goal()
|
||||
goal_msg.pose = self.getStampedPoseMsg(self.goal_pose)
|
||||
|
||||
self.info_msg('Sending goal request...')
|
||||
send_goal_future = self.action_client.send_goal_async(goal_msg)
|
||||
|
||||
rclpy.spin_until_future_complete(self, send_goal_future)
|
||||
goal_handle = send_goal_future.result()
|
||||
|
||||
if not goal_handle.accepted:
|
||||
self.error_msg('Goal rejected')
|
||||
return False
|
||||
|
||||
self.info_msg('Goal accepted')
|
||||
get_result_future = goal_handle.get_result_async()
|
||||
|
||||
self.info_msg("Waiting for 'NavigateToPose' action to complete")
|
||||
rclpy.spin_until_future_complete(self, get_result_future)
|
||||
status = get_result_future.result().status
|
||||
if status != GoalStatus.STATUS_SUCCEEDED:
|
||||
self.info_msg(f'Goal failed with status code: {status}')
|
||||
return False
|
||||
|
||||
self.info_msg('Goal succeeded!')
|
||||
return True
|
||||
|
||||
def isInKeepout(self, x, y):
|
||||
mx, my = self.filter_mask.worldToMap(x, y)
|
||||
if mx == -1 and my == -1: # Out of mask's area
|
||||
return False
|
||||
if self.filter_mask.getValue(mx, my) == 100: # Occupied
|
||||
return True
|
||||
return False
|
||||
|
||||
# Checks that (x, y) position does not belong to a keepout zone.
|
||||
def checkKeepout(self, x, y):
|
||||
if not self.mask_received:
|
||||
self.warn_msg('Filter mask was not received')
|
||||
elif self.isInKeepout(x, y):
|
||||
self.filter_test_result = False
|
||||
self.error_msg(f'Pose ({x}, {y}) belongs to keepout zone')
|
||||
return False
|
||||
return True
|
||||
|
||||
# Checks that currently received speed_limit is equal to the it-th item
|
||||
# of expected speed "limits" array.
|
||||
# If so, sets it-th item of permissive array "limit_passed" to be true.
|
||||
# Otherwise it will be remained to be false.
|
||||
# Also verifies that speed limit messages received no more than N-times
|
||||
# (where N - is the length of "limits" array),
|
||||
# otherwise sets overall "filter_test_result" to be false.
|
||||
def checkSpeed(self, it, speed_limit):
|
||||
if it >= len(self.limits):
|
||||
self.error_msg('Got excess speed limit')
|
||||
self.filter_test_result = False
|
||||
return
|
||||
if speed_limit == self.limits[it]:
|
||||
self.limit_passed[it] = True
|
||||
else:
|
||||
self.error_msg('Incorrect speed limit received: ' + str(speed_limit) +
|
||||
', but should be: ' + str(self.limits[it]))
|
||||
|
||||
def poseCallback(self, msg):
|
||||
self.info_msg('Received amcl_pose')
|
||||
self.current_pose = msg.pose.pose
|
||||
self.initial_pose_received = True
|
||||
if self.test_type == TestType.KEEPOUT:
|
||||
if not self.checkKeepout(msg.pose.pose.position.x, msg.pose.pose.position.y):
|
||||
self.error_msg('Robot goes into keepout zone')
|
||||
|
||||
def planCallback(self, msg):
|
||||
self.info_msg('Received plan')
|
||||
for pose in msg.poses:
|
||||
if not self.checkKeepout(pose.pose.position.x, pose.pose.position.y):
|
||||
self.error_msg('Path plan intersects with keepout zone')
|
||||
return
|
||||
|
||||
def clearingEndpointsCallback(self, msg):
|
||||
if len(msg.data) > 0:
|
||||
self.clearing_endpoints_received = True
|
||||
|
||||
def voxelMarkedCallback(self, msg):
|
||||
if len(msg.data) > 0:
|
||||
self.voxel_marked_received = True
|
||||
|
||||
def voxelUnknownCallback(self, msg):
|
||||
if len(msg.data) > 0:
|
||||
self.voxel_unknown_received = True
|
||||
|
||||
def dwbCostCloudCallback(self, msg):
|
||||
self.info_msg('Received cost_cloud points')
|
||||
if len(msg.data) > 0:
|
||||
self.cost_cloud_received = True
|
||||
|
||||
def speedLimitCallback(self, msg):
|
||||
self.info_msg(f'Received speed limit: {msg.speed_limit}')
|
||||
self.checkSpeed(self.speed_it, msg.speed_limit)
|
||||
self.speed_it += 1
|
||||
|
||||
def maskCallback(self, msg):
|
||||
self.info_msg('Received filter mask')
|
||||
self.filter_mask = FilterMask(msg)
|
||||
self.mask_received = True
|
||||
|
||||
def wait_for_filter_mask(self, timeout):
|
||||
start_time = time.time()
|
||||
|
||||
while not self.mask_received:
|
||||
self.info_msg('Waiting for filter mask to be received ...')
|
||||
rclpy.spin_once(self, timeout_sec=1)
|
||||
if (time.time() - start_time) > timeout:
|
||||
self.error_msg('Time out to waiting filter mask')
|
||||
return False
|
||||
return True
|
||||
|
||||
def wait_for_pointcloud_subscribers(self, timeout):
|
||||
start_time = time.time()
|
||||
while not self.voxel_unknown_received or not self.voxel_marked_received \
|
||||
or not self.clearing_endpoints_received:
|
||||
self.info_msg(
|
||||
'Waiting for voxel_marked_cloud/voxel_unknown_cloud/\
|
||||
clearing_endpoints msg to be received ...')
|
||||
rclpy.spin_once(self, timeout_sec=1)
|
||||
if (time.time() - start_time) > timeout:
|
||||
self.error_msg(
|
||||
'Time out to waiting for voxel_marked_cloud/voxel_unknown_cloud/\
|
||||
clearing_endpoints msgs')
|
||||
return False
|
||||
return True
|
||||
|
||||
def reachesGoal(self, timeout, distance):
|
||||
goalReached = False
|
||||
start_time = time.time()
|
||||
|
||||
while not goalReached:
|
||||
rclpy.spin_once(self, timeout_sec=1)
|
||||
if self.distanceFromGoal() < distance:
|
||||
goalReached = True
|
||||
self.info_msg('*** GOAL REACHED ***')
|
||||
return True
|
||||
elif timeout is not None:
|
||||
if (time.time() - start_time) > timeout:
|
||||
self.error_msg('Robot timed out reaching its goal!')
|
||||
return False
|
||||
|
||||
def distanceFromGoal(self):
|
||||
d_x = self.current_pose.position.x - self.goal_pose.position.x
|
||||
d_y = self.current_pose.position.y - self.goal_pose.position.y
|
||||
distance = math.sqrt(d_x * d_x + d_y * d_y)
|
||||
self.info_msg(f'Distance from goal is: {distance}')
|
||||
return distance
|
||||
|
||||
def wait_for_node_active(self, node_name: str):
|
||||
# Waits for the node within the tester namespace to become active
|
||||
self.info_msg(f'Waiting for {node_name} to become active')
|
||||
node_service = f'{node_name}/get_state'
|
||||
state_client = self.create_client(GetState, node_service)
|
||||
while not state_client.wait_for_service(timeout_sec=1.0):
|
||||
self.info_msg(f'{node_service} service not available, waiting...')
|
||||
req = GetState.Request() # empty request
|
||||
state = 'UNKNOWN'
|
||||
while (state != 'active'):
|
||||
self.info_msg(f'Getting {node_name} state...')
|
||||
future = state_client.call_async(req)
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
if future.result() is not None:
|
||||
state = future.result().current_state.label
|
||||
self.info_msg(f'Result of get_state: {state}')
|
||||
else:
|
||||
self.error_msg('Exception while calling service: %r' %
|
||||
future.exception())
|
||||
time.sleep(5)
|
||||
|
||||
def shutdown(self):
|
||||
self.info_msg('Shutting down')
|
||||
self.action_client.destroy()
|
||||
|
||||
transition_service = 'lifecycle_manager_navigation/manage_nodes'
|
||||
mgr_client = self.create_client(
|
||||
ManageLifecycleNodes, transition_service)
|
||||
while not mgr_client.wait_for_service(timeout_sec=1.0):
|
||||
self.info_msg(f'{transition_service} service not available, waiting...')
|
||||
|
||||
req = ManageLifecycleNodes.Request()
|
||||
req.command = ManageLifecycleNodes.Request().SHUTDOWN
|
||||
future = mgr_client.call_async(req)
|
||||
try:
|
||||
self.info_msg('Shutting down navigation lifecycle manager...')
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
future.result()
|
||||
self.info_msg(
|
||||
'Shutting down navigation lifecycle manager complete.')
|
||||
except Exception as e: # noqa: B902
|
||||
self.error_msg(f'Service call failed {e!r}')
|
||||
transition_service = 'lifecycle_manager_localization/manage_nodes'
|
||||
mgr_client = self.create_client(
|
||||
ManageLifecycleNodes, transition_service)
|
||||
while not mgr_client.wait_for_service(timeout_sec=1.0):
|
||||
self.info_msg(f'{transition_service} service not available, waiting...')
|
||||
|
||||
req = ManageLifecycleNodes.Request()
|
||||
req.command = ManageLifecycleNodes.Request().SHUTDOWN
|
||||
future = mgr_client.call_async(req)
|
||||
try:
|
||||
self.info_msg('Shutting down localization lifecycle manager...')
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
future.result()
|
||||
self.info_msg(
|
||||
'Shutting down localization lifecycle manager complete')
|
||||
except Exception as e: # noqa: B902
|
||||
self.error_msg(f'Service call failed {e!r}')
|
||||
|
||||
def wait_for_initial_pose(self):
|
||||
self.initial_pose_received = False
|
||||
while not self.initial_pose_received:
|
||||
self.info_msg('Setting initial pose')
|
||||
self.setInitialPose()
|
||||
self.info_msg('Waiting for amcl_pose to be received')
|
||||
rclpy.spin_once(self, timeout_sec=1)
|
||||
|
||||
|
||||
def test_RobotMovesToGoal(robot_tester):
|
||||
robot_tester.info_msg('Setting goal pose')
|
||||
robot_tester.publishGoalPose()
|
||||
robot_tester.info_msg('Waiting 60 seconds for robot to reach goal')
|
||||
return robot_tester.reachesGoal(timeout=60, distance=0.5)
|
||||
|
||||
|
||||
# Tests that all received speed limits are correct:
|
||||
# If overall "filter_test_result" is true
|
||||
# checks that all items in "limit_passed" permissive array are also true.
|
||||
# In other words, it verifies that all speed limits are received
|
||||
# exactly (by count and values) as expected by "limits" array.
|
||||
def test_SpeedLimitsAllCorrect(robot_tester):
|
||||
if not robot_tester.filter_test_result:
|
||||
return False
|
||||
for passed in robot_tester.limit_passed:
|
||||
if not passed:
|
||||
robot_tester.error_msg('Did not meet one of the speed limit')
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def run_all_tests(robot_tester):
|
||||
# set transforms to use_sim_time
|
||||
result = True
|
||||
if (result):
|
||||
robot_tester.wait_for_node_active('amcl')
|
||||
robot_tester.wait_for_initial_pose()
|
||||
robot_tester.wait_for_node_active('bt_navigator')
|
||||
result = robot_tester.wait_for_filter_mask(10)
|
||||
if (result):
|
||||
result = robot_tester.runNavigateAction()
|
||||
|
||||
if robot_tester.test_type == TestType.KEEPOUT:
|
||||
result = result and robot_tester.wait_for_pointcloud_subscribers(10)
|
||||
|
||||
if (result):
|
||||
result = test_RobotMovesToGoal(robot_tester)
|
||||
|
||||
if (result):
|
||||
if robot_tester.test_type == TestType.KEEPOUT:
|
||||
result = robot_tester.filter_test_result
|
||||
result = result and robot_tester.cost_cloud_received
|
||||
elif robot_tester.test_type == TestType.SPEED:
|
||||
result = test_SpeedLimitsAllCorrect(robot_tester)
|
||||
|
||||
# Add more tests here if desired
|
||||
|
||||
if (result):
|
||||
robot_tester.info_msg('Test PASSED')
|
||||
else:
|
||||
robot_tester.error_msg('Test FAILED')
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def fwd_pose(x=0.0, y=0.0, z=0.01):
|
||||
initial_pose = Pose()
|
||||
initial_pose.position.x = x
|
||||
initial_pose.position.y = y
|
||||
initial_pose.position.z = z
|
||||
initial_pose.orientation.x = 0.0
|
||||
initial_pose.orientation.y = 0.0
|
||||
initial_pose.orientation.z = 0.0
|
||||
initial_pose.orientation.w = 1.0
|
||||
return initial_pose
|
||||
|
||||
|
||||
def get_tester(args):
|
||||
|
||||
# Requested tester for one robot
|
||||
type_str = args.type
|
||||
init_x, init_y, final_x, final_y = args.robot[0]
|
||||
test_type = TestType.KEEPOUT # Default value
|
||||
if type_str == 'speed':
|
||||
test_type = TestType.SPEED
|
||||
|
||||
tester = NavTester(
|
||||
test_type,
|
||||
initial_pose=fwd_pose(float(init_x), float(init_y)),
|
||||
goal_pose=fwd_pose(float(final_x), float(final_y)))
|
||||
tester.info_msg(
|
||||
'Starting tester, robot going from ' + init_x + ', ' + init_y +
|
||||
' to ' + final_x + ', ' + final_y + '.')
|
||||
return tester
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
# The robot(s) positions from the input arguments
|
||||
parser = argparse.ArgumentParser(
|
||||
description='System-level costmap filters tester node')
|
||||
parser.add_argument('-t', '--type', type=str, action='store', dest='type',
|
||||
help='Type of costmap filter being tested.')
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-r', '--robot', action='append', nargs=4,
|
||||
metavar=('init_x', 'init_y', 'final_x', 'final_y'),
|
||||
help='The robot starting and final positions.')
|
||||
|
||||
args, unknown = parser.parse_known_args()
|
||||
|
||||
rclpy.init()
|
||||
|
||||
# Create tester for the robot
|
||||
tester = get_tester(args)
|
||||
|
||||
# wait a few seconds to make sure entire stacks are up
|
||||
time.sleep(10)
|
||||
|
||||
passed = run_all_tests(tester)
|
||||
|
||||
# stop and shutdown the nav stack to exit cleanly
|
||||
tester.shutdown()
|
||||
tester.info_msg('Done Shutting Down.')
|
||||
|
||||
if not passed:
|
||||
tester.info_msg('Exiting failed')
|
||||
exit(1)
|
||||
else:
|
||||
tester.info_msg('Exiting passed')
|
||||
exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,13 @@
|
||||
add_executable(dummy_controller_node
|
||||
src/dummy_controller/main.cpp
|
||||
src/dummy_controller/dummy_controller.cpp
|
||||
)
|
||||
|
||||
ament_target_dependencies(dummy_controller_node
|
||||
rclcpp
|
||||
std_msgs
|
||||
nav2_util
|
||||
nav2_behavior_tree
|
||||
nav2_msgs
|
||||
nav_msgs
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "dummy_controller.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
DummyController::DummyController()
|
||||
: Node("DummyController")
|
||||
{
|
||||
RCLCPP_INFO(get_logger(), "Initializing DummyController...");
|
||||
|
||||
auto temp_node = std::shared_ptr<rclcpp::Node>(this, [](auto) {});
|
||||
|
||||
vel_pub_ =
|
||||
this->create_publisher<geometry_msgs::msg::Twist>("cmd_vel", 1);
|
||||
|
||||
task_server_ = std::make_unique<nav2_behavior_tree::FollowPathTaskServer>(temp_node, false),
|
||||
task_server_->setExecuteCallback(
|
||||
std::bind(&DummyController::followPath, this, std::placeholders::_1));
|
||||
|
||||
// Start listening for incoming ComputePathToPose action server requests
|
||||
task_server_->start();
|
||||
|
||||
RCLCPP_INFO(get_logger(), "Initialized DummyController");
|
||||
}
|
||||
|
||||
DummyController::~DummyController()
|
||||
{
|
||||
RCLCPP_INFO(get_logger(), "Shutting down DummyController");
|
||||
}
|
||||
|
||||
void
|
||||
DummyController::followPath(const nav2_behavior_tree::FollowPathCommand::SharedPtr /*command*/)
|
||||
{
|
||||
RCLCPP_INFO(get_logger(), "Starting controller ");
|
||||
|
||||
auto start_time = std::chrono::system_clock::now();
|
||||
auto time_since_msg = std::chrono::system_clock::now();
|
||||
|
||||
while (true) {
|
||||
// Dummy controller computation time
|
||||
std::this_thread::sleep_for(50ms);
|
||||
|
||||
if (task_server_->cancelRequested()) {
|
||||
RCLCPP_INFO(get_logger(), "Task cancelled");
|
||||
setZeroVelocity();
|
||||
task_server_->setCanceled();
|
||||
return;
|
||||
}
|
||||
|
||||
// Log a message every second
|
||||
auto current_time = std::chrono::system_clock::now();
|
||||
if (current_time - time_since_msg >= 1s) {
|
||||
RCLCPP_INFO(get_logger(), "Following path");
|
||||
time_since_msg = std::chrono::system_clock::now();
|
||||
}
|
||||
|
||||
// Output control command
|
||||
auto cmd_vel = std::make_unique<geometry_msgs::msg::Twist>();
|
||||
cmd_vel->linear.x = 0.1;
|
||||
vel_pub_->publish(std::move(cmd_vel));
|
||||
|
||||
if (current_time - start_time >= 30s) {
|
||||
RCLCPP_INFO(get_logger(), "Reached end point");
|
||||
setZeroVelocity();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
nav2_behavior_tree::FollowPathResult result;
|
||||
task_server_->setResult(result);
|
||||
}
|
||||
|
||||
void DummyController::setZeroVelocity()
|
||||
{
|
||||
auto cmd_vel = std::make_unique<geometry_msgs::msg::Twist>();
|
||||
cmd_vel->linear.x = 0.0;
|
||||
cmd_vel->linear.y = 0.0;
|
||||
cmd_vel->angular.z = 0.0;
|
||||
vel_pub_->publish(std::move(cmd_vel));
|
||||
}
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef DUMMY_CONTROLLER__DUMMY_CONTROLLER_HPP_
|
||||
#define DUMMY_CONTROLLER__DUMMY_CONTROLLER_HPP_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "nav2_behavior_tree/follow_path_task.hpp"
|
||||
#include "geometry_msgs/msg/twist.hpp"
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
class DummyController : public rclcpp::Node
|
||||
{
|
||||
public:
|
||||
DummyController();
|
||||
~DummyController();
|
||||
|
||||
nav2_behavior_tree::TaskStatus followPath(
|
||||
const nav2_behavior_tree::FollowPathCommand::SharedPtr command);
|
||||
|
||||
private:
|
||||
void setZeroVelocity();
|
||||
|
||||
std::unique_ptr<nav2_behavior_tree::FollowPathTaskServer> task_server_;
|
||||
|
||||
std::shared_ptr<rclcpp::Publisher<geometry_msgs::msg::Twist>> vel_pub_;
|
||||
};
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
|
||||
#endif // DUMMY_CONTROLLER__DUMMY_CONTROLLER_HPP_
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <memory>
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "dummy_controller.hpp"
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
{
|
||||
rclcpp::init(argc, argv);
|
||||
rclcpp::spin(std::make_shared<nav2_system_tests::DummyController>());
|
||||
rclcpp::shutdown();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
add_executable(dummy_planner_node
|
||||
src/dummy_planner/main.cpp
|
||||
src/dummy_planner/dummy_planner.cpp
|
||||
)
|
||||
|
||||
ament_target_dependencies(dummy_planner_node
|
||||
rclcpp
|
||||
std_msgs
|
||||
nav2_util
|
||||
nav2_behavior_tree
|
||||
nav2_msgs
|
||||
nav_msgs
|
||||
)
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
|
||||
#include "dummy_planner.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
DummyPlanner::DummyPlanner()
|
||||
: Node("DummyPlanner")
|
||||
{
|
||||
RCLCPP_INFO(get_logger(), "Initializing DummyPlanner...");
|
||||
|
||||
auto temp_node = std::shared_ptr<rclcpp::Node>(this, [](auto) {});
|
||||
|
||||
task_server_ =
|
||||
std::make_unique<nav2_behavior_tree::ComputePathToPoseTaskServer>(temp_node, false),
|
||||
task_server_->setExecuteCallback(
|
||||
std::bind(&DummyPlanner::computePlan, this, std::placeholders::_1));
|
||||
|
||||
// Start listening for incoming ComputePathToPose task requests
|
||||
task_server_->start();
|
||||
|
||||
RCLCPP_INFO(get_logger(), "Initialized DummyPlanner");
|
||||
}
|
||||
|
||||
DummyPlanner::~DummyPlanner()
|
||||
{
|
||||
RCLCPP_INFO(get_logger(), "Shutting down DummyPlanner");
|
||||
}
|
||||
|
||||
void
|
||||
DummyPlanner::computePlan(const nav2_behavior_tree::ComputePathToPoseCommand::SharedPtr cmd)
|
||||
{
|
||||
RCLCPP_INFO(
|
||||
get_logger(), "Attempting to a find path from (%.2f, %.2f) to "
|
||||
"(%.2f, %.2f).", cmd->start.position.x, cmd->start.position.y,
|
||||
cmd->goal.position.x, cmd->goal.position.y);
|
||||
|
||||
// Dummy path computation time
|
||||
std::this_thread::sleep_for(500ms);
|
||||
|
||||
if (task_server_->cancelRequested()) {
|
||||
RCLCPP_INFO(get_logger(), "Cancelled planning task.");
|
||||
task_server_->setCanceled();
|
||||
return;
|
||||
}
|
||||
|
||||
RCLCPP_INFO(get_logger(), "Found a dummy path");
|
||||
|
||||
nav2_behavior_tree::ComputePathToPoseResult result;
|
||||
// set succeeded
|
||||
task_server_->setResult(result);
|
||||
}
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef DUMMY_PLANNER__DUMMY_PLANNER_HPP_
|
||||
#define DUMMY_PLANNER__DUMMY_PLANNER_HPP_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "nav2_behavior_tree/compute_path_to_pose_task.hpp"
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
class DummyPlanner : public rclcpp::Node
|
||||
{
|
||||
public:
|
||||
DummyPlanner();
|
||||
~DummyPlanner();
|
||||
|
||||
nav2_behavior_tree::TaskStatus computePathToPose(
|
||||
const nav2_behavior_tree::ComputePathToPoseCommand::SharedPtr command);
|
||||
|
||||
private:
|
||||
std::unique_ptr<nav2_behavior_tree::ComputePathToPoseTaskServer> task_server_;
|
||||
};
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
|
||||
#endif // DUMMY_PLANNER__DUMMY_PLANNER_HPP_
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <memory>
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "dummy_planner.hpp"
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
{
|
||||
rclcpp::init(argc, argv);
|
||||
rclcpp::spin(std::make_shared<nav2_system_tests::DummyPlanner>());
|
||||
rclcpp::shutdown();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
ament_add_gtest_executable(test_localization_node
|
||||
test_localization_node.cpp
|
||||
)
|
||||
ament_target_dependencies(test_localization_node
|
||||
${dependencies}
|
||||
)
|
||||
|
||||
|
||||
ament_add_test(test_localization
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_localization_launch.py"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_EXECUTABLE=$<TARGET_FILE:test_localization_node>
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
# Localization Testing
|
||||
|
||||
The intention of the localization test is to ensure robot’s pose and transforms are available.
|
||||
|
||||
Currently, only a simple test that checks the `initialpose` has been implemented. The `test_localization` module publishes an initial pose on `initialpose` topic and then it listens to `amcl_pose` topic. If the `amcl_pose` is similar to `initial pose` within a predefined tolerance the test passes.
|
||||
|
||||
## To run the test
|
||||
First, build the package
|
||||
```
|
||||
colcon build --symlink-install
|
||||
```
|
||||
After building, from /build/nav2_system_tests directory run:
|
||||
```
|
||||
ctest -V -R test_localization
|
||||
```
|
||||
Alternately you can run all the tests in the package using colcon:
|
||||
```
|
||||
colcon test --packages-select nav2_system_tests
|
||||
```
|
||||
|
||||
## Future Plan
|
||||
Once rosbag functionality becomes available, this test can be extended to utilize a recorded trajectory with map and scan data to monitor the `amcl_pose` and `transforms` without a need to run Gazebo and map_server.
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2018 Intel Corporation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
import launch.actions
|
||||
from launch.actions import ExecuteProcess
|
||||
import launch_ros.actions
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
mapFile = os.getenv('TEST_MAP')
|
||||
testExecutable = os.getenv('TEST_EXECUTABLE')
|
||||
world = os.getenv('TEST_WORLD')
|
||||
|
||||
launch_gazebo = launch.actions.ExecuteProcess(
|
||||
cmd=['gzserver', '-s', 'libgazebo_ros_init.so', '--minimal_comms', world],
|
||||
output='screen')
|
||||
link_footprint = launch_ros.actions.Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_footprint', 'base_link'])
|
||||
footprint_scan = launch_ros.actions.Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_scan'])
|
||||
run_map_server = launch_ros.actions.Node(
|
||||
package='nav2_map_server',
|
||||
executable='map_server',
|
||||
name='map_server',
|
||||
output='screen',
|
||||
parameters=[{'yaml_filename': mapFile}])
|
||||
run_amcl = launch_ros.actions.Node(
|
||||
package='nav2_amcl',
|
||||
executable='amcl',
|
||||
output='screen')
|
||||
run_lifecycle_manager = launch_ros.actions.Node(
|
||||
package='nav2_lifecycle_manager',
|
||||
executable='lifecycle_manager',
|
||||
name='lifecycle_manager',
|
||||
output='screen',
|
||||
parameters=[{'node_names': ['map_server', 'amcl']}, {'autostart': True}])
|
||||
ld = LaunchDescription([launch_gazebo, link_footprint, footprint_scan,
|
||||
run_map_server, run_amcl, run_lifecycle_manager])
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[testExecutable],
|
||||
name='test_localization_node',
|
||||
output='screen'
|
||||
)
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include <memory>
|
||||
#include "gtest/gtest.h"
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "nav2_amcl/amcl_node.hpp"
|
||||
#include "std_msgs/msg/string.hpp"
|
||||
#include "geometry_msgs/msg/pose_array.hpp"
|
||||
#include "geometry_msgs/msg/pose_stamped.hpp"
|
||||
|
||||
using std::placeholders::_1;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// rclcpp::init can only be called once per process, so this needs to be a global variable
|
||||
class RclCppFixture
|
||||
{
|
||||
public:
|
||||
RclCppFixture() {rclcpp::init(0, nullptr);}
|
||||
~RclCppFixture() {rclcpp::shutdown();}
|
||||
};
|
||||
RclCppFixture g_rclcppfixture;
|
||||
|
||||
class TestAmclPose : public ::testing::Test
|
||||
{
|
||||
public:
|
||||
TestAmclPose()
|
||||
{
|
||||
pose_callback_ = false;
|
||||
initTestPose();
|
||||
tol_ = 0.25;
|
||||
|
||||
node = rclcpp::Node::make_shared("localization_test");
|
||||
|
||||
while (node->count_subscribers("scan") < 1) {
|
||||
std::this_thread::sleep_for(100ms);
|
||||
rclcpp::spin_some(node);
|
||||
}
|
||||
|
||||
initial_pose_pub_ = node->create_publisher<geometry_msgs::msg::PoseWithCovarianceStamped>(
|
||||
"initialpose", rclcpp::SystemDefaultsQoS());
|
||||
subscription_ = node->create_subscription<geometry_msgs::msg::PoseWithCovarianceStamped>(
|
||||
"amcl_pose", rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable(),
|
||||
std::bind(&TestAmclPose::amcl_pose_callback, this, _1));
|
||||
initial_pose_pub_->publish(testPose_);
|
||||
}
|
||||
|
||||
bool defaultAmclTest();
|
||||
|
||||
protected:
|
||||
std::shared_ptr<rclcpp::Node> node;
|
||||
void initTestPose();
|
||||
|
||||
private:
|
||||
void amcl_pose_callback(const geometry_msgs::msg::PoseWithCovarianceStamped::SharedPtr msg)
|
||||
{
|
||||
auto amcl_pose = msg->pose;
|
||||
amcl_pose_x = amcl_pose.pose.position.x;
|
||||
amcl_pose_y = amcl_pose.pose.position.y;
|
||||
pose_callback_ = true;
|
||||
}
|
||||
rclcpp::Publisher<geometry_msgs::msg::PoseWithCovarianceStamped>::SharedPtr initial_pose_pub_;
|
||||
rclcpp::Subscription<geometry_msgs::msg::PoseWithCovarianceStamped>::SharedPtr subscription_;
|
||||
geometry_msgs::msg::PoseWithCovarianceStamped testPose_;
|
||||
double amcl_pose_x;
|
||||
double amcl_pose_y;
|
||||
bool pose_callback_;
|
||||
float tol_;
|
||||
};
|
||||
|
||||
bool TestAmclPose::defaultAmclTest()
|
||||
{
|
||||
initial_pose_pub_->publish(testPose_);
|
||||
while (!pose_callback_) {
|
||||
// TODO(mhpanah): Initial pose should only be published once.
|
||||
initial_pose_pub_->publish(testPose_);
|
||||
std::this_thread::sleep_for(1s);
|
||||
rclcpp::spin_some(node);
|
||||
}
|
||||
if (std::abs(amcl_pose_x - testPose_.pose.pose.position.x) < tol_ &&
|
||||
std::abs(amcl_pose_y - testPose_.pose.pose.position.y) < tol_)
|
||||
{
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void TestAmclPose::initTestPose()
|
||||
{
|
||||
testPose_.header.frame_id = "map";
|
||||
testPose_.header.stamp = rclcpp::Time();
|
||||
testPose_.pose.pose.position.x = -2.0;
|
||||
testPose_.pose.pose.position.y = -0.5;
|
||||
testPose_.pose.pose.position.z = 0.0;
|
||||
testPose_.pose.pose.orientation.x = 0.0;
|
||||
testPose_.pose.pose.orientation.y = 0.0;
|
||||
testPose_.pose.pose.orientation.z = 0.0;
|
||||
testPose_.pose.pose.orientation.w = 1.0;
|
||||
for (int i = 0; i < 35; i++) {
|
||||
testPose_.pose.covariance[i] = 0.0;
|
||||
}
|
||||
testPose_.pose.covariance[0] = 0.08;
|
||||
testPose_.pose.covariance[7] = 0.08;
|
||||
testPose_.pose.covariance[35] = 0.05;
|
||||
}
|
||||
|
||||
TEST_F(TestAmclPose, SimpleAmclTest)
|
||||
{
|
||||
EXPECT_EQ(true, defaultAmclTest());
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
set(test_planner_costmaps_exec test_planner_costmaps_node)
|
||||
|
||||
ament_add_gtest_executable(${test_planner_costmaps_exec}
|
||||
test_planner_costmaps_node.cpp
|
||||
planner_tester.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(${test_planner_costmaps_exec}
|
||||
${nav2_map_server_LIBRARIES})
|
||||
|
||||
ament_target_dependencies(${test_planner_costmaps_exec}
|
||||
${dependencies}
|
||||
)
|
||||
|
||||
set(test_planner_random_exec test_planner_random_node)
|
||||
|
||||
ament_add_gtest_executable(${test_planner_random_exec}
|
||||
test_planner_random_node.cpp
|
||||
planner_tester.cpp
|
||||
)
|
||||
|
||||
ament_target_dependencies(${test_planner_random_exec}
|
||||
${dependencies}
|
||||
)
|
||||
|
||||
target_link_libraries(${test_planner_random_exec}
|
||||
${nav2_map_server_LIBRARIES})
|
||||
|
||||
ament_add_test(test_planner_costmaps
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_planner_costmaps_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
ENV
|
||||
TEST_LAUNCH_DIR=${TEST_LAUNCH_DIR}
|
||||
TEST_EXECUTABLE=$<TARGET_FILE:${test_planner_costmaps_exec}>
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map.pgm
|
||||
)
|
||||
|
||||
ament_add_test(test_planner_random
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_planner_random_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
ENV
|
||||
TEST_LAUNCH_DIR=${TEST_LAUNCH_DIR}
|
||||
TEST_EXECUTABLE=$<TARGET_FILE:${test_planner_random_exec}>
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map.pgm
|
||||
)
|
||||
|
||||
ament_add_gtest(test_planner_plugins
|
||||
test_planner_plugins.cpp
|
||||
TIMEOUT 10
|
||||
)
|
||||
|
||||
ament_target_dependencies(test_planner_plugins rclcpp geometry_msgs nav2_msgs ${dependencies})
|
||||
target_link_libraries(test_planner_plugins
|
||||
# stdc++fs
|
||||
)
|
||||
|
||||
ament_add_gtest(test_planner_is_path_valid
|
||||
test_planner_is_path_valid.cpp
|
||||
planner_tester.cpp)
|
||||
|
||||
ament_target_dependencies(test_planner_is_path_valid rclcpp geometry_msgs nav2_msgs ${dependencies})
|
||||
@@ -0,0 +1,15 @@
|
||||
# Global Planner Component Testing
|
||||
|
||||
A PlannerTester node provides the world representation in the form of a costmap, sends a request to generate a path, and receives and checks the quality of the generated path.
|
||||
|
||||
As mentioned above, currently the world is represented as a costmap. Simplified versions of the world model and costmap are used for testing.
|
||||
|
||||
PlannerTester can sequentially pass random starting and goal poses and check the returned path for possible collision along the path.
|
||||
|
||||
Below is an example of the output from randomized testing. Blue spheres represent the starting locations, green, the goals. Red lines are the computed paths. Grey cells represent obstacles.
|
||||
|
||||

|
||||
|
||||
*Note: Currently robot size is 1x1 cells, no obstacle inflation is done on the costmap*
|
||||
|
||||
*Note: The Navfn algorithm sometimes fails to generate a path as you can see from the 'orphan' spheres.*
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 285 KiB |
@@ -0,0 +1,513 @@
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <string>
|
||||
#include <random>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include "planner_tester.hpp"
|
||||
#include "geometry_msgs/msg/twist.hpp"
|
||||
#include "nav2_map_server/map_mode.hpp"
|
||||
#include "nav2_map_server/map_io.hpp"
|
||||
#include "nav2_msgs/msg/costmap_meta_data.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace std::chrono; // NOLINT
|
||||
using nav2_util::Costmap;
|
||||
using nav2_util::TestCostmap;
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
PlannerTester::PlannerTester()
|
||||
: Node("PlannerTester"), is_active_(false),
|
||||
map_set_(false), costmap_set_(false),
|
||||
using_fake_costmap_(true), trinary_costmap_(true),
|
||||
track_unknown_space_(false), lethal_threshold_(100), unknown_cost_value_(-1),
|
||||
testCostmapType_(TestCostmap::open_space), base_transform_(nullptr),
|
||||
map_publish_rate_(100s)
|
||||
{
|
||||
}
|
||||
|
||||
void PlannerTester::activate()
|
||||
{
|
||||
if (is_active_) {
|
||||
throw std::runtime_error("Trying to activate while already active");
|
||||
return;
|
||||
}
|
||||
is_active_ = true;
|
||||
|
||||
// Launch a thread to process the messages for this node
|
||||
spin_thread_ = std::make_unique<nav2_util::NodeThread>(this);
|
||||
|
||||
// We start with a 10x10 grid with no obstacles
|
||||
costmap_ = std::make_unique<Costmap>(this);
|
||||
loadSimpleCostmap(TestCostmap::open_space);
|
||||
|
||||
startRobotTransform();
|
||||
|
||||
// The navfn wrapper
|
||||
auto state = rclcpp_lifecycle::State();
|
||||
planner_tester_ = std::make_shared<NavFnPlannerTester>();
|
||||
planner_tester_->declare_parameter(
|
||||
"GridBased.use_astar", rclcpp::ParameterValue(true));
|
||||
planner_tester_->set_parameter(
|
||||
rclcpp::Parameter(std::string("GridBased.use_astar"), rclcpp::ParameterValue(true)));
|
||||
planner_tester_->set_parameter(
|
||||
rclcpp::Parameter(std::string("expected_planner_frequency"), rclcpp::ParameterValue(-1.0)));
|
||||
planner_tester_->onConfigure(state);
|
||||
publishRobotTransform();
|
||||
map_pub_ = this->create_publisher<nav_msgs::msg::OccupancyGrid>("map", 1);
|
||||
path_valid_client_ = this->create_client<nav2_msgs::srv::IsPathValid>("is_path_valid");
|
||||
rclcpp::Rate r(1);
|
||||
r.sleep();
|
||||
planner_tester_->onActivate(state);
|
||||
}
|
||||
|
||||
void PlannerTester::deactivate()
|
||||
{
|
||||
if (!is_active_) {
|
||||
throw std::runtime_error("Trying to deactivate while already inactive");
|
||||
return;
|
||||
}
|
||||
is_active_ = false;
|
||||
|
||||
spin_thread_.reset();
|
||||
|
||||
auto state = rclcpp_lifecycle::State();
|
||||
planner_tester_->onDeactivate(state);
|
||||
planner_tester_->onCleanup(state);
|
||||
|
||||
map_timer_.reset();
|
||||
map_pub_.reset();
|
||||
map_.reset();
|
||||
tf_broadcaster_.reset();
|
||||
}
|
||||
|
||||
PlannerTester::~PlannerTester()
|
||||
{
|
||||
if (is_active_) {
|
||||
deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
void PlannerTester::startRobotTransform()
|
||||
{
|
||||
// Provide the robot pose transform
|
||||
tf_broadcaster_ = std::make_shared<tf2_ros::TransformBroadcaster>(this);
|
||||
|
||||
// Set an initial pose
|
||||
geometry_msgs::msg::Point robot_position;
|
||||
robot_position.x = 1.0;
|
||||
robot_position.y = 1.0;
|
||||
updateRobotPosition(robot_position);
|
||||
|
||||
// Publish the transform periodically
|
||||
transform_timer_ = create_wall_timer(
|
||||
100ms, std::bind(&PlannerTester::publishRobotTransform, this));
|
||||
}
|
||||
|
||||
void PlannerTester::updateRobotPosition(const geometry_msgs::msg::Point & position)
|
||||
{
|
||||
if (!base_transform_) {
|
||||
base_transform_ = std::make_unique<geometry_msgs::msg::TransformStamped>();
|
||||
base_transform_->header.frame_id = "map";
|
||||
base_transform_->child_frame_id = "base_link";
|
||||
}
|
||||
std::cout << now().nanoseconds() << std::endl;
|
||||
|
||||
base_transform_->header.stamp = now() + rclcpp::Duration(0.25s);
|
||||
base_transform_->transform.translation.x = position.x;
|
||||
base_transform_->transform.translation.y = position.y;
|
||||
base_transform_->transform.rotation.w = 1.0;
|
||||
|
||||
publishRobotTransform();
|
||||
}
|
||||
|
||||
void PlannerTester::publishRobotTransform()
|
||||
{
|
||||
if (base_transform_) {
|
||||
tf_broadcaster_->sendTransform(*base_transform_);
|
||||
}
|
||||
}
|
||||
|
||||
void PlannerTester::loadDefaultMap()
|
||||
{
|
||||
// Specs for the default map
|
||||
double resolution = 1.0;
|
||||
bool negate = false;
|
||||
double occupancy_threshold = 0.65;
|
||||
double free_threshold = 0.196;
|
||||
|
||||
// Define origin offset
|
||||
std::vector<double> origin = {0.0, 0.0, 0.0};
|
||||
|
||||
nav2_map_server::MapMode mode = nav2_map_server::MapMode::Trinary;
|
||||
|
||||
std::string file_path = "";
|
||||
char const * path = getenv("TEST_MAP");
|
||||
if (path == NULL) {
|
||||
throw std::runtime_error(
|
||||
"Path to map image file"
|
||||
" has not been specified in environment variable `TEST_MAP`.");
|
||||
} else {
|
||||
file_path = std::string(path);
|
||||
}
|
||||
|
||||
RCLCPP_INFO(this->get_logger(), "Loading map with file_path: %s", file_path.c_str());
|
||||
|
||||
try {
|
||||
map_ = std::make_shared<nav_msgs::msg::OccupancyGrid>();
|
||||
|
||||
nav2_map_server::LoadParameters load_parameters;
|
||||
load_parameters.image_file_name = file_path;
|
||||
load_parameters.resolution = resolution;
|
||||
load_parameters.origin = origin;
|
||||
load_parameters.free_thresh = free_threshold;
|
||||
load_parameters.occupied_thresh = occupancy_threshold;
|
||||
load_parameters.mode = mode;
|
||||
load_parameters.negate = negate;
|
||||
loadMapFromFile(load_parameters, *map_);
|
||||
} catch (...) {
|
||||
RCLCPP_ERROR(
|
||||
this->get_logger(),
|
||||
"Failed to load image from file: %s", file_path.c_str());
|
||||
throw;
|
||||
}
|
||||
|
||||
map_->header.stamp = this->now();
|
||||
map_->header.frame_id = "map";
|
||||
map_->info.map_load_time = this->now();
|
||||
|
||||
// TODO(orduno): #443 replace with a latched topic
|
||||
map_timer_ = create_wall_timer(1s, [this]() -> void {map_pub_->publish(*map_);});
|
||||
|
||||
map_set_ = true;
|
||||
costmap_set_ = false;
|
||||
using_fake_costmap_ = false;
|
||||
|
||||
setCostmap();
|
||||
}
|
||||
|
||||
void PlannerTester::loadSimpleCostmap(const TestCostmap & testCostmapType)
|
||||
{
|
||||
RCLCPP_INFO(get_logger(), "loadSimpleCostmap called.");
|
||||
if (costmap_set_) {
|
||||
RCLCPP_DEBUG(this->get_logger(), "Setting a new costmap with fake values");
|
||||
}
|
||||
|
||||
costmap_->set_test_costmap(testCostmapType);
|
||||
|
||||
costmap_set_ = true;
|
||||
using_fake_costmap_ = true;
|
||||
}
|
||||
|
||||
void PlannerTester::setCostmap()
|
||||
{
|
||||
if (!map_set_) {
|
||||
RCLCPP_ERROR(this->get_logger(), "Map has not been provided");
|
||||
return;
|
||||
}
|
||||
|
||||
costmap_ = std::make_unique<Costmap>(
|
||||
this, trinary_costmap_, track_unknown_space_, lethal_threshold_, unknown_cost_value_);
|
||||
|
||||
costmap_->set_static_map(*map_);
|
||||
|
||||
costmap_set_ = true;
|
||||
using_fake_costmap_ = false;
|
||||
}
|
||||
|
||||
bool PlannerTester::defaultPlannerTest(
|
||||
ComputePathToPoseResult & path,
|
||||
const double /*deviation_tolerance*/)
|
||||
{
|
||||
if (!costmap_set_) {
|
||||
RCLCPP_ERROR(this->get_logger(), "Costmap must be set before requesting a plan");
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO(orduno) #443 Add support for planners that take into account robot orientation
|
||||
geometry_msgs::msg::Point robot_position;
|
||||
ComputePathToPoseCommand goal;
|
||||
auto costmap_properties = costmap_->get_properties();
|
||||
|
||||
if (using_fake_costmap_) {
|
||||
RCLCPP_DEBUG(this->get_logger(), "Planning using a fake costmap");
|
||||
|
||||
robot_position.x = 1.0;
|
||||
robot_position.y = 1.0;
|
||||
|
||||
goal.pose.position.x = 8.0;
|
||||
goal.pose.position.y = 8.0;
|
||||
|
||||
} else {
|
||||
RCLCPP_DEBUG(this->get_logger(), "Planning using the provided map");
|
||||
|
||||
// Defined with respect to world coordinate system
|
||||
// Planner will do coordinate transformation to map internally
|
||||
robot_position.x = 390.0;
|
||||
robot_position.y = 10.0;
|
||||
|
||||
goal.pose.position.x = 10.0;
|
||||
goal.pose.position.y = 390.0;
|
||||
}
|
||||
|
||||
// TODO(orduno): #443 On a default test, provide the reference path to compare with the planner
|
||||
// result.
|
||||
return plannerTest(robot_position, goal, path);
|
||||
}
|
||||
|
||||
bool PlannerTester::defaultPlannerRandomTests(
|
||||
const unsigned int number_tests,
|
||||
const float acceptable_fail_ratio = 0.1)
|
||||
{
|
||||
if (!costmap_set_) {
|
||||
RCLCPP_ERROR(this->get_logger(), "Costmap must be set before requesting a plan");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (using_fake_costmap_) {
|
||||
RCLCPP_ERROR(
|
||||
this->get_logger(),
|
||||
"Randomized testing with hardcoded costmaps not implemented yet");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize random number generator
|
||||
std::random_device random_device;
|
||||
std::mt19937 generator(random_device());
|
||||
|
||||
// Obtain random positions within map
|
||||
std::uniform_int_distribution<> distribution_x(1, costmap_->get_properties().size_x - 1);
|
||||
std::uniform_int_distribution<> distribution_y(1, costmap_->get_properties().size_y - 1);
|
||||
|
||||
auto generate_random = [&]() mutable -> std::pair<int, int> {
|
||||
bool point_is_free = false;
|
||||
int x, y;
|
||||
while (!point_is_free) {
|
||||
x = distribution_x(generator);
|
||||
y = distribution_y(generator);
|
||||
point_is_free = costmap_->is_free(x, y);
|
||||
}
|
||||
return std::make_pair(x, y);
|
||||
};
|
||||
|
||||
// TODO(orduno) #443 Add support for planners that take into account robot orientation
|
||||
geometry_msgs::msg::Point robot_position;
|
||||
ComputePathToPoseCommand goal;
|
||||
ComputePathToPoseResult path;
|
||||
|
||||
unsigned int num_fail = 0;
|
||||
auto start = high_resolution_clock::now();
|
||||
for (unsigned int test_num = 0; test_num < number_tests; ++test_num) {
|
||||
RCLCPP_DEBUG(this->get_logger(), "Running test #%u", test_num + 1);
|
||||
|
||||
// Compose the robot start position and goal using random numbers
|
||||
// Defined with respect to world coordinate system
|
||||
// Planner will do coordinate transformation to map internally
|
||||
|
||||
auto vals = generate_random();
|
||||
robot_position.x = vals.first;
|
||||
robot_position.y = vals.second;
|
||||
|
||||
vals = generate_random();
|
||||
goal.pose.position.x = vals.first;
|
||||
goal.pose.position.y = vals.second;
|
||||
|
||||
if (!plannerTest(robot_position, goal, path)) {
|
||||
RCLCPP_WARN(
|
||||
this->get_logger(), "Failed with start at %0.2f, %0.2f and goal at %0.2f, %0.2f",
|
||||
robot_position.x, robot_position.y, goal.pose.position.x, goal.pose.position.y);
|
||||
++num_fail;
|
||||
}
|
||||
}
|
||||
auto end = high_resolution_clock::now();
|
||||
auto elapsed = duration_cast<milliseconds>(end - start);
|
||||
|
||||
RCLCPP_INFO(
|
||||
this->get_logger(),
|
||||
"Tested with %u tests. Planner failed on %u. Test time %ld ms",
|
||||
number_tests, num_fail, elapsed.count());
|
||||
|
||||
if ((num_fail / number_tests) > acceptable_fail_ratio) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlannerTester::plannerTest(
|
||||
const geometry_msgs::msg::Point & robot_position,
|
||||
const ComputePathToPoseCommand & goal,
|
||||
ComputePathToPoseResult & path)
|
||||
{
|
||||
RCLCPP_DEBUG(this->get_logger(), "Getting the path from the planner");
|
||||
|
||||
// First make available the current robot position for the planner to take as starting point
|
||||
updateRobotPosition(robot_position);
|
||||
sleep(0.05);
|
||||
|
||||
// Then request to compute a path
|
||||
TaskStatus status = createPlan(goal, path);
|
||||
|
||||
RCLCPP_DEBUG(this->get_logger(), "Path request status: %d", static_cast<int8_t>(status));
|
||||
|
||||
if (status == TaskStatus::FAILED) {
|
||||
return false;
|
||||
} else if (status == TaskStatus::SUCCEEDED) {
|
||||
// TODO(orduno): #443 check why task may report success while planner returns a path of 0 points
|
||||
RCLCPP_DEBUG(this->get_logger(), "Got path, checking for possible collisions");
|
||||
return isCollisionFree(path) && isWithinTolerance(robot_position, goal, path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
TaskStatus PlannerTester::createPlan(
|
||||
const ComputePathToPoseCommand & goal,
|
||||
ComputePathToPoseResult & path)
|
||||
{
|
||||
// Update the costmap of the planner to the set data
|
||||
planner_tester_->setCostmap(costmap_.get());
|
||||
|
||||
// Call planning algorithm
|
||||
if (planner_tester_->createPath(goal, path)) {
|
||||
return TaskStatus::SUCCEEDED;
|
||||
}
|
||||
|
||||
return TaskStatus::FAILED;
|
||||
}
|
||||
|
||||
bool PlannerTester::isPathValid(nav_msgs::msg::Path & path)
|
||||
{
|
||||
planner_tester_->setCostmap(costmap_.get());
|
||||
// create a fake service request
|
||||
auto request = std::make_shared<nav2_msgs::srv::IsPathValid::Request>();
|
||||
request->path = path;
|
||||
auto result = path_valid_client_->async_send_request(request);
|
||||
|
||||
RCLCPP_INFO(this->get_logger(), "Waiting for service complete");
|
||||
if (rclcpp::spin_until_future_complete(
|
||||
this->planner_tester_, result,
|
||||
std::chrono::milliseconds(100)) ==
|
||||
rclcpp::FutureReturnCode::SUCCESS)
|
||||
{
|
||||
return result.get()->is_valid;
|
||||
} else {
|
||||
RCLCPP_INFO(get_logger(), "Failed to call is_path_valid service");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool PlannerTester::isCollisionFree(const ComputePathToPoseResult & path)
|
||||
{
|
||||
// At each point of the path, check if the corresponding cell is free
|
||||
|
||||
// TODO(orduno): #443 for now we are assuming the robot is the size of a single cell
|
||||
// costmap/world_model has consider the robot footprint
|
||||
|
||||
// TODO(orduno): #443 Tweak criteria for defining if a path goes into obstacles.
|
||||
// Current navfn planner will sometimes produce paths that cut corners
|
||||
// i.e. some points are around the corner are actually inside the obstacle
|
||||
|
||||
bool collisionFree = true;
|
||||
|
||||
for (auto pose : path.poses) {
|
||||
collisionFree = costmap_->is_free(
|
||||
static_cast<unsigned int>(std::round(pose.pose.position.x)),
|
||||
static_cast<unsigned int>(std::round(pose.pose.position.y)));
|
||||
|
||||
if (!collisionFree) {
|
||||
RCLCPP_WARN(
|
||||
this->get_logger(), "Path has collision at (%.2f, %.2f)",
|
||||
pose.pose.position.x, pose.pose.position.y);
|
||||
printPath(path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
RCLCPP_DEBUG(this->get_logger(), "Path has no collisions");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlannerTester::isWithinTolerance(
|
||||
const geometry_msgs::msg::Point & robot_position,
|
||||
const ComputePathToPoseCommand & goal,
|
||||
const ComputePathToPoseResult & path) const
|
||||
{
|
||||
return isWithinTolerance(
|
||||
robot_position, goal, path, 0.0, ComputePathToPoseResult());
|
||||
}
|
||||
|
||||
bool PlannerTester::isWithinTolerance(
|
||||
const geometry_msgs::msg::Point & robot_position,
|
||||
const ComputePathToPoseCommand & goal,
|
||||
const ComputePathToPoseResult & path,
|
||||
const double /*deviationTolerance*/,
|
||||
const ComputePathToPoseResult & /*reference_path*/) const
|
||||
{
|
||||
// TODO(orduno) #443 Work in progress, for now we only check that the path start matches the
|
||||
// robot start location and that the path end matches the goal.
|
||||
|
||||
auto path_start = path.poses[0];
|
||||
auto path_end = path.poses.end()[-1];
|
||||
|
||||
if (
|
||||
path_start.pose.position.x == robot_position.x &&
|
||||
path_start.pose.position.y == robot_position.y &&
|
||||
path_end.pose.position.x == goal.pose.position.x &&
|
||||
path_end.pose.position.y == goal.pose.position.y)
|
||||
{
|
||||
RCLCPP_DEBUG(this->get_logger(), "Path has correct start and end points");
|
||||
|
||||
return true;
|
||||
}
|
||||
RCLCPP_WARN(this->get_logger(), "Path deviates from requested start and end points");
|
||||
|
||||
RCLCPP_DEBUG(
|
||||
this->get_logger(), "Requested path starts at (%.2f, %.2f) and ends at (%.2f, %.2f)",
|
||||
robot_position.x, robot_position.y, goal.pose.position.x, goal.pose.position.y);
|
||||
|
||||
RCLCPP_DEBUG(
|
||||
this->get_logger(), "Computed path starts at (%.2f, %.2f) and ends at (%.2f, %.2f)",
|
||||
path_start.pose.position.x, path_start.pose.position.y,
|
||||
path_end.pose.position.x, path_end.pose.position.y);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void PlannerTester::printPath(const ComputePathToPoseResult & path) const
|
||||
{
|
||||
auto index = 0;
|
||||
auto ss = std::stringstream{};
|
||||
|
||||
for (auto pose : path.poses) {
|
||||
ss << " point #" << index << " with" <<
|
||||
" x: " << std::setprecision(3) << pose.pose.position.x <<
|
||||
" y: " << std::setprecision(3) << pose.pose.position.y << '\n';
|
||||
++index;
|
||||
}
|
||||
|
||||
RCLCPP_INFO(get_logger(), ss.str().c_str());
|
||||
}
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
@@ -0,0 +1,241 @@
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#ifndef PLANNING__PLANNER_TESTER_HPP_
|
||||
#define PLANNING__PLANNER_TESTER_HPP_
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <algorithm>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "rclcpp_action/rclcpp_action.hpp"
|
||||
#include "nav2_msgs/action/compute_path_to_pose.hpp"
|
||||
#include "nav_msgs/msg/occupancy_grid.hpp"
|
||||
#include "nav2_msgs/msg/costmap.hpp"
|
||||
#include "nav2_msgs/srv/get_costmap.hpp"
|
||||
#include "nav2_msgs/srv/is_path_valid.hpp"
|
||||
#include "visualization_msgs/msg/marker.hpp"
|
||||
#include "nav2_util/costmap.hpp"
|
||||
#include "nav2_util/node_thread.hpp"
|
||||
#include "geometry_msgs/msg/pose_stamped.hpp"
|
||||
#include "geometry_msgs/msg/transform_stamped.hpp"
|
||||
#include "tf2_msgs/msg/tf_message.hpp"
|
||||
#include "nav2_planner/planner_server.hpp"
|
||||
#include "tf2_ros/transform_broadcaster.h"
|
||||
|
||||
namespace nav2_system_tests
|
||||
{
|
||||
|
||||
class NavFnPlannerTester : public nav2_planner::PlannerServer
|
||||
{
|
||||
public:
|
||||
NavFnPlannerTester()
|
||||
: PlannerServer()
|
||||
{
|
||||
}
|
||||
|
||||
void printCostmap()
|
||||
{
|
||||
// print costmap for debug
|
||||
for (size_t i = 0; i != costmap_->getSizeInCellsX() * costmap_->getSizeInCellsY(); i++) {
|
||||
if (i % costmap_->getSizeInCellsX() == 0) {
|
||||
std::cout << "" << std::endl;
|
||||
}
|
||||
std::cout << costmap_ros_->getCostmap()->getCharMap()[i] << " ";
|
||||
}
|
||||
std::cout << "" << std::endl;
|
||||
}
|
||||
|
||||
void setCostmap(nav2_util::Costmap * costmap)
|
||||
{
|
||||
std::unique_lock<nav2_costmap_2d::Costmap2D::mutex_t> lock(
|
||||
*(costmap_ros_->getCostmap()->getMutex()));
|
||||
|
||||
nav2_msgs::msg::CostmapMetaData prop;
|
||||
nav2_msgs::msg::Costmap cm = costmap->get_costmap(prop);
|
||||
prop = cm.metadata;
|
||||
costmap_ros_->getCostmap()->resizeMap(
|
||||
prop.size_x, prop.size_y,
|
||||
prop.resolution, prop.origin.position.x, prop.origin.position.x);
|
||||
// Volatile prevents compiler from treating costmap_ptr as unused or changing its address
|
||||
volatile unsigned char * costmap_ptr = costmap_ros_->getCostmap()->getCharMap();
|
||||
delete[] costmap_ptr;
|
||||
costmap_ptr = new unsigned char[prop.size_x * prop.size_y];
|
||||
std::copy(cm.data.begin(), cm.data.end(), costmap_ptr);
|
||||
}
|
||||
|
||||
bool createPath(
|
||||
const geometry_msgs::msg::PoseStamped & goal,
|
||||
nav_msgs::msg::Path & path)
|
||||
{
|
||||
geometry_msgs::msg::PoseStamped start;
|
||||
if (!nav2_util::getCurrentPose(start, *tf_, "map", "base_link", 0.1)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
path = planners_["GridBased"]->createPlan(start, goal);
|
||||
// The situation when createPlan() did not throw any exception
|
||||
// does not guarantee that plan was created correctly.
|
||||
// So it should be checked additionally that path is correct.
|
||||
if (!path.poses.size()) {
|
||||
return false;
|
||||
}
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void onCleanup(const rclcpp_lifecycle::State & state)
|
||||
{
|
||||
on_cleanup(state);
|
||||
}
|
||||
|
||||
void onActivate(const rclcpp_lifecycle::State & state)
|
||||
{
|
||||
on_activate(state);
|
||||
}
|
||||
|
||||
void onDeactivate(const rclcpp_lifecycle::State & state)
|
||||
{
|
||||
on_deactivate(state);
|
||||
}
|
||||
|
||||
void onConfigure(const rclcpp_lifecycle::State & state)
|
||||
{
|
||||
on_configure(state);
|
||||
}
|
||||
};
|
||||
|
||||
enum class TaskStatus : int8_t
|
||||
{
|
||||
SUCCEEDED = 1,
|
||||
FAILED = 2,
|
||||
RUNNING = 3,
|
||||
};
|
||||
|
||||
class PlannerTester : public rclcpp::Node
|
||||
{
|
||||
public:
|
||||
using ComputePathToPoseCommand = geometry_msgs::msg::PoseStamped;
|
||||
using ComputePathToPoseResult = nav_msgs::msg::Path;
|
||||
|
||||
PlannerTester();
|
||||
~PlannerTester();
|
||||
|
||||
// Activate the tester before running tests
|
||||
void activate();
|
||||
void deactivate();
|
||||
|
||||
// Loads the provided map and and generates a costmap from it.
|
||||
void loadDefaultMap();
|
||||
|
||||
// Alternatively, use a preloaded 10x10 costmap
|
||||
void loadSimpleCostmap(const nav2_util::TestCostmap & testCostmapType);
|
||||
|
||||
// Runs a single test with default poses depending on the loaded map
|
||||
// Success criteria is a collision free path and a deviation to a
|
||||
// reference path smaller than a tolerance.
|
||||
bool defaultPlannerTest(
|
||||
ComputePathToPoseResult & path,
|
||||
const double deviation_tolerance = 1.0);
|
||||
|
||||
|
||||
// Runs multiple tests with random initial and goal poses
|
||||
bool defaultPlannerRandomTests(
|
||||
const unsigned int number_tests,
|
||||
const float acceptable_fail_ratio);
|
||||
|
||||
bool isPathValid(nav_msgs::msg::Path & path);
|
||||
|
||||
private:
|
||||
void setCostmap();
|
||||
|
||||
TaskStatus createPlan(
|
||||
const ComputePathToPoseCommand & goal,
|
||||
ComputePathToPoseResult & path
|
||||
);
|
||||
|
||||
bool is_active_;
|
||||
bool map_set_;
|
||||
bool costmap_set_;
|
||||
bool using_fake_costmap_;
|
||||
|
||||
// Parameters of the costmap
|
||||
bool trinary_costmap_;
|
||||
bool track_unknown_space_;
|
||||
int lethal_threshold_;
|
||||
int unknown_cost_value_;
|
||||
nav2_util::TestCostmap testCostmapType_;
|
||||
|
||||
// The static map
|
||||
std::shared_ptr<nav_msgs::msg::OccupancyGrid> map_;
|
||||
|
||||
// The costmap representation of the static map
|
||||
std::unique_ptr<nav2_util::Costmap> costmap_;
|
||||
|
||||
// The global planner
|
||||
std::shared_ptr<NavFnPlannerTester> planner_tester_;
|
||||
|
||||
// The is path valid client
|
||||
rclcpp::Client<nav2_msgs::srv::IsPathValid>::SharedPtr path_valid_client_;
|
||||
|
||||
// A thread for spinning the ROS node
|
||||
std::unique_ptr<nav2_util::NodeThread> spin_thread_;
|
||||
|
||||
// The tester must provide the robot pose through a transform
|
||||
std::unique_ptr<geometry_msgs::msg::TransformStamped> base_transform_;
|
||||
std::shared_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
|
||||
rclcpp::TimerBase::SharedPtr transform_timer_;
|
||||
void publishRobotTransform();
|
||||
void startRobotTransform();
|
||||
void updateRobotPosition(const geometry_msgs::msg::Point & position);
|
||||
|
||||
// Occupancy grid publisher for visualization
|
||||
rclcpp::Publisher<nav_msgs::msg::OccupancyGrid>::SharedPtr map_pub_;
|
||||
rclcpp::TimerBase::SharedPtr map_timer_;
|
||||
rclcpp::WallRate map_publish_rate_;
|
||||
void mapCallback();
|
||||
|
||||
// Executes a test run with the provided end points.
|
||||
// Success criteria is a collision free path.
|
||||
// TODO(orduno): #443 Assuming a robot the size of a costmap cell
|
||||
bool plannerTest(
|
||||
const geometry_msgs::msg::Point & robot_position,
|
||||
const ComputePathToPoseCommand & goal,
|
||||
ComputePathToPoseResult & path);
|
||||
|
||||
bool isCollisionFree(const ComputePathToPoseResult & path);
|
||||
|
||||
bool isWithinTolerance(
|
||||
const geometry_msgs::msg::Point & robot_position,
|
||||
const ComputePathToPoseCommand & goal,
|
||||
const ComputePathToPoseResult & path) const;
|
||||
|
||||
bool isWithinTolerance(
|
||||
const geometry_msgs::msg::Point & robot_position,
|
||||
const ComputePathToPoseCommand & goal,
|
||||
const ComputePathToPoseResult & path,
|
||||
const double deviationTolerance,
|
||||
const ComputePathToPoseResult & reference_path) const;
|
||||
|
||||
void printPath(const ComputePathToPoseResult & path) const;
|
||||
};
|
||||
|
||||
} // namespace nav2_system_tests
|
||||
|
||||
#endif // PLANNING__PLANNER_TESTER_HPP_
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2018 Intel Corporation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
from launch.actions import ExecuteProcess
|
||||
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
testExecutable = os.getenv('TEST_EXECUTABLE')
|
||||
|
||||
ld = LaunchDescription([])
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[testExecutable, '--ros-args -p use_sim_time:=True'],
|
||||
name='test_planner_costmaps_node',
|
||||
output='screen'
|
||||
)
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "planner_tester.hpp"
|
||||
#include "nav2_util/lifecycle_utils.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using nav2_system_tests::PlannerTester;
|
||||
using nav2_util::TestCostmap;
|
||||
|
||||
using ComputePathToPoseCommand = geometry_msgs::msg::PoseStamped;
|
||||
using ComputePathToPoseResult = nav_msgs::msg::Path;
|
||||
|
||||
TEST(testSimpleCostmaps, testSimpleCostmaps)
|
||||
{
|
||||
auto obj = std::make_shared<PlannerTester>();
|
||||
|
||||
std::vector<TestCostmap> costmaps = {
|
||||
TestCostmap::open_space,
|
||||
TestCostmap::bounded,
|
||||
TestCostmap::top_left_obstacle,
|
||||
TestCostmap::bottom_left_obstacle,
|
||||
TestCostmap::maze1,
|
||||
TestCostmap::maze2
|
||||
};
|
||||
|
||||
ComputePathToPoseResult result;
|
||||
|
||||
obj->activate();
|
||||
|
||||
for (auto costmap : costmaps) {
|
||||
obj->loadSimpleCostmap(costmap);
|
||||
EXPECT_EQ(true, obj->defaultPlannerTest(result));
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
||||
// initialize ROS
|
||||
rclcpp::init(argc, argv);
|
||||
|
||||
bool all_successful = RUN_ALL_TESTS();
|
||||
|
||||
// shutdown ROS
|
||||
rclcpp::shutdown();
|
||||
|
||||
return all_successful;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2022 Joshua Wallace
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "nav2_msgs/srv/is_path_valid.hpp"
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "planner_tester.hpp"
|
||||
#include "nav2_util/lifecycle_utils.hpp"
|
||||
|
||||
using nav2_system_tests::PlannerTester;
|
||||
using nav2_util::TestCostmap;
|
||||
|
||||
TEST(testIsPathValid, testIsPathValid)
|
||||
{
|
||||
auto planner_tester = std::make_shared<PlannerTester>();
|
||||
planner_tester->activate();
|
||||
planner_tester->loadSimpleCostmap(TestCostmap::top_left_obstacle);
|
||||
|
||||
nav_msgs::msg::Path path;
|
||||
|
||||
// empty path
|
||||
bool is_path_valid = planner_tester->isPathValid(path);
|
||||
EXPECT_FALSE(is_path_valid);
|
||||
|
||||
// invalid path
|
||||
for (float i = 0; i < 10; i += 1.0) {
|
||||
for (float j = 0; j < 10; j += 1.0) {
|
||||
geometry_msgs::msg::PoseStamped pose;
|
||||
pose.pose.position.x = i;
|
||||
pose.pose.position.y = j;
|
||||
path.poses.push_back(pose);
|
||||
}
|
||||
}
|
||||
is_path_valid = planner_tester->isPathValid(path);
|
||||
EXPECT_FALSE(is_path_valid);
|
||||
|
||||
// valid path
|
||||
path.poses.clear();
|
||||
for (float i = 0; i < 10; i += 1.0) {
|
||||
geometry_msgs::msg::PoseStamped pose;
|
||||
pose.pose.position.x = 1.0;
|
||||
pose.pose.position.y = i;
|
||||
path.poses.push_back(pose);
|
||||
}
|
||||
is_path_valid = planner_tester->isPathValid(path);
|
||||
EXPECT_TRUE(is_path_valid);
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
||||
// initialize ROS
|
||||
rclcpp::init(argc, argv);
|
||||
|
||||
bool all_successful = RUN_ALL_TESTS();
|
||||
|
||||
// shutdown ROS
|
||||
rclcpp::shutdown();
|
||||
return all_successful;
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
// Copyright (c) 2020 Samsung Research
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "planner_tester.hpp"
|
||||
#include "nav2_util/lifecycle_utils.hpp"
|
||||
#include "nav2_util/geometry_utils.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using nav2_system_tests::PlannerTester;
|
||||
using nav2_util::TestCostmap;
|
||||
|
||||
using ComputePathToPoseCommand = geometry_msgs::msg::PoseStamped;
|
||||
using ComputePathToPoseResult = nav_msgs::msg::Path;
|
||||
|
||||
void callback(const nav_msgs::msg::Path::ConstSharedPtr /*grid*/)
|
||||
{
|
||||
}
|
||||
|
||||
void testSmallPathValidityAndOrientation(std::string plugin, double length)
|
||||
{
|
||||
auto obj = std::make_shared<nav2_system_tests::NavFnPlannerTester>();
|
||||
rclcpp_lifecycle::State state;
|
||||
obj->set_parameter(rclcpp::Parameter("GridBased.plugin", plugin));
|
||||
obj->declare_parameter(
|
||||
"GridBased.use_final_approach_orientation", rclcpp::ParameterValue(false));
|
||||
obj->onConfigure(state);
|
||||
|
||||
geometry_msgs::msg::PoseStamped start;
|
||||
geometry_msgs::msg::PoseStamped goal;
|
||||
|
||||
start.pose.position.x = 0.5;
|
||||
start.pose.position.y = 0.5;
|
||||
start.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(M_PI_2);
|
||||
start.header.frame_id = "map";
|
||||
|
||||
goal.pose.position.x = 0.5;
|
||||
goal.pose.position.y = start.pose.position.y + length;
|
||||
goal.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(-M_PI);
|
||||
goal.header.frame_id = "map";
|
||||
|
||||
// Test without use_final_approach_orientation
|
||||
// expecting end path pose orientation to be equal to goal orientation
|
||||
auto path = obj->getPlan(start, goal, "GridBased");
|
||||
EXPECT_GT((int)path.poses.size(), 0);
|
||||
EXPECT_NEAR(tf2::getYaw(path.poses.back().pose.orientation), -M_PI, 0.01);
|
||||
// obj->onCleanup(state);
|
||||
obj.reset();
|
||||
}
|
||||
|
||||
void testSmallPathValidityAndNoOrientation(std::string plugin, double length)
|
||||
{
|
||||
auto obj = std::make_shared<nav2_system_tests::NavFnPlannerTester>();
|
||||
rclcpp_lifecycle::State state;
|
||||
obj->set_parameter(rclcpp::Parameter("GridBased.plugin", plugin));
|
||||
|
||||
// Test WITH use_final_approach_orientation
|
||||
// expecting end path pose orientation to be equal to approach orientation
|
||||
// which in the one pose corner case should be the start pose orientation
|
||||
obj->declare_parameter(
|
||||
"GridBased.use_final_approach_orientation", rclcpp::ParameterValue(true));
|
||||
obj->set_parameter(rclcpp::Parameter("GridBased.use_final_approach_orientation", true));
|
||||
obj->onConfigure(state);
|
||||
|
||||
geometry_msgs::msg::PoseStamped start;
|
||||
geometry_msgs::msg::PoseStamped goal;
|
||||
|
||||
start.pose.position.x = 0.5;
|
||||
start.pose.position.y = 0.5;
|
||||
start.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(M_PI_2);
|
||||
start.header.frame_id = "map";
|
||||
|
||||
goal.pose.position.x = 0.5;
|
||||
goal.pose.position.y = start.pose.position.y + length;
|
||||
goal.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(-M_PI);
|
||||
goal.header.frame_id = "map";
|
||||
|
||||
auto path = obj->getPlan(start, goal, "GridBased");
|
||||
EXPECT_GT((int)path.poses.size(), 0);
|
||||
|
||||
int path_size = path.poses.size();
|
||||
if (path_size == 1) {
|
||||
EXPECT_NEAR(
|
||||
tf2::getYaw(path.poses.back().pose.orientation),
|
||||
tf2::getYaw(start.pose.orientation),
|
||||
0.01);
|
||||
} else {
|
||||
double dx = path.poses.back().pose.position.x - path.poses.front().pose.position.x;
|
||||
double dy = path.poses.back().pose.position.y - path.poses.front().pose.position.y;
|
||||
EXPECT_NEAR(
|
||||
tf2::getYaw(path.poses.back().pose.orientation),
|
||||
atan2(dy, dx),
|
||||
0.01);
|
||||
}
|
||||
// obj->onCleanup(state);
|
||||
obj.reset();
|
||||
}
|
||||
|
||||
TEST(testPluginMap, Failures)
|
||||
{
|
||||
auto obj = std::make_shared<nav2_system_tests::NavFnPlannerTester>();
|
||||
rclcpp_lifecycle::State state;
|
||||
obj->set_parameter(rclcpp::Parameter("expected_planner_frequency", 100000.0));
|
||||
obj->onConfigure(state);
|
||||
obj->create_subscription<nav_msgs::msg::Path>(
|
||||
"plan", rclcpp::SystemDefaultsQoS(), callback);
|
||||
|
||||
geometry_msgs::msg::PoseStamped start;
|
||||
geometry_msgs::msg::PoseStamped goal;
|
||||
std::string plugin_fake = "fake";
|
||||
std::string plugin_none = "";
|
||||
auto path = obj->getPlan(start, goal, plugin_none);
|
||||
EXPECT_EQ(path.header.frame_id, std::string("map"));
|
||||
|
||||
path = obj->getPlan(start, goal, plugin_fake);
|
||||
EXPECT_EQ(path.poses.size(), 0ul);
|
||||
|
||||
obj->onCleanup(state);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, Smac2dEqualStartGoal)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_smac_planner/SmacPlanner2D", 0.0);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, Smac2dEqualStartGoalN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_smac_planner/SmacPlanner2D", 0.0);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, Smac2dVerySmallPath)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_smac_planner/SmacPlanner2D", 0.00001);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, Smac2dVerySmallPathN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_smac_planner/SmacPlanner2D", 0.00001);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, Smac2dBelowCostmapResolution)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_smac_planner/SmacPlanner2D", 0.09);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, Smac2dBelowCostmapResolutionN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_smac_planner/SmacPlanner2D", 0.09);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, Smac2dJustAboveCostmapResolution)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_smac_planner/SmacPlanner2D", 0.102);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, Smac2dJustAboveCostmapResolutionN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_smac_planner/SmacPlanner2D", 0.102);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, Smac2dAboveCostmapResolution)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_smac_planner/SmacPlanner2D", 1.5);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, Smac2dAboveCostmapResolutionN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_smac_planner/SmacPlanner2D", 1.5);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, NavFnEqualStartGoal)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_navfn_planner/NavfnPlanner", 0.0);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, NavFnEqualStartGoalN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_navfn_planner/NavfnPlanner", 0.0);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, NavFnVerySmallPath)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_navfn_planner/NavfnPlanner", 0.00001);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, NavFnVerySmallPathN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_navfn_planner/NavfnPlanner", 0.00001);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, NavFnBelowCostmapResolution)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_navfn_planner/NavfnPlanner", 0.09);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, NavFnBelowCostmapResolutionN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_navfn_planner/NavfnPlanner", 0.09);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, NavFnJustAboveCostmapResolution)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_navfn_planner/NavfnPlanner", 0.102);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, NavFnJustAboveCostmapResolutionN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_navfn_planner/NavfnPlanner", 0.102);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, NavFnAboveCostmapResolution)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_navfn_planner/NavfnPlanner", 1.5);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, NavFnAboveCostmapResolutionN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_navfn_planner/NavfnPlanner", 1.5);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, ThetaStarEqualStartGoal)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_theta_star_planner/ThetaStarPlanner", 0.0);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, ThetaStarEqualStartGoalN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_theta_star_planner/ThetaStarPlanner", 0.0);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, ThetaStarVerySmallPath)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_theta_star_planner/ThetaStarPlanner", 0.00001);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, ThetaStarVerySmallPathN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_theta_star_planner/ThetaStarPlanner", 0.00001);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, ThetaStarBelowCostmapResolution)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_theta_star_planner/ThetaStarPlanner", 0.09);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, ThetaStarBelowCostmapResolutionN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_theta_star_planner/ThetaStarPlanner", 0.09);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, ThetaStarJustAboveCostmapResolution)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_theta_star_planner/ThetaStarPlanner", 0.102);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, ThetaStarJustAboveCostmapResolutionN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_theta_star_planner/ThetaStarPlanner", 0.102);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, ThetaStarAboveCostmapResolution)
|
||||
{
|
||||
testSmallPathValidityAndOrientation("nav2_theta_star_planner/ThetaStarPlanner", 1.5);
|
||||
}
|
||||
|
||||
TEST(testPluginMap, ThetaStarAboveCostmapResolutionN)
|
||||
{
|
||||
testSmallPathValidityAndNoOrientation("nav2_theta_star_planner/ThetaStarPlanner", 1.5);
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
||||
// initialize ROS
|
||||
rclcpp::init(argc, argv);
|
||||
|
||||
bool all_successful = RUN_ALL_TESTS();
|
||||
|
||||
// shutdown ROS
|
||||
rclcpp::shutdown();
|
||||
|
||||
return all_successful;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2018 Intel Corporation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
from launch.actions import ExecuteProcess
|
||||
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
testExecutable = os.getenv('TEST_EXECUTABLE')
|
||||
|
||||
ld = LaunchDescription([])
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[testExecutable, '--ros-args -p use_sim_time:=True'],
|
||||
name='test_planner_random_node',
|
||||
output='screen'
|
||||
)
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License. Reserved.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "planner_tester.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using nav2_system_tests::PlannerTester;
|
||||
using nav2_util::TestCostmap;
|
||||
|
||||
using ComputePathToPoseCommand = geometry_msgs::msg::PoseStamped;
|
||||
using ComputePathToPoseResult = nav_msgs::msg::Path;
|
||||
|
||||
TEST(testWithHundredRandomEndPoints, testWithHundredRandomEndPoints)
|
||||
{
|
||||
auto obj = std::make_shared<PlannerTester>();
|
||||
obj->activate();
|
||||
obj->loadDefaultMap();
|
||||
|
||||
bool success = false;
|
||||
int num_tries = 3;
|
||||
for (int i = 0; i != num_tries; i++) {
|
||||
success = success || obj->defaultPlannerRandomTests(100, 0.1);
|
||||
if (success) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_EQ(true, success);
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
||||
// initialize ROS
|
||||
rclcpp::init(argc, argv);
|
||||
|
||||
bool all_successful = RUN_ALL_TESTS();
|
||||
|
||||
// shutdown ROS
|
||||
rclcpp::shutdown();
|
||||
|
||||
return all_successful;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
ament_add_test(test_bt_navigator
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_system_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
TESTER=nav_to_pose_tester_node.py
|
||||
ASTAR=True
|
||||
CONTROLLER=nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController
|
||||
PLANNER=nav2_navfn_planner/NavfnPlanner
|
||||
)
|
||||
|
||||
ament_add_test(test_bt_navigator_with_wrong_init_pose
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_wrong_init_pose_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
TESTER=nav_to_pose_tester_node.py
|
||||
ASTAR=True
|
||||
CONTROLLER=nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController
|
||||
PLANNER=nav2_navfn_planner/NavfnPlanner
|
||||
)
|
||||
|
||||
ament_add_test(test_bt_navigator_with_dijkstra
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_system_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
TESTER=nav_to_pose_tester_node.py
|
||||
ASTAR=False
|
||||
CONTROLLER=dwb_core::DWBLocalPlanner
|
||||
PLANNER=nav2_navfn_planner/NavfnPlanner
|
||||
)
|
||||
|
||||
ament_add_test(test_bt_navigator_2
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_system_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
TESTER=nav_to_pose_tester_node.py
|
||||
ASTAR=False
|
||||
CONTROLLER=dwb_core::DWBLocalPlanner
|
||||
PLANNER=nav2_navfn_planner/NavfnPlanner
|
||||
)
|
||||
|
||||
ament_add_test(test_dynamic_obstacle
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_system_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo_obstacle.world
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
TESTER=nav_to_pose_tester_node.py
|
||||
ASTAR=False
|
||||
CONTROLLER=dwb_core::DWBLocalPlanner
|
||||
PLANNER=nav2_navfn_planner/NavfnPlanner
|
||||
)
|
||||
|
||||
ament_add_test(test_nav_through_poses
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_system_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo_obstacle.world
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_through_poses_w_replanning_and_recovery.xml
|
||||
TESTER=nav_through_poses_tester_node.py
|
||||
ASTAR=False
|
||||
CONTROLLER=dwb_core::DWBLocalPlanner
|
||||
PLANNER=nav2_navfn_planner/NavfnPlanner
|
||||
)
|
||||
|
||||
# ament_add_test(test_multi_robot
|
||||
# GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
# COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_multi_robot_launch.py"
|
||||
# WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
# TIMEOUT 180
|
||||
# ENV
|
||||
# TEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
# TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
# TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/world_only.model
|
||||
# TEST_URDF=${PROJECT_SOURCE_DIR}/urdf/turtlebot3_waffle.urdf
|
||||
# TEST_SDF=${PROJECT_SOURCE_DIR}/models/turtlebot3_waffle/model.sdf
|
||||
# BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
# CONTROLLER=dwb_core::DWBLocalPlanner
|
||||
# PLANNER=nav2_navfn_planner/NavfnPlanner
|
||||
# TESTER=nav_to_pose_tester_node.py
|
||||
# )
|
||||
@@ -0,0 +1,38 @@
|
||||
# Nav2 System Tests
|
||||
|
||||
This is a 'top level' system test which will use Gazebo to simulate a Robot moving from an known initial starting position to a goal pose.
|
||||
|
||||
## To run the test
|
||||
First, you must build Nav2 including this package:
|
||||
```
|
||||
colcon build --symlink-install
|
||||
```
|
||||
Then you can run all the system tests:
|
||||
```
|
||||
colcon test --packages-select nav2_system_tests
|
||||
```
|
||||
Output results will go to the screen, and will be logged to the "log/latest_test/nav2_system_tests/" path relative to where colcon test was run.
|
||||
|
||||
To run just the bt_navigator test:
|
||||
```
|
||||
cd build/nav2_system_tests
|
||||
ctest -V bt_navigator$
|
||||
```
|
||||
|
||||
To loop over the bt_navigator test, a script has been provided:
|
||||
```
|
||||
nav2_system_tests/scripts/ctest_loop.bash -c <# loops> -o <path/to/summary/filename.txt> -l <path/to/store/failing/logfiles.log> -d <dds to use>
|
||||
```
|
||||
|
||||
Example (loop 100 times using fastrtps):
|
||||
```
|
||||
nav2_system_tests/scripts/getopt_ctest_loop.bash -c 100 -o /home/robot/data/results.txt -l /home/robot/data/results.log -d rmw_fastrtps_cpp
|
||||
```
|
||||
|
||||
## Notes: (updated Aug 2019)
|
||||
* This currently uses a turtlebot3 robot model, world and map.
|
||||
* The test normally takes 1-2 minutes to run, with a timeout of 2 minutes
|
||||
|
||||
## Future Work
|
||||
* Add additional goal poses if the first one successfully passes
|
||||
* Remove the dependency on the turtlebot3 model and map by adding a simple / dummy robot and creating a world and map
|
||||
@@ -0,0 +1,344 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright 2018 Intel Corporation.
|
||||
# Copyright 2020 Florian Gramss
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from action_msgs.msg import GoalStatus
|
||||
from geometry_msgs.msg import Pose
|
||||
from geometry_msgs.msg import PoseStamped
|
||||
from geometry_msgs.msg import PoseWithCovarianceStamped
|
||||
from lifecycle_msgs.srv import GetState
|
||||
from nav2_msgs.action import NavigateThroughPoses
|
||||
from nav2_msgs.srv import ManageLifecycleNodes
|
||||
|
||||
import rclpy
|
||||
|
||||
from rclpy.action import ActionClient
|
||||
from rclpy.node import Node
|
||||
from rclpy.qos import QoSDurabilityPolicy, QoSHistoryPolicy, QoSReliabilityPolicy
|
||||
from rclpy.qos import QoSProfile
|
||||
|
||||
|
||||
class NavTester(Node):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
initial_pose: Pose,
|
||||
goal_pose: Pose,
|
||||
namespace: str = ''
|
||||
):
|
||||
super().__init__(node_name='nav2_tester', namespace=namespace)
|
||||
self.initial_pose_pub = self.create_publisher(PoseWithCovarianceStamped,
|
||||
'initialpose', 10)
|
||||
|
||||
pose_qos = QoSProfile(
|
||||
durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
|
||||
reliability=QoSReliabilityPolicy.RELIABLE,
|
||||
history=QoSHistoryPolicy.KEEP_LAST,
|
||||
depth=1)
|
||||
|
||||
self.model_pose_sub = self.create_subscription(PoseWithCovarianceStamped,
|
||||
'amcl_pose', self.poseCallback, pose_qos)
|
||||
self.initial_pose_received = False
|
||||
self.initial_pose = initial_pose
|
||||
self.goal_pose = goal_pose
|
||||
self.action_client = ActionClient(self, NavigateThroughPoses, 'navigate_through_poses')
|
||||
|
||||
def info_msg(self, msg: str):
|
||||
self.get_logger().info('\033[1;37;44m' + msg + '\033[0m')
|
||||
|
||||
def warn_msg(self, msg: str):
|
||||
self.get_logger().warn('\033[1;37;43m' + msg + '\033[0m')
|
||||
|
||||
def error_msg(self, msg: str):
|
||||
self.get_logger().error('\033[1;37;41m' + msg + '\033[0m')
|
||||
|
||||
def setInitialPose(self):
|
||||
msg = PoseWithCovarianceStamped()
|
||||
msg.pose.pose = self.initial_pose
|
||||
msg.header.frame_id = 'map'
|
||||
self.info_msg('Publishing Initial Pose')
|
||||
self.initial_pose_pub.publish(msg)
|
||||
self.currentPose = self.initial_pose
|
||||
|
||||
def getStampedPoseMsg(self, pose: Pose):
|
||||
msg = PoseStamped()
|
||||
msg.header.frame_id = 'map'
|
||||
msg.pose = pose
|
||||
return msg
|
||||
|
||||
def runNavigateAction(self, goal_pose: Optional[Pose] = None):
|
||||
# Sends a `NavToPose` action request and waits for completion
|
||||
self.info_msg("Waiting for 'NavigateThroughPoses' action server")
|
||||
while not self.action_client.wait_for_server(timeout_sec=1.0):
|
||||
self.info_msg("'NavigateThroughPoses' action server not available, waiting...")
|
||||
|
||||
self.goal_pose = goal_pose if goal_pose is not None else self.goal_pose
|
||||
goal_msg = NavigateThroughPoses.Goal()
|
||||
goal_msg.poses = [self.getStampedPoseMsg(self.goal_pose),
|
||||
self.getStampedPoseMsg(self.goal_pose)]
|
||||
|
||||
self.info_msg('Sending goal request...')
|
||||
send_goal_future = self.action_client.send_goal_async(goal_msg)
|
||||
|
||||
rclpy.spin_until_future_complete(self, send_goal_future)
|
||||
goal_handle = send_goal_future.result()
|
||||
|
||||
if not goal_handle.accepted:
|
||||
self.error_msg('Goal rejected')
|
||||
return False
|
||||
|
||||
self.info_msg('Goal accepted')
|
||||
get_result_future = goal_handle.get_result_async()
|
||||
|
||||
self.info_msg("Waiting for 'NavigateToPose' action to complete")
|
||||
rclpy.spin_until_future_complete(self, get_result_future)
|
||||
status = get_result_future.result().status
|
||||
if status != GoalStatus.STATUS_SUCCEEDED:
|
||||
self.info_msg(f'Goal failed with status code: {status}')
|
||||
return False
|
||||
|
||||
self.info_msg('Goal succeeded!')
|
||||
return True
|
||||
|
||||
def runFakeNavigateAction(self):
|
||||
# Sends a `NavToPose` action request and waits for completion
|
||||
self.info_msg("Waiting for 'NavigateThroughPoses' action server")
|
||||
while not self.action_client.wait_for_server(timeout_sec=1.0):
|
||||
self.info_msg("'NavigateThroughPoses' action server not available, waiting...")
|
||||
|
||||
goal_msg = NavigateThroughPoses.Goal()
|
||||
|
||||
self.info_msg('Sending goal request...')
|
||||
send_goal_future = self.action_client.send_goal_async(goal_msg)
|
||||
|
||||
rclpy.spin_until_future_complete(self, send_goal_future)
|
||||
goal_handle = send_goal_future.result()
|
||||
|
||||
if not goal_handle.accepted:
|
||||
self.error_msg('Goal rejected')
|
||||
return False
|
||||
|
||||
self.info_msg('Goal accepted')
|
||||
get_result_future = goal_handle.get_result_async()
|
||||
|
||||
self.info_msg("Waiting for 'NavigateToPose' action to complete")
|
||||
rclpy.spin_until_future_complete(self, get_result_future)
|
||||
status = get_result_future.result().status
|
||||
if status != GoalStatus.STATUS_SUCCEEDED:
|
||||
self.info_msg(f'Goal failed with status code: {status}')
|
||||
return False
|
||||
|
||||
self.info_msg('Goal succeeded!')
|
||||
return True
|
||||
|
||||
def runNavigatePreemptionAction(self, block):
|
||||
# Sends a `NavToPose` action request and waits for completion
|
||||
self.info_msg("Waiting for 'NavigateThroughPoses' action server")
|
||||
while not self.action_client.wait_for_server(timeout_sec=1.0):
|
||||
self.info_msg("'NavigateThroughPoses' action server not available, waiting...")
|
||||
|
||||
goal_msg = NavigateThroughPoses.Goal()
|
||||
goal_msg.poses = [self.getStampedPoseMsg(self.initial_pose)]
|
||||
|
||||
self.info_msg('Sending goal request...')
|
||||
send_goal_future = self.action_client.send_goal_async(goal_msg)
|
||||
|
||||
rclpy.spin_until_future_complete(self, send_goal_future)
|
||||
goal_handle = send_goal_future.result()
|
||||
|
||||
if not goal_handle.accepted:
|
||||
self.error_msg('Goal rejected')
|
||||
return False
|
||||
|
||||
if not block:
|
||||
return True
|
||||
|
||||
self.info_msg('Goal accepted')
|
||||
get_result_future = goal_handle.get_result_async()
|
||||
|
||||
self.info_msg("Waiting for 'NavigateToPose' action to complete")
|
||||
rclpy.spin_until_future_complete(self, get_result_future)
|
||||
status = get_result_future.result().status
|
||||
if status != GoalStatus.STATUS_SUCCEEDED:
|
||||
self.info_msg(f'Goal failed with status code: {status}')
|
||||
return False
|
||||
|
||||
self.info_msg('Goal succeeded!')
|
||||
return True
|
||||
|
||||
def poseCallback(self, msg):
|
||||
self.info_msg('Received amcl_pose')
|
||||
self.current_pose = msg.pose.pose
|
||||
self.initial_pose_received = True
|
||||
|
||||
def wait_for_node_active(self, node_name: str):
|
||||
# Waits for the node within the tester namespace to become active
|
||||
self.info_msg(f'Waiting for {node_name} to become active')
|
||||
node_service = f'{node_name}/get_state'
|
||||
state_client = self.create_client(GetState, node_service)
|
||||
while not state_client.wait_for_service(timeout_sec=1.0):
|
||||
self.info_msg(f'{node_service} service not available, waiting...')
|
||||
req = GetState.Request() # empty request
|
||||
state = 'UNKNOWN'
|
||||
while (state != 'active'):
|
||||
self.info_msg(f'Getting {node_name} state...')
|
||||
future = state_client.call_async(req)
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
if future.result() is not None:
|
||||
state = future.result().current_state.label
|
||||
self.info_msg(f'Result of get_state: {state}')
|
||||
else:
|
||||
self.error_msg(f'Exception while calling service: {future.exception()!r}')
|
||||
time.sleep(5)
|
||||
|
||||
def shutdown(self):
|
||||
self.info_msg('Shutting down')
|
||||
self.action_client.destroy()
|
||||
|
||||
transition_service = 'lifecycle_manager_navigation/manage_nodes'
|
||||
mgr_client = self.create_client(ManageLifecycleNodes, transition_service)
|
||||
while not mgr_client.wait_for_service(timeout_sec=1.0):
|
||||
self.info_msg(f'{transition_service} service not available, waiting...')
|
||||
|
||||
req = ManageLifecycleNodes.Request()
|
||||
req.command = ManageLifecycleNodes.Request().SHUTDOWN
|
||||
future = mgr_client.call_async(req)
|
||||
try:
|
||||
self.info_msg('Shutting down navigation lifecycle manager...')
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
future.result()
|
||||
self.info_msg('Shutting down navigation lifecycle manager complete.')
|
||||
except Exception as e: # noqa: B902
|
||||
self.error_msg(f'Service call failed {e!r}')
|
||||
transition_service = 'lifecycle_manager_localization/manage_nodes'
|
||||
mgr_client = self.create_client(ManageLifecycleNodes, transition_service)
|
||||
while not mgr_client.wait_for_service(timeout_sec=1.0):
|
||||
self.info_msg(f'{transition_service} service not available, waiting...')
|
||||
|
||||
req = ManageLifecycleNodes.Request()
|
||||
req.command = ManageLifecycleNodes.Request().SHUTDOWN
|
||||
future = mgr_client.call_async(req)
|
||||
try:
|
||||
self.info_msg('Shutting down localization lifecycle manager...')
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
future.result()
|
||||
self.info_msg('Shutting down localization lifecycle manager complete')
|
||||
except Exception as e: # noqa: B902
|
||||
self.error_msg(f'Service call failed {e!r}')
|
||||
|
||||
def wait_for_initial_pose(self):
|
||||
self.initial_pose_received = False
|
||||
while not self.initial_pose_received:
|
||||
self.info_msg('Setting initial pose')
|
||||
self.setInitialPose()
|
||||
self.info_msg('Waiting for amcl_pose to be received')
|
||||
rclpy.spin_once(self, timeout_sec=1)
|
||||
|
||||
|
||||
def run_all_tests(robot_tester):
|
||||
# set transforms to use_sim_time
|
||||
result = True
|
||||
if (result):
|
||||
robot_tester.wait_for_node_active('amcl')
|
||||
robot_tester.wait_for_initial_pose()
|
||||
robot_tester.wait_for_node_active('bt_navigator')
|
||||
result = robot_tester.runNavigateAction()
|
||||
# Test empty navigation request
|
||||
result = result and not robot_tester.runFakeNavigateAction()
|
||||
# Test preempting NHP with the same goal
|
||||
result = result and robot_tester.runNavigatePreemptionAction(False)
|
||||
result = result and robot_tester.runNavigatePreemptionAction(True)
|
||||
|
||||
# Add more tests here if desired
|
||||
|
||||
if (result):
|
||||
robot_tester.info_msg('Test PASSED')
|
||||
else:
|
||||
robot_tester.error_msg('Test FAILED')
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def fwd_pose(x=0.0, y=0.0, z=0.01):
|
||||
initial_pose = Pose()
|
||||
initial_pose.position.x = x
|
||||
initial_pose.position.y = y
|
||||
initial_pose.position.z = z
|
||||
initial_pose.orientation.x = 0.0
|
||||
initial_pose.orientation.y = 0.0
|
||||
initial_pose.orientation.z = 0.0
|
||||
initial_pose.orientation.w = 1.0
|
||||
return initial_pose
|
||||
|
||||
|
||||
def get_testers(args):
|
||||
testers = []
|
||||
|
||||
init_x, init_y, final_x, final_y = args.robot[0]
|
||||
tester = NavTester(
|
||||
initial_pose=fwd_pose(float(init_x), float(init_y)),
|
||||
goal_pose=fwd_pose(float(final_x), float(final_y)))
|
||||
tester.info_msg(
|
||||
'Starting tester, robot going from ' + init_x + ', ' + init_y +
|
||||
' to ' + final_x + ', ' + final_y + '.')
|
||||
testers.append(tester)
|
||||
return testers
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
# The robot(s) positions from the input arguments
|
||||
parser = argparse.ArgumentParser(description='System-level navigation tester node')
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-r', '--robot', action='append', nargs=4,
|
||||
metavar=('init_x', 'init_y', 'final_x', 'final_y'),
|
||||
help='The robot starting and final positions.')
|
||||
|
||||
args, unknown = parser.parse_known_args()
|
||||
|
||||
rclpy.init()
|
||||
|
||||
# Create testers for each robot
|
||||
testers = get_testers(args)
|
||||
|
||||
# wait a few seconds to make sure entire stacks are up
|
||||
time.sleep(10)
|
||||
|
||||
for tester in testers:
|
||||
passed = run_all_tests(tester)
|
||||
if not passed:
|
||||
break
|
||||
|
||||
for tester in testers:
|
||||
# stop and shutdown the nav stack to exit cleanly
|
||||
tester.shutdown()
|
||||
|
||||
testers[0].info_msg('Done Shutting Down.')
|
||||
|
||||
if not passed:
|
||||
testers[0].info_msg('Exiting failed')
|
||||
exit(1)
|
||||
else:
|
||||
testers[0].info_msg('Exiting passed')
|
||||
exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,377 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright 2018 Intel Corporation.
|
||||
# Copyright 2020 Florian Gramss
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import sys
|
||||
import time
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from action_msgs.msg import GoalStatus
|
||||
from geometry_msgs.msg import Pose
|
||||
from geometry_msgs.msg import PoseStamped
|
||||
from geometry_msgs.msg import PoseWithCovarianceStamped
|
||||
from lifecycle_msgs.srv import GetState
|
||||
from nav2_msgs.action import NavigateToPose
|
||||
from nav2_msgs.srv import ManageLifecycleNodes
|
||||
|
||||
import rclpy
|
||||
|
||||
from rclpy.action import ActionClient
|
||||
from rclpy.node import Node
|
||||
from rclpy.qos import QoSDurabilityPolicy, QoSHistoryPolicy, QoSReliabilityPolicy
|
||||
from rclpy.qos import QoSProfile
|
||||
|
||||
|
||||
class NavTester(Node):
|
||||
|
||||
def __init__(self, initial_pose: Pose, goal_pose: Pose, namespace: str = ''):
|
||||
super().__init__(node_name='nav2_tester', namespace=namespace)
|
||||
self.initial_pose_pub = self.create_publisher(
|
||||
PoseWithCovarianceStamped, 'initialpose', 10
|
||||
)
|
||||
self.goal_pub = self.create_publisher(PoseStamped, 'goal_pose', 10)
|
||||
|
||||
pose_qos = QoSProfile(
|
||||
durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
|
||||
reliability=QoSReliabilityPolicy.RELIABLE,
|
||||
history=QoSHistoryPolicy.KEEP_LAST,
|
||||
depth=1,
|
||||
)
|
||||
|
||||
self.model_pose_sub = self.create_subscription(
|
||||
PoseWithCovarianceStamped, 'amcl_pose', self.poseCallback, pose_qos
|
||||
)
|
||||
self.initial_pose_received = False
|
||||
self.initial_pose = initial_pose
|
||||
self.goal_pose = goal_pose
|
||||
self.action_client = ActionClient(self, NavigateToPose, 'navigate_to_pose')
|
||||
|
||||
def info_msg(self, msg: str):
|
||||
self.get_logger().info('\033[1;37;44m' + msg + '\033[0m')
|
||||
|
||||
def warn_msg(self, msg: str):
|
||||
self.get_logger().warn('\033[1;37;43m' + msg + '\033[0m')
|
||||
|
||||
def error_msg(self, msg: str):
|
||||
self.get_logger().error('\033[1;37;41m' + msg + '\033[0m')
|
||||
|
||||
def setInitialPose(self):
|
||||
msg = PoseWithCovarianceStamped()
|
||||
msg.pose.pose = self.initial_pose
|
||||
msg.header.frame_id = 'map'
|
||||
self.info_msg('Publishing Initial Pose')
|
||||
self.initial_pose_pub.publish(msg)
|
||||
self.currentPose = self.initial_pose
|
||||
|
||||
def getStampedPoseMsg(self, pose: Pose):
|
||||
msg = PoseStamped()
|
||||
msg.header.frame_id = 'map'
|
||||
msg.pose = pose
|
||||
return msg
|
||||
|
||||
def publishGoalPose(self, goal_pose: Optional[Pose] = None):
|
||||
self.goal_pose = goal_pose if goal_pose is not None else self.goal_pose
|
||||
self.goal_pub.publish(self.getStampedPoseMsg(self.goal_pose))
|
||||
|
||||
def runNavigateAction(self, goal_pose: Optional[Pose] = None):
|
||||
# Sends a `NavToPose` action request and waits for completion
|
||||
self.info_msg("Waiting for 'NavigateToPose' action server")
|
||||
while not self.action_client.wait_for_server(timeout_sec=1.0):
|
||||
self.info_msg("'NavigateToPose' action server not available, waiting...")
|
||||
|
||||
self.goal_pose = goal_pose if goal_pose is not None else self.goal_pose
|
||||
goal_msg = NavigateToPose.Goal()
|
||||
goal_msg.pose = self.getStampedPoseMsg(self.goal_pose)
|
||||
|
||||
self.info_msg('Sending goal request...')
|
||||
send_goal_future = self.action_client.send_goal_async(goal_msg)
|
||||
|
||||
rclpy.spin_until_future_complete(self, send_goal_future)
|
||||
goal_handle = send_goal_future.result()
|
||||
|
||||
if not goal_handle.accepted:
|
||||
self.error_msg('Goal rejected')
|
||||
return False
|
||||
|
||||
self.info_msg('Goal accepted')
|
||||
get_result_future = goal_handle.get_result_async()
|
||||
|
||||
future_return = True
|
||||
|
||||
self.info_msg("Waiting for 'NavigateToPose' action to complete")
|
||||
rclpy.spin_until_future_complete(self, get_result_future)
|
||||
status = get_result_future.result().status
|
||||
if status != GoalStatus.STATUS_SUCCEEDED:
|
||||
self.info_msg(f'Goal failed with status code: {status}')
|
||||
return False
|
||||
|
||||
if not future_return:
|
||||
return False
|
||||
|
||||
self.info_msg('Goal succeeded!')
|
||||
return True
|
||||
|
||||
def poseCallback(self, msg):
|
||||
self.info_msg('Received amcl_pose')
|
||||
self.current_pose = msg.pose.pose
|
||||
self.initial_pose_received = True
|
||||
|
||||
def reachesGoal(self, timeout, distance):
|
||||
goalReached = False
|
||||
start_time = time.time()
|
||||
|
||||
while not goalReached:
|
||||
rclpy.spin_once(self, timeout_sec=1)
|
||||
if self.distanceFromGoal() < distance:
|
||||
goalReached = True
|
||||
self.info_msg('*** GOAL REACHED ***')
|
||||
return True
|
||||
elif timeout is not None:
|
||||
if (time.time() - start_time) > timeout:
|
||||
self.error_msg('Robot timed out reaching its goal!')
|
||||
return False
|
||||
|
||||
def distanceFromGoal(self):
|
||||
d_x = self.current_pose.position.x - self.goal_pose.position.x
|
||||
d_y = self.current_pose.position.y - self.goal_pose.position.y
|
||||
distance = math.sqrt(d_x * d_x + d_y * d_y)
|
||||
self.info_msg(f'Distance from goal is: {distance}')
|
||||
return distance
|
||||
|
||||
def wait_for_node_active(self, node_name: str):
|
||||
# Waits for the node within the tester namespace to become active
|
||||
self.info_msg(f'Waiting for {node_name} to become active')
|
||||
node_service = f'{node_name}/get_state'
|
||||
state_client = self.create_client(GetState, node_service)
|
||||
while not state_client.wait_for_service(timeout_sec=1.0):
|
||||
self.info_msg(f'{node_service} service not available, waiting...')
|
||||
req = GetState.Request() # empty request
|
||||
state = 'UNKNOWN'
|
||||
while state != 'active':
|
||||
self.info_msg(f'Getting {node_name} state...')
|
||||
future = state_client.call_async(req)
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
if future.result() is not None:
|
||||
state = future.result().current_state.label
|
||||
self.info_msg(f'Result of get_state: {state}')
|
||||
else:
|
||||
self.error_msg(
|
||||
f'Exception while calling service: {future.exception()!r}'
|
||||
)
|
||||
time.sleep(5)
|
||||
|
||||
def shutdown(self):
|
||||
self.info_msg('Shutting down')
|
||||
self.action_client.destroy()
|
||||
|
||||
transition_service = 'lifecycle_manager_navigation/manage_nodes'
|
||||
mgr_client = self.create_client(ManageLifecycleNodes, transition_service)
|
||||
while not mgr_client.wait_for_service(timeout_sec=1.0):
|
||||
self.info_msg(f'{transition_service} service not available, waiting...')
|
||||
|
||||
req = ManageLifecycleNodes.Request()
|
||||
req.command = ManageLifecycleNodes.Request().SHUTDOWN
|
||||
future = mgr_client.call_async(req)
|
||||
try:
|
||||
self.info_msg('Shutting down navigation lifecycle manager...')
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
future.result()
|
||||
self.info_msg('Shutting down navigation lifecycle manager complete.')
|
||||
except Exception as e: # noqa: B902
|
||||
self.error_msg(f'Service call failed {e!r}')
|
||||
transition_service = 'lifecycle_manager_localization/manage_nodes'
|
||||
mgr_client = self.create_client(ManageLifecycleNodes, transition_service)
|
||||
while not mgr_client.wait_for_service(timeout_sec=1.0):
|
||||
self.info_msg(f'{transition_service} service not available, waiting...')
|
||||
|
||||
req = ManageLifecycleNodes.Request()
|
||||
req.command = ManageLifecycleNodes.Request().SHUTDOWN
|
||||
future = mgr_client.call_async(req)
|
||||
try:
|
||||
self.info_msg('Shutting down localization lifecycle manager...')
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
future.result()
|
||||
self.info_msg('Shutting down localization lifecycle manager complete')
|
||||
except Exception as e: # noqa: B902
|
||||
self.error_msg(f'Service call failed {e!r}')
|
||||
|
||||
def wait_for_initial_pose(self):
|
||||
self.initial_pose_received = False
|
||||
while not self.initial_pose_received:
|
||||
self.info_msg('Setting initial pose')
|
||||
self.setInitialPose()
|
||||
self.info_msg('Waiting for amcl_pose to be received')
|
||||
rclpy.spin_once(self, timeout_sec=1)
|
||||
|
||||
|
||||
def test_RobotMovesToGoal(robot_tester):
|
||||
robot_tester.info_msg('Setting goal pose')
|
||||
robot_tester.publishGoalPose()
|
||||
robot_tester.info_msg('Waiting 60 seconds for robot to reach goal')
|
||||
return robot_tester.reachesGoal(timeout=60, distance=0.5)
|
||||
|
||||
|
||||
def run_all_tests(robot_tester):
|
||||
# set transforms to use_sim_time
|
||||
result = True
|
||||
if result:
|
||||
robot_tester.wait_for_node_active('amcl')
|
||||
robot_tester.wait_for_initial_pose()
|
||||
robot_tester.wait_for_node_active('bt_navigator')
|
||||
result = robot_tester.runNavigateAction()
|
||||
|
||||
if result:
|
||||
result = test_RobotMovesToGoal(robot_tester)
|
||||
|
||||
# Add more tests here if desired
|
||||
|
||||
if result:
|
||||
robot_tester.info_msg('Test PASSED')
|
||||
else:
|
||||
robot_tester.error_msg('Test FAILED')
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def fwd_pose(x=0.0, y=0.0, z=0.01):
|
||||
initial_pose = Pose()
|
||||
initial_pose.position.x = x
|
||||
initial_pose.position.y = y
|
||||
initial_pose.position.z = z
|
||||
initial_pose.orientation.x = 0.0
|
||||
initial_pose.orientation.y = 0.0
|
||||
initial_pose.orientation.z = 0.0
|
||||
initial_pose.orientation.w = 1.0
|
||||
return initial_pose
|
||||
|
||||
|
||||
def get_testers(args):
|
||||
testers = []
|
||||
|
||||
if args.robot:
|
||||
# Requested tester for one robot
|
||||
init_x, init_y, final_x, final_y = args.robot[0]
|
||||
tester = NavTester(
|
||||
initial_pose=fwd_pose(float(init_x), float(init_y)),
|
||||
goal_pose=fwd_pose(float(final_x), float(final_y)),
|
||||
)
|
||||
tester.info_msg(
|
||||
'Starting tester, robot going from '
|
||||
+ init_x
|
||||
+ ', '
|
||||
+ init_y
|
||||
+ ' to '
|
||||
+ final_x
|
||||
+ ', '
|
||||
+ final_y
|
||||
+ '.'
|
||||
)
|
||||
testers.append(tester)
|
||||
return testers
|
||||
|
||||
# Requested tester for multiple robots
|
||||
for robot in args.robots:
|
||||
namespace, init_x, init_y, final_x, final_y = robot
|
||||
tester = NavTester(
|
||||
namespace=namespace,
|
||||
initial_pose=fwd_pose(float(init_x), float(init_y)),
|
||||
goal_pose=fwd_pose(float(final_x), float(final_y)),
|
||||
)
|
||||
tester.info_msg(
|
||||
'Starting tester for '
|
||||
+ namespace
|
||||
+ ' going from '
|
||||
+ init_x
|
||||
+ ', '
|
||||
+ init_y
|
||||
+ ' to '
|
||||
+ final_x
|
||||
+ ', '
|
||||
+ final_y
|
||||
)
|
||||
testers.append(tester)
|
||||
return testers
|
||||
|
||||
|
||||
def check_args(expect_failure: str):
|
||||
# Check if --expect_failure is True or False
|
||||
if expect_failure != 'True' and expect_failure != 'False':
|
||||
print(
|
||||
'\033[1;37;41m' + ' -e flag must be set to True or False only. ' + '\033[0m'
|
||||
)
|
||||
exit(1)
|
||||
else:
|
||||
return eval(expect_failure)
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
# The robot(s) positions from the input arguments
|
||||
parser = argparse.ArgumentParser(description='System-level navigation tester node')
|
||||
parser.add_argument('-e', '--expect_failure')
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument(
|
||||
'-r',
|
||||
'--robot',
|
||||
action='append',
|
||||
nargs=4,
|
||||
metavar=('init_x', 'init_y', 'final_x', 'final_y'),
|
||||
help='The robot starting and final positions.',
|
||||
)
|
||||
group.add_argument(
|
||||
'-rs',
|
||||
'--robots',
|
||||
action='append',
|
||||
nargs=5,
|
||||
metavar=('name', 'init_x', 'init_y', 'final_x', 'final_y'),
|
||||
help="The robot's namespace and starting and final positions. "
|
||||
+ 'Repeating the argument for multiple robots is supported.',
|
||||
)
|
||||
|
||||
args, unknown = parser.parse_known_args()
|
||||
|
||||
expect_failure = check_args(args.expect_failure)
|
||||
|
||||
rclpy.init()
|
||||
|
||||
# Create testers for each robot
|
||||
testers = get_testers(args)
|
||||
|
||||
# wait a few seconds to make sure entire stacks are up
|
||||
time.sleep(10)
|
||||
|
||||
for tester in testers:
|
||||
passed = run_all_tests(tester)
|
||||
if passed != expect_failure:
|
||||
break
|
||||
|
||||
for tester in testers:
|
||||
# stop and shutdown the nav stack to exit cleanly
|
||||
tester.shutdown()
|
||||
|
||||
testers[0].info_msg('Done Shutting Down.')
|
||||
|
||||
if passed != expect_failure:
|
||||
testers[0].info_msg('Exiting failed')
|
||||
exit(1)
|
||||
else:
|
||||
testers[0].info_msg('Exiting passed')
|
||||
exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2018 Intel Corporation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
from launch import LaunchDescription, LaunchService
|
||||
from launch.actions import (ExecuteProcess, GroupAction,
|
||||
IncludeLaunchDescription, SetEnvironmentVariable)
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch.substitutions import TextSubstitution
|
||||
from launch_ros.actions import Node, PushRosNamespace
|
||||
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
map_yaml_file = os.getenv('TEST_MAP')
|
||||
world = os.getenv('TEST_WORLD')
|
||||
urdf = os.getenv('TEST_URDF')
|
||||
sdf = os.getenv('TEST_SDF')
|
||||
|
||||
bt_xml_file = os.path.join(get_package_share_directory('nav2_bt_navigator'),
|
||||
'behavior_trees',
|
||||
os.getenv('BT_NAVIGATOR_XML'))
|
||||
|
||||
bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
robot1_params_file = os.path.join(bringup_dir, # noqa: F841
|
||||
'params/nav2_multirobot_params_1.yaml')
|
||||
robot2_params_file = os.path.join(bringup_dir, # noqa: F841
|
||||
'params/nav2_multirobot_params_2.yaml')
|
||||
|
||||
# Names and poses of the robots
|
||||
robots = [
|
||||
{'name': 'robot1', 'x_pose': 0.0, 'y_pose': 0.5, 'z_pose': 0.01},
|
||||
{'name': 'robot2', 'x_pose': 0.0, 'y_pose': -0.5, 'z_pose': 0.01}]
|
||||
|
||||
# Launch Gazebo server for simulation
|
||||
start_gazebo_cmd = ExecuteProcess(
|
||||
cmd=['gzserver', '-s', 'libgazebo_ros_init.so',
|
||||
'-s', 'libgazebo_ros_factory.so', '--minimal_comms', world],
|
||||
output='screen')
|
||||
|
||||
# Define commands for spawing the robots into Gazebo
|
||||
spawn_robots_cmds = []
|
||||
for robot in robots:
|
||||
spawn_robots_cmds.append(
|
||||
Node(
|
||||
package='gazebo_ros',
|
||||
executable='spawn_entity.py',
|
||||
output='screen',
|
||||
arguments=[
|
||||
'-entity', TextSubstitution(text=robot['name']),
|
||||
'-robot_namespace', TextSubstitution(text=robot['name']),
|
||||
'-file', TextSubstitution(text=sdf),
|
||||
'-x', TextSubstitution(text=str(robot['x_pose'])),
|
||||
'-y', TextSubstitution(text=str(robot['y_pose'])),
|
||||
'-z', TextSubstitution(text=str(robot['z_pose']))]
|
||||
))
|
||||
|
||||
with open(urdf, 'r') as infp:
|
||||
robot_description = infp.read()
|
||||
|
||||
# Define commands for launching the robot state publishers
|
||||
robot_state_pubs_cmds = []
|
||||
for robot in robots:
|
||||
robot_state_pubs_cmds.append(
|
||||
Node(
|
||||
package='robot_state_publisher',
|
||||
executable='robot_state_publisher',
|
||||
namespace=TextSubstitution(text=robot['name']),
|
||||
output='screen',
|
||||
parameters=[{'use_sim_time': True, 'robot_description': robot_description}],
|
||||
remappings=[('/tf', 'tf'), ('/tf_static', 'tf_static')]))
|
||||
|
||||
# Define commands for launching the navigation instances
|
||||
nav_instances_cmds = []
|
||||
for robot in robots:
|
||||
params_file = eval(f"{robot['name']}_params_file")
|
||||
|
||||
group = GroupAction([
|
||||
# Instances use the robot's name for namespace
|
||||
PushRosNamespace(robot['name']),
|
||||
IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(bringup_dir, 'launch', 'bringup_launch.py')),
|
||||
launch_arguments={
|
||||
'namespace': robot['name'],
|
||||
'map': map_yaml_file,
|
||||
'use_sim_time': 'True',
|
||||
'params_file': params_file,
|
||||
'bt_xml_file': bt_xml_file,
|
||||
'autostart': 'True',
|
||||
'use_remappings': 'True'}.items())
|
||||
])
|
||||
nav_instances_cmds.append(group)
|
||||
|
||||
ld = LaunchDescription()
|
||||
ld.add_action(SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM', '1'),)
|
||||
ld.add_action(SetEnvironmentVariable('RCUTILS_LOGGING_USE_STDOUT', '1'),)
|
||||
ld.add_action(start_gazebo_cmd)
|
||||
for spawn_robot in spawn_robots_cmds:
|
||||
ld.add_action(spawn_robot)
|
||||
for state_pub in robot_state_pubs_cmds:
|
||||
ld.add_action(state_pub)
|
||||
for nav_instance in nav_instances_cmds:
|
||||
ld.add_action(nav_instance)
|
||||
|
||||
return ld
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
ld = generate_launch_description()
|
||||
|
||||
# TODO(orduno) remove duplicated definition of robots on `generate_launch_description`
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[os.path.join(os.getenv('TEST_DIR'), os.getenv('TESTER')),
|
||||
'-rs', 'robot1', '0.0', '0.5', '1.0', '0.5',
|
||||
'-rs', 'robot2', '0.0', '-0.5', '1.0', '-0.5',
|
||||
'-e', 'True'],
|
||||
name='tester_node',
|
||||
output='screen')
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2018 Intel Corporation
|
||||
# Copyright (c) 2020 Florian Gramss
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
from launch.actions import ExecuteProcess, IncludeLaunchDescription, SetEnvironmentVariable
|
||||
from launch.launch_context import LaunchContext
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch_ros.actions import Node
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
from nav2_common.launch import RewrittenYaml
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
map_yaml_file = os.getenv('TEST_MAP')
|
||||
world = os.getenv('TEST_WORLD')
|
||||
|
||||
bt_navigator_xml = os.path.join(get_package_share_directory('nav2_bt_navigator'),
|
||||
'behavior_trees',
|
||||
os.getenv('BT_NAVIGATOR_XML'))
|
||||
|
||||
bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
params_file = os.path.join(bringup_dir, 'params', 'nav2_params.yaml')
|
||||
|
||||
# Replace the default parameter values for testing special features
|
||||
# without having multiple params_files inside the nav2 stack
|
||||
context = LaunchContext()
|
||||
param_substitutions = {}
|
||||
|
||||
if (os.getenv('ASTAR') == 'True'):
|
||||
param_substitutions.update({'use_astar': 'True'})
|
||||
|
||||
param_substitutions.update(
|
||||
{'planner_server.ros__parameters.GridBased.plugin': os.getenv('PLANNER')})
|
||||
param_substitutions.update(
|
||||
{'controller_server.ros__parameters.FollowPath.plugin': os.getenv('CONTROLLER')})
|
||||
|
||||
configured_params = RewrittenYaml(
|
||||
source_file=params_file,
|
||||
root_key='',
|
||||
param_rewrites=param_substitutions,
|
||||
convert_types=True)
|
||||
|
||||
new_yaml = configured_params.perform(context)
|
||||
|
||||
return LaunchDescription([
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM', '1'),
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_USE_STDOUT', '1'),
|
||||
|
||||
# Launch gazebo server for simulation
|
||||
ExecuteProcess(
|
||||
cmd=['gzserver', '-s', 'libgazebo_ros_init.so',
|
||||
'--minimal_comms', world],
|
||||
output='screen'),
|
||||
|
||||
# TODO(orduno) Launch the robot state publisher instead
|
||||
# using a local copy of TB3 urdf file
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_footprint', 'base_link']),
|
||||
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_scan']),
|
||||
|
||||
IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(bringup_dir, 'launch', 'bringup_launch.py')),
|
||||
launch_arguments={'namespace': '',
|
||||
'use_namespace': 'False',
|
||||
'map': map_yaml_file,
|
||||
'use_sim_time': 'True',
|
||||
'params_file': new_yaml,
|
||||
'bt_xml_file': bt_navigator_xml,
|
||||
'autostart': 'True'}.items()),
|
||||
])
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
ld = generate_launch_description()
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[os.path.join(os.getenv('TEST_DIR'), os.getenv('TESTER')),
|
||||
'-r', '-2.0', '-0.5', '0.0', '2.0',
|
||||
'-e', 'True'],
|
||||
name='tester_node',
|
||||
output='screen')
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2018 Intel Corporation
|
||||
# Copyright (c) 2020 Florian Gramss
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
from launch.actions import ExecuteProcess, IncludeLaunchDescription, SetEnvironmentVariable
|
||||
from launch.launch_context import LaunchContext
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch_ros.actions import Node
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
from nav2_common.launch import RewrittenYaml
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
map_yaml_file = os.getenv('TEST_MAP')
|
||||
world = os.getenv('TEST_WORLD')
|
||||
|
||||
bt_navigator_xml = os.path.join(get_package_share_directory('nav2_bt_navigator'),
|
||||
'behavior_trees',
|
||||
os.getenv('BT_NAVIGATOR_XML'))
|
||||
|
||||
bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
params_file = os.path.join(bringup_dir, 'params', 'nav2_params.yaml')
|
||||
|
||||
# Replace the default parameter values for testing special features
|
||||
# without having multiple params_files inside the nav2 stack
|
||||
context = LaunchContext()
|
||||
param_substitutions = {}
|
||||
|
||||
if (os.getenv('ASTAR') == 'True'):
|
||||
param_substitutions.update({'use_astar': 'True'})
|
||||
|
||||
param_substitutions.update(
|
||||
{'planner_server.ros__parameters.GridBased.plugin': os.getenv('PLANNER')})
|
||||
param_substitutions.update(
|
||||
{'controller_server.ros__parameters.FollowPath.plugin': os.getenv('CONTROLLER')})
|
||||
|
||||
configured_params = RewrittenYaml(
|
||||
source_file=params_file,
|
||||
root_key='',
|
||||
param_rewrites=param_substitutions,
|
||||
convert_types=True)
|
||||
|
||||
new_yaml = configured_params.perform(context)
|
||||
|
||||
return LaunchDescription([
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM', '1'),
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_USE_STDOUT', '1'),
|
||||
|
||||
# Launch gazebo server for simulation
|
||||
ExecuteProcess(
|
||||
cmd=['gzserver', '-s', 'libgazebo_ros_init.so',
|
||||
'--minimal_comms', world],
|
||||
output='screen'),
|
||||
|
||||
# TODO(orduno) Launch the robot state publisher instead
|
||||
# using a local copy of TB3 urdf file
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_footprint', 'base_link']),
|
||||
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_scan']),
|
||||
|
||||
IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(bringup_dir, 'launch', 'bringup_launch.py')),
|
||||
launch_arguments={'namespace': '',
|
||||
'use_namespace': 'False',
|
||||
'map': map_yaml_file,
|
||||
'use_sim_time': 'True',
|
||||
'params_file': new_yaml,
|
||||
'bt_xml_file': bt_navigator_xml,
|
||||
'autostart': 'True',
|
||||
'use_composition': 'False'}.items()),
|
||||
])
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
ld = generate_launch_description()
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[os.path.join(os.getenv('TEST_DIR'), os.getenv('TESTER')),
|
||||
'-r', '-200000.0', '-200000.0', '0.0', '2.0',
|
||||
'-e', 'False'],
|
||||
name='tester_node',
|
||||
output='screen')
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,12 @@
|
||||
ament_add_test(test_failure_navigator
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_system_failure_launch.py"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Nav2 System Tests - Failure
|
||||
|
||||
High level system failures tests
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2018 Intel Corporation
|
||||
# Copyright (c) 2020 Samsung Research America
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
from launch.actions import ExecuteProcess, IncludeLaunchDescription, SetEnvironmentVariable
|
||||
from launch.launch_context import LaunchContext
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch_ros.actions import Node
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
from nav2_common.launch import RewrittenYaml
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
map_yaml_file = os.getenv('TEST_MAP')
|
||||
world = os.getenv('TEST_WORLD')
|
||||
|
||||
bt_navigator_xml = os.path.join(get_package_share_directory('nav2_bt_navigator'),
|
||||
'behavior_trees',
|
||||
os.getenv('BT_NAVIGATOR_XML'))
|
||||
|
||||
bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
params_file = os.path.join(bringup_dir, 'params', 'nav2_params.yaml')
|
||||
|
||||
# Replace the `use_astar` setting on the params file
|
||||
param_substitutions = {
|
||||
'planner_server.ros__parameters.GridBased.use_astar': 'False'}
|
||||
configured_params = RewrittenYaml(
|
||||
source_file=params_file,
|
||||
root_key='',
|
||||
param_rewrites=param_substitutions,
|
||||
convert_types=True)
|
||||
|
||||
context = LaunchContext()
|
||||
new_yaml = configured_params.perform(context)
|
||||
return LaunchDescription([
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM', '1'),
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_USE_STDOUT', '1'),
|
||||
|
||||
# Launch gazebo server for simulation
|
||||
ExecuteProcess(
|
||||
cmd=['gzserver', '-s', 'libgazebo_ros_init.so',
|
||||
'--minimal_comms', world],
|
||||
output='screen'),
|
||||
|
||||
# TODO(orduno) Launch the robot state publisher instead
|
||||
# using a local copy of TB3 urdf file
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_footprint', 'base_link']),
|
||||
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_scan']),
|
||||
|
||||
IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(bringup_dir, 'launch', 'bringup_launch.py')),
|
||||
launch_arguments={'namespace': '',
|
||||
'use_namespace': 'False',
|
||||
'map': map_yaml_file,
|
||||
'use_sim_time': 'True',
|
||||
'params_file': new_yaml,
|
||||
'bt_xml_file': bt_navigator_xml,
|
||||
'autostart': 'True'}.items()),
|
||||
])
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
ld = generate_launch_description()
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[os.path.join(os.getenv('TEST_DIR'), 'tester_node.py'),
|
||||
'-r', '-2.0', '-0.5', '100.0', '100.0'],
|
||||
name='tester_node',
|
||||
output='screen')
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,307 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright 2018 Intel Corporation.
|
||||
# Copyright 2020 Samsung Research America
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from action_msgs.msg import GoalStatus
|
||||
from geometry_msgs.msg import Pose
|
||||
from geometry_msgs.msg import PoseStamped
|
||||
from geometry_msgs.msg import PoseWithCovarianceStamped
|
||||
from lifecycle_msgs.srv import GetState
|
||||
from nav2_msgs.action import NavigateToPose
|
||||
from nav2_msgs.srv import ManageLifecycleNodes
|
||||
|
||||
import rclpy
|
||||
|
||||
from rclpy.action import ActionClient
|
||||
from rclpy.node import Node
|
||||
from rclpy.qos import QoSDurabilityPolicy, QoSHistoryPolicy, QoSReliabilityPolicy
|
||||
from rclpy.qos import QoSProfile
|
||||
|
||||
|
||||
class NavTester(Node):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
initial_pose: Pose,
|
||||
goal_pose: Pose,
|
||||
namespace: str = ''
|
||||
):
|
||||
super().__init__(node_name='nav2_tester', namespace=namespace)
|
||||
self.initial_pose_pub = self.create_publisher(PoseWithCovarianceStamped,
|
||||
'initialpose', 10)
|
||||
self.goal_pub = self.create_publisher(PoseStamped, 'goal_pose', 10)
|
||||
|
||||
pose_qos = QoSProfile(
|
||||
durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
|
||||
reliability=QoSReliabilityPolicy.RELIABLE,
|
||||
history=QoSHistoryPolicy.KEEP_LAST,
|
||||
depth=1)
|
||||
|
||||
self.model_pose_sub = self.create_subscription(PoseWithCovarianceStamped,
|
||||
'amcl_pose', self.poseCallback, pose_qos)
|
||||
self.initial_pose_received = False
|
||||
self.initial_pose = initial_pose
|
||||
self.goal_pose = goal_pose
|
||||
self.action_client = ActionClient(self, NavigateToPose, 'navigate_to_pose')
|
||||
|
||||
def info_msg(self, msg: str):
|
||||
self.get_logger().info('\033[1;37;44m' + msg + '\033[0m')
|
||||
|
||||
def warn_msg(self, msg: str):
|
||||
self.get_logger().warn('\033[1;37;43m' + msg + '\033[0m')
|
||||
|
||||
def error_msg(self, msg: str):
|
||||
self.get_logger().error('\033[1;37;41m' + msg + '\033[0m')
|
||||
|
||||
def setInitialPose(self):
|
||||
msg = PoseWithCovarianceStamped()
|
||||
msg.pose.pose = self.initial_pose
|
||||
msg.header.frame_id = 'map'
|
||||
self.info_msg('Publishing Initial Pose')
|
||||
self.initial_pose_pub.publish(msg)
|
||||
self.currentPose = self.initial_pose
|
||||
|
||||
def getStampedPoseMsg(self, pose: Pose):
|
||||
msg = PoseStamped()
|
||||
msg.header.frame_id = 'map'
|
||||
msg.pose = pose
|
||||
return msg
|
||||
|
||||
def publishGoalPose(self, goal_pose: Optional[Pose] = None):
|
||||
self.goal_pose = goal_pose if goal_pose is not None else self.goal_pose
|
||||
self.goal_pub.publish(self.getStampedPoseMsg(self.goal_pose))
|
||||
|
||||
def runNavigateAction(self, goal_pose: Optional[Pose] = None):
|
||||
# Sends a `NavToPose` action request and waits for completion
|
||||
self.info_msg("Waiting for 'NavigateToPose' action server")
|
||||
while not self.action_client.wait_for_server(timeout_sec=1.0):
|
||||
self.info_msg("'NavigateToPose' action server not available, waiting...")
|
||||
|
||||
self.goal_pose = goal_pose if goal_pose is not None else self.goal_pose
|
||||
goal_msg = NavigateToPose.Goal()
|
||||
goal_msg.pose = self.getStampedPoseMsg(self.goal_pose)
|
||||
|
||||
self.info_msg('Sending goal request...')
|
||||
send_goal_future = self.action_client.send_goal_async(goal_msg)
|
||||
|
||||
rclpy.spin_until_future_complete(self, send_goal_future)
|
||||
goal_handle = send_goal_future.result()
|
||||
|
||||
if not goal_handle.accepted:
|
||||
self.error_msg('Goal rejected')
|
||||
return False
|
||||
|
||||
self.info_msg('Goal accepted')
|
||||
get_result_future = goal_handle.get_result_async()
|
||||
|
||||
self.info_msg("Waiting for 'NavigateToPose' action to complete")
|
||||
rclpy.spin_until_future_complete(self, get_result_future)
|
||||
status = get_result_future.result().status
|
||||
if status != GoalStatus.STATUS_ABORTED:
|
||||
self.info_msg(f'Goal failed with status code: {status}')
|
||||
return False
|
||||
|
||||
self.info_msg('Goal failed, as expected!')
|
||||
return True
|
||||
|
||||
def poseCallback(self, msg):
|
||||
self.info_msg('Received amcl_pose')
|
||||
self.current_pose = msg.pose.pose
|
||||
self.initial_pose_received = True
|
||||
|
||||
def reachesGoal(self, timeout, distance):
|
||||
goalReached = False
|
||||
start_time = time.time()
|
||||
|
||||
while not goalReached:
|
||||
rclpy.spin_once(self, timeout_sec=1)
|
||||
if self.distanceFromGoal() < distance:
|
||||
goalReached = True
|
||||
self.info_msg('*** GOAL REACHED ***')
|
||||
return True
|
||||
elif timeout is not None:
|
||||
if (time.time() - start_time) > timeout:
|
||||
self.error_msg('Robot timed out reaching its goal!')
|
||||
return False
|
||||
|
||||
def distanceFromGoal(self):
|
||||
d_x = self.current_pose.position.x - self.goal_pose.position.x
|
||||
d_y = self.current_pose.position.y - self.goal_pose.position.y
|
||||
distance = math.sqrt(d_x * d_x + d_y * d_y)
|
||||
self.info_msg(f'Distance from goal is: {distance}')
|
||||
return distance
|
||||
|
||||
def wait_for_node_active(self, node_name: str):
|
||||
# Waits for the node within the tester namespace to become active
|
||||
self.info_msg(f'Waiting for {node_name} to become active')
|
||||
node_service = f'{node_name}/get_state'
|
||||
state_client = self.create_client(GetState, node_service)
|
||||
while not state_client.wait_for_service(timeout_sec=1.0):
|
||||
self.info_msg(f'{node_service} service not available, waiting...')
|
||||
req = GetState.Request() # empty request
|
||||
state = 'UNKNOWN'
|
||||
while (state != 'active'):
|
||||
self.info_msg(f'Getting {node_name} state...')
|
||||
future = state_client.call_async(req)
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
if future.result() is not None:
|
||||
state = future.result().current_state.label
|
||||
self.info_msg(f'Result of get_state: {state}')
|
||||
else:
|
||||
self.error_msg(f'Exception while calling service: {future.exception()!r}')
|
||||
time.sleep(5)
|
||||
|
||||
def shutdown(self):
|
||||
self.info_msg('Shutting down')
|
||||
self.action_client.destroy()
|
||||
|
||||
transition_service = 'lifecycle_manager_navigation/manage_nodes'
|
||||
mgr_client = self.create_client(ManageLifecycleNodes, transition_service)
|
||||
while not mgr_client.wait_for_service(timeout_sec=1.0):
|
||||
self.info_msg(f'{transition_service} service not available, waiting...')
|
||||
|
||||
req = ManageLifecycleNodes.Request()
|
||||
req.command = ManageLifecycleNodes.Request().SHUTDOWN
|
||||
future = mgr_client.call_async(req)
|
||||
try:
|
||||
self.info_msg('Shutting down navigation lifecycle manager...')
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
future.result()
|
||||
self.info_msg('Shutting down navigation lifecycle manager complete.')
|
||||
except Exception as e: # noqa: B902
|
||||
self.error_msg(f'Service call failed {e!r}')
|
||||
transition_service = 'lifecycle_manager_localization/manage_nodes'
|
||||
mgr_client = self.create_client(ManageLifecycleNodes, transition_service)
|
||||
while not mgr_client.wait_for_service(timeout_sec=1.0):
|
||||
self.info_msg(f'{transition_service} service not available, waiting...')
|
||||
|
||||
req = ManageLifecycleNodes.Request()
|
||||
req.command = ManageLifecycleNodes.Request().SHUTDOWN
|
||||
future = mgr_client.call_async(req)
|
||||
try:
|
||||
self.info_msg('Shutting down localization lifecycle manager...')
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
future.result()
|
||||
self.info_msg('Shutting down localization lifecycle manager complete')
|
||||
except Exception as e: # noqa: B902
|
||||
self.error_msg(f'Service call failed {e!r}')
|
||||
|
||||
def wait_for_initial_pose(self):
|
||||
self.initial_pose_received = False
|
||||
while not self.initial_pose_received:
|
||||
self.info_msg('Setting initial pose')
|
||||
self.setInitialPose()
|
||||
self.info_msg('Waiting for amcl_pose to be received')
|
||||
rclpy.spin_once(self, timeout_sec=1)
|
||||
|
||||
|
||||
def run_all_tests(robot_tester):
|
||||
# set transforms to use_sim_time
|
||||
result = True
|
||||
if (result):
|
||||
robot_tester.wait_for_node_active('amcl')
|
||||
robot_tester.wait_for_initial_pose()
|
||||
robot_tester.wait_for_node_active('bt_navigator')
|
||||
result = robot_tester.runNavigateAction()
|
||||
|
||||
# Add more tests here if desired
|
||||
|
||||
if (result):
|
||||
robot_tester.info_msg('Test PASSED')
|
||||
else:
|
||||
robot_tester.error_msg('Test FAILED')
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def fwd_pose(x=0.0, y=0.0, z=0.01):
|
||||
initial_pose = Pose()
|
||||
initial_pose.position.x = x
|
||||
initial_pose.position.y = y
|
||||
initial_pose.position.z = z
|
||||
initial_pose.orientation.x = 0.0
|
||||
initial_pose.orientation.y = 0.0
|
||||
initial_pose.orientation.z = 0.0
|
||||
initial_pose.orientation.w = 1.0
|
||||
return initial_pose
|
||||
|
||||
|
||||
def get_testers(args):
|
||||
testers = []
|
||||
|
||||
if args.robot:
|
||||
# Requested tester for one robot
|
||||
init_x, init_y, final_x, final_y = args.robot[0]
|
||||
tester = NavTester(
|
||||
initial_pose=fwd_pose(float(init_x), float(init_y)),
|
||||
goal_pose=fwd_pose(float(final_x), float(final_y)))
|
||||
tester.info_msg(
|
||||
'Starting tester, robot going from ' + init_x + ', ' + init_y +
|
||||
' to ' + final_x + ', ' + final_y + '.')
|
||||
testers.append(tester)
|
||||
return testers
|
||||
|
||||
return testers
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
# The robot(s) positions from the input arguments
|
||||
parser = argparse.ArgumentParser(description='System-level navigation tester node')
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-r', '--robot', action='append', nargs=4,
|
||||
metavar=('init_x', 'init_y', 'final_x', 'final_y'),
|
||||
help='The robot starting and final positions.')
|
||||
group.add_argument('-rs', '--robots', action='append', nargs=5,
|
||||
metavar=('name', 'init_x', 'init_y', 'final_x', 'final_y'),
|
||||
help="The robot's namespace and starting and final positions. " +
|
||||
'Repeating the argument for multiple robots is supported.')
|
||||
|
||||
args, unknown = parser.parse_known_args()
|
||||
|
||||
rclpy.init()
|
||||
|
||||
# Create testers for each robot
|
||||
testers = get_testers(args)
|
||||
|
||||
# wait a few seconds to make sure entire stacks are up
|
||||
time.sleep(10)
|
||||
|
||||
for tester in testers:
|
||||
passed = run_all_tests(tester)
|
||||
if not passed:
|
||||
break
|
||||
|
||||
for tester in testers:
|
||||
# stop and shutdown the nav stack to exit cleanly
|
||||
tester.shutdown()
|
||||
|
||||
testers[0].info_msg('Done Shutting Down.')
|
||||
|
||||
if not passed:
|
||||
testers[0].info_msg('Exiting failed')
|
||||
exit(1)
|
||||
else:
|
||||
testers[0].info_msg('Exiting passed')
|
||||
exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,11 @@
|
||||
add_executable(test_updown
|
||||
test_updown.cpp
|
||||
)
|
||||
|
||||
ament_target_dependencies(test_updown
|
||||
${dependencies}
|
||||
)
|
||||
|
||||
install(TARGETS test_updown RUNTIME DESTINATION lib/${PROJECT_NAME})
|
||||
install(FILES test_updown_launch.py DESTINATION share/${PROJECT_NAME})
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Nav2 Updown Test
|
||||
|
||||
This is a 'top level' system test which tests the lifecycle bringup and shutdown of the system.
|
||||
|
||||
## To run the test
|
||||
```
|
||||
ros2 launch nav2_system_tests test_updown_launch.py
|
||||
```
|
||||
|
||||
If the test passes, you should see this comment in the output:
|
||||
```
|
||||
[test_updown-13] [INFO] [test_updown]: **************************************************** TEST PASSED!
|
||||
```
|
||||
|
||||
To run the test in a loop 1000x, run the `test_updown_reliablity` script and log the output:
|
||||
```
|
||||
./test_updown_reliablity |& tee /tmp/updown.log
|
||||
```
|
||||
When the test is completed, pipe the log to the `updownresults.py` script to get a summary of the results:
|
||||
```
|
||||
./updownresults.py < /tmp/updown.log`
|
||||
```
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
ros2 launch ./test_updown_launch.py
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2018 Intel Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
#include "geometry_msgs/msg/pose_stamped.hpp"
|
||||
#include "nav2_lifecycle_manager/lifecycle_manager_client.hpp"
|
||||
#include "rcutils/cmdline_parser.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
{
|
||||
rclcpp::init(argc, argv);
|
||||
RCLCPP_INFO(rclcpp::get_logger("test_updown"), "Initializing test");
|
||||
auto node = std::make_shared<rclcpp::Node>("lifecycle_manager_service_client");
|
||||
nav2_lifecycle_manager::LifecycleManagerClient client_nav("lifecycle_manager_navigation", node);
|
||||
nav2_lifecycle_manager::LifecycleManagerClient client_loc("lifecycle_manager_localization", node);
|
||||
bool test_passed = true;
|
||||
|
||||
// Wait for a few seconds to let all of the nodes come up
|
||||
std::this_thread::sleep_for(5s);
|
||||
|
||||
// Start the nav2 system, bringing it to the ACTIVE state
|
||||
client_nav.startup();
|
||||
client_loc.startup();
|
||||
|
||||
// Wait for a couple secs to make sure the nodes have processed all discovery
|
||||
// info before starting
|
||||
RCLCPP_INFO(rclcpp::get_logger("test_updown"), "Waiting for nodes to be active");
|
||||
std::this_thread::sleep_for(2s);
|
||||
|
||||
// The system should now be active
|
||||
int retries = 0;
|
||||
while ((client_nav.is_active() != nav2_lifecycle_manager::SystemStatus::ACTIVE) &&
|
||||
(client_loc.is_active() != nav2_lifecycle_manager::SystemStatus::ACTIVE) &&
|
||||
(retries < 10))
|
||||
{
|
||||
std::this_thread::sleep_for(2s);
|
||||
retries++;
|
||||
}
|
||||
if (retries == 10) {
|
||||
// the system isn't active
|
||||
RCLCPP_ERROR(rclcpp::get_logger("test_updown"), "System startup failed");
|
||||
test_passed = false;
|
||||
}
|
||||
|
||||
// Shut down the nav2 system, bringing it to the FINALIZED state
|
||||
client_nav.shutdown();
|
||||
client_loc.shutdown();
|
||||
|
||||
if (test_passed) {
|
||||
RCLCPP_INFO(
|
||||
rclcpp::get_logger("test_updown"),
|
||||
"**************************************************** TEST PASSED!");
|
||||
} else {
|
||||
RCLCPP_INFO(
|
||||
rclcpp::get_logger("test_updown"),
|
||||
"**************************************************** TEST FAILED!");
|
||||
}
|
||||
rclcpp::shutdown();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
# Copyright (c) 2019 Intel Corporation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
|
||||
from ament_index_python.packages import get_package_prefix
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
import launch.actions
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch_ros.actions import Node
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
|
||||
# Configuration parameters for the launch
|
||||
launch_dir = os.path.join(
|
||||
get_package_share_directory('nav2_bringup'), 'launch')
|
||||
|
||||
map_yaml_file = os.path.join(
|
||||
get_package_share_directory('nav2_system_tests'), 'maps/map_circular.yaml')
|
||||
|
||||
# Specify the actions
|
||||
start_tf_cmd_1 = Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'map', 'odom'])
|
||||
|
||||
start_tf_cmd_2 = Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'odom', 'base_footprint'])
|
||||
|
||||
start_tf_cmd_3 = Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_footprint', 'base_link'])
|
||||
|
||||
start_tf_cmd_4 = Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_scan'])
|
||||
|
||||
nav2_bringup = launch.actions.IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(launch_dir, 'bringup_launch.py')),
|
||||
launch_arguments={'map': map_yaml_file,
|
||||
'use_sim_time': 'True',
|
||||
'autostart': 'False'}.items())
|
||||
|
||||
start_test = launch.actions.ExecuteProcess(
|
||||
cmd=[
|
||||
os.path.join(
|
||||
get_package_prefix('nav2_system_tests'),
|
||||
'lib/nav2_system_tests/test_updown')],
|
||||
cwd=[launch_dir], output='screen')
|
||||
|
||||
test_exit_event_handler = launch.actions.RegisterEventHandler(
|
||||
event_handler=launch.event_handlers.OnProcessExit(
|
||||
target_action=start_test,
|
||||
on_exit=launch.actions.EmitEvent(event=launch.events.Shutdown(reason='Done!'))))
|
||||
|
||||
# Compose the launch description
|
||||
|
||||
ld = launch.LaunchDescription()
|
||||
|
||||
ld.add_action(start_tf_cmd_1)
|
||||
ld.add_action(start_tf_cmd_2)
|
||||
ld.add_action(start_tf_cmd_3)
|
||||
ld.add_action(start_tf_cmd_4)
|
||||
|
||||
ld.add_action(nav2_bringup)
|
||||
ld.add_action(start_test)
|
||||
ld.add_action(test_exit_event_handler)
|
||||
|
||||
return ld
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
for i in `seq 1 1000`;
|
||||
do
|
||||
echo "======= START OF RUN: $i ========="
|
||||
|
||||
# Start with a new ros2 daemon
|
||||
ros2 daemon stop
|
||||
|
||||
# There shouldn't be any nodes in the list
|
||||
ros2 node list
|
||||
echo "----------------------------------"
|
||||
|
||||
# Bound the time of the bringup/shutdown in case it hangs
|
||||
timeout 100 ./start_nav2
|
||||
|
||||
# Make sure there aren't any stray gazebo or ros2 processes hanging around
|
||||
# that were not properly killed by the launch script
|
||||
#kill -9 $(pgrep gzserver) &> /dev/null
|
||||
kill -9 $(pgrep ros2) &> /dev/null
|
||||
|
||||
echo "======== END OF RUN: $i =========="
|
||||
done
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/python3
|
||||
# Copyright (c) 2019 Intel Corporation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
# To use this script, run the `test_updown_reliablity` script and log the output
|
||||
# > ./test_updown_reliablity |& tee /tmp/updown.log
|
||||
# When the test is completed, pipe the log to this script to get a summary of the
|
||||
# results
|
||||
# > ./updownresults.py < /tmp/updown.log
|
||||
#
|
||||
# This reports the number of successful tests, but also the number of times the
|
||||
# tests were able to make it to the active state as well as the shutdown state.
|
||||
# It can frequently occur that the system makes all the lifecycle state transitions
|
||||
# but has an error during the final process termination.
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
log = sys.stdin
|
||||
test_count = 0
|
||||
fail_count = 0
|
||||
successful_bringup_count = 0
|
||||
successful_shutdown_count = 0
|
||||
for line in log.readlines():
|
||||
if line.startswith('======= START OF RUN:'):
|
||||
test_successful = True
|
||||
shutdown_successful = False
|
||||
bringup_successful = False
|
||||
|
||||
if line.startswith('======== END OF RUN:'):
|
||||
test_count += 1
|
||||
conclusion = ''
|
||||
if bringup_successful:
|
||||
successful_bringup_count += 1
|
||||
conclusion = ' but bringup was successful'
|
||||
if shutdown_successful:
|
||||
successful_shutdown_count += 1
|
||||
conclusion = ' but shutdown was successful'
|
||||
if not test_successful:
|
||||
fail_count += 1
|
||||
print('Failure in test ', test_count, conclusion)
|
||||
|
||||
if '[ERROR]' in line:
|
||||
test_successful = False
|
||||
|
||||
if 'The system is active' in line:
|
||||
bringup_successful = True
|
||||
|
||||
if 'The system has been sucessfully shut down' in line:
|
||||
shutdown_successful = True
|
||||
|
||||
print('Number of tests: ', test_count)
|
||||
print('Number of successes: ', test_count-fail_count)
|
||||
print('Number of successful bringups', successful_bringup_count)
|
||||
print('Number of successful shutdowns', successful_shutdown_count)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,12 @@
|
||||
ament_add_test(test_waypoint_follower
|
||||
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_case_py.launch"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
TIMEOUT 180
|
||||
ENV
|
||||
TEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
TEST_MAP=${PROJECT_SOURCE_DIR}/maps/map_circular.yaml
|
||||
TEST_WORLD=${PROJECT_SOURCE_DIR}/worlds/turtlebot3_ros2_demo.world
|
||||
GAZEBO_MODEL_PATH=${PROJECT_SOURCE_DIR}/models
|
||||
BT_NAVIGATOR_XML=navigate_to_pose_w_replanning_and_recovery.xml
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Waypoint Follower Test
|
||||
|
||||
This is a simple test for the waypoint follower. It creates an instance of the stack, calls with waypoint follower, and checks for successful navigation with 4 predetermined waypoints in the turtlebot map.
|
||||
@@ -0,0 +1,104 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright (c) 2019 Samsung Research America
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch import LaunchService
|
||||
from launch.actions import ExecuteProcess, IncludeLaunchDescription, SetEnvironmentVariable
|
||||
from launch.launch_context import LaunchContext
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch_ros.actions import Node
|
||||
from launch_testing.legacy import LaunchTestService
|
||||
|
||||
from nav2_common.launch import RewrittenYaml
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
map_yaml_file = os.getenv('TEST_MAP')
|
||||
world = os.getenv('TEST_WORLD')
|
||||
|
||||
bt_navigator_xml = os.path.join(get_package_share_directory('nav2_bt_navigator'),
|
||||
'behavior_trees',
|
||||
os.getenv('BT_NAVIGATOR_XML'))
|
||||
|
||||
bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
params_file = os.path.join(bringup_dir, 'params/nav2_params.yaml')
|
||||
|
||||
# Replace the `use_astar` setting on the params file
|
||||
configured_params = RewrittenYaml(
|
||||
source_file=params_file,
|
||||
root_key='',
|
||||
param_rewrites='',
|
||||
convert_types=True)
|
||||
|
||||
context = LaunchContext()
|
||||
new_yaml = configured_params.perform(context)
|
||||
return LaunchDescription([
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM', '1'),
|
||||
SetEnvironmentVariable('RCUTILS_LOGGING_USE_STDOUT', '1'),
|
||||
|
||||
# Launch gazebo server for simulation
|
||||
ExecuteProcess(
|
||||
cmd=['gzserver', '-s', 'libgazebo_ros_init.so',
|
||||
'--minimal_comms', world],
|
||||
output='screen'),
|
||||
|
||||
# TODO(orduno) Launch the robot state publisher instead
|
||||
# using a local copy of TB3 urdf file
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_footprint', 'base_link']),
|
||||
|
||||
Node(
|
||||
package='tf2_ros',
|
||||
executable='static_transform_publisher',
|
||||
output='screen',
|
||||
arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_scan']),
|
||||
|
||||
IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(bringup_dir, 'launch', 'bringup_launch.py')),
|
||||
launch_arguments={
|
||||
'map': map_yaml_file,
|
||||
'use_sim_time': 'True',
|
||||
'params_file': new_yaml,
|
||||
'bt_xml_file': bt_navigator_xml,
|
||||
'autostart': 'True'}.items()),
|
||||
])
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
ld = generate_launch_description()
|
||||
|
||||
test1_action = ExecuteProcess(
|
||||
cmd=[os.path.join(os.getenv('TEST_DIR'), 'tester.py')],
|
||||
name='tester_node',
|
||||
output='screen')
|
||||
|
||||
lts = LaunchTestService()
|
||||
lts.add_test_action(ld, test1_action)
|
||||
ls = LaunchService(argv=argv)
|
||||
ls.include_launch_description(ld)
|
||||
return lts.run(ls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,230 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright 2019 Samsung Research America
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
from action_msgs.msg import GoalStatus
|
||||
from geometry_msgs.msg import PoseStamped, PoseWithCovarianceStamped
|
||||
from nav2_msgs.action import FollowWaypoints
|
||||
from nav2_msgs.srv import ManageLifecycleNodes
|
||||
|
||||
import rclpy
|
||||
from rclpy.action import ActionClient
|
||||
from rclpy.node import Node
|
||||
from rclpy.qos import QoSDurabilityPolicy, QoSHistoryPolicy, QoSReliabilityPolicy
|
||||
from rclpy.qos import QoSProfile
|
||||
|
||||
|
||||
class WaypointFollowerTest(Node):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(node_name='nav2_waypoint_tester', namespace='')
|
||||
self.waypoints = None
|
||||
self.action_client = ActionClient(self, FollowWaypoints, 'follow_waypoints')
|
||||
self.initial_pose_pub = self.create_publisher(PoseWithCovarianceStamped,
|
||||
'initialpose', 10)
|
||||
self.initial_pose_received = False
|
||||
self.goal_handle = None
|
||||
|
||||
pose_qos = QoSProfile(
|
||||
durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
|
||||
reliability=QoSReliabilityPolicy.RELIABLE,
|
||||
history=QoSHistoryPolicy.KEEP_LAST,
|
||||
depth=1)
|
||||
|
||||
self.model_pose_sub = self.create_subscription(PoseWithCovarianceStamped,
|
||||
'amcl_pose', self.poseCallback, pose_qos)
|
||||
|
||||
def setInitialPose(self, pose):
|
||||
self.init_pose = PoseWithCovarianceStamped()
|
||||
self.init_pose.pose.pose.position.x = pose[0]
|
||||
self.init_pose.pose.pose.position.y = pose[1]
|
||||
self.init_pose.header.frame_id = 'map'
|
||||
self.publishInitialPose()
|
||||
time.sleep(5)
|
||||
|
||||
def poseCallback(self, msg):
|
||||
self.info_msg('Received amcl_pose')
|
||||
self.initial_pose_received = True
|
||||
|
||||
def setWaypoints(self, waypoints):
|
||||
self.waypoints = []
|
||||
for wp in waypoints:
|
||||
msg = PoseStamped()
|
||||
msg.header.frame_id = 'map'
|
||||
msg.pose.position.x = wp[0]
|
||||
msg.pose.position.y = wp[1]
|
||||
msg.pose.orientation.w = 1.0
|
||||
self.waypoints.append(msg)
|
||||
|
||||
def run(self, block):
|
||||
if not self.waypoints:
|
||||
rclpy.error_msg('Did not set valid waypoints before running test!')
|
||||
return False
|
||||
|
||||
while not self.action_client.wait_for_server(timeout_sec=1.0):
|
||||
self.info_msg("'follow_waypoints' action server not available, waiting...")
|
||||
|
||||
action_request = FollowWaypoints.Goal()
|
||||
action_request.poses = self.waypoints
|
||||
|
||||
self.info_msg('Sending goal request...')
|
||||
send_goal_future = self.action_client.send_goal_async(action_request)
|
||||
try:
|
||||
rclpy.spin_until_future_complete(self, send_goal_future)
|
||||
self.goal_handle = send_goal_future.result()
|
||||
except Exception as e: # noqa: B902
|
||||
self.error_msg(f'Service call failed {e!r}')
|
||||
|
||||
if not self.goal_handle.accepted:
|
||||
self.error_msg('Goal rejected')
|
||||
return False
|
||||
|
||||
self.info_msg('Goal accepted')
|
||||
if not block:
|
||||
return True
|
||||
|
||||
get_result_future = self.goal_handle.get_result_async()
|
||||
|
||||
self.info_msg("Waiting for 'follow_waypoints' action to complete")
|
||||
try:
|
||||
rclpy.spin_until_future_complete(self, get_result_future)
|
||||
status = get_result_future.result().status
|
||||
result = get_result_future.result().result
|
||||
except Exception as e: # noqa: B902
|
||||
self.error_msg(f'Service call failed {e!r}')
|
||||
|
||||
if status != GoalStatus.STATUS_SUCCEEDED:
|
||||
self.info_msg(f'Goal failed with status code: {status}')
|
||||
return False
|
||||
if len(result.missed_waypoints) > 0:
|
||||
self.info_msg('Goal failed to process all waypoints,'
|
||||
' missed {0} wps.'.format(len(result.missed_waypoints)))
|
||||
return False
|
||||
|
||||
self.info_msg('Goal succeeded!')
|
||||
return True
|
||||
|
||||
def publishInitialPose(self):
|
||||
self.initial_pose_pub.publish(self.init_pose)
|
||||
|
||||
def shutdown(self):
|
||||
self.info_msg('Shutting down')
|
||||
|
||||
self.action_client.destroy()
|
||||
self.info_msg('Destroyed follow_waypoints action client')
|
||||
|
||||
transition_service = 'lifecycle_manager_navigation/manage_nodes'
|
||||
mgr_client = self.create_client(ManageLifecycleNodes, transition_service)
|
||||
while not mgr_client.wait_for_service(timeout_sec=1.0):
|
||||
self.info_msg(f'{transition_service} service not available, waiting...')
|
||||
|
||||
req = ManageLifecycleNodes.Request()
|
||||
req.command = ManageLifecycleNodes.Request().SHUTDOWN
|
||||
future = mgr_client.call_async(req)
|
||||
try:
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
future.result()
|
||||
except Exception as e: # noqa: B902
|
||||
self.error_msg(f'{transition_service} service call failed {e!r}')
|
||||
|
||||
self.info_msg(f'{transition_service} finished')
|
||||
|
||||
transition_service = 'lifecycle_manager_localization/manage_nodes'
|
||||
mgr_client = self.create_client(ManageLifecycleNodes, transition_service)
|
||||
while not mgr_client.wait_for_service(timeout_sec=1.0):
|
||||
self.info_msg(f'{transition_service} service not available, waiting...')
|
||||
|
||||
req = ManageLifecycleNodes.Request()
|
||||
req.command = ManageLifecycleNodes.Request().SHUTDOWN
|
||||
future = mgr_client.call_async(req)
|
||||
try:
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
future.result()
|
||||
except Exception as e: # noqa: B902
|
||||
self.error_msg(f'{transition_service} service call failed {e!r}')
|
||||
|
||||
self.info_msg(f'{transition_service} finished')
|
||||
|
||||
def cancel_goal(self):
|
||||
cancel_future = self.goal_handle.cancel_goal_async()
|
||||
rclpy.spin_until_future_complete(self, cancel_future)
|
||||
|
||||
def info_msg(self, msg: str):
|
||||
self.get_logger().info(msg)
|
||||
|
||||
def warn_msg(self, msg: str):
|
||||
self.get_logger().warn(msg)
|
||||
|
||||
def error_msg(self, msg: str):
|
||||
self.get_logger().error(msg)
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
rclpy.init()
|
||||
|
||||
# wait a few seconds to make sure entire stacks are up
|
||||
time.sleep(10)
|
||||
|
||||
wps = [[-0.52, -0.54], [0.58, -0.55], [0.58, 0.52]]
|
||||
starting_pose = [-2.0, -0.5]
|
||||
|
||||
test = WaypointFollowerTest()
|
||||
test.setWaypoints(wps)
|
||||
|
||||
retry_count = 0
|
||||
retries = 2
|
||||
while not test.initial_pose_received and retry_count <= retries:
|
||||
retry_count += 1
|
||||
test.info_msg('Setting initial pose')
|
||||
test.setInitialPose(starting_pose)
|
||||
test.info_msg('Waiting for amcl_pose to be received')
|
||||
rclpy.spin_once(test, timeout_sec=1.0) # wait for poseCallback
|
||||
|
||||
result = test.run(True)
|
||||
assert result
|
||||
|
||||
# preempt with new point
|
||||
test.setWaypoints([starting_pose])
|
||||
result = test.run(False)
|
||||
time.sleep(2)
|
||||
test.setWaypoints([wps[1]])
|
||||
result = test.run(False)
|
||||
|
||||
# cancel
|
||||
time.sleep(2)
|
||||
test.cancel_goal()
|
||||
|
||||
# a failure case
|
||||
time.sleep(2)
|
||||
test.setWaypoints([[100.0, 100.0]])
|
||||
result = test.run(True)
|
||||
assert not result
|
||||
result = not result
|
||||
|
||||
test.shutdown()
|
||||
test.info_msg('Done Shutting Down.')
|
||||
|
||||
if not result:
|
||||
test.info_msg('Exiting failed')
|
||||
exit(1)
|
||||
else:
|
||||
test.info_msg('Exiting passed')
|
||||
exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user