ROS2 实战
本文是我学习 ROS2(Robot Operating System 2)的 一些实操演练
任务要求
使用ROS2,实现pluginlib插件的使用,完成动态参数的设置以及用Composable Nodes多线程容器组合启动
1、创建工作区与功能包
1 2 3 4 5
| mkdir -p ~/ros2_advanced_ws/src cd ~/ros2_advanced_ws/src ros2 pkg create --build-type ament_cmake adv_demo \ --dependencies rclcpp rclcpp_components pluginlib nav_msgs geometry_msgs
|
其中,上述最后一段是创建adv_demo文件夹以及各种文件依赖
随后,我们补全文件目录
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| adv_demo/ CMakeLists.txt package.xml include/adv_demo/ planner_base.hpp src/ planner_component.cpp plugins/astar_planner.cpp plugins/dwa_planner.cpp resource/ planner_plugins.xml launch/ composed.launch.py
|
2、定义插件接口(pluginlib 抽象基类)
include/adv_demo/planner_base.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #pragma once #include <nav_msgs/msg/path.hpp> #include <geometry_msgs/msg/pose_stamped.hpp> #include <vector>
namespace adv_demo {
struct PlanOptions { double max_speed{5.0}; };
class PlannerBase { public: virtual ~PlannerBase() = default; virtual std::string name() const = 0;
virtual nav_msgs::msg::Path plan(const geometry_msgs::msg::PoseStamped& start, const geometry_msgs::msg::PoseStamped& goal, const PlanOptions& opts) = 0; };
}
|
用纯虚函数重写命名以及规划路线的函数
geometry_msgs::msg是 ROS 中用于表示基础几何信息的消息包,msg 表示这是消息类型的定义
常见消息类型:
PoseStamped: 带时间戳和坐标系的位姿(位置 + 姿态)
Point:三维点(x, y, z)
Quaternion:四元数(表示旋转姿态)
Twist:线速度和角速度(用于控制机器人运动)
Vector3:三维向量
nav_msgs 是 ROS 中用于导航相关功能的消息包,msg 同样表示消息类型。
常见消息类型:
Path:由一系列 PoseStamped 组成的路径(如规划器输出的轨迹)
Odometry:里程计信息(包含机器人的位置、速度等)
OccupancyGrid:占据栅格地图(用于导航中的环境表示)
两个插件的实现
src/plugins/astar_planner.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include "adv_demo/planner_base.hpp" #include <rclcpp/rclcpp.hpp> #include <pluginlib/class_list_macros.hpp>
namespace adv_demo {
class AStarPlanner : public PlannerBase { public: std::string name() const override { return "AStarPlanner"; }
nav_msgs::msg::Path plan(const geometry_msgs::msg::PoseStamped& start, const geometry_msgs::msg::PoseStamped& goal, const PlanOptions& opts) override { nav_msgs::msg::Path path; path.header = goal.header; for (int i = 0; i <= 20; ++i) { geometry_msgs::msg::PoseStamped p; double t = static_cast<double>(i)/20.0; p.header = goal.header; p.pose.position.x = start.pose.position.x * (1 - t) + goal.pose.position.x * t; p.pose.position.y = start.pose.position.y * (1 - t) + goal.pose.position.y * t; path.poses.push_back(p); } (void)opts; return path; } };
}
PLUGINLIB_EXPORT_CLASS(adv_demo::AStarPlanner, adv_demo::PlannerBase)
|
src/plugins/dwa_planner.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include "adv_demo/planner_base.hpp" #include <rclcpp/rclcpp.hpp> #include <pluginlib/class_list_macros.hpp>
namespace adv_demo {
class DwaPlanner : public PlannerBase { public: std::string name() const override { return "DwaPlanner"; }
nav_msgs::msg::Path plan(const geometry_msgs::msg::PoseStamped& start, const geometry_msgs::msg::PoseStamped& goal, const PlanOptions& opts) override { nav_msgs::msg::Path path; path.header = goal.header; for (int i = 0; i <= 40; ++i) { geometry_msgs::msg::PoseStamped p; double t = static_cast<double>(i)/40.0; p.header = goal.header; p.pose.position.x = start.pose.position.x*(1-t) + goal.pose.position.x*t; p.pose.position.y = start.pose.position.y*(1-t) + goal.pose.position.y*t + 0.5*sin(6.28*t); path.poses.push_back(p); } (void)opts; return path; } };
}
PLUGINLIB_EXPORT_CLASS(adv_demo::DwaPlanner, adv_demo::PlannerBase)
|
插件声明
resource/planner_plugins.xml
1 2 3 4 5 6 7 8 9 10 11 12 13
| <library path="libadv_demo_planners"> <class name="adv_demo/AStarPlanner" type="adv_demo::AStarPlanner" base_class_type="adv_demo::PlannerBase"> <description>A* demo planner</description> </class> <class name="adv_demo/DwaPlanner" type="adv_demo::DwaPlanner" base_class_type="adv_demo::PlannerBase"> <description>DWA demo planner</description> </class> </library>
|
上面的插件声明,是为了告诉ROS系统插件在哪,去哪可以加载插件。
组件化规划节点
src/planner_component.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| #include <rclcpp/rclcpp.hpp> #include <rclcpp_components/register_node_macro.hpp> #include <pluginlib/class_loader.hpp> #include "adv_demo/planner_base.hpp" #include <nav_msgs/msg/path.hpp> #include <geometry_msgs/msg/pose_stamped.hpp>
namespace adv_demo {
class PlannerComponent : public rclcpp::Node { public: explicit PlannerComponent(const rclcpp::NodeOptions& options) : Node("planner_component", options), loader_("adv_demo", "adv_demo::PlannerBase") { this->declare_parameter<std::string>("planner_type", "adv_demo/AStarPlanner"); this->declare_parameter<double>("max_speed", 5.0);
param_cb_handle_ = this->add_on_set_parameters_callback( [this](const std::vector<rclcpp::Parameter>& params) { rcl_interfaces::msg::SetParametersResult res; res.successful = true; for (auto& p : params) { if (p.get_name() == "planner_type") { desired_plugin_ = p.as_string(); need_reload_ = true; } else if (p.get_name() == "max_speed") { opts_.max_speed = p.as_double(); RCLCPP_INFO(this->get_logger(), "max_speed updated: %.2f", opts_.max_speed); } } return res; });
start_sub_ = this->create_subscription<geometry_msgs::msg::PoseStamped>( "start", 1, [this](const geometry_msgs::msg::PoseStamped::SharedPtr msg){ start_ = *msg; has_start_ = true; tryPlan(); }); goal_sub_ = this->create_subscription<geometry_msgs::msg::PoseStamped>( "goal", 1, [this](const geometry_msgs::msg::PoseStamped::SharedPtr msg){ goal_ = *msg; has_goal_ = true; tryPlan(); });
path_pub_ = this->create_publisher<nav_msgs::msg::Path>("planned_path", 1);
desired_plugin_ = this->get_parameter("planner_type").as_string(); loadPlanner(desired_plugin_); }
private: void loadPlanner(const std::string& type) { try { planner_ = loader_.createSharedInstance(type); RCLCPP_INFO(get_logger(), "Loaded planner plugin: %s", planner_->name().c_str()); } catch (const std::exception& e) { RCLCPP_ERROR(get_logger(), "Failed to load plugin '%s': %s", type.c_str(), e.what()); planner_.reset(); } }
void tryPlan() { if (need_reload_) { loadPlanner(desired_plugin_); need_reload_ = false; } if (!planner_ || !has_start_ || !has_goal_) return; auto path = planner_->plan(start_, goal_, opts_); path_pub_->publish(path); RCLCPP_INFO(get_logger(), "Published path with %zu poses using %s", path.poses.size(), planner_->name().c_str()); }
rclcpp::Subscription<geometry_msgs::msg::PoseStamped>::SharedPtr start_sub_, goal_sub_; rclcpp::Publisher<nav_msgs::msg::Path>::SharedPtr path_pub_; geometry_msgs::msg::PoseStamped start_, goal_; bool has_start_{false}, has_goal_{false};
pluginlib::ClassLoader<PlannerBase> loader_; std::shared_ptr<PlannerBase> planner_;
rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr param_cb_handle_; PlanOptions opts_; std::string desired_plugin_; bool need_reload_{false}; };
}
RCLCPP_COMPONENTS_REGISTER_NODE(adv_demo::PlannerComponent)
|
整体功能:
该类创建了一个 ROS 2 节点,能够:动态加载不同的路径规划插件(如 A*、DWA 等,基于之前定义的PlannerBase接口)
接收起点(start)和终点(goal)位姿
根据加载的规划器插件计算路径,并发布规划结果(planned_path)
支持动态参数调整(如切换规划器类型、修改最大速度)
组件、插件以及依赖等设置
CMakeLists.txt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| cmake_minimum_required(VERSION 3.5) project(adv_demo)
if(NOT CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 17) endif()
find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(rclcpp_components REQUIRED) find_package(pluginlib REQUIRED) find_package(nav_msgs REQUIRED) find_package(geometry_msgs REQUIRED)
include_directories(include)
add_library(adv_demo_planner_component SHARED src/planner_component.cpp) target_link_libraries(adv_demo_planner_component) ament_target_dependencies(adv_demo_planner_component rclcpp rclcpp_components pluginlib nav_msgs geometry_msgs) rclcpp_components_register_nodes(adv_demo_planner_component "adv_demo::PlannerComponent")
add_library(adv_demo_planners SHARED src/plugins/astar_planner.cpp src/plugins/dwa_planner.cpp) ament_target_dependencies(adv_demo_planners rclcpp pluginlib nav_msgs geometry_msgs)
install(FILES resource/planner_plugins.xml DESTINATION share/${PROJECT_NAME})
pluginlib_export_plugin_description_file(adv_demo resource/planner_plugins.xml)
install(TARGETS adv_demo_planner_component adv_demo_planners ARCHIVE DESTINATION lib LIBRARY DESTINATION lib RUNTIME DESTINATION bin)
install(DIRECTORY include/ DESTINATION include) install(DIRECTORY launch/ DESTINATION share/${PROJECT_NAME}/launch)
ament_package()
|
package.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <package format="3"> <name>adv_demo</name> <version>0.0.1</version> <description>Advanced ROS2 demo with pluginlib, dynamic params, components</description>
<maintainer email="you@example.com">you</maintainer> <license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend> <depend>rclcpp_components</depend> <depend>pluginlib</depend> <depend>nav_msgs</depend> <depend>geometry_msgs</depend>
<export> <pluginlib plugin="${prefix}/share/adv_demo/planner_plugins.xml"/> <rclcpp_components/> </export> </package>
|
组合式启动(组件容器)
launch/composed.launch.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| from launch import LaunchDescription from launch_ros.actions import ComposableNodeContainer from launch_ros.descriptions import ComposableNode
def generate_launch_description(): planner = ComposableNode( package='adv_demo', plugin='adv_demo::PlannerComponent', name='planner_component', parameters=[{ 'planner_type': 'adv_demo/AStarPlanner', 'max_speed': 5.0 }] )
container = ComposableNodeContainer( name='adv_container', namespace='', package='rclcpp_components', executable='component_container_mt', composable_node_descriptions=[planner], output='screen' ) return LaunchDescription([container])
|
这段代码是 ROS 2(机器人操作系统 2)的启动文件(Launch File),用于配置和启动一个包含路径规划器组件的节点容器。它使用了 ROS 2 的组件化(Composable Nodes)机制,实现更灵活的节点管理。
编译与运行
1 2 3 4 5
| cd ~/ros2_advanced_ws colcon build --symlink-install \ --cmake-args -DPYTHON_EXECUTABLE=/usr/bin/python3 source install/setup.bash
|
启动组件容器:
1 2
| ros2 launch adv_demo composed.launch.py
|
在新终端(同样要 source ~/ros2_advanced_ws/install/setup.bash)发布起点与终点:
1 2 3 4 5
| ros2 topic pub /start geometry_msgs/PoseStamped "{header: {frame_id: 'map'}, pose: {position: {x: 0.0, y: 0.0}}}" -1
ros2 topic pub /goal geometry_msgs/PoseStamped "{header: {frame_id: 'map'}, pose: {position: {x: 10.0, y: 0.0}}}" -1
|
观察输出的路径:
1 2
| ros2 topic echo /planned_path --qos-durability transient_local
|
动态参数在线切换与调参
切换到 DWA 插件:
1
| ros2 param set /planner_component planner_type adv_demo/DwaPlanner
|
调整最大速度(供你的算法使用):
1 2 3
| ros2 param set /planner_component max_speed 8.0
|
再次发一次 goal 就会触发新插件规划。