初识ROS2
ROS 2(Robot Operating System 2)是一个面向机器人开发的开源软件框架,可以理解为机器人软件的"操作系统",但它本身并不是一个真正的操作系统,而是一套帮助开发机器人应用的工具、库和通信机制。在ROS2中提供了一套简便易用的进程间通信机制,通过这套机制可以将机器人的各个模块间的通信变得标准化易用化。而且在ROS2强大的生态支持下,很多常用包(Package)的安装和使用都变得非常简单,开发者可以专注于算法的实现,而不必过多关注底层通信细节。
ROS2 的核心概念包括节点(Node)、话题(Topic)、服务(Service)、动作(Action)、消息(Message)、软件包(Package)和启动文件(Launch)。这些概念构成了ROS2的基础,使得开发者能够高效地构建和管理机器人应用。不同部分的作用如下表所示:
| 概念 | 作用 |
|---|---|
| Node(节点) | 一个独立运行的程序 |
| Topic(话题) | 发布/订阅消息,实现数据广播 |
| Service(服务) | 请求-响应通信 |
| Action(动作) | 用于执行耗时任务(如导航) |
| Message(消息) | 节点之间传输的数据格式 |
| Package(软件包) | 管理代码和资源 |
| Launch | 一键启动多个节点 |
对于一个使用ROS2的项目,其项目结构通常如下所示:
bash
my_robot_ws/ # ROS 2 工作空间 (Workspace)
│
├── src/ # 源代码空间 (Source Space)
│ │ # 存放所有 ROS 2 软件包 (Package)
│ │
│ └── my_robot_package/ # 自定义 ROS 2 软件包
│ │
│ ├── CMakeLists.txt # CMake 构建配置文件
│ ├── package.xml # 软件包描述文件与依赖管理
│ │
│ ├── src/ # C++ 源文件目录
│ │ └── *.cpp
│ │
│ └── include/ # 头文件目录
│ └── my_robot_package/
│ └── *.hpp
│
├── build/ # 构建空间 (Build Space)
│ # 存放 CMake 编译过程产生的中间文件
│
├── install/ # 安装空间 (Install Space)
│ # 存放编译安装后的可执行文件、库、配置文件
│
└── log/ # 日志空间 (Log Space)
# 存放 colcon 构建过程日志
当然,上面的结构是用C++编写的ROS2包的典型结构,如果使用Python编写,结构会有所不同,但核心概念和工作空间的组织方式是类似的。
下面我将一一解释这些概念,并提供一些常用的ROS2命令。
工作空间(Workspace)、软件包(Package)、节点(Node)
在ROS2中,工作空间是整个项目的根目录,软件包是组织代码和资源的基本单位,每个软件包可以包含一个或多个节点。节点是ROS2中最小的执行单元,通常对应一个独立运行的程序。节点之间通过话题、服务和动作进行通信。
比如在一个机器人项目中,需要使用的硬件功能有摄像头(Camera),导航(Navigation)和机械臂(Arm)等功能,而每个硬件又可以对应一个或多个节点来实现其功能,如摄像头包中可能包含一个节点用于图像采集,一个节点用于图像处理。则其相对应的工作空间结构可能如下:
bash
my_robot_ws/ # 使用后缀_ws表示工作空间(即workspace的缩写)
├── src/
│ ├── camera_package/ # 摄像头软件包
│ │ ├── camera_node.cpp # 摄像头节点
│ │ └── image_processing_node.cpp # 图像处理节点
│ ├── navigation_package/ # 导航软件包
│ │ └── navigation_node.cpp # 导航节点
│ └── arm_package/ # 机械臂软件包
│ └── arm_control_node.cpp # 机械臂控制节点
这个结构清晰地展示了工作空间、软件包和节点之间的关系。每个软件包可以包含多个节点,每个节点可以独立运行并与其他节点进行通信。
软件包的创建和管理
在ROS2中,软件包是组织代码和资源的基本单位。创建一个新的软件包通常使用ros2 pkg create命令。例如,要创建一个名为robot_package的软件包,可以使用以下命令:
bash
ros2 pkg create \
<package_name> \ # 软件包名称
--build-type <build_type> \ # 构建类型(如 ament_cmake 或 ament_python)
--dependencies <dependency1> <dependency2> ... \ # 依赖的其他软件包
--maintainer-name <name> \ # 维护者名称
--maintainer-email <email> \ # 维护者邮箱
--description "<description>" \ # 软件包描述
--license <license> \ # 许可证类型
--node-name <node_name> # 节点名称
一个简单的C++软件包创建示例:
bash
ros2 pkg create robot_package --build-type ament_cmake --dependencies rclcpp --license Apache-2.0
# 创建一个名为robot_package的软件包,使用ament_cmake构建类型,并依赖rclcpp库,并指定Apache-2.0许可证。
# 当然,若创建Python软件包,则可以使用ament_python构建类型,并依赖rclpy库。
dependencies参数用于指明软件包所依赖的其他ROS2软件包,这些依赖关系在创建包时会被记录在package.xml文件中以及会自动的在CMakeLists.txt中添加相应的依赖配置(find_package(依赖包))。
创建完毕的软件包如下图所示:

