LK 博客
ros2节点创建
嵌入式
约 1 分钟阅读 0 赞 0 条评论 鸿蒙黑体

ros2节点创建

Jokerbai
Jokerbai @Jokerbai
累计点赞 0 登录后每个账号只能点一次
内容长度 0 正文词元数
正文
目录会跟随阅读位置移动。
阅读进度

从零开始创建一个 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_cmake
  • cpp_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_WARN
  • RCLCPP_ERROR
  • RCLCPP_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 中大量逻辑是通过回调执行的
  • 事件驱动:节点不是写一个大循环死跑,而是等事件来了再处理

作者名片

Jokerbai
Jokerbai
@Jokerbai

这个作者暂时还没有填写个人简介。

评论区
文章作者和管理员都可以管理这里的评论。
0 条评论
登录后即可参与评论。 去登录
还没有评论,欢迎留下第一条交流内容。