add humble-navigation2

This commit is contained in:
X-lanni
2025-05-27 19:03:40 +08:00
parent 974abb5e1e
commit e74ec539c2
1280 changed files with 204114 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
cmake_minimum_required(VERSION 3.5)
project(nav2_smoother)
find_package(ament_cmake REQUIRED)
find_package(nav2_core REQUIRED)
find_package(nav2_common REQUIRED)
find_package(angles REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclcpp_components REQUIRED)
find_package(rclcpp_action REQUIRED)
find_package(rclcpp_components REQUIRED)
find_package(std_msgs REQUIRED)
find_package(nav2_util REQUIRED)
find_package(nav2_msgs REQUIRED)
find_package(nav_2d_utils REQUIRED)
find_package(nav_2d_msgs REQUIRED)
find_package(pluginlib REQUIRED)
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
endif()
nav2_package()
include_directories(
include
)
set(executable_name smoother_server)
set(library_name ${executable_name}_core)
set(dependencies
angles
rclcpp
rclcpp_components
rclcpp_action
rclcpp_components
std_msgs
nav2_msgs
nav_2d_utils
nav_2d_msgs
nav2_util
nav2_core
pluginlib
)
# Main library
add_library(${library_name} SHARED
src/nav2_smoother.cpp
)
ament_target_dependencies(${library_name}
${dependencies}
)
# Main executable
add_executable(${executable_name}
src/main.cpp
)
ament_target_dependencies(${executable_name}
${dependencies}
)
target_link_libraries(${executable_name} ${library_name})
# Simple Smoother plugin
add_library(simple_smoother SHARED
src/simple_smoother.cpp
)
ament_target_dependencies(simple_smoother
${dependencies}
)
# Savitzky Golay Smoother plugin
add_library(savitzky_golay_smoother SHARED
src/savitzky_golay_smoother.cpp
)
ament_target_dependencies(savitzky_golay_smoother
${dependencies}
)
pluginlib_export_plugin_description_file(nav2_core plugins.xml)
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# the following line skips the linter which checks for copyrights
set(ament_cmake_copyright_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
find_package(ament_cmake_gtest REQUIRED)
endif()
rclcpp_components_register_nodes(${library_name} "nav2_smoother::SmootherServer")
install(
TARGETS ${library_name} simple_smoother savitzky_golay_smoother
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
install(TARGETS ${executable_name}
RUNTIME DESTINATION lib/${PROJECT_NAME}
)
install(DIRECTORY include/
DESTINATION include/
)
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()
find_package(ament_cmake_gtest REQUIRED)
add_subdirectory(test)
endif()
ament_export_include_directories(include)
ament_export_libraries(${library_name} simple_smoother savitzky_golay_smoother)
ament_export_dependencies(${dependencies})
ament_package()
+11
View File
@@ -0,0 +1,11 @@
# Nav2 Smoother
The Nav2 smoother is a Task Server in Nav2 that implements the `nav2_behavior_tree::SmoothPath` interface.
A smoothing module implementing the `nav2_behavior_tree::SmoothPath` interface is responsible for improving path smoothness and/or quality, typically given an unsmoothed path from the planner module in `nav2_planner`. It loads a map of potential smoother plugins to do the path smoothing in different user-defined situations.
See the [Navigation Plugin list](https://navigation.ros.org/plugins/index.html) for a list of the currently known and available smoother plugins.
See its [Configuration Guide Page](https://navigation.ros.org/configuration/packages/configuring-smoother-server.html) for additional parameter descriptions.
This package contains the Simple Smoother and Savitzky-Golay Smoother plugins.
@@ -0,0 +1,165 @@
// Copyright (c) 2021 RoboTech Vision
// 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.
#ifndef NAV2_SMOOTHER__NAV2_SMOOTHER_HPP_
#define NAV2_SMOOTHER__NAV2_SMOOTHER_HPP_
#include <memory>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
#include "nav2_core/smoother.hpp"
#include "nav2_costmap_2d/costmap_2d_ros.hpp"
#include "nav2_costmap_2d/costmap_subscriber.hpp"
#include "nav2_costmap_2d/costmap_topic_collision_checker.hpp"
#include "nav2_costmap_2d/footprint_subscriber.hpp"
#include "nav2_msgs/action/smooth_path.hpp"
#include "nav2_util/lifecycle_node.hpp"
#include "nav2_util/robot_utils.hpp"
#include "nav2_util/simple_action_server.hpp"
#include "pluginlib/class_list_macros.hpp"
#include "pluginlib/class_loader.hpp"
namespace nav2_smoother
{
/**
* @class nav2_smoother::SmootherServer
* @brief This class hosts variety of plugins of different algorithms to
* smooth or refine a path from the exposed SmoothPath action server.
*/
class SmootherServer : public nav2_util::LifecycleNode
{
public:
using SmootherMap = std::unordered_map<std::string, nav2_core::Smoother::Ptr>;
/**
* @brief A constructor for nav2_smoother::SmootherServer
* @param options Additional options to control creation of the node.
*/
explicit SmootherServer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions());
/**
* @brief Destructor for nav2_smoother::SmootherServer
*/
~SmootherServer();
protected:
/**
* @brief Configures smoother parameters and member variables
*
* Configures smoother plugin and costmap; Initialize odom subscriber,
* velocity publisher and smooth path action server.
* @param state LifeCycle Node's state
* @return Success or Failure
* @throw pluginlib::PluginlibException When failed to initialize smoother
* plugin
*/
nav2_util::CallbackReturn on_configure(const rclcpp_lifecycle::State & state) override;
/**
* @brief Loads smoother plugins from parameter file
* @return bool if successfully loaded the plugins
*/
bool loadSmootherPlugins();
/**
* @brief Activates member variables
*
* Activates smoother, costmap, velocity publisher and smooth path action
* server
* @param state LifeCycle Node's state
* @return Success or Failure
*/
nav2_util::CallbackReturn on_activate(const rclcpp_lifecycle::State & state) override;
/**
* @brief Deactivates member variables
*
* Deactivates smooth path action server, smoother, costmap and velocity
* publisher. Before calling deactivate state, velocity is being set to zero.
* @param state LifeCycle Node's state
* @return Success or Failure
*/
nav2_util::CallbackReturn on_deactivate(const rclcpp_lifecycle::State & state) override;
/**
* @brief Calls clean up states and resets member variables.
*
* Smoother and costmap clean up state is called, and resets rest of the
* variables
* @param state LifeCycle Node's state
* @return Success or Failure
*/
nav2_util::CallbackReturn on_cleanup(const rclcpp_lifecycle::State & state) override;
/**
* @brief Called when in Shutdown state
* @param state LifeCycle Node's state
* @return Success or Failure
*/
nav2_util::CallbackReturn on_shutdown(const rclcpp_lifecycle::State & state) override;
using Action = nav2_msgs::action::SmoothPath;
using ActionServer = nav2_util::SimpleActionServer<Action>;
/**
* @brief SmoothPath action server callback. Handles action server updates and
* spins server until goal is reached
*
* Provides global path to smoother received from action client. Local
* section of the path is optimized using smoother.
* @throw nav2_core::PlannerException
*/
void smoothPlan();
/**
* @brief Find the valid smoother ID name for the given request
*
* @param c_name The requested smoother name
* @param name Reference to the name to use for control if any valid available
* @return bool Whether it found a valid smoother to use
*/
bool findSmootherId(const std::string & c_name, std::string & name);
// Our action server implements the SmoothPath action
std::unique_ptr<ActionServer> action_server_;
// Transforms
std::shared_ptr<tf2_ros::Buffer> tf_;
std::shared_ptr<tf2_ros::TransformListener> transform_listener_;
// Publishers and subscribers
rclcpp_lifecycle::LifecyclePublisher<nav_msgs::msg::Path>::SharedPtr plan_publisher_;
// Smoother Plugins
pluginlib::ClassLoader<nav2_core::Smoother> lp_loader_;
SmootherMap smoothers_;
std::vector<std::string> default_ids_;
std::vector<std::string> default_types_;
std::vector<std::string> smoother_ids_;
std::vector<std::string> smoother_types_;
std::string smoother_ids_concat_, current_smoother_;
// Utilities
std::shared_ptr<nav2_costmap_2d::CostmapSubscriber> costmap_sub_;
std::shared_ptr<nav2_costmap_2d::FootprintSubscriber> footprint_sub_;
std::shared_ptr<nav2_costmap_2d::CostmapTopicCollisionChecker> collision_checker_;
};
} // namespace nav2_smoother
#endif // NAV2_SMOOTHER__NAV2_SMOOTHER_HPP_
@@ -0,0 +1,108 @@
// Copyright (c) 2022, 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. Reserved.
#ifndef NAV2_SMOOTHER__SAVITZKY_GOLAY_SMOOTHER_HPP_
#define NAV2_SMOOTHER__SAVITZKY_GOLAY_SMOOTHER_HPP_
#include <cmath>
#include <vector>
#include <string>
#include <iostream>
#include <memory>
#include <queue>
#include <utility>
#include "nav2_core/smoother.hpp"
#include "nav2_smoother/smoother_utils.hpp"
#include "nav2_costmap_2d/costmap_2d.hpp"
#include "nav2_costmap_2d/cost_values.hpp"
#include "nav2_util/geometry_utils.hpp"
#include "nav2_util/node_utils.hpp"
#include "nav_msgs/msg/path.hpp"
#include "angles/angles.h"
#include "tf2/utils.h"
namespace nav2_smoother
{
/**
* @class nav2_smoother::SavitzkyGolaySmoother
* @brief A path smoother implementation using Savitzky Golay filters
*/
class SavitzkyGolaySmoother : public nav2_core::Smoother
{
public:
/**
* @brief A constructor for nav2_smoother::SavitzkyGolaySmoother
*/
SavitzkyGolaySmoother() = default;
/**
* @brief A destructor for nav2_smoother::SavitzkyGolaySmoother
*/
~SavitzkyGolaySmoother() override = default;
void configure(
const rclcpp_lifecycle::LifecycleNode::WeakPtr &,
std::string name, std::shared_ptr<tf2_ros::Buffer>,
std::shared_ptr<nav2_costmap_2d::CostmapSubscriber>,
std::shared_ptr<nav2_costmap_2d::FootprintSubscriber>) override;
/**
* @brief Method to cleanup resources.
*/
void cleanup() override {}
/**
* @brief Method to activate smoother and any threads involved in execution.
*/
void activate() override {}
/**
* @brief Method to deactivate smoother and any threads involved in execution.
*/
void deactivate() override {}
/**
* @brief Method to smooth given path
*
* @param path In-out path to be smoothed
* @param max_time Maximum duration smoothing should take
* @return If smoothing was completed (true) or interrupted by time limit (false)
*/
bool smooth(
nav_msgs::msg::Path & path,
const rclcpp::Duration & max_time) override;
protected:
/**
* @brief Smoother method - does the smoothing on a segment
* @param path Reference to path
* @param reversing_segment Return if this is a reversing segment
* @param costmap Pointer to minimal costmap
* @param max_time Maximum time to compute, stop early if over limit
* @return If smoothing was successful
*/
bool smoothImpl(
nav_msgs::msg::Path & path,
bool & reversing_segment);
bool do_refinement_;
int refinement_num_;
rclcpp::Logger logger_{rclcpp::get_logger("SGSmoother")};
};
} // namespace nav2_smoother
#endif // NAV2_SMOOTHER__SAVITZKY_GOLAY_SMOOTHER_HPP_
@@ -0,0 +1,132 @@
// Copyright (c) 2022, 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. Reserved.
#ifndef NAV2_SMOOTHER__SIMPLE_SMOOTHER_HPP_
#define NAV2_SMOOTHER__SIMPLE_SMOOTHER_HPP_
#include <cmath>
#include <vector>
#include <string>
#include <iostream>
#include <memory>
#include <queue>
#include <utility>
#include "nav2_core/smoother.hpp"
#include "nav2_smoother/smoother_utils.hpp"
#include "nav2_costmap_2d/costmap_2d.hpp"
#include "nav2_costmap_2d/cost_values.hpp"
#include "nav2_util/geometry_utils.hpp"
#include "nav2_util/node_utils.hpp"
#include "nav_msgs/msg/path.hpp"
#include "angles/angles.h"
#include "tf2/utils.h"
namespace nav2_smoother
{
/**
* @class nav2_smoother::SimpleSmoother
* @brief A path smoother implementation
*/
class SimpleSmoother : public nav2_core::Smoother
{
public:
/**
* @brief A constructor for nav2_smoother::SimpleSmoother
*/
SimpleSmoother() = default;
/**
* @brief A destructor for nav2_smoother::SimpleSmoother
*/
~SimpleSmoother() override = default;
void configure(
const rclcpp_lifecycle::LifecycleNode::WeakPtr &,
std::string name, std::shared_ptr<tf2_ros::Buffer>,
std::shared_ptr<nav2_costmap_2d::CostmapSubscriber>,
std::shared_ptr<nav2_costmap_2d::FootprintSubscriber>) override;
/**
* @brief Method to cleanup resources.
*/
void cleanup() override {costmap_sub_.reset();}
/**
* @brief Method to activate smoother and any threads involved in execution.
*/
void activate() override {}
/**
* @brief Method to deactivate smoother and any threads involved in execution.
*/
void deactivate() override {}
/**
* @brief Method to smooth given path
*
* @param path In-out path to be smoothed
* @param max_time Maximum duration smoothing should take
* @return If smoothing was completed (true) or interrupted by time limit (false)
*/
bool smooth(
nav_msgs::msg::Path & path,
const rclcpp::Duration & max_time) override;
protected:
/**
* @brief Smoother method - does the smoothing on a segment
* @param path Reference to path
* @param reversing_segment Return if this is a reversing segment
* @param costmap Pointer to minimal costmap
* @param max_time Maximum time to compute, stop early if over limit
* @return If smoothing was successful
*/
bool smoothImpl(
nav_msgs::msg::Path & path,
bool & reversing_segment,
const nav2_costmap_2d::Costmap2D * costmap,
const double & max_time);
/**
* @brief Get the field value for a given dimension
* @param msg Current pose to sample
* @param dim Dimension ID of interest
* @return dim value
*/
inline double getFieldByDim(
const geometry_msgs::msg::PoseStamped & msg,
const unsigned int & dim);
/**
* @brief Set the field value for a given dimension
* @param msg Current pose to sample
* @param dim Dimension ID of interest
* @param value to set the dimention to for the pose
*/
inline void setFieldByDim(
geometry_msgs::msg::PoseStamped & msg, const unsigned int dim,
const double & value);
double tolerance_, data_w_, smooth_w_;
int max_its_, refinement_ctr_;
bool do_refinement_;
std::shared_ptr<nav2_costmap_2d::CostmapSubscriber> costmap_sub_;
rclcpp::Logger logger_{rclcpp::get_logger("SimpleSmoother")};
};
} // namespace nav2_smoother
#endif // NAV2_SMOOTHER__SIMPLE_SMOOTHER_HPP_
@@ -0,0 +1,132 @@
// Copyright (c) 2022, 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. Reserved.
#ifndef NAV2_SMOOTHER__SMOOTHER_UTILS_HPP_
#define NAV2_SMOOTHER__SMOOTHER_UTILS_HPP_
#include <cmath>
#include <vector>
#include <string>
#include <iostream>
#include <memory>
#include <queue>
#include <utility>
#include "nav2_core/smoother.hpp"
#include "nav2_costmap_2d/costmap_2d.hpp"
#include "nav2_costmap_2d/cost_values.hpp"
#include "nav2_util/geometry_utils.hpp"
#include "nav2_util/node_utils.hpp"
#include "nav_msgs/msg/path.hpp"
#include "angles/angles.h"
#include "tf2/utils.h"
namespace smoother_utils
{
/**
* @class nav2_smoother::PathSegment
* @brief A segment of a path in start/end indices
*/
struct PathSegment
{
unsigned int start;
unsigned int end;
};
typedef std::vector<geometry_msgs::msg::PoseStamped>::iterator PathIterator;
typedef std::vector<geometry_msgs::msg::PoseStamped>::reverse_iterator ReversePathIterator;
inline std::vector<PathSegment> findDirectionalPathSegments(
const nav_msgs::msg::Path & path)
{
std::vector<PathSegment> segments;
PathSegment curr_segment;
curr_segment.start = 0;
// Iterating through the path to determine the position of the cusp
for (unsigned int idx = 1; idx < path.poses.size() - 1; ++idx) {
// We have two vectors for the dot product OA and AB. Determining the vectors.
double oa_x = path.poses[idx].pose.position.x -
path.poses[idx - 1].pose.position.x;
double oa_y = path.poses[idx].pose.position.y -
path.poses[idx - 1].pose.position.y;
double ab_x = path.poses[idx + 1].pose.position.x -
path.poses[idx].pose.position.x;
double ab_y = path.poses[idx + 1].pose.position.y -
path.poses[idx].pose.position.y;
// Checking for the existance of cusp, in the path, using the dot product.
double dot_product = (oa_x * ab_x) + (oa_y * ab_y);
if (dot_product < 0.0) {
curr_segment.end = idx;
segments.push_back(curr_segment);
curr_segment.start = idx;
}
// Checking for the existance of a differential rotation in place.
double cur_theta = tf2::getYaw(path.poses[idx].pose.orientation);
double next_theta = tf2::getYaw(path.poses[idx + 1].pose.orientation);
double dtheta = angles::shortest_angular_distance(cur_theta, next_theta);
if (fabs(ab_x) < 1e-4 && fabs(ab_y) < 1e-4 && fabs(dtheta) > 1e-4) {
curr_segment.end = idx;
segments.push_back(curr_segment);
curr_segment.start = idx;
}
}
curr_segment.end = path.poses.size() - 1;
segments.push_back(curr_segment);
return segments;
}
inline void updateApproximatePathOrientations(
nav_msgs::msg::Path & path,
bool & reversing_segment)
{
double dx, dy, theta, pt_yaw;
reversing_segment = false;
// Find if this path segment is in reverse
dx = path.poses[2].pose.position.x - path.poses[1].pose.position.x;
dy = path.poses[2].pose.position.y - path.poses[1].pose.position.y;
theta = atan2(dy, dx);
pt_yaw = tf2::getYaw(path.poses[1].pose.orientation);
if (fabs(angles::shortest_angular_distance(pt_yaw, theta)) > M_PI_2) {
reversing_segment = true;
}
// Find the angle relative the path position vectors
for (unsigned int i = 0; i != path.poses.size() - 1; i++) {
dx = path.poses[i + 1].pose.position.x - path.poses[i].pose.position.x;
dy = path.poses[i + 1].pose.position.y - path.poses[i].pose.position.y;
theta = atan2(dy, dx);
// If points are overlapping, pass
if (fabs(dx) < 1e-4 && fabs(dy) < 1e-4) {
continue;
}
// Flip the angle if this path segment is in reverse
if (reversing_segment) {
theta += M_PI; // orientationAroundZAxis will normalize
}
path.poses[i].pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(theta);
}
}
} // namespace smoother_utils
#endif // NAV2_SMOOTHER__SMOOTHER_UTILS_HPP_
+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>nav2_smoother</name>
<version>1.1.18</version>
<description>Smoother action interface</description>
<maintainer email="vargovcik@robotechvision.com">Matej Vargovcik</maintainer>
<maintainer email="stevenmacenski@gmail.com">Steve Macenski</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<build_depend>nav2_common</build_depend>
<depend>angles</depend>
<depend>rclcpp</depend>
<depend>rclcpp_components</depend>
<depend>rclcpp_action</depend>
<depend>std_msgs</depend>
<depend>nav2_util</depend>
<depend>nav2_msgs</depend>
<depend>nav_2d_utils</depend>
<depend>nav_2d_msgs</depend>
<depend>nav2_core</depend>
<depend>pluginlib</depend>
<test_depend>ament_lint_common</test_depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_cmake_gtest</test_depend>
<test_depend>ament_cmake_pytest</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
+13
View File
@@ -0,0 +1,13 @@
<class_libraries>
<library path="simple_smoother">
<class type="nav2_smoother::SimpleSmoother" base_class_type="nav2_core::Smoother">
<description>Does a simple smoothing process with collision checking</description>
</class>
</library>
<library path="savitzky_golay_smoother">
<class type="nav2_smoother::SavitzkyGolaySmoother" base_class_type="nav2_core::Smoother">
<description>Does Savitzky-Golay smoothing</description>
</class>
</library>
</class_libraries>
+29
View File
@@ -0,0 +1,29 @@
// Copyright (c) 2021 RoboTech Vision
// 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.
#include <memory>
#include "nav2_smoother/nav2_smoother.hpp"
#include "rclcpp/rclcpp.hpp"
int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<nav2_smoother::SmootherServer>();
rclcpp::spin(node->get_node_base_interface());
rclcpp::shutdown();
return 0;
}
@@ -0,0 +1,338 @@
// Copyright (c) 2019 RoboTech Vision
// Copyright (c) 2019 Intel Corporation
// Copyright (c) 2022 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.
#include <chrono>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "nav2_core/exceptions.hpp"
#include "nav2_smoother/nav2_smoother.hpp"
#include "nav2_util/geometry_utils.hpp"
#include "nav2_util/node_utils.hpp"
#include "nav_2d_utils/conversions.hpp"
#include "nav_2d_utils/tf_help.hpp"
#include "tf2_ros/create_timer_ros.h"
using namespace std::chrono_literals;
namespace nav2_smoother
{
SmootherServer::SmootherServer(const rclcpp::NodeOptions & options)
: LifecycleNode("smoother_server", "", options),
lp_loader_("nav2_core", "nav2_core::Smoother"),
default_ids_{"simple_smoother"},
default_types_{"nav2_smoother::SimpleSmoother"}
{
RCLCPP_INFO(get_logger(), "Creating smoother server");
declare_parameter(
"costmap_topic", rclcpp::ParameterValue(
std::string(
"global_costmap/costmap_raw")));
declare_parameter(
"footprint_topic",
rclcpp::ParameterValue(
std::string("global_costmap/published_footprint")));
declare_parameter(
"robot_base_frame",
rclcpp::ParameterValue(std::string("base_link")));
declare_parameter("transform_tolerance", rclcpp::ParameterValue(0.1));
declare_parameter("smoother_plugins", default_ids_);
}
SmootherServer::~SmootherServer()
{
smoothers_.clear();
}
nav2_util::CallbackReturn
SmootherServer::on_configure(const rclcpp_lifecycle::State &)
{
RCLCPP_INFO(get_logger(), "Configuring smoother server");
auto node = shared_from_this();
get_parameter("smoother_plugins", smoother_ids_);
if (smoother_ids_ == default_ids_) {
for (size_t i = 0; i < default_ids_.size(); ++i) {
nav2_util::declare_parameter_if_not_declared(
node, default_ids_[i] + ".plugin",
rclcpp::ParameterValue(default_types_[i]));
}
}
tf_ = std::make_shared<tf2_ros::Buffer>(get_clock());
auto timer_interface = std::make_shared<tf2_ros::CreateTimerROS>(
get_node_base_interface(), get_node_timers_interface());
tf_->setCreateTimerInterface(timer_interface);
transform_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_);
std::string costmap_topic, footprint_topic, robot_base_frame;
double transform_tolerance;
this->get_parameter("costmap_topic", costmap_topic);
this->get_parameter("footprint_topic", footprint_topic);
this->get_parameter("transform_tolerance", transform_tolerance);
this->get_parameter("robot_base_frame", robot_base_frame);
costmap_sub_ = std::make_shared<nav2_costmap_2d::CostmapSubscriber>(
shared_from_this(), costmap_topic);
footprint_sub_ = std::make_shared<nav2_costmap_2d::FootprintSubscriber>(
shared_from_this(), footprint_topic, *tf_, robot_base_frame, transform_tolerance);
collision_checker_ =
std::make_shared<nav2_costmap_2d::CostmapTopicCollisionChecker>(
*costmap_sub_, *footprint_sub_, this->get_name());
if (!loadSmootherPlugins()) {
return nav2_util::CallbackReturn::FAILURE;
}
// Initialize pubs & subs
plan_publisher_ = create_publisher<nav_msgs::msg::Path>("plan_smoothed", 1);
// Create the action server that we implement with our smoothPath method
action_server_ = std::make_unique<ActionServer>(
shared_from_this(),
"smooth_path",
std::bind(&SmootherServer::smoothPlan, this),
nullptr,
std::chrono::milliseconds(500),
true);
return nav2_util::CallbackReturn::SUCCESS;
}
bool SmootherServer::loadSmootherPlugins()
{
auto node = shared_from_this();
smoother_types_.resize(smoother_ids_.size());
for (size_t i = 0; i != smoother_ids_.size(); i++) {
try {
smoother_types_[i] =
nav2_util::get_plugin_type_param(node, smoother_ids_[i]);
nav2_core::Smoother::Ptr smoother =
lp_loader_.createUniqueInstance(smoother_types_[i]);
RCLCPP_INFO(
get_logger(), "Created smoother : %s of type %s",
smoother_ids_[i].c_str(), smoother_types_[i].c_str());
smoother->configure(
node, smoother_ids_[i], tf_, costmap_sub_,
footprint_sub_);
smoothers_.insert({smoother_ids_[i], smoother});
} catch (const pluginlib::PluginlibException & ex) {
RCLCPP_FATAL(
get_logger(), "Failed to create smoother. Exception: %s",
ex.what());
return false;
}
}
for (size_t i = 0; i != smoother_ids_.size(); i++) {
smoother_ids_concat_ += smoother_ids_[i] + std::string(" ");
}
RCLCPP_INFO(
get_logger(), "Smoother Server has %s smoothers available.",
smoother_ids_concat_.c_str());
return true;
}
nav2_util::CallbackReturn
SmootherServer::on_activate(const rclcpp_lifecycle::State &)
{
RCLCPP_INFO(get_logger(), "Activating");
plan_publisher_->on_activate();
SmootherMap::iterator it;
for (it = smoothers_.begin(); it != smoothers_.end(); ++it) {
it->second->activate();
}
action_server_->activate();
// create bond connection
createBond();
return nav2_util::CallbackReturn::SUCCESS;
}
nav2_util::CallbackReturn
SmootherServer::on_deactivate(const rclcpp_lifecycle::State &)
{
RCLCPP_INFO(get_logger(), "Deactivating");
action_server_->deactivate();
SmootherMap::iterator it;
for (it = smoothers_.begin(); it != smoothers_.end(); ++it) {
it->second->deactivate();
}
plan_publisher_->on_deactivate();
// destroy bond connection
destroyBond();
return nav2_util::CallbackReturn::SUCCESS;
}
nav2_util::CallbackReturn
SmootherServer::on_cleanup(const rclcpp_lifecycle::State &)
{
RCLCPP_INFO(get_logger(), "Cleaning up");
// Cleanup the helper classes
SmootherMap::iterator it;
for (it = smoothers_.begin(); it != smoothers_.end(); ++it) {
it->second->cleanup();
}
smoothers_.clear();
// Release any allocated resources
action_server_.reset();
plan_publisher_.reset();
transform_listener_.reset();
tf_.reset();
footprint_sub_.reset();
costmap_sub_.reset();
collision_checker_.reset();
return nav2_util::CallbackReturn::SUCCESS;
}
nav2_util::CallbackReturn
SmootherServer::on_shutdown(const rclcpp_lifecycle::State &)
{
RCLCPP_INFO(get_logger(), "Shutting down");
return nav2_util::CallbackReturn::SUCCESS;
}
bool SmootherServer::findSmootherId(
const std::string & c_name,
std::string & current_smoother)
{
if (smoothers_.find(c_name) == smoothers_.end()) {
if (smoothers_.size() == 1 && c_name.empty()) {
RCLCPP_WARN_ONCE(
get_logger(),
"No smoother was specified in action call."
" Server will use only plugin loaded %s. "
"This warning will appear once.",
smoother_ids_concat_.c_str());
current_smoother = smoothers_.begin()->first;
} else {
RCLCPP_ERROR(
get_logger(),
"SmoothPath called with smoother name %s, "
"which does not exist. Available smoothers are: %s.",
c_name.c_str(), smoother_ids_concat_.c_str());
return false;
}
} else {
RCLCPP_DEBUG(get_logger(), "Selected smoother: %s.", c_name.c_str());
current_smoother = c_name;
}
return true;
}
void SmootherServer::smoothPlan()
{
auto start_time = this->now();
RCLCPP_INFO(get_logger(), "Received a path to smooth.");
auto result = std::make_shared<Action::Result>();
try {
auto goal = action_server_->get_current_goal();
if (!goal) {
return; // if action_server_ is inactivate, goal would be a nullptr
}
std::string c_name = goal->smoother_id;
std::string current_smoother;
if (findSmootherId(c_name, current_smoother)) {
current_smoother_ = current_smoother;
} else {
action_server_->terminate_current();
return;
}
// Perform smoothing
result->path = goal->path;
result->was_completed = smoothers_[current_smoother_]->smooth(
result->path, goal->max_smoothing_duration);
result->smoothing_duration = this->now() - start_time;
if (!result->was_completed) {
RCLCPP_INFO(
get_logger(),
"Smoother %s did not complete smoothing in specified time limit"
"(%lf seconds) and was interrupted after %lf seconds",
current_smoother_.c_str(),
rclcpp::Duration(goal->max_smoothing_duration).seconds(),
rclcpp::Duration(result->smoothing_duration).seconds());
}
plan_publisher_->publish(result->path);
// Check for collisions
if (goal->check_for_collisions) {
geometry_msgs::msg::Pose2D pose2d;
bool fetch_data = true;
for (const auto & pose : result->path.poses) {
pose2d.x = pose.pose.position.x;
pose2d.y = pose.pose.position.y;
pose2d.theta = tf2::getYaw(pose.pose.orientation);
if (!collision_checker_->isCollisionFree(pose2d, fetch_data)) {
RCLCPP_ERROR(
get_logger(),
"Smoothed path leads to a collision at x: %lf, y: %lf, theta: %lf",
pose2d.x, pose2d.y, pose2d.theta);
action_server_->terminate_current(result);
return;
}
fetch_data = false;
}
}
RCLCPP_DEBUG(
get_logger(), "Smoother succeeded (time: %lf), setting result",
rclcpp::Duration(result->smoothing_duration).seconds());
action_server_->succeeded_current(result);
} catch (nav2_core::PlannerException & e) {
RCLCPP_ERROR(this->get_logger(), "%s", e.what());
action_server_->terminate_current();
return;
} catch (std::exception & ex) {
RCLCPP_ERROR(this->get_logger(), "%s", ex.what());
action_server_->terminate_current(result);
return;
}
}
} // namespace nav2_smoother
#include "rclcpp_components/register_node_macro.hpp"
// Register the component with class_loader.
// This acts as a sort of entry point, allowing the component to be discoverable when its library
// is being loaded into a running process.
RCLCPP_COMPONENTS_REGISTER_NODE(nav2_smoother::SmootherServer)
@@ -0,0 +1,198 @@
// Copyright (c) 2022, 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. Reserved.
#include <vector>
#include <memory>
#include "nav2_smoother/savitzky_golay_smoother.hpp"
namespace nav2_smoother
{
using namespace smoother_utils; // NOLINT
using namespace nav2_util::geometry_utils; // NOLINT
using namespace std::chrono; // NOLINT
using nav2_util::declare_parameter_if_not_declared;
void SavitzkyGolaySmoother::configure(
const rclcpp_lifecycle::LifecycleNode::WeakPtr & parent,
std::string name, std::shared_ptr<tf2_ros::Buffer>/*tf*/,
std::shared_ptr<nav2_costmap_2d::CostmapSubscriber>/*costmap_sub*/,
std::shared_ptr<nav2_costmap_2d::FootprintSubscriber>/*footprint_sub*/)
{
auto node = parent.lock();
logger_ = node->get_logger();
declare_parameter_if_not_declared(
node, name + ".do_refinement", rclcpp::ParameterValue(true));
declare_parameter_if_not_declared(
node, name + ".refinement_num", rclcpp::ParameterValue(2));
node->get_parameter(name + ".do_refinement", do_refinement_);
node->get_parameter(name + ".refinement_num", refinement_num_);
}
bool SavitzkyGolaySmoother::smooth(
nav_msgs::msg::Path & path,
const rclcpp::Duration & max_time)
{
steady_clock::time_point start = steady_clock::now();
double time_remaining = max_time.seconds();
bool success = true, reversing_segment;
nav_msgs::msg::Path curr_path_segment;
curr_path_segment.header = path.header;
std::vector<PathSegment> path_segments = findDirectionalPathSegments(path);
for (unsigned int i = 0; i != path_segments.size(); i++) {
if (path_segments[i].end - path_segments[i].start > 9) {
// Populate path segment
curr_path_segment.poses.clear();
std::copy(
path.poses.begin() + path_segments[i].start,
path.poses.begin() + path_segments[i].end + 1,
std::back_inserter(curr_path_segment.poses));
// Make sure we're still able to smooth with time remaining
steady_clock::time_point now = steady_clock::now();
time_remaining = max_time.seconds() - duration_cast<duration<double>>(now - start).count();
if (time_remaining <= 0.0) {
RCLCPP_WARN(
logger_,
"Smoothing time exceeded allowed duration of %0.2f.", max_time.seconds());
return false;
}
// Smooth path segment
success = success && smoothImpl(curr_path_segment, reversing_segment);
// Assemble the path changes to the main path
std::copy(
curr_path_segment.poses.begin(),
curr_path_segment.poses.end(),
path.poses.begin() + path_segments[i].start);
}
}
return success;
}
bool SavitzkyGolaySmoother::smoothImpl(
nav_msgs::msg::Path & path,
bool & reversing_segment)
{
// Must be at least 10 in length to enter function
const unsigned int & path_size = path.poses.size();
// 7-point SG filter
const std::array<double, 7> filter = {
-2.0 / 21.0,
3.0 / 21.0,
6.0 / 21.0,
7.0 / 21.0,
6.0 / 21.0,
3.0 / 21.0,
-2.0 / 21.0};
auto applyFilter = [&](const std::vector<geometry_msgs::msg::Point> & data)
-> geometry_msgs::msg::Point
{
geometry_msgs::msg::Point val;
for (unsigned int i = 0; i != filter.size(); i++) {
val.x += filter[i] * data[i].x;
val.y += filter[i] * data[i].y;
}
return val;
};
auto applyFilterOverAxes =
[&](std::vector<geometry_msgs::msg::PoseStamped> & plan_pts) -> void
{
// Handle initial boundary conditions, first point is fixed
unsigned int idx = 1;
plan_pts[idx].pose.position = applyFilter(
{
plan_pts[idx - 1].pose.position,
plan_pts[idx - 1].pose.position,
plan_pts[idx - 1].pose.position,
plan_pts[idx].pose.position,
plan_pts[idx + 1].pose.position,
plan_pts[idx + 2].pose.position,
plan_pts[idx + 3].pose.position});
idx++;
plan_pts[idx].pose.position = applyFilter(
{
plan_pts[idx - 2].pose.position,
plan_pts[idx - 2].pose.position,
plan_pts[idx - 1].pose.position,
plan_pts[idx].pose.position,
plan_pts[idx + 1].pose.position,
plan_pts[idx + 2].pose.position,
plan_pts[idx + 3].pose.position});
// Apply nominal filter
for (idx = 3; idx < path_size - 4; ++idx) {
plan_pts[idx].pose.position = applyFilter(
{
plan_pts[idx - 3].pose.position,
plan_pts[idx - 2].pose.position,
plan_pts[idx - 1].pose.position,
plan_pts[idx].pose.position,
plan_pts[idx + 1].pose.position,
plan_pts[idx + 2].pose.position,
plan_pts[idx + 3].pose.position});
}
// Handle terminal boundary conditions, last point is fixed
idx++;
plan_pts[idx].pose.position = applyFilter(
{
plan_pts[idx - 3].pose.position,
plan_pts[idx - 2].pose.position,
plan_pts[idx - 1].pose.position,
plan_pts[idx].pose.position,
plan_pts[idx + 1].pose.position,
plan_pts[idx + 2].pose.position,
plan_pts[idx + 2].pose.position});
idx++;
plan_pts[idx].pose.position = applyFilter(
{
plan_pts[idx - 3].pose.position,
plan_pts[idx - 2].pose.position,
plan_pts[idx - 1].pose.position,
plan_pts[idx].pose.position,
plan_pts[idx + 1].pose.position,
plan_pts[idx + 1].pose.position,
plan_pts[idx + 1].pose.position});
};
applyFilterOverAxes(path.poses);
// Lets do additional refinement, it shouldn't take more than a couple milliseconds
if (do_refinement_) {
for (int i = 0; i < refinement_num_; i++) {
applyFilterOverAxes(path.poses);
}
}
updateApproximatePathOrientations(path, reversing_segment);
return true;
}
} // namespace nav2_smoother
#include "pluginlib/class_list_macros.hpp"
PLUGINLIB_EXPORT_CLASS(nav2_smoother::SavitzkyGolaySmoother, nav2_core::Smoother)
@@ -0,0 +1,221 @@
// Copyright (c) 2022, 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. Reserved.
#include <vector>
#include <memory>
#include "nav2_smoother/simple_smoother.hpp"
namespace nav2_smoother
{
using namespace smoother_utils; // NOLINT
using namespace nav2_util::geometry_utils; // NOLINT
using namespace std::chrono; // NOLINT
using nav2_util::declare_parameter_if_not_declared;
void SimpleSmoother::configure(
const rclcpp_lifecycle::LifecycleNode::WeakPtr & parent,
std::string name, std::shared_ptr<tf2_ros::Buffer>/*tf*/,
std::shared_ptr<nav2_costmap_2d::CostmapSubscriber> costmap_sub,
std::shared_ptr<nav2_costmap_2d::FootprintSubscriber>/*footprint_sub*/)
{
costmap_sub_ = costmap_sub;
auto node = parent.lock();
logger_ = node->get_logger();
declare_parameter_if_not_declared(
node, name + ".tolerance", rclcpp::ParameterValue(1e-10));
declare_parameter_if_not_declared(
node, name + ".max_its", rclcpp::ParameterValue(1000));
declare_parameter_if_not_declared(
node, name + ".w_data", rclcpp::ParameterValue(0.2));
declare_parameter_if_not_declared(
node, name + ".w_smooth", rclcpp::ParameterValue(0.3));
declare_parameter_if_not_declared(
node, name + ".do_refinement", rclcpp::ParameterValue(true));
node->get_parameter(name + ".tolerance", tolerance_);
node->get_parameter(name + ".max_its", max_its_);
node->get_parameter(name + ".w_data", data_w_);
node->get_parameter(name + ".w_smooth", smooth_w_);
node->get_parameter(name + ".do_refinement", do_refinement_);
}
bool SimpleSmoother::smooth(
nav_msgs::msg::Path & path,
const rclcpp::Duration & max_time)
{
auto costmap = costmap_sub_->getCostmap();
refinement_ctr_ = 0;
steady_clock::time_point start = steady_clock::now();
double time_remaining = max_time.seconds();
bool success = true, reversing_segment;
nav_msgs::msg::Path curr_path_segment;
curr_path_segment.header = path.header;
std::vector<PathSegment> path_segments = findDirectionalPathSegments(path);
for (unsigned int i = 0; i != path_segments.size(); i++) {
if (path_segments[i].end - path_segments[i].start > 9) {
// Populate path segment
curr_path_segment.poses.clear();
std::copy(
path.poses.begin() + path_segments[i].start,
path.poses.begin() + path_segments[i].end + 1,
std::back_inserter(curr_path_segment.poses));
// Make sure we're still able to smooth with time remaining
steady_clock::time_point now = steady_clock::now();
time_remaining = max_time.seconds() - duration_cast<duration<double>>(now - start).count();
// Smooth path segment naively
success = success && smoothImpl(
curr_path_segment, reversing_segment, costmap.get(), time_remaining);
// Assemble the path changes to the main path
std::copy(
curr_path_segment.poses.begin(),
curr_path_segment.poses.end(),
path.poses.begin() + path_segments[i].start);
}
}
return success;
}
bool SimpleSmoother::smoothImpl(
nav_msgs::msg::Path & path,
bool & reversing_segment,
const nav2_costmap_2d::Costmap2D * costmap,
const double & max_time)
{
steady_clock::time_point a = steady_clock::now();
rclcpp::Duration max_dur = rclcpp::Duration::from_seconds(max_time);
int its = 0;
double change = tolerance_;
const unsigned int & path_size = path.poses.size();
double x_i, y_i, y_m1, y_ip1, y_i_org;
unsigned int mx, my;
nav_msgs::msg::Path new_path = path;
nav_msgs::msg::Path last_path = path;
while (change >= tolerance_) {
its += 1;
change = 0.0;
// Make sure the smoothing function will converge
if (its >= max_its_) {
RCLCPP_WARN(
logger_,
"Number of iterations has exceeded limit of %i.", max_its_);
path = last_path;
updateApproximatePathOrientations(path, reversing_segment);
return false;
}
// Make sure still have time left to process
steady_clock::time_point b = steady_clock::now();
rclcpp::Duration timespan(duration_cast<duration<double>>(b - a));
if (timespan > max_dur) {
RCLCPP_WARN(
logger_,
"Smoothing time exceeded allowed duration of %0.2f.", max_time);
path = last_path;
updateApproximatePathOrientations(path, reversing_segment);
return false;
}
for (unsigned int i = 1; i != path_size - 1; i++) {
for (unsigned int j = 0; j != 2; j++) {
x_i = getFieldByDim(path.poses[i], j);
y_i = getFieldByDim(new_path.poses[i], j);
y_m1 = getFieldByDim(new_path.poses[i - 1], j);
y_ip1 = getFieldByDim(new_path.poses[i + 1], j);
y_i_org = y_i;
// Smooth based on local 3 point neighborhood and original data locations
y_i += data_w_ * (x_i - y_i) + smooth_w_ * (y_ip1 + y_m1 - (2.0 * y_i));
setFieldByDim(new_path.poses[i], j, y_i);
change += abs(y_i - y_i_org);
}
// validate update is admissible, only checks cost if a valid costmap pointer is provided
float cost = 0.0;
if (costmap) {
costmap->worldToMap(
getFieldByDim(new_path.poses[i], 0),
getFieldByDim(new_path.poses[i], 1),
mx, my);
cost = static_cast<float>(costmap->getCost(mx, my));
}
if (cost > nav2_costmap_2d::MAX_NON_OBSTACLE && cost != nav2_costmap_2d::NO_INFORMATION) {
RCLCPP_DEBUG(
rclcpp::get_logger("SmacPlannerSmoother"),
"Smoothing process resulted in an infeasible collision. "
"Returning the last path before the infeasibility was introduced.");
path = last_path;
updateApproximatePathOrientations(path, reversing_segment);
return false;
}
}
last_path = new_path;
}
// Lets do additional refinement, it shouldn't take more than a couple milliseconds
// but really puts the path quality over the top.
if (do_refinement_ && refinement_ctr_ < 4) {
refinement_ctr_++;
smoothImpl(new_path, reversing_segment, costmap, max_time);
}
updateApproximatePathOrientations(new_path, reversing_segment);
path = new_path;
return true;
}
double SimpleSmoother::getFieldByDim(
const geometry_msgs::msg::PoseStamped & msg, const unsigned int & dim)
{
if (dim == 0) {
return msg.pose.position.x;
} else if (dim == 1) {
return msg.pose.position.y;
} else {
return msg.pose.position.z;
}
}
void SimpleSmoother::setFieldByDim(
geometry_msgs::msg::PoseStamped & msg, const unsigned int dim,
const double & value)
{
if (dim == 0) {
msg.pose.position.x = value;
} else if (dim == 1) {
msg.pose.position.y = value;
} else {
msg.pose.position.z = value;
}
}
} // namespace nav2_smoother
#include "pluginlib/class_list_macros.hpp"
PLUGINLIB_EXPORT_CLASS(nav2_smoother::SimpleSmoother, nav2_core::Smoother)
@@ -0,0 +1,36 @@
ament_add_gtest(test_smoother_server
test_smoother_server.cpp
)
target_link_libraries(test_smoother_server
${library_name}
)
ament_target_dependencies(test_smoother_server
${dependencies}
)
ament_add_gtest(test_simple_smoother
test_simple_smoother.cpp
)
target_link_libraries(test_simple_smoother
simple_smoother
)
ament_target_dependencies(test_simple_smoother
${dependencies}
)
ament_add_gtest(test_savitzky_golay_smoother
test_savitzky_golay_smoother.cpp
)
target_link_libraries(test_savitzky_golay_smoother
savitzky_golay_smoother
)
ament_target_dependencies(test_savitzky_golay_smoother
${dependencies}
)
@@ -0,0 +1,331 @@
// Copyright (c) 2022, 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. Reserved.
#include <math.h>
#include <memory>
#include <string>
#include <vector>
#include <chrono>
#include <limits>
#include <random>
#include "gtest/gtest.h"
#include "rclcpp/rclcpp.hpp"
#include "nav2_costmap_2d/costmap_2d.hpp"
#include "nav2_costmap_2d/costmap_subscriber.hpp"
#include "nav2_msgs/msg/costmap.hpp"
#include "nav2_util/lifecycle_node.hpp"
#include "nav2_smoother/savitzky_golay_smoother.hpp"
#include "ament_index_cpp/get_package_share_directory.hpp"
using namespace smoother_utils; // NOLINT
using namespace nav2_smoother; // NOLINT
using namespace std::chrono_literals; // NOLINT
class RclCppFixture
{
public:
RclCppFixture() {rclcpp::init(0, nullptr);}
~RclCppFixture() {rclcpp::shutdown();}
};
RclCppFixture g_rclcppfixture;
TEST(SmootherTest, test_sg_smoother_basics)
{
rclcpp_lifecycle::LifecycleNode::SharedPtr node =
std::make_shared<rclcpp_lifecycle::LifecycleNode>("SmacSGSmootherTest");
std::shared_ptr<nav2_msgs::msg::Costmap> costmap_msg =
std::make_shared<nav2_msgs::msg::Costmap>();
costmap_msg->header.stamp = node->now();
costmap_msg->header.frame_id = "map";
costmap_msg->data.resize(100 * 100);
costmap_msg->metadata.resolution = 0.05;
costmap_msg->metadata.size_x = 100;
costmap_msg->metadata.size_y = 100;
std::weak_ptr<rclcpp_lifecycle::LifecycleNode> parent = node;
std::shared_ptr<nav2_costmap_2d::CostmapSubscriber> dummy_costmap;
dummy_costmap = std::make_shared<nav2_costmap_2d::CostmapSubscriber>(parent, "dummy_topic");
dummy_costmap->costmapCallback(costmap_msg);
// Make smoother
std::shared_ptr<tf2_ros::Buffer> dummy_tf;
std::shared_ptr<nav2_costmap_2d::FootprintSubscriber> dummy_footprint;
node->declare_parameter("test.do_refinement", rclcpp::ParameterValue(false));
auto smoother = std::make_unique<nav2_smoother::SavitzkyGolaySmoother>();
smoother->configure(parent, "test", dummy_tf, dummy_costmap, dummy_footprint);
smoother->activate();
rclcpp::Duration max_time = rclcpp::Duration::from_seconds(1.0); // 1 seconds
// Test regular path, should see no effective change
nav_msgs::msg::Path straight_regular_path, straight_regular_path_baseline;
straight_regular_path.header.frame_id = "map";
straight_regular_path.header.stamp = node->now();
straight_regular_path.poses.resize(11);
straight_regular_path.poses[0].pose.position.x = 0.5;
straight_regular_path.poses[0].pose.position.y = 0.1;
straight_regular_path.poses[1].pose.position.x = 0.5;
straight_regular_path.poses[1].pose.position.y = 0.2;
straight_regular_path.poses[2].pose.position.x = 0.5;
straight_regular_path.poses[2].pose.position.y = 0.3;
straight_regular_path.poses[3].pose.position.x = 0.5;
straight_regular_path.poses[3].pose.position.y = 0.4;
straight_regular_path.poses[4].pose.position.x = 0.5;
straight_regular_path.poses[4].pose.position.y = 0.5;
straight_regular_path.poses[5].pose.position.x = 0.5;
straight_regular_path.poses[5].pose.position.y = 0.6;
straight_regular_path.poses[6].pose.position.x = 0.5;
straight_regular_path.poses[6].pose.position.y = 0.7;
straight_regular_path.poses[7].pose.position.x = 0.5;
straight_regular_path.poses[7].pose.position.y = 0.8;
straight_regular_path.poses[8].pose.position.x = 0.5;
straight_regular_path.poses[8].pose.position.y = 0.9;
straight_regular_path.poses[9].pose.position.x = 0.5;
straight_regular_path.poses[9].pose.position.y = 1.0;
straight_regular_path.poses[10].pose.position.x = 0.5;
straight_regular_path.poses[10].pose.position.y = 1.1;
straight_regular_path_baseline = straight_regular_path;
EXPECT_TRUE(smoother->smooth(straight_regular_path, max_time));
for (uint i = 0; i != straight_regular_path.poses.size() - 1; i++) {
// Check distances are still the same
EXPECT_NEAR(
fabs(
straight_regular_path.poses[i].pose.position.y -
straight_regular_path_baseline.poses[i].pose.position.y), 0.0, 0.011);
}
// Attempt smoothing with no time given, should fail
rclcpp::Duration no_time = rclcpp::Duration::from_seconds(-1.0); // 0 seconds
EXPECT_FALSE(smoother->smooth(straight_regular_path, no_time));
smoother->deactivate();
smoother->cleanup();
}
TEST(SmootherTest, test_sg_smoother_noisey_path)
{
rclcpp_lifecycle::LifecycleNode::SharedPtr node =
std::make_shared<rclcpp_lifecycle::LifecycleNode>("SmacSGSmootherTest");
std::shared_ptr<nav2_msgs::msg::Costmap> costmap_msg =
std::make_shared<nav2_msgs::msg::Costmap>();
costmap_msg->header.stamp = node->now();
costmap_msg->header.frame_id = "map";
costmap_msg->data.resize(100 * 100);
costmap_msg->metadata.resolution = 0.05;
costmap_msg->metadata.size_x = 100;
costmap_msg->metadata.size_y = 100;
std::weak_ptr<rclcpp_lifecycle::LifecycleNode> parent = node;
std::shared_ptr<nav2_costmap_2d::CostmapSubscriber> dummy_costmap;
dummy_costmap = std::make_shared<nav2_costmap_2d::CostmapSubscriber>(parent, "dummy_topic");
dummy_costmap->costmapCallback(costmap_msg);
// Make smoother
std::shared_ptr<tf2_ros::Buffer> dummy_tf;
std::shared_ptr<nav2_costmap_2d::FootprintSubscriber> dummy_footprint;
node->declare_parameter("test.do_refinement", rclcpp::ParameterValue(false));
auto smoother = std::make_unique<nav2_smoother::SavitzkyGolaySmoother>();
smoother->configure(parent, "test", dummy_tf, dummy_costmap, dummy_footprint);
rclcpp::Duration max_time = rclcpp::Duration::from_seconds(1.0); // 1 seconds
// Given nominal irregular/noisey path, test that the output is shorter and smoother
nav_msgs::msg::Path noisey_path, noisey_path_baseline;
noisey_path.header.frame_id = "map";
noisey_path.header.stamp = node->now();
noisey_path.poses.resize(11);
noisey_path.poses[0].pose.position.x = 0.5;
noisey_path.poses[0].pose.position.y = 0.1;
noisey_path.poses[1].pose.position.x = 0.5;
noisey_path.poses[1].pose.position.y = 0.2;
noisey_path.poses[2].pose.position.x = 0.5;
noisey_path.poses[2].pose.position.y = 0.3;
noisey_path.poses[3].pose.position.x = 0.5;
noisey_path.poses[3].pose.position.y = 0.4;
noisey_path.poses[4].pose.position.x = 0.5;
noisey_path.poses[4].pose.position.y = 0.5;
noisey_path.poses[5].pose.position.x = 0.5;
noisey_path.poses[5].pose.position.y = 0.6;
noisey_path.poses[6].pose.position.x = 0.5;
noisey_path.poses[6].pose.position.y = 0.7;
noisey_path.poses[7].pose.position.x = 0.5;
noisey_path.poses[7].pose.position.y = 0.8;
noisey_path.poses[8].pose.position.x = 0.5;
noisey_path.poses[8].pose.position.y = 0.9;
noisey_path.poses[9].pose.position.x = 0.5;
noisey_path.poses[9].pose.position.y = 1.0;
noisey_path.poses[10].pose.position.x = 0.5;
noisey_path.poses[10].pose.position.y = 1.1;
// Add random but deterministic noises
std::random_device rd{};
std::mt19937 gen{rd()};
std::normal_distribution<> normal_distribution{0.0, 0.02};
for (unsigned int i = 0; i != noisey_path.poses.size(); i++) {
auto noise = normal_distribution(gen);
noisey_path.poses[i].pose.position.x += noise;
}
noisey_path_baseline = noisey_path;
EXPECT_TRUE(smoother->smooth(noisey_path, max_time));
// Compute metric, should be shorter if smoother
double length = 0;
double base_length = 0;
for (unsigned int i = 0; i != noisey_path.poses.size() - 1; i++) {
length += std::hypot(
noisey_path.poses[i + 1].pose.position.x - noisey_path.poses[i].pose.position.x,
noisey_path.poses[i + 1].pose.position.y - noisey_path.poses[i].pose.position.y);
base_length += std::hypot(
noisey_path_baseline.poses[i + 1].pose.position.x -
noisey_path_baseline.poses[i].pose.position.x,
noisey_path_baseline.poses[i + 1].pose.position.y -
noisey_path_baseline.poses[i].pose.position.y);
}
EXPECT_LT(length, base_length);
// Test again with refinement, even shorter and smoother
node->set_parameter(rclcpp::Parameter("test.do_refinement", rclcpp::ParameterValue(true)));
smoother->configure(parent, "test", dummy_tf, dummy_costmap, dummy_footprint);
nav_msgs::msg::Path noisey_path_refined = noisey_path_baseline;
EXPECT_TRUE(smoother->smooth(noisey_path_refined, max_time));
length = 0;
for (unsigned int i = 0; i != noisey_path.poses.size() - 1; i++) {
length += std::hypot(
noisey_path_refined.poses[i + 1].pose.position.x -
noisey_path_refined.poses[i].pose.position.x,
noisey_path_refined.poses[i + 1].pose.position.y -
noisey_path_refined.poses[i].pose.position.y);
// std::hypot(
// noisey_path.poses[i + 1].pose.position.x - noisey_path_baseline.poses[i].pose.position.x,
// noisey_path.poses[i + 1].pose.position.y - noisey_path_baseline.poses[i].pose.position.y);
}
EXPECT_LT(length, base_length);
}
TEST(SmootherTest, test_sg_smoother_reversing)
{
rclcpp_lifecycle::LifecycleNode::SharedPtr node =
std::make_shared<rclcpp_lifecycle::LifecycleNode>("SmacSGSmootherTest");
std::shared_ptr<nav2_msgs::msg::Costmap> costmap_msg =
std::make_shared<nav2_msgs::msg::Costmap>();
costmap_msg->header.stamp = node->now();
costmap_msg->header.frame_id = "map";
costmap_msg->data.resize(100 * 100);
costmap_msg->metadata.resolution = 0.05;
costmap_msg->metadata.size_x = 100;
costmap_msg->metadata.size_y = 100;
std::weak_ptr<rclcpp_lifecycle::LifecycleNode> parent = node;
std::shared_ptr<nav2_costmap_2d::CostmapSubscriber> dummy_costmap;
dummy_costmap = std::make_shared<nav2_costmap_2d::CostmapSubscriber>(parent, "dummy_topic");
dummy_costmap->costmapCallback(costmap_msg);
// Make smoother
std::shared_ptr<tf2_ros::Buffer> dummy_tf;
std::shared_ptr<nav2_costmap_2d::FootprintSubscriber> dummy_footprint;
node->declare_parameter("test.do_refinement", rclcpp::ParameterValue(false));
auto smoother = std::make_unique<nav2_smoother::SavitzkyGolaySmoother>();
smoother->configure(parent, "test", dummy_tf, dummy_costmap, dummy_footprint);
rclcpp::Duration max_time = rclcpp::Duration::from_seconds(1.0); // 1 seconds
// Test reversing / multiple segments via a cusp
nav_msgs::msg::Path cusp_path, cusp_path_baseline;
cusp_path.header.frame_id = "map";
cusp_path.header.stamp = node->now();
cusp_path.poses.resize(22);
cusp_path.poses[0].pose.position.x = 0.5;
cusp_path.poses[0].pose.position.y = 0.1;
cusp_path.poses[1].pose.position.x = 0.5;
cusp_path.poses[1].pose.position.y = 0.2;
cusp_path.poses[2].pose.position.x = 0.5;
cusp_path.poses[2].pose.position.y = 0.3;
cusp_path.poses[3].pose.position.x = 0.5;
cusp_path.poses[3].pose.position.y = 0.4;
cusp_path.poses[4].pose.position.x = 0.5;
cusp_path.poses[4].pose.position.y = 0.5;
cusp_path.poses[5].pose.position.x = 0.5;
cusp_path.poses[5].pose.position.y = 0.6;
cusp_path.poses[6].pose.position.x = 0.5;
cusp_path.poses[6].pose.position.y = 0.7;
cusp_path.poses[7].pose.position.x = 0.5;
cusp_path.poses[7].pose.position.y = 0.8;
cusp_path.poses[8].pose.position.x = 0.5;
cusp_path.poses[8].pose.position.y = 0.9;
cusp_path.poses[9].pose.position.x = 0.5;
cusp_path.poses[9].pose.position.y = 1.0;
cusp_path.poses[10].pose.position.x = 0.5;
cusp_path.poses[10].pose.position.y = 1.1;
cusp_path.poses[11].pose.position.x = 0.5;
cusp_path.poses[11].pose.position.y = 1.0;
cusp_path.poses[12].pose.position.x = 0.5;
cusp_path.poses[12].pose.position.y = 0.9;
cusp_path.poses[13].pose.position.x = 0.5;
cusp_path.poses[13].pose.position.y = 0.8;
cusp_path.poses[14].pose.position.x = 0.5;
cusp_path.poses[14].pose.position.y = 0.7;
cusp_path.poses[15].pose.position.x = 0.5;
cusp_path.poses[15].pose.position.y = 0.6;
cusp_path.poses[16].pose.position.x = 0.5;
cusp_path.poses[16].pose.position.y = 0.5;
cusp_path.poses[17].pose.position.x = 0.5;
cusp_path.poses[17].pose.position.y = 0.4;
cusp_path.poses[18].pose.position.x = 0.5;
cusp_path.poses[18].pose.position.y = 0.3;
cusp_path.poses[19].pose.position.x = 0.5;
cusp_path.poses[19].pose.position.y = 0.2;
cusp_path.poses[20].pose.position.x = 0.5;
cusp_path.poses[20].pose.position.y = 0.1;
cusp_path.poses[21].pose.position.x = 0.5;
cusp_path.poses[21].pose.position.y = 0.0;
// Add random but deterministic noises
std::random_device rd{};
std::mt19937 gen{rd()};
std::normal_distribution<> normal_distribution{0.0, 0.02};
for (unsigned int i = 0; i != cusp_path.poses.size(); i++) {
auto noise = normal_distribution(gen);
cusp_path.poses[i].pose.position.x += noise;
}
cusp_path_baseline = cusp_path;
EXPECT_TRUE(smoother->smooth(cusp_path, max_time));
// If it detected the cusp, the cusp point should be fixed
EXPECT_EQ(cusp_path.poses[10].pose.position.x, cusp_path_baseline.poses[10].pose.position.x);
EXPECT_EQ(cusp_path.poses[10].pose.position.y, cusp_path_baseline.poses[10].pose.position.y);
// But the path also should be smoother / shorter
double length = 0;
double base_length = 0;
for (unsigned int i = 0; i != cusp_path.poses.size() - 1; i++) {
length += std::hypot(
cusp_path.poses[i + 1].pose.position.x - cusp_path.poses[i].pose.position.x,
cusp_path.poses[i + 1].pose.position.y - cusp_path.poses[i].pose.position.y);
base_length += std::hypot(
cusp_path_baseline.poses[i + 1].pose.position.x -
cusp_path_baseline.poses[i].pose.position.x,
cusp_path_baseline.poses[i + 1].pose.position.y -
cusp_path_baseline.poses[i].pose.position.y);
}
EXPECT_LT(length, base_length);
}
@@ -0,0 +1,280 @@
// Copyright (c) 2022, 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. Reserved.
#include <math.h>
#include <memory>
#include <string>
#include <vector>
#include <chrono>
#include <limits>
#include "gtest/gtest.h"
#include "rclcpp/rclcpp.hpp"
#include "nav2_costmap_2d/costmap_2d.hpp"
#include "nav2_costmap_2d/costmap_subscriber.hpp"
#include "nav2_msgs/msg/costmap.hpp"
#include "nav2_util/lifecycle_node.hpp"
#include "nav2_smoother/simple_smoother.hpp"
#include "ament_index_cpp/get_package_share_directory.hpp"
using namespace smoother_utils; // NOLINT
using namespace nav2_smoother; // NOLINT
using namespace std::chrono_literals; // NOLINT
class RclCppFixture
{
public:
RclCppFixture() {rclcpp::init(0, nullptr);}
~RclCppFixture() {rclcpp::shutdown();}
};
RclCppFixture g_rclcppfixture;
class SmootherWrapper : public nav2_smoother::SimpleSmoother
{
public:
SmootherWrapper()
: nav2_smoother::SimpleSmoother()
{
}
std::vector<PathSegment> findDirectionalPathSegmentsWrapper(nav_msgs::msg::Path path)
{
return findDirectionalPathSegments(path);
}
void setMaxItsToInvalid()
{
max_its_ = 0;
}
};
TEST(SmootherTest, test_simple_smoother)
{
rclcpp_lifecycle::LifecycleNode::SharedPtr node =
std::make_shared<rclcpp_lifecycle::LifecycleNode>("SmacSmootherTest");
std::shared_ptr<nav2_msgs::msg::Costmap> costmap_msg =
std::make_shared<nav2_msgs::msg::Costmap>();
costmap_msg->header.stamp = node->now();
costmap_msg->header.frame_id = "map";
costmap_msg->data.resize(100 * 100);
costmap_msg->metadata.resolution = 0.05;
costmap_msg->metadata.size_x = 100;
costmap_msg->metadata.size_y = 100;
// island in the middle of lethal cost to cross
for (unsigned int i = 20; i <= 30; ++i) {
for (unsigned int j = 20; j <= 30; ++j) {
costmap_msg->data[j * 100 + i] = 254;
}
}
std::weak_ptr<rclcpp_lifecycle::LifecycleNode> parent = node;
std::shared_ptr<nav2_costmap_2d::CostmapSubscriber> dummy_costmap;
dummy_costmap = std::make_shared<nav2_costmap_2d::CostmapSubscriber>(parent, "dummy_topic");
dummy_costmap->costmapCallback(costmap_msg);
// Make smoother
std::shared_ptr<tf2_ros::Buffer> dummy_tf;
std::shared_ptr<nav2_costmap_2d::FootprintSubscriber> dummy_footprint;
auto smoother = std::make_unique<SmootherWrapper>();
smoother->configure(parent, "test", dummy_tf, dummy_costmap, dummy_footprint);
// Test that an irregular distributed path becomes more distributed
nav_msgs::msg::Path straight_irregular_path;
straight_irregular_path.header.frame_id = "map";
straight_irregular_path.header.stamp = node->now();
straight_irregular_path.poses.resize(11);
straight_irregular_path.poses[0].pose.position.x = 0.5;
straight_irregular_path.poses[0].pose.position.y = 0.0;
straight_irregular_path.poses[1].pose.position.x = 0.5;
straight_irregular_path.poses[1].pose.position.y = 0.1;
straight_irregular_path.poses[2].pose.position.x = 0.5;
straight_irregular_path.poses[2].pose.position.y = 0.2;
straight_irregular_path.poses[3].pose.position.x = 0.5;
straight_irregular_path.poses[3].pose.position.y = 0.35;
straight_irregular_path.poses[4].pose.position.x = 0.5;
straight_irregular_path.poses[4].pose.position.y = 0.4;
straight_irregular_path.poses[5].pose.position.x = 0.5;
straight_irregular_path.poses[5].pose.position.y = 0.56;
straight_irregular_path.poses[6].pose.position.x = 0.5;
straight_irregular_path.poses[6].pose.position.y = 0.9;
straight_irregular_path.poses[7].pose.position.x = 0.5;
straight_irregular_path.poses[7].pose.position.y = 0.95;
straight_irregular_path.poses[8].pose.position.x = 0.5;
straight_irregular_path.poses[8].pose.position.y = 1.3;
straight_irregular_path.poses[9].pose.position.x = 0.5;
straight_irregular_path.poses[9].pose.position.y = 2.0;
straight_irregular_path.poses[10].pose.position.x = 0.5;
straight_irregular_path.poses[10].pose.position.y = 2.5;
rclcpp::Duration no_time = rclcpp::Duration::from_seconds(0.0); // 0 seconds
rclcpp::Duration max_time = rclcpp::Duration::from_seconds(1); // 1 second
EXPECT_FALSE(smoother->smooth(straight_irregular_path, no_time));
EXPECT_TRUE(smoother->smooth(straight_irregular_path, max_time));
for (uint i = 0; i != straight_irregular_path.poses.size() - 1; i++) {
// Check distances are more evenly spaced out now
EXPECT_LT(
fabs(
straight_irregular_path.poses[i].pose.position.y -
straight_irregular_path.poses[i + 1].pose.position.y), 0.38);
}
// Test regular path, should see no effective change
nav_msgs::msg::Path straight_regular_path;
straight_regular_path.header = straight_irregular_path.header;
straight_regular_path.poses.resize(11);
straight_regular_path.poses[0].pose.position.x = 0.5;
straight_regular_path.poses[0].pose.position.y = 0.0;
straight_regular_path.poses[1].pose.position.x = 0.5;
straight_regular_path.poses[1].pose.position.y = 0.1;
straight_regular_path.poses[2].pose.position.x = 0.5;
straight_regular_path.poses[2].pose.position.y = 0.2;
straight_regular_path.poses[3].pose.position.x = 0.5;
straight_regular_path.poses[3].pose.position.y = 0.3;
straight_regular_path.poses[4].pose.position.x = 0.5;
straight_regular_path.poses[4].pose.position.y = 0.4;
straight_regular_path.poses[5].pose.position.x = 0.5;
straight_regular_path.poses[5].pose.position.y = 0.5;
straight_regular_path.poses[6].pose.position.x = 0.5;
straight_regular_path.poses[6].pose.position.y = 0.6;
straight_regular_path.poses[7].pose.position.x = 0.5;
straight_regular_path.poses[7].pose.position.y = 0.7;
straight_regular_path.poses[8].pose.position.x = 0.5;
straight_regular_path.poses[8].pose.position.y = 0.8;
straight_regular_path.poses[9].pose.position.x = 0.5;
straight_regular_path.poses[9].pose.position.y = 0.9;
straight_regular_path.poses[10].pose.position.x = 0.5;
straight_regular_path.poses[10].pose.position.y = 1.0;
EXPECT_TRUE(smoother->smooth(straight_regular_path, max_time));
for (uint i = 0; i != straight_regular_path.poses.size() - 1; i++) {
// Check distances are still very evenly spaced
EXPECT_NEAR(
fabs(
straight_regular_path.poses[i].pose.position.y -
straight_regular_path.poses[i + 1].pose.position.y), 0.1, 0.001);
}
// test shorter and curved if given a right angle
nav_msgs::msg::Path right_angle_path;
right_angle_path = straight_regular_path;
straight_regular_path.poses[6].pose.position.x = 0.6;
straight_regular_path.poses[6].pose.position.y = 0.5;
straight_regular_path.poses[7].pose.position.x = 0.7;
straight_regular_path.poses[7].pose.position.y = 0.5;
straight_regular_path.poses[8].pose.position.x = 0.8;
straight_regular_path.poses[8].pose.position.y = 0.5;
straight_regular_path.poses[9].pose.position.x = 0.9;
straight_regular_path.poses[9].pose.position.y = 0.5;
straight_regular_path.poses[10].pose.position.x = 0.95;
straight_regular_path.poses[10].pose.position.y = 0.5;
EXPECT_TRUE(smoother->smooth(straight_regular_path, max_time));
EXPECT_NEAR(straight_regular_path.poses[5].pose.position.x, 0.637, 0.01);
EXPECT_NEAR(straight_regular_path.poses[5].pose.position.y, 0.353, 0.01);
// Test that collisions are rejected
nav_msgs::msg::Path collision_path;
collision_path.poses.resize(11);
collision_path.poses[0].pose.position.x = 0.0;
collision_path.poses[0].pose.position.y = 0.0;
collision_path.poses[1].pose.position.x = 0.2;
collision_path.poses[1].pose.position.y = 0.2;
collision_path.poses[2].pose.position.x = 0.4;
collision_path.poses[2].pose.position.y = 0.4;
collision_path.poses[3].pose.position.x = 0.6;
collision_path.poses[3].pose.position.y = 0.6;
collision_path.poses[4].pose.position.x = 0.8;
collision_path.poses[4].pose.position.y = 0.8;
collision_path.poses[5].pose.position.x = 1.0;
collision_path.poses[5].pose.position.y = 1.0;
collision_path.poses[6].pose.position.x = 1.1;
collision_path.poses[6].pose.position.y = 1.1;
collision_path.poses[7].pose.position.x = 1.2;
collision_path.poses[7].pose.position.y = 1.2;
collision_path.poses[8].pose.position.x = 1.3;
collision_path.poses[8].pose.position.y = 1.3;
collision_path.poses[9].pose.position.x = 1.4;
collision_path.poses[9].pose.position.y = 1.4;
collision_path.poses[10].pose.position.x = 1.5;
collision_path.poses[10].pose.position.y = 1.5;
EXPECT_FALSE(smoother->smooth(collision_path, max_time));
// test cusp / reversing segments
nav_msgs::msg::Path reversing_path;
reversing_path.poses.resize(11);
reversing_path.poses[0].pose.position.x = 0.5;
reversing_path.poses[0].pose.position.y = 0.0;
reversing_path.poses[1].pose.position.x = 0.5;
reversing_path.poses[1].pose.position.y = 0.1;
reversing_path.poses[2].pose.position.x = 0.5;
reversing_path.poses[2].pose.position.y = 0.2;
reversing_path.poses[3].pose.position.x = 0.5;
reversing_path.poses[3].pose.position.y = 0.3;
reversing_path.poses[4].pose.position.x = 0.5;
reversing_path.poses[4].pose.position.y = 0.4;
reversing_path.poses[5].pose.position.x = 0.5;
reversing_path.poses[5].pose.position.y = 0.5;
reversing_path.poses[6].pose.position.x = 0.5;
reversing_path.poses[6].pose.position.y = 0.4;
reversing_path.poses[7].pose.position.x = 0.5;
reversing_path.poses[7].pose.position.y = 0.3;
reversing_path.poses[8].pose.position.x = 0.5;
reversing_path.poses[8].pose.position.y = 0.2;
reversing_path.poses[9].pose.position.x = 0.5;
reversing_path.poses[9].pose.position.y = 0.1;
reversing_path.poses[10].pose.position.x = 0.5;
reversing_path.poses[10].pose.position.y = 0.0;
EXPECT_TRUE(smoother->smooth(reversing_path, max_time));
// // test rotate in place
tf2::Quaternion quat1, quat2;
quat1.setRPY(0.0, 0.0, 0.0);
quat2.setRPY(0.0, 0.0, 1.0);
straight_irregular_path.poses[5].pose.position.x = 0.5;
straight_irregular_path.poses[5].pose.position.y = 0.5;
straight_irregular_path.poses[5].pose.orientation = tf2::toMsg(quat1);
straight_irregular_path.poses[6].pose.position.x = 0.5;
straight_irregular_path.poses[6].pose.position.y = 0.5;
straight_irregular_path.poses[6].pose.orientation = tf2::toMsg(quat2);
EXPECT_TRUE(smoother->smooth(straight_irregular_path, max_time));
// test max iterations
smoother->setMaxItsToInvalid();
nav_msgs::msg::Path max_its_path;
max_its_path.poses.resize(11);
max_its_path.poses[0].pose.position.x = 0.5;
max_its_path.poses[0].pose.position.y = 0.0;
max_its_path.poses[1].pose.position.x = 0.5;
max_its_path.poses[1].pose.position.y = 0.1;
max_its_path.poses[2].pose.position.x = 0.5;
max_its_path.poses[2].pose.position.y = 0.2;
max_its_path.poses[3].pose.position.x = 0.5;
max_its_path.poses[3].pose.position.y = 0.3;
max_its_path.poses[4].pose.position.x = 0.5;
max_its_path.poses[4].pose.position.y = 0.4;
max_its_path.poses[5].pose.position.x = 0.5;
max_its_path.poses[5].pose.position.y = 0.5;
max_its_path.poses[6].pose.position.x = 0.5;
max_its_path.poses[6].pose.position.y = 0.6;
max_its_path.poses[7].pose.position.x = 0.5;
max_its_path.poses[7].pose.position.y = 0.7;
max_its_path.poses[8].pose.position.x = 0.5;
max_its_path.poses[8].pose.position.y = 0.8;
max_its_path.poses[9].pose.position.x = 0.5;
max_its_path.poses[9].pose.position.y = 0.9;
max_its_path.poses[10].pose.position.x = 0.5;
max_its_path.poses[10].pose.position.y = 1.0;
EXPECT_FALSE(smoother->smooth(max_its_path, max_time));
}
@@ -0,0 +1,440 @@
// Copyright (c) 2021 RoboTech Vision
//
// 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 <memory>
#include <chrono>
#include <iostream>
#include <future>
#include <thread>
#include <algorithm>
#include <vector>
#include "gtest/gtest.h"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "nav2_core/smoother.hpp"
#include "nav2_core/exceptions.hpp"
#include "nav2_msgs/action/smooth_path.hpp"
#include "nav2_smoother/nav2_smoother.hpp"
#include "tf2_ros/create_timer_ros.h"
using SmoothAction = nav2_msgs::action::SmoothPath;
using ClientGoalHandle = rclcpp_action::ClientGoalHandle<SmoothAction>;
using namespace std::chrono_literals;
// A smoother for testing the base class
class DummySmoother : public nav2_core::Smoother
{
public:
DummySmoother() {}
~DummySmoother() {}
virtual void configure(
const rclcpp_lifecycle::LifecycleNode::WeakPtr &,
std::string, std::shared_ptr<tf2_ros::Buffer>,
std::shared_ptr<nav2_costmap_2d::CostmapSubscriber>,
std::shared_ptr<nav2_costmap_2d::FootprintSubscriber>) {}
virtual void cleanup() {}
virtual void activate() {}
virtual void deactivate() {}
virtual bool smooth(
nav_msgs::msg::Path & path,
const rclcpp::Duration & max_time)
{
assert(path.poses.size() == 2);
if (path.poses.front() == path.poses.back()) {
throw nav2_core::PlannerException("Start and goal pose must differ");
}
auto max_time_ms = max_time.to_chrono<std::chrono::milliseconds>();
std::this_thread::sleep_for(std::min(max_time_ms, 100ms));
// place dummy pose in the middle of the path
geometry_msgs::msg::PoseStamped pose;
pose.pose.position.x =
(path.poses.front().pose.position.x + path.poses.back().pose.position.x) / 2;
pose.pose.position.y =
(path.poses.front().pose.position.y + path.poses.back().pose.position.y) / 2;
pose.pose.orientation.w = 1.0;
path.poses.push_back(pose);
return max_time_ms > 100ms;
}
private:
std::string command_;
std::chrono::system_clock::time_point start_time_;
};
// Mocked class loader
void onPluginDeletion(nav2_core::Smoother * obj)
{
if (nullptr != obj) {
delete (obj);
}
}
template<>
pluginlib::UniquePtr<nav2_core::Smoother> pluginlib::ClassLoader<nav2_core::Smoother>::
createUniqueInstance(const std::string & lookup_name)
{
if (lookup_name != "DummySmoother") {
// original method body
if (!isClassLoaded(lookup_name)) {
loadLibraryForClass(lookup_name);
}
try {
std::string class_type = getClassType(lookup_name);
pluginlib::UniquePtr<nav2_core::Smoother> obj =
lowlevel_class_loader_.createUniqueInstance<nav2_core::Smoother>(class_type);
return obj;
} catch (const class_loader::CreateClassException & ex) {
throw pluginlib::CreateClassException(ex.what());
}
}
// mocked plugin creation
return std::unique_ptr<nav2_core::Smoother,
class_loader::ClassLoader::DeleterType<nav2_core::Smoother>>(
new DummySmoother(),
onPluginDeletion);
}
class DummyCostmapSubscriber : public nav2_costmap_2d::CostmapSubscriber
{
public:
DummyCostmapSubscriber(
nav2_util::LifecycleNode::SharedPtr node,
const std::string & topic_name)
: CostmapSubscriber(node, topic_name)
{
auto costmap = std::make_shared<nav2_msgs::msg::Costmap>();
costmap->metadata.size_x = 100;
costmap->metadata.size_y = 100;
costmap->metadata.resolution = 0.1;
costmap->metadata.origin.position.x = -5.0;
costmap->metadata.origin.position.y = -5.0;
costmap->data.resize(costmap->metadata.size_x * costmap->metadata.size_y, 0);
for (unsigned int i = 0; i < costmap->metadata.size_y; ++i) {
for (unsigned int j = 20; j < 40; ++j) {
costmap->data[i * costmap->metadata.size_x + j] = 254;
}
}
setCostmap(costmap);
}
void setCostmap(nav2_msgs::msg::Costmap::SharedPtr msg)
{
costmap_msg_ = msg;
costmap_received_ = true;
}
};
class DummyFootprintSubscriber : public nav2_costmap_2d::FootprintSubscriber
{
public:
DummyFootprintSubscriber(
nav2_util::LifecycleNode::SharedPtr node,
const std::string & topic_name,
tf2_ros::Buffer & tf_)
: FootprintSubscriber(node, topic_name, tf_)
{
auto footprint = std::make_shared<geometry_msgs::msg::PolygonStamped>();
footprint->header.frame_id = "base_link"; // global frame = robot frame to avoid tf lookup
footprint->header.stamp = node->get_clock()->now();
geometry_msgs::msg::Point32 point;
point.x = -0.2f;
point.y = -0.2f;
footprint->polygon.points.push_back(point);
point.y = 0.2f;
footprint->polygon.points.push_back(point);
point.x = 0.2f;
point.y = 0.0f;
footprint->polygon.points.push_back(point);
setFootprint(footprint);
}
void setFootprint(geometry_msgs::msg::PolygonStamped::SharedPtr msg)
{
footprint_ = msg;
footprint_received_ = true;
}
};
class DummySmootherServer : public nav2_smoother::SmootherServer
{
public:
DummySmootherServer()
{
// Override defaults
default_ids_.clear();
default_ids_.resize(1, "SmoothPath");
set_parameter(rclcpp::Parameter("smoother_plugins", default_ids_));
default_types_.clear();
default_types_.resize(1, "DummySmoother");
}
nav2_util::CallbackReturn
on_configure(const rclcpp_lifecycle::State & state)
{
auto result = SmootherServer::on_configure(state);
if (result != nav2_util::CallbackReturn::SUCCESS) {
return result;
}
// Create dummy subscribers and collision checker
auto node = shared_from_this();
costmap_sub_ =
std::make_shared<DummyCostmapSubscriber>(
node, "costmap_topic");
footprint_sub_ =
std::make_shared<DummyFootprintSubscriber>(
node, "footprint_topic", *tf_);
collision_checker_ =
std::make_shared<nav2_costmap_2d::CostmapTopicCollisionChecker>(
*costmap_sub_, *footprint_sub_,
node->get_name());
return result;
}
};
// Define a test class to hold the context for the tests
class SmootherTest : public ::testing::Test
{
public:
SmootherTest() {}
~SmootherTest() {}
void SetUp() override
{
node_ =
std::make_shared<rclcpp::Node>(
"LifecycleSmootherTestNode", rclcpp::NodeOptions());
smoother_server_ = std::make_shared<DummySmootherServer>();
smoother_server_->set_parameter(
rclcpp::Parameter(
"smoother_plugins",
rclcpp::ParameterValue(std::vector<std::string>(1, "DummySmoothPath"))));
smoother_server_->declare_parameter(
"DummySmoothPath.plugin",
rclcpp::ParameterValue(std::string("DummySmoother")));
smoother_server_->configure();
smoother_server_->activate();
client_ = rclcpp_action::create_client<SmoothAction>(
node_->get_node_base_interface(),
node_->get_node_graph_interface(),
node_->get_node_logging_interface(),
node_->get_node_waitables_interface(), "smooth_path");
std::cout << "Setup complete." << std::endl;
}
void TearDown() override
{
smoother_server_->deactivate();
smoother_server_->cleanup();
smoother_server_->shutdown();
smoother_server_.reset();
client_.reset();
node_.reset();
}
bool sendGoal(
std::string smoother_id, double x_start, double y_start, double x_goal,
double y_goal, std::chrono::milliseconds max_time, bool check_for_collisions)
{
if (!client_->wait_for_action_server(4s)) {
std::cout << "Server not up" << std::endl;
return false;
}
geometry_msgs::msg::PoseStamped pose;
pose.pose.orientation.w = 1.0;
auto goal = SmoothAction::Goal();
goal.smoother_id = smoother_id;
pose.pose.position.x = x_start;
pose.pose.position.y = y_start;
goal.path.poses.push_back(pose);
pose.pose.position.x = x_goal;
pose.pose.position.y = y_goal;
goal.path.poses.push_back(pose);
goal.check_for_collisions = check_for_collisions;
goal.max_smoothing_duration = rclcpp::Duration(max_time);
auto future_goal = client_->async_send_goal(goal);
if (rclcpp::spin_until_future_complete(node_, future_goal) !=
rclcpp::FutureReturnCode::SUCCESS)
{
std::cout << "failed sending goal" << std::endl;
// failed sending the goal
return false;
}
goal_handle_ = future_goal.get();
if (!goal_handle_) {
std::cout << "goal was rejected" << std::endl;
// goal was rejected by the action server
return false;
}
return true;
}
ClientGoalHandle::WrappedResult getResult()
{
std::cout << "Getting async result..." << std::endl;
auto future_result = client_->async_get_result(goal_handle_);
std::cout << "Waiting on future..." << std::endl;
rclcpp::spin_until_future_complete(node_, future_result);
std::cout << "future received!" << std::endl;
return future_result.get();
}
std::shared_ptr<rclcpp::Node> node_;
std::shared_ptr<DummySmootherServer> smoother_server_;
std::shared_ptr<rclcpp_action::Client<SmoothAction>> client_;
std::shared_ptr<rclcpp_action::ClientGoalHandle<SmoothAction>> goal_handle_;
};
// Define the tests
TEST_F(SmootherTest, testingSuccess)
{
ASSERT_TRUE(sendGoal("DummySmoothPath", 0.0, 0.0, 1.0, 0.0, 500ms, true));
auto result = getResult();
EXPECT_EQ(result.code, rclcpp_action::ResultCode::SUCCEEDED);
EXPECT_EQ(result.result->path.poses.size(), (std::size_t)3);
EXPECT_TRUE(result.result->was_completed);
SUCCEED();
}
TEST_F(SmootherTest, testingFailureOnInvalidSmootherId)
{
ASSERT_TRUE(sendGoal("InvalidSmoother", 0.0, 0.0, 1.0, 0.0, 500ms, true));
auto result = getResult();
EXPECT_EQ(result.code, rclcpp_action::ResultCode::ABORTED);
SUCCEED();
}
TEST_F(SmootherTest, testingSuccessOnEmptyPlugin)
{
ASSERT_TRUE(sendGoal("", 0.0, 0.0, 1.0, 0.0, 500ms, true));
auto result = getResult();
EXPECT_EQ(result.code, rclcpp_action::ResultCode::SUCCEEDED);
SUCCEED();
}
TEST_F(SmootherTest, testingIncomplete)
{
ASSERT_TRUE(sendGoal("DummySmoothPath", 0.0, 0.0, 1.0, 0.0, 50ms, true));
auto result = getResult();
EXPECT_EQ(result.code, rclcpp_action::ResultCode::SUCCEEDED);
EXPECT_FALSE(result.result->was_completed);
SUCCEED();
}
TEST_F(SmootherTest, testingFailureOnException)
{
ASSERT_TRUE(sendGoal("DummySmoothPath", 0.0, 0.0, 0.0, 0.0, 500ms, true));
auto result = getResult();
EXPECT_EQ(result.code, rclcpp_action::ResultCode::ABORTED);
SUCCEED();
}
TEST_F(SmootherTest, testingFailureOnCollision)
{
ASSERT_TRUE(sendGoal("DummySmoothPath", -4.0, 0.0, 0.0, 0.0, 500ms, true));
auto result = getResult();
EXPECT_EQ(result.code, rclcpp_action::ResultCode::ABORTED);
SUCCEED();
}
TEST_F(SmootherTest, testingCollisionCheckDisabled)
{
ASSERT_TRUE(sendGoal("DummySmoothPath", -4.0, 0.0, 0.0, 0.0, 500ms, false));
auto result = getResult();
EXPECT_EQ(result.code, rclcpp_action::ResultCode::SUCCEEDED);
SUCCEED();
}
TEST(SmootherConfigTest, testingConfigureSuccessWithValidSmootherPlugin)
{
auto smoother_server = std::make_shared<DummySmootherServer>();
smoother_server->set_parameter(
rclcpp::Parameter(
"smoother_plugins",
rclcpp::ParameterValue(std::vector<std::string>(1, "DummySmoothPath"))));
smoother_server->declare_parameter(
"DummySmoothPath.plugin",
rclcpp::ParameterValue(std::string("DummySmoother")));
auto state = smoother_server->configure();
EXPECT_EQ(state.id(), 2); // 1 on failure, 2 on success
SUCCEED();
}
TEST(SmootherConfigTest, testingConfigureFailureWithInvalidSmootherPlugin)
{
auto smoother_server = std::make_shared<DummySmootherServer>();
smoother_server->set_parameter(
rclcpp::Parameter(
"smoother_plugins",
rclcpp::ParameterValue(std::vector<std::string>(1, "DummySmoothPath"))));
smoother_server->declare_parameter(
"DummySmoothPath.plugin",
rclcpp::ParameterValue(std::string("InvalidSmootherPlugin")));
auto state = smoother_server->configure();
EXPECT_EQ(state.id(), 1); // 1 on failure, 2 on success
SUCCEED();
}
TEST(SmootherConfigTest, testingConfigureSuccessWithDefaultPlugin)
{
auto smoother_server = std::make_shared<DummySmootherServer>();
auto state = smoother_server->configure();
EXPECT_EQ(state.id(), 2); // 1 on failure, 2 on success
SUCCEED();
}
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;
}