初识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的项目,其项目结构通常如下所示:

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)等功能,而每个硬件又可以对应一个或多个节点来实现其功能,如摄像头包中可能包含一个节点用于图像采集,一个节点用于图像处理。则其相对应的工作空间结构可能如下:

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的软件包,可以使用以下命令:

ros2 pkg create
<package_name> \ # 软件包名称
–build-type <build_type> \ # 构建类型(如 ament_cmake 或 ament_python)
–dependencies … \ # 依赖的其他软件包
–maintainer-name \ # 维护者名称
–maintainer-email \ # 维护者邮箱
–description “” \ # 软件包描述
–license \ # 许可证类型
–node-name <node_name> # 节点名称
一个简单的C++软件包创建示例:

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(依赖包))。

创建完毕的软件包如下图所示:

image

其中include目录用于存放头文件,src目录用于存放源代码文件,CMakeLists.txt和package.xml是软件包的配置文件。CMakeLists.txt用于定义如何构建软件包,而package.xml用于描述软件包的元数据和依赖关系。其中package.xml文件的内容如下所示,可以看见文件中将软件包的各类信息进行了详细的描述,包括软件包名称、版本、描述、维护者信息、许可证类型以及依赖关系等。这个就相当于软件包的“身份证”,在ROS2中是非常重要的文件。

<?xml version="1.0"?> <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> robot_package 0.0.0 TODO: Package description rose Apache-2.0

<buildtool_depend>ament_cmake</buildtool_depend>

rclcpp

<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>

ament_cmake 注意,此后在编写软件包时如果需要添加新的依赖包,则需要在package.xml中添加相应的依赖声明,并在CMakeLists.txt中添加相应的find_package()配置。

new_dependency

opencv
节点的创建和运行
在ROS2中,节点是最小的执行单元,通常对应一个独立运行的程序。每个节点可以独立运行,并与其他节点进行通信。一个节点对应到一个可执行文件,通常在src目录下编写源代码文件,并在CMakeLists.txt中进行配置以生成可执行文件。

在C++中创建一个简单的节点示例:

#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_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中添加以下配置:

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使用指南进行学习。

值得注意的是,下面的两种写法是相同的:

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的标准安装路径,方便在运行时找到可执行文件。

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>是软件包的名称。

此后执行命令

ros2 run <package_name> <executable_name>
即可运行该节点。

假设软件包名称为robot_package,可执行文件名称为my_node,则运行命令为:ros2 run robot_package my_node。
但是当你构建成功后,执行该指令后,会有会提示找不到包,
image

这是因为ros2 run指令会在环境变量AMENT_PREFIX_PATH所在的路径中查找包,但是构建完毕后我们并没有更新环境变量,所以需要执行下面的命令来更新环境变量:

source install/setup.bash
其中install/setup.bash是ROS2在构建完成后自动生成的一个脚本文件,用于设置环境变量,使得ROS2能够找到安装的包和可执行文件。执行该命令后,AMENT_PREFIX_PATH环境变量将被更新,会将安装路径添加到其中,从而使得ros2 run robot_package my_node命令能够正确找到并运行节点。

最后再来解决一个问题:为什么install指令只指出安装路径是lib/ P R O J E C T N A M E ,而不是 i n s t a l l / {PROJECT_NAME},而不是install/ PROJECTNAME,而不是install/{PROJECT_NAME}/lib/ P R O J E C T N A M E , 但是安装后却可以将可执行文件安装到 i n s t a l l / {PROJECT_NAME},但是安装后却可以将可执行文件安装到install/ PROJECTNAME,但是安装后却可以将可执行文件安装到install/{PROJECT_NAME}/lib/ P R O J E C T N A M E 目录下呢?这是因为在 R O S 2 中, i n s t a l l 指令中的路径是相对 C m a k e 的安装前缀路径(即 C M A K E I N S T A L L P R E F I X 这个变量)的,而当我们使用 c o l c o n b u i l d 调用 a m e n t c m a k e 进行构建时, C M A K E I N S T A L L P R E F I X 会被自动指向 i n s t a l l / {PROJECT_NAME}目录下呢? 这是因为在ROS2中,install指令中的路径是相对Cmake的安装前缀路径(即CMAKE_INSTALL_PREFIX这个变量)的,而当我们使用colcon build调用ament_cmake进行构建时,CMAKE_INSTALL_PREFIX会被自动指向install/ PROJECTNAME目录下呢?这是因为在ROS2中,install指令中的路径是相对Cmake的安装前缀路径(即CMAKEINSTALLPREFIX这个变量)的,而当我们使用colconbuild调用amentcmake进行构建时,CMAKEINSTALLPREFIX会被自动指向install/{PROJECT_NAME}/目录下的工作空间路径。可以通过查看build目录下的CMakeCache.txt文件来验证这一点。

image

最后总结一下

使用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接口的定义:

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

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