
ros2节点创建
从零开始创建一个 ROS 2 C++ 节点
在学习 ROS 2 的过程中,节点(Node) 是最核心的概念之一。无论是传感器驱动、控制程序、建图算法,还是路径规划模块,本质上都可以看成一个个节点在协同工作。
本文以 ROS 2 + C++ + ament_cmake 为例,适用于 Humble、Jazzy 等常见版本。
一、什么是 ROS 2 节点
在 ROS 2 里,一个节点可以理解成一个独立运行的功能单元。
比如:
- 一个节点负责读取激光雷达数据
- 一个节点负责处理图像
- 一个节点负责发布速度指令
- 一个节点负责建图或定位
这些节点彼此之间通过 话题(topic)、服务(service)、动作(action) 等通信机制完成协作。
所以,学习 ROS 2,第一件事往往就是:先学会自己创建一个节点。
二、创建节点前要准备什么
在开始之前,需要确保你的 ROS 2 环境已经正常安装,并且终端中已经 source 过环境。
比如:
source /opt/ros/humble/setup.bash
如果你用的是自己的工作空间,还需要再 source 一次工作空间:
source ~/ws/install/setup.bash
这里的 ~/ws 只是示例,你的工作空间名字可以不同。
三、创建工作空间
ROS 2 推荐把自己的项目放到一个工作空间中管理。常见结构如下:
~/ros2_ws/
├── src/
├── build/
├── install/
└── log/
其中:
src/:放源码包build/:编译中间文件install/:安装后的可运行文件和环境log/:构建日志
如果你还没有工作空间,可以先创建:
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
四、创建一个 C++ 功能包
在 ROS 2 中,节点通常放在一个“包(package)”里。
先进入 src 目录,然后使用 ros2 pkg create 创建包:
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake cpp_node_demo --dependencies rclcpp
这条命令的含义是:
ros2 pkg create:创建一个新的 ROS 2 包--build-type ament_cmake:指定这是一个 C++ 风格的包,构建工具使用ament_cmakecpp_node_demo:包名--dependencies rclcpp:提前声明依赖rclcpp,因为 C++ 节点开发主要使用它
执行完成后,会生成一个包目录:
cpp_node_demo/
├── CMakeLists.txt
├── package.xml
├── include/
└── src/
这几个文件和目录非常重要。
五、包中的关键文件分别是什么
1. package.xml
这是包的元信息文件,用来描述:
- 包名
- 版本
- 维护者
- 许可证
- 依赖项
ROS 2 在构建和依赖管理时会读取它。
2. CMakeLists.txt
这是 CMake 的构建脚本。
对于 C++ 包来说,它负责:
- 指定编译规则
- 查找依赖
- 生成可执行文件
- 安装目标文件
3. src/
这个目录一般用来放 .cpp 源文件,也就是节点代码最主要的位置。
4. include/
如果项目中有头文件,一般放在这里。
六、编写第一个 C++ 节点
现在我们在 src/ 下新建一个文件,比如 hello_node.cpp:
cd ~/ros2_ws/src/cpp_node_demo/src
touch hello_node.cpp
然后写入下面的代码:
#include "rclcpp/rclcpp.hpp"
class HelloNode : public rclcpp::Node
{
public:
HelloNode() : Node("hello_node")
{
RCLCPP_INFO(this->get_logger(), "Hello ROS 2, 节点已启动!");
}
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv); // 初始化 ROS 2
auto node = std::make_shared<HelloNode>(); // 创建节点对象
rclcpp::spin(node); // 保持节点运行
rclcpp::shutdown(); // 关闭 ROS 2
return 0;
}
七、代码解释
这段代码很短,但几乎把 ROS 2 C++ 节点的基本骨架都展示出来了。
1. 头文件
#include "rclcpp/rclcpp.hpp"
rclcpp 是 ROS 2 的 C++ 客户端库。
你写 C++ 节点时,最常接触的类和函数基本都来自这个库。
2. 定义节点类
class HelloNode : public rclcpp::Node
这里定义了一个类 HelloNode,并让它继承自 rclcpp::Node。
这意味着:
你的类本身就是一个 ROS 2 节点。
也就是说,节点不仅仅是一个进程里的概念,在代码层面,它通常就是一个继承 rclcpp::Node 的 C++ 类对象。
3. 构造函数
HelloNode() : Node("hello_node")
这一句很关键。
Node("hello_node") 的意思是调用父类 rclcpp::Node 的构造函数,并把节点名设置为 "hello_node"。
所以这个节点启动后,在 ROS 图里显示的名字就是:
/hello_node
4. 日志输出
RCLCPP_INFO(this->get_logger(), "Hello ROS 2, 节点已启动!");
这是 ROS 2 中常见的日志输出方式。
RCLCPP_INFO:输出普通信息this->get_logger():获取当前节点的日志器- 后面是日志内容
这和普通 C++ 的 std::cout 不同。
ROS 2 自带日志系统,可以更规范地输出调试信息、警告和错误。
常见还有:
RCLCPP_WARNRCLCPP_ERRORRCLCPP_DEBUG
5. main 函数
初始化 ROS 2
rclcpp::init(argc, argv);
这一步相当于告诉程序:“ROS 2 环境要开始工作了。”
很多内部资源、通信机制都要在这里完成初始化。
创建节点对象
auto node = std::make_shared<HelloNode>();
这里创建了一个 HelloNode 对象,并使用智能指针管理。
ROS 2 中大量接口都基于 shared_ptr,这是因为节点对象往往会被执行器、回调、通信对象等共享持有。
保持节点运行
rclcpp::spin(node);
spin() 可以理解为“让节点进入循环等待状态”。
如果没有这一句,程序执行完构造函数后就直接退出了。
而节点通常需要长期运行,等待消息、处理回调,所以需要 spin()。
哪怕这个例子只是打印一句话,也建议保留这个结构,因为以后订阅、发布、定时器几乎都依赖这种运行机制。
关闭 ROS 2
rclcpp::shutdown();
程序结束前关闭 ROS 2,完成资源清理。
八、修改 CMakeLists.txt
仅仅写完 .cpp 文件还不够,还要告诉 CMake:
这个源文件要被编译成一个可执行程序。
打开 cpp_node_demo/CMakeLists.txt,找到合适位置,加入下面内容:
cmake_minimum_required(VERSION 3.8)
project(cpp_node_demo)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
add_executable(hello_node src/hello_node.cpp)
ament_target_dependencies(hello_node rclcpp)
install(TARGETS
hello_node
DESTINATION lib/${PROJECT_NAME}
)
ament_package()
九、这份 CMakeLists.txt 的关键点
1. 查找依赖
find_package(rclcpp REQUIRED)
告诉 CMake:这个项目依赖 rclcpp,必须找到它。
2. 生成可执行文件
add_executable(hello_node src/hello_node.cpp)
表示将 src/hello_node.cpp 编译为名为 hello_node 的可执行程序。
注意:
hello_node是可执行文件名字- 不一定非要和类名一致
- 但最好命名清晰,方便运行和维护
3. 绑定依赖
ament_target_dependencies(hello_node rclcpp)
告诉构建系统:hello_node 这个目标依赖 rclcpp。
这样编译器才知道去哪里找头文件、链接哪个库。
4. 安装目标
install(TARGETS
hello_node
DESTINATION lib/${PROJECT_NAME}
)
这一步非常重要。
如果不 install,很多时候你编译完也没法通过 ros2 run 找到这个节点。
ROS 2 的运行机制依赖安装目录中的布局,而不是直接从源码目录裸跑。
十、检查 package.xml
如果你创建包时已经加了 --dependencies rclcpp,通常它已经自动写好了。
你可以检查一下是否包含类似内容:
<depend>rclcpp</depend>
一个简化版的 package.xml 可能长这样:
<?xml version="1.0"?>
<package format="3">
<name>cpp_node_demo</name>
<version>0.0.0</version>
<description>My first ROS 2 C++ node</description>
<maintainer email="you@example.com">your_name</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
十一、编译功能包
回到工作空间根目录:
cd ~/ros2_ws
colcon build
如果你只想编译这个包,可以写成:
colcon build --packages-select cpp_node_demo
编译成功后,别忘了 source:
source install/setup.bash
这一步的作用是让当前终端识别你刚刚编译好的包。
十二、运行节点
使用下面的命令运行:
ros2 run cpp_node_demo hello_node
这里要注意:
cpp_node_demo是包名hello_node是可执行文件名
如果一切正常,你会看到类似输出:
[INFO] [时间戳] [hello_node]: Hello ROS 2, 节点已启动!
说明你的第一个 ROS 2 C++ 节点已经成功运行了。
十三、顺带理解几个最容易混淆的名字
初学 ROS 2 时,最容易混的就是下面这几个名字:
1. 包名
比如:
cpp_node_demo
这是整个功能包的名字。
2. 节点名
在代码里:
Node("hello_node")
这里的 hello_node 是 节点名。
它主要体现在 ROS 图、日志输出、节点查询中。
比如你可以用:
ros2 node list
查看当前正在运行的节点。
3. 可执行文件名
在 CMakeLists.txt 里:
add_executable(hello_node src/hello_node.cpp)
这里的 hello_node 是 可执行文件名。
ros2 run 运行时用的是它。
4. 类名
在代码里:
class HelloNode
这是 C++ 的类名,只在代码层面起作用。
虽然这几个名字可以一样,但它们本质上不是一回事。
这一点在后面写复杂项目时非常重要。
十四、为什么 ROS 2 节点通常写成类
有些初学者会疑惑:直接在 main() 里写代码不行吗?为什么非要继承 rclcpp::Node?
当然可以直接写,但 ROS 2 官方和工程实践里,通常更推荐面向对象的写法,原因有三个:
1. 结构更清晰
节点相关的:
- 发布者
- 订阅者
- 定时器
- 参数
- 回调函数
都可以封装在一个类里,逻辑会更清楚。
2. 更适合扩展
后面你写发布订阅、服务、参数服务器时,类写法更自然。
3. 更符合大型工程习惯
实际项目里,一个节点往往不止几行代码,而是几十到几百行,甚至更多。
用类组织代码更利于维护。
十五、常见报错与排查思路
1. ros2: command not found
说明 ROS 2 环境没有 source。
解决:
source /opt/ros/humble/setup.bash
如果想每次自动生效,可以把这句加入 ~/.bashrc。
2. Package 'cpp_node_demo' not found
通常有几种原因:
- 还没有编译
- 编译失败
- 没有 source
install/setup.bash - 当前终端不是那个工作空间环境
可以按顺序检查:
cd ~/ros2_ws
colcon build --packages-select cpp_node_demo
source install/setup.bash
3. No executable found
这通常说明:
CMakeLists.txt没有add_executable- 没有写
install(TARGETS ...) - 可执行文件名写错了
这是初学者最常见的问题之一。
4. 头文件找不到,比如 rclcpp/rclcpp.hpp
说明依赖没有正确声明或环境没配好。
检查:
package.xml是否有<depend>rclcpp</depend>CMakeLists.txt是否有find_package(rclcpp REQUIRED)- 是否 source 了 ROS 2 环境
十六、进一步扩展:让节点周期性输出日志
如果你觉得上面的节点太简单,还可以加一个定时器,让它每秒打印一次日志。
例如:
#include "rclcpp/rclcpp.hpp"
#include <chrono>
using namespace std::chrono_literals;
class HelloNode : public rclcpp::Node
{
public:
HelloNode() : Node("hello_node")
{
timer_ = this->create_wall_timer(
1s,
std::bind(&HelloNode::timer_callback, this)
);
}
private:
void timer_callback()
{
RCLCPP_INFO(this->get_logger(), "我正在持续运行...");
}
rclcpp::TimerBase::SharedPtr timer_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
auto node = std::make_shared<HelloNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
这个版本就更像一个真正的 ROS 2 节点了,因为它不是启动后只打印一次,而是会持续运行并周期性执行回调。
这里你可以提前感受两个重要思想:
- 回调函数:ROS 2 中大量逻辑是通过回调执行的
- 事件驱动:节点不是写一个大循环死跑,而是等事件来了再处理