其中include目录用于存放头文件,src目录用于存放源代码文件,CMakeLists.txt和package.xml是软件包的配置文件。CMakeLists.txt用于定义如何构建软件包,而package.xml用于描述软件包的元数据和依赖关系。其中package.xml文件的内容如下所示,可以看见文件中将软件包的各类信息进行了详细的描述,包括软件包名称、版本、描述、维护者信息、许可证类型以及依赖关系等。这个就相当于软件包的"身份证",在ROS2中是非常重要的文件。
xml
<?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>robot_package</name>
<version>0.0.0</version>
<description>TODO: Package description</description>
<maintainer email="CrescentRose.com">rose</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>
注意,此后在编写软件包时如果需要添加新的依赖包,则需要在package.xml中添加相应的依赖声明,并在CMakeLists.txt中添加相应的find_package()配置。
xml
<depend>new_dependency</depend>
如
<depend>opencv</depend>
节点的创建和运行
在ROS2中,节点是最小的执行单元,通常对应一个独立运行的程序。每个节点可以独立运行,并与其他节点进行通信。一个节点对应到一个可执行文件,通常在src目录下编写源代码文件,并在CMakeLists.txt中进行配置以生成可执行文件。
在C++中创建一个简单的节点示例:
cpp
#include "rclcpp/rclcpp.hpp"
// rclcpp即ros2 client library for C++,是ROS2中C++客户端库的核心头文件,包含了创建节点、发布/订阅消息、服务调用等功能所需的类和函数。
// 同理 rclpy是ROS2中Python客户端库的核心模块,提供了类似的功能。
class MyNode : public rclcpp::Node // 所有节点必须继承自rclcpp::Node类
{
public:
MyNode() : Node("my_node") // 构造函数中调用父类构造函数,并传入节点名称
{
RCLCPP_INFO(this->get_logger(), "Hello ROS2 Node!");
RCLCPP_INFO(this->get_logger(), "Node name: %s", this->get_name());
// 使用RCLCPP_INFO宏函数打印日志信息,this->get_logger()获取节点的日志记录器来输出日志信息,后面的参数可以是字符串也可以是格式化字符串,类似于printf函数的用法。
}
};
int main(int argc, char **argv)
{
// 初始化 ROS2
rclcpp::init(argc, argv);
// 创建节点的智能指针,在ROS2中,使用智能指针去管理各类对象是非常常用的,能够有效避免内存泄漏和悬空指针等问题。
auto node = std::make_shared<MyNode>();
// 保持节点运行,进入循环并等待事件的发生,直到节点被关闭
// 如果学过Qt编程,这个操作很像Qt中的事件循环机制,Qt中使用app.exec()来启动事件循环,而ROS2中使用rclcpp::spin(node)来启动节点的事件循环。
rclcpp::spin(node);
// 关闭 ROS2 并释放资源
rclcpp::shutdown();
return 0;
}
在编写完节点代码后,需要在CMakeLists.txt中添加相应的配置以生成可执行文件。在创建软件包时,CMakeLists.txt文件中已经包含了基本的构建配置。
cmake
cmake_minimum_required(VERSION 3.8)
project(robot_package)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic) # 添加编译选项,启用警告信息
endif()
# find dependencies 添加依赖包
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
# 如果开启了测试才执行下面的代码
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# the following line skips the linter which checks for copyrights
# comment the line when a copyright and license is added to all source files
set(ament_cmake_copyright_FOUND TRUE)
# the following line skips cpplint (only works in a git repo)
# comment the line when this package is in a git repo and when
# a copyright and license is added to all source files
set(ament_cmake_cpplint_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
endif()
ament_package() # 在这句之前添加可执行文件的构建配置,此后的语句将不会被执行到
为了能够生成可执行文件,以及在编译时链接所需的依赖库,需要在CMakeLists.txt中添加以下配置:
cmake
add_executable(my_node src/my_node.cpp) # 添加可执行文件,指定源代码
ament_target_dependencies(my_node rclcpp) # 指定可执行文件所依赖的库,这里依赖rclcpp库
install(TARGETS my_node DESTINATION lib/${PROJECT_NAME}) # 安装可执行文件到指定目录
这部分需要一些CMake的基础知识,可以通过我的博客从编译链接到Cmake以及find_package使用指南进行学习。
值得注意的是,下面的两种写法是相同的:
cmake
# ament_cmake的写法
add_executable(my_node src/my_node.cpp)
ament_target_dependencies(my_node rclcpp)
# 常见的CMake写法
add_executable(my_node src/my_node.cpp)
target_include_directories(my_node PUBLIC ${rclcpp_INCLUDE_DIRS})
target_link_libraries(my_node ${rclcpp_LIBRARIES})
在ROS2中,其使用了ament_cmake作为构建系统的扩展,提供了一些便捷的宏和函数来简化CMake的配置。ament_target_dependencies就是其中之一。
但是,虽然使用ament_target_dependencies可以简化CMake的配置,但是这只适用于ament_cmake构建的package,即在package.xml中指定了
<build_type>ament_cmake</build_type>的package。如果是使用其他构建系统(如catkin或纯CMake),则需要使用传统的CMake方式来配置依赖关系.
最后要使用install命令将可执行文件安装到指定目录,这样在构建完成后,可执行文件将被放置在install/lib/<package_name>目录下,该目录是ROS2的标准安装路径,方便在运行时找到可执行文件。
cmake
install(TARGETS my_node
DESTINATION lib/${PROJECT_NAME}) # 安装可执行文件到指定目录
# 这里TARGETS指定要安装的目标文件,DESTINATION指定安装路径,${PROJECT_NAME}是CMake中的变量,表示当前项目的名称,这里将可执行文件安装到lib目录下。
在ROS2中,使用colcon build来对软件包进行编译和构建。colcon build命令会根据CMakeLists.txt中的配置调用ament_cmake工具来生成可执行文件,并将其安装到指定的安装路径中。安装路径通常是install/<package_name>/lib/<package_name>,其中<package_name>是软件包的名称。
此后执行命令
bash
ros2 run <package_name> <executable_name>
即可运行该节点。
假设软件包名称为
robot_package,可执行文件名称为my_node,则运行命令为:ros2 run robot_package my_node。但是当你构建成功后,执行该指令后,会有会提示找不到包,
这是因为ros2 run指令会在环境变量AMENT_PREFIX_PATH所在的路径中查找包,但是构建完毕后我们并没有更新环境变量,所以需要执行下面的命令来更新环境变量:
bash
source install/setup.bash
其中install/setup.bash是ROS2在构建完成后自动生成的一个脚本文件,用于设置环境变量,使得ROS2能够找到安装的包和可执行文件。执行该命令后,AMENT_PREFIX_PATH环境变量将被更新,会将安装路径添加到其中,从而使得ros2 run robot_package my_node命令能够正确找到并运行节点。
最后再来解决一个问题:为什么install指令只指出安装路径是lib/${PROJECT_NAME},而不是install/${PROJECT_NAME}/lib/${PROJECT_NAME},但是安装后却可以将可执行文件安装到install/${PROJECT_NAME}/lib/${PROJECT_NAME}目录下呢?
这是因为在ROS2中,install指令中的路径是相对Cmake的安装前缀路径(即CMAKE_INSTALL_PREFIX这个变量)的,而当我们使用colcon build调用ament_cmake进行构建时,CMAKE_INSTALL_PREFIX会被自动指向install/${PROJECT_NAME}/目录下的工作空间路径。可以通过查看build目录下的CMakeCache.txt文件来验证这一点。

最后总结一下
- 使用
ros2 pkg create命令创建软件包,指定构建类型和依赖关系。- 在
src目录下新建代码文件编写节点的源代码,并在CMakeLists.txt中配置生成可执行文件,并指定依赖关系。- 使用
colcon build命令进行构建,生成可执行文件并安装到指定路径。- 使用
source install/setup.bash更新环境变量,使得ROS2能够找到安装的包和可执行文件。- 注意!!!!使用
colcon build命令最好在工作空间目录下执行,而不是包内执行,否则可能会导致构建失败或找不到包的问题,且这样执行可以直接在工作空间下构建所有包,而不需要进入每个包的目录进行构建,同时也会考虑到包之间的依赖关系,确保构建顺序正确。- 使用
ros2 run <package_name> <executable_name>命令运行节点,确保在运行前已经更新了环境变量。
话题通信与接口
上面我们说了,节点之间可以通过话题进行通信。对于一个节点,他可以作为话题的发布者(Publisher)或者订阅者(Subscriber)。发布者负责将消息发送到话题,而订阅者则接收来自话题的消息。话题通信是ROS2中最常用的通信方式。话题通信所发布的消息类型是由接口(Interface)定义的,接口定义了消息的结构和数据类型。ROS2提供了多种内置的消息类型,如
std_msgs、sensor_msgs等,也支持自定义消息类型。可以将消息接口类比为一个定义好的数据包,发布者和订阅者都需要遵循这个数据包的格式来进行通信。
我们可以使用ros2 interface list命令来查看所有可用的接口类型,使用ros2 interface show <interface_name>命令来查看某个接口的详细定义。
比如,查看std_msgs/msg/String接口的定义:
bash
ros2 interface show std_msgs/msg/String
输出结果如下:
string data
这代表着std_msgs/msg/String这个接口定义了一个名为data的字符串类型字段,发布者在发送消息时需要填充这个字段,而订阅者在接收消息时也会获取到这个字段的值。
其中,string是ROS2中定义的基本数据类型之一,表示一个字符串类型的数据。除了string,ROS2还支持多种基本数据类型,如int32、float64、bool等,以及数组和自定义消息类型。其他基本类型见下表:
| 类型 | 说明 | C++对应类型 |
|---|---|---|
| bool | 布尔 | bool |
| byte | 8位无符号整数 | uint8_t |
| char | 8位字符 | uint8_t |
| float32 | 32位浮点 | float |
| float64 | 64位浮点 | double |
| int8 | 8位有符号整数 | int8_t |
| uint8 | 8位无符号整数 | uint8_t |
| int16 | 16位有符号整数 | int16_t |
| uint16 | 16位无符号整数 | uint16_t |
| int32 | 32位有符号整数 | int32_t |
| uint32 | 32位无符号整数 | uint32_t |
| int64 | 64位有符号整数 | int64_t |
| uint64 | 64位无符号整数 | uint64_t |
| string | 字符串 | std::string |
当然,通过组合这些基本类型,我们可以定义出更复杂的消息类型。后面我们将介绍如何自定义消息类型,并在节点之间进行通信。
话题通信
在ROS2中,话题通信是节点之间最常用的通信方式。一个节点可以作为话题的发布者(Publisher)或者订阅者(Subscriber)。发布者负责将消息发送到话题,而订阅者则接收来自话题的消息。话题通信是基于发布-订阅模式的,这种模式允许多个发布者和订阅者同时存在,从而实现了灵活的数据传输。在创建一个节点时,我们所继承的
rclcpp::Node类提供了创建发布者和订阅者的接口create_publisher和create_subscription,通过这两个接口,我们可以轻松地创建发布者和订阅者,并指定消息类型和话题名称。
下面是一个简单的发布者节点示例:
cpp
/*
* in pub_node.cpp
*/
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class MyNode : public rclcpp::Node
{
private:
// 创建一个指向发布者的指针,发布 std_msgs::msg::String 类型的消息,主题名为 "chatter",队列大小为 10
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
public:
MyNode() : Node("pub_node")
{
RCLCPP_INFO(this->get_logger(), "Hello ROS2 Node!");
// 创建一个发布者,发布 std_msgs::msg::String 类型的消息,主题名为 "chatter",队列大小为 10
publisher_ = this->create_publisher<std_msgs::msg::String>("chatter", 10);
timer_ = this->create_wall_timer(
std::chrono::seconds(1),
[this]() { this->timer_callback(); }
);
}
void timer_callback()
{
static int count = 0;
auto message = std_msgs::msg::String();
message.data = "Hello, ROS2!" + std::to_string(count);
publisher_->publish(message);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
count++;
}
};
int main(int argc, char **argv)
{
// 初始化 ROS2
rclcpp::init(argc, argv);
// 创建节点的智能指针
auto node = std::make_shared<MyNode>();
// 保持节点运行
rclcpp::spin(node);
// 关闭 ROS2 并释放资源
rclcpp::shutdown();
return 0;
}
其中需要注意的是下面几个api
cpp
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
Node->create_publisher<std_msgs::msg::String>("chatter", 10);
Node->create_wall_timer(std::chrono::seconds(1), [this]() { this->timer_callback(); });
其中
rclcpp::Publisher<消息类型>::SharedPtr publisher_;:声明了一个指向发布者的智能指针,发布者用于发送指定类型的消息。在本例中,发布者将发送std_msgs::msg::String类型的消息。即使用代码
cpp
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
// 实际上这是ROS2的封装写法,等价于
std::shared_ptr<rclcpp::Publisher<std_msgs::msg::String>> publisher_;
// 但是显然前者更易读,而且是ROS2官方推荐的写法。
而rclcpp::TimerBase::SharedPtr timer_;:声明了一个指向定时器的智能指针,定时器用于在指定的时间间隔内触发回调函数。在本例中,定时器每隔1秒触发一次timer_callback函数。
在构造函数中,使用create_publisher方法创建了一个发布者,指定了消息类型为std_msgs::msg::String,话题名称为"chatter",队列大小为10。队列大小用于控制消息的缓存数量,当发布者发送消息的速度超过订阅者接收消息的速度时,队列会缓存一定数量的消息,以避免丢失数据。
cpp
publisher_ = this->create_publisher<std_msgs::msg::String>("chatter", 10);
同时,使用create_wall_timer方法创建了一个定时器,每隔1秒触发一次timer_callback函数。在定时器的回调函数中,创建了一个std_msgs::msg::String类型的消息,并将其发布到话题"chatter"上。每次发布消息时,都会在日志中打印出发布的消息内容。
cpp
timer_ = this->create_wall_timer(
std::chrono::seconds(1),
[this]() { this->timer_callback(); }
);
// 这里提供了一种将类内函数作为回调函数的方式,使用了C++11的lambda表达式,将类的成员函数`timer_callback`绑定到定时器上。这样,每当定时器触发时,就会调用`timer_callback`函数,从而实现定时发布消息的功能。
// 当然,还可以使用std::bind去实现这个功能,这里不再赘述。
其中timer_callback函数的实现如下:
cpp
void timer_callback()
{
static int count = 0;
auto message = std_msgs::msg::String(); // 创建一个std_msgs::msg::String类型的消息对象
message.data = "Hello, ROS2!" + std::to_string(count); // 设置消息的内容,这里使用了一个计数器来生成不同的消息
publisher_->publish(message); // 发布消息到话题"chatter"
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str()); // 打印日志信息,显示发布的消息内容
count++;
}
这样就实现了一个简单的发布者节点,每隔1秒发布一条消息到话题"chatter"上。
然后我们可以创建一个订阅者节点来接收这个话题的消息。订阅者节点的实现如下:
cpp
/*
* in sub_node.cpp
*/
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class MyNode : public rclcpp::Node
{
private:
// 创建一个指向发布者的指针,发布 std_msgs::msg::String 类型的消息,主题名为 "chatter",队列大小为 10
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscriber_;
public:
MyNode() : Node("sub_node")
{
RCLCPP_INFO(this->get_logger(), "Hello ROS2 Node!");
// 创建一个订阅者,订阅 std_msgs::msg::String 类型的消息,主题名为 "chatter",队列大小为 10
subscriber_ = this->create_subscription<std_msgs::msg::String>("chatter",10,
[this](const std_msgs::msg::String::SharedPtr msg) {on_recive_msg(msg); });
}
void on_recive_msg(const std_msgs::msg::String::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(), "Received: '%s'", msg->data.c_str());
}
};
int main(int argc, char **argv)
{
// 初始化 ROS2
rclcpp::init(argc, argv);
// 创建节点的智能指针
auto node = std::make_shared<MyNode>();
// 保持节点运行
rclcpp::spin(node);
// 关闭 ROS2 并释放资源
rclcpp::shutdown();
return 0;
}
我们关注以下几个api
cpp
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscriber_;
Node->create_subscription<std_msgs::msg::String>("chatter",10, [this](const std_msgs::msg::String::SharedPtr msg) {on_recive_msg(msg); });
其中
rclcpp::Subscription<消息类型>::SharedPtr subscriber_;:声明了一个指向订阅者的智能指针,订阅者用于接收指定类型的消息,在本例中,订阅者将接收std_msgs::msg::String类型的消息。即使用代码
cpp
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscriber_;
// 类似于上面的写法,这也是ROS2的封装写法,等价于
std::shared_ptr<rclcpp::Subscription<std_msgs::msg::String>> subscriber_;
随后在构造函数中,使用create_subscription方法创建了一个订阅者,指定了消息类型为std_msgs::msg::String,话题名称为"chatter",队列大小为10,并绑定了一个回调函数on_recive_msg。当订阅者接收到消息时,会调用这个回调函数来处理消息。
cpp
subscriber_ = this->create_subscription<std_msgs::msg::String>("chatter",10,
[this](const std_msgs::msg::String::SharedPtr msg) {on_recive_msg(msg); });
也就是说,当订阅者接收到来自话题"chatter"的消息时,会自动调用on_recive_msg函数,并将接收到的消息作为参数传递给该函数。而on_recive_msg函数的实现如下:
cpp
void on_recive_msg(const std_msgs::msg::String::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(), "Received: '%s'", msg->data.c_str());
// 打印日志信息,显示接收到的消息内容
}
这里需要注意点
- 可执行文件名\(\ne\)节点名。在使用
ros2 run <package_name> <executable_name>命令运行节点时,<executable_name>是指在CMakeLists.txt中通过add_executable命令指定的可执行文件名,而不是节点的名称。而节点名字是通过在节点类的构造函数中调用Node基类的构造函数时传入的字符串参数来指定的。一个可执行文件可以包含多个节点。- 订阅者与发布者必须使用相同的话题名称和消息类型才能进行通信。在本例中,发布者和订阅者都使用了话题名称
"chatter"和消息类型std_msgs::msg::String,因此它们可以成功地进行通信。
当编码工作结束后,我们可以使用colcon build命令进行构建,然后分别在两个终端中运行发布者和订阅者节点,观察它们之间的通信效果。发布者节点会每隔1秒发布一条消息,而订阅者节点会接收到这些消息并打印出来。注意:在运行节点之前,请确保已经执行了source install/setup.bash命令,以便更新环境变量,使得ROS2能够找到安装的包和可执行文件。
bash
# in terminal 1
source install/setup.bash
ros2 run robot_package pub_node # 运行发布者节点
bash
# in terminal 2
source install/setup.bash
ros2 run robot_package sub_node # 运行订阅者节点
此时,订阅者节点会接收到发布者节点发送的消息,并在终端中打印出来,显示通信成功。

可以使用下面的命令来查看一些运行信息
bash
ros2 node list # 查看当前运行的节点列表

可以看到当前运行的节点列表中包含了
pub_node和sub_node两个节点,分别对应发布者和订阅者节点。注意,这里的节点名称是我们在节点类的构造函数中指定的,而不是可执行文件的名称。
bash
ros2 topic list # 查看当前运行的节点所使用的话题列表

可以看到当前运行的节点所使用的话题列表中包含了
/chatter话题,这是我们在发布者和订阅者节点中指定的话题名称。
使用如下命令会
bash
rqt_graph # 查看当前运行的节点之间的通信关系图

可以看到,pub_node节点通过话题/chatter与sub_node节点进行通信,形成了一个发布-订阅的关系。这个图形化界面可以帮助我们更直观地理解节点之间的通信关系。
接口
在ROS2中,接口(Interface)是定义消息类型和服务类型的规范。接口定义了数据的结构和格式,使得不同节点之间能够以统一的方式进行通信。ROS2提供了多种内置的接口类型,如
std_msgs、sensor_msgs等,也支持自定义接口类型。上面所用的std_msgs/msg/String就是一个内置的消息接口类型,它定义了一个包含字符串数据的消息结构。通过接口,发布者和订阅者可以确保发送和接收的数据格式一致,从而实现可靠的通信。
假设目前的工作空间为ros2_study_ws,我们在下面新建一个src文件夹,此后所有的包都放在这个文件夹下,这正符合我们之前所说的工作空间结构。然后我们在src文件夹下新建一个名为test_create_interface的包,用于存放自定义接口类型。使用如下命令创建该包:
bash
ros2 pkg create test_create_interface --build-type ament_cmake --dependencies rclcpp rosidl_default_generators builtin_interfaces --license Apache-2.0
这里需要注意的是,依赖项中的rosidl_default_generators(即 ros2 interface defination library),是ROS2中用于生成接口代码的工具包,它提供了将后缀为.msg or .srv or .action的文件生成接口所需的CMake宏和函数,对于自定义接口,这个工具包是必需的
而builtin_interfaces是ROS2中内置的接口类型库,包含了一些常用的接口类型,如时间、持续时间等。在自定义接口类型时,我们需要依赖这些工具包来生成相应的代码。我们使用这个包来演示自定义接口不仅可以使用基础的数据类型,也可以使用别人定义好的接口类型作为自己定义的接口的一个成员。
可以使用ros2 interface list命令来查看当前工作空间中所有可用的接口类型,包括内置接口和自定义接口。使用ros2 interface show <interface_name>命令可以查看某个接口的详细定义。

可以看到,builtin_interfaces包中包含了Duration和Time两个接口类型,同时也可以看到builtin_interfaces/msg/Time接口由于两个基础类型组成
bash
int32 sec
uint32 nanosec
这分别为秒和纳秒,其中纳秒用于表示小数部分的时间。
其包的目录结构如下:
bash
test_create_interface/
├── CMakeLists.txt
├── package.xml
├── msg/ # 存放自定义消息类型的目录
│ └── People.msg # 自定义消息类型文件
注意我们需要自己新建一个msg文件夹,用于存放自定义消息类型的定义文件。然后在msg文件夹下创建一个后缀为.msg的文件,这是ros2中自定义消息类型的规范。在该文件中,我们可以定义自定义消息类型的结构和数据类型。例如,我们可以定义一个包含字符串、整数和浮点数的自定义消息类型, 取名为People.msg:
plaintext
# People.msg
string name # 字符串, 表示人的姓名
int32 age # 整数,表示人的年龄
float32 height # 浮点数,表示人的身高
# 在.msg文件中,使用# 来添加注释,该注释内容会在ros2 interface show命令中显示出来,方便我们理解消息类型的结构和含义。
builtin_interfaces/Time birth_time # 使用内置接口类型,表示人的出生时间
注意,在使用现成的接口类型时,填入的不是完整的路径,如builtin_interfaces/msg/Time,而是builtin_interfaces/Time,将msg省略掉了,这是ROS2的规范,表示我们使用的是该包中的消息类型。
在完成上面的.msg文件的编写后,下一步就是去修改CMakeLists.txt文件,以便在构建过程中生成自定义消息类型的代码。我们需要在CMakeLists.txt中添加以下内容:
cmake
find_package(rosidl_default_generators REQUIRED) # 查找rosidl_default_generators包
# 当然,若在创建包时已经指定了依赖项,则不需要再次查找该包,因为在创建包时,`CMakeLists.txt`中已经包含了相应的`find_package()`配置。
# 反之,若在创建包时没有指定依赖项,则需要在`CMakeLists.txt`中添加相应的`find_package()`配置,以便在构建过程中找到所需的依赖包。同时也要在`package.xml`中添加相应的依赖声明,以确保在构建过程中能够正确地找到所需的依赖包。
# 该Cmake宏用于生成接口代码,指定当前包名和自定义消息类型文件的路径,并指定依赖的其他接口包。
rosidl_generate_interfaces(${PROJECT_NAME} # 生成接口代码,指定当前包名
"msg/People.msg" # 指定自定义消息类型文件的路径
DEPENDENCIES builtin_interfaces # 指定依赖的其他接口包,这里依赖了builtin_interfaces包,因为People.msg中使用了该包中的Time接口类型
)
配置完毕后,还需在package.xml中添加相应的接口声明,
xml
<member_of_group>rosidl_interface_packages</member_of_group>
这句声明表示该包是一个接口包,属于ROS2的接口包组。这样,在构建过程中,ROS2会将该包识别为一个接口包,并生成相应的接口代码。
也就是说这个包仅仅用于定义接口类型,而不包含任何节点的实现。其他包可以依赖这个接口包,并使用其中定义的接口类型进行通信。
当上述一切配置完成后,我们可以使用colcon build命令进行构建(注意要在工作空间目录下执行),然后使用source ./install/setup.bash命令更新环境变量,使得ROS2能够找到安装的包和可执行文件。然后使用ros2 interface list看一下我们自定义的接口类型是否已经生成成功。然后使用ros2 interface show <interface_name>命令查看自定义接口类型的详细定义,验证其是否符合预期。具体演示见下图。

此时,我们已经成功地创建了一个自定义接口类型People,并且可以在其他包中使用该接口类型进行通信。
发布者node
cpp
#include "rclcpp/rclcpp.hpp"
#include "test_create_interface/msg/people.hpp"
class MyNode : public rclcpp::Node
{
private:
rclcpp::Publisher<test_create_interface::msg::People>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
public:
MyNode() : Node("pub_node")
{
RCLCPP_INFO(this->get_logger(), "Hello ROS2 Node!");
publisher_ = this->create_publisher<test_create_interface::msg::People>("people", 10);
timer_ = this->create_wall_timer(
std::chrono::seconds(1),
[this]() { this->timer_callback(); }
);
}
void timer_callback()
{
static int count = 0;
auto message = test_create_interface::msg::People();
message.name = "Person " + std::to_string(count);
message.age = count;
message.height = 1.5;
publisher_->publish(message);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.name.c_str());
count++;
}
};
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<MyNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
订阅者node
cpp
#include "rclcpp/rclcpp.hpp"
#include "test_create_interface/msg/people.hpp"
class MyNode : public rclcpp::Node
{
private:
rclcpp::Subscription<test_create_interface::msg::People>::SharedPtr subscription_;
public:
MyNode() : Node("sub_node")
{
RCLCPP_INFO(this->get_logger(), "Hello ROS2 Node!");
subscription_ = this->create_subscription<test_create_interface::msg::People>(
"people",
10,
[this](const test_create_interface::msg::People::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(), "Received: '%s', Age: %d, Height: %.2f", msg->name.c_str(), msg->age, msg->height);
}
);
}
};
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<MyNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}


可以看到,发布者节点每隔1秒发布一条包含姓名、年龄和身高的自定义消息,而订阅者节点接收到这些消息后,会打印出接收到的消息内容,包括姓名、年龄和身高。这样,我们就成功地实现了使用自定义接口类型进行话题通信的功能。
总结
常用ros2命令
bash
# 新建包
ros2 pkg create <package_name> --build-type ament_cmake --dependencies <dependencies> --license <license>
# 查看包信息
ros2 pkg list # 查看当前工作空间中所有可用的包
ros2 pkg prefix <package_name> # 查看指定包的安装路径
ros2 pkg executables <package_name> # 查看指定包中所有可执行文件的列表
ros2 pkg xml <package_name> # 查看指定包的package.xml文件内容
# 查看接口信息
ros2 interface list # 查看当前工作空间中所有可用的接口类型,包括内置接口和自定义接口
ros2 interface show <interface_name> # 查看某个接口的详细定义,包括消息结构和数据类型
# 查看节点信息
ros2 node list # 查看当前运行的节点列表
ros2 node info <node_name> # 查看指定节点的详细信息,包括节点名称、话题、服务等
# 查看话题信息
ros2 topic list # 查看当前运行的节点所使用的话题列表
ros2 topic info <topic_name> # 查看指定话题的详细信息,包括消息类型、发布者和订阅者等
ros2 topic echo <topic_name> # 查看指定话题的消息内容,实时打印接收到的消息
