# ROS2基础教程 - 3 HelloWorld

下面从 HelloWorld 开始,讲解 ROS2 的开发。

ROS 开发主要使用 C++ 或 Python 实现,如果要实现的功能,对性能有要求,可以使用 C++ 实现,如果对性能没有特别高的要求,可以使用 Python 实现,Python 的开发效率要高一些。

ROS1核心是C++03,而ROS2广泛使用C++11;ROS1 的Python使用Python2,ROS2 使用的Python版本至少是3.5及以上。


在开始之前,先介绍几个概念:工作空间功能包节点

  • 功能包

功能包类似于模块或子项目,它们可以被独立开发和复用,每个功能包通常有一个明确的功能目标,比如实现某种算法、驱动硬件等。

我们在开发机器人的时候,机器人的摄像头、雷达等硬件,厂家会提供 ROS 开发的功能包,我们只需要在我们的项目中集成厂家提供的 ROS 功能包,就能获取到硬件传感器的数据。同样,在开发机器人 SLAM 建图导航功能的时候,可以将建图封装在一个功能包中,导航封装在一个功能包中,这样也便于管理、复用和维护。

  • 节点

节点是 ROS 中的执行单元,是一个正在运行的 ROS 程序。每个节点都独立运行,可以与其他节点通过 ROS 通信机制进行数据交换。

节点的职责是完成特定的功能,比如控制机器人、处理传感器数据、发布指令等,节点是在功能包中的,一个功能包中可以存在多个节点。

  • 工作空间

工作空间是 ROS 项目开发的顶层目录,可以理解为一个项目,用来存放和管理多个功能包。它包含了所有与项目相关的代码、配置和依赖项。

三者关系:

  • 工作空间是一个容器,它包含多个功能包
  • 功能包是功能代码的逻辑单元,里面可能包含多个节点
  • 节点是实际执行功能的程序,通过 ROS 的通信机制与其他节点协作完成系统功能。

所以在 ROS 中实现功能是通过节点来实现的,节点是在功能包中,一个功能包可以提供多个功能,功能包是在工作空间中进行管理的。

类似于 项目 -> 子模块 -> 功能 ,所以在 ROS 中实现功能,从0开始创建,需要 创建工作空间 -> 创建功能包 -> 创建节点

# 3.1 创建工作空间

ROS2 中,工作空间的目录结构:

ros2_ws/          # 工作空间根目录,自定义名称
├── src/          # 源代码目录,存放所有的功能包
├── build/        # 编译目录,存放构建文件(colcon build 时生成)
├── install/      # 安装目录,存放可执行文件和已编译的依赖项(colcon build 时生成)
└── log/          # 日志文件目录,存放构建和运行时的日志信息
1
2
3
4
5

在进行 ROS 开发,首先需要创建工作空间,然后在工作空间的 src/ 目录下创建功能包。开发完成通过 colcon build 命令将功能包编译到 install/ 目录中,然后运行。


下面来创建工作空间。

可以使用界面,在你放置的位置创建工作空间的文件夹,或者使用终端命令来创建也是可以的,工作空间的名称是自定义的。

例如我在主目录(~表示主目录)创建一个 ros2_ws 的工作空间:

mkdir ~/ros2_ws    # 创建文件夹
cd ros2_ws    # 进入到 ros2_ws 目录中
mkdir src    # 在 ros2_ws 目录下创建 src 目录
1
2
3

这样工作空间就创建好了,其实就是创建了两个目录。

创建完成,可以进入到工作空间下进行构建:

cd mkdir ~/ros2_ws
colcon build
1
2

构建完成,工作空间下就会存在 src、install、build、log 四个目录了。

# 3.2 创建功能包

功能包是在工作空间的 src 目录下创建的。ROS2 中功能包需要指定使用的语言,创建功能包使用如下命令:

# 首先进入到工作空间的 src 目录下
cd ~/dev_ws/src

# 创建C++功能包
ros2 pkg create --build-type ament_cmake hello_pkg_cpp

# 或者,创建Python功能包
ros2 pkg create --build-type ament_python hello_pkg_python
1
2
3
4
5
6
7
8
  • ros2 pkg create :ROS2 中创建功能包的命令
  • --build-type :表示新创建的功能包是C++还是Python的,如果使用C++或者C,参数值为ament_cmake,如果使用Python,值为ament_python;
  • 命令后面的 hello_pkg_cpphello_pkg_python 表示的是功能包的名称。
  • 功能包的名字不要包含 -,可以使用 _

这样就会在工作空间下的 src 目录下创建功能包,每个功能包下都会有一个 package.xml 文件,里面有功能包的描述和功能包的依赖信息等。

# 3.3 Python实现节点

下面就来实现一个简单的节点,功能很简单,就是循环打印 Hello World! ,主要是看如何实现一个节点。

在上面已经创建了两个功能包,hello_pkg_cpphello_pkg_python

下面在 hello_pkg_python 功能包下,使用 Python 实现一个节点。

# 1 创建节点文件

hello_pkg_python/hello_pkg_python 目录下创建 python 文件,名称自定义。

例如我这里叫 hello_world.py ,内容如下:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import rclpy        # 导入 rclpy 库,这是 ROS2 的 Python 接口
from rclpy.node import Node     # 导入 Node 类,这是 ROS2 的节点类
import time     # 导入 time 模块,用于实现线程的休眠

def main(args=None):        # main 函数是 ROS2 节点的入口函数,参数 args 通常保留为 None,表示不需要额外的启动参数
    # 1.创建节点
    rclpy.init(args=args)       # 初始化 ROS2 Python 客户端库,必须在创建节点之前调用
    node = Node("hello_node")     # 创建一个名为 "hello_node" 的节点

    # 2.实现功能
    while rclpy.ok():		# 当 ROS2 系统处于正常运行状态时,循环会持续执行
        node.get_logger().info("Hello World!")  # ROS2中打印日志
        time.sleep(1)  
        # 线程休眠 1 秒,防止打印过于频繁

    # 3.销毁节点
    node.destroy_node()     # 销毁节点,释放资源
    rclpy.shutdown()        # 关闭 ROS2 客户端库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

实现节点主要分为三个步骤:

  1. 创建节点
  2. 实现功能,循环打印 Hello World!
  3. 销毁节点

# 2 配置文件可执行权限

需要授权 python 文件可执行权限:

# 进入到python文件目录
cd src/hello_pkg_python/hello_pkg_python
chmod +x hello_world.py
1
2
3

# 3 配置节点

在功能包下的 setup.py 中配置节点,在 entry_points 中配置:

entry_points={
    'console_scripts': [
        'hello_node = hello_pkg_python.hello_world:main',
    ],
},
1
2
3
4
5

上面就是配置节点运行的是哪个 Python 文件的哪个方法。前面的 hello_node 是自定义的,建议和 Node("hello_node") 对应。

一个功能包下是可以创建多个节点的,创建多个 Python 文件即可,进行同样的配置。

# 4 构建项目

现在代码已经编写完成了,需要构建项目。

在工作空间下执行如下命令:

colcon build
1

构建完成,会在 install 目录下生成文件。

也可以使用如下命令,单独编译一个功能包:

colcon build --packages-select hello_pkg_python
1

# 5 运行节点

首先执行 source 命令,在工作空间下执行:

source install/local_setup.sh
1

执行的作用主要是为了让 ros2 命令可以找到我们的功能包。

每次都执行上面的命令有些麻烦,我们可以将命令添加到环境变量中,这样每次打开终端就能 source 一下,就不用每次都运行 source 了。

source /opt/ros/humble/setup.bash    # 安装ros2的时候已经添加了,指定ROS2的路径
source ~/ros2_ws/install/local_setup.sh    # 指定工作空间的路径,让ROS可以找到我们的功能包
1
2

然后使用如下命令启动节点:

ros2 run hello_pkg_python hello_node
1
  • ros2 run :这个是 ros2 中运行节点的命令;
  • hello_pkg_python :是功能包的名字;
  • hello_node :是节点的名称。

执行效果如下,每秒打印 Hello World!


在 ROS 1 中,roscore是一个非常关键的组件。它主要用于提供名称服务(ROS Master)、参数服务器等功能。节点在启动时需要向roscore中的 ROS Master 进行注册,以便让其他节点能够发现它。所以在 ROS1 中启动节点前,需要先运行 roscore

但是ROS2 采用了一种分布式的发现机制,不再依赖于像roscore这样的中央节点管理器。它使用了 DDS(Data - Distribution Service)中间件。DDS 是一种以数据为中心的发布 - 订阅通信标准,能够自动发现网络中的节点和话题。所以在 ROS2 中启动节点不需要roscore

# 3.4 C++实现节点

使用 C++ 实现节点也是类似的。

# 1 创建节点文件

hello_pkg_cpp/src 目录下创建 C++ 文件,名称自定义。

例如我这里叫 hello_world.cpp ,内容如下:

#include <rclcpp/rclcpp.hpp>  // 导入 ROS2 C++ 客户端库
#include <chrono>             // 导入时间处理库
#include <thread>             // 导入线程库

int main(int argc, char **argv) {
    // 1.创建和初始化操作
    rclcpp::init(argc, argv);  // 初始化 ROS2 客户端库
    auto node = rclcpp::Node::make_shared("hello_node");  // 创建一个名为 "hello_node" 的节点

    // 2.实现功能
    while (rclcpp::ok()) {  // 当 ROS2 系统正常运行时
        RCLCPP_INFO(node->get_logger(), "Hello World!");  // 打印日志信息 "Hello World!"
        std::this_thread::sleep_for(std::chrono::seconds(1));  // 线程休眠 1 秒,防止打印过于频繁
    }

    // 3.关闭节点
    rclcpp::shutdown();  // 关闭 ROS2 客户端库
    return 0;  // 返回 0,表示程序正常结束
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 2 配置CMakeLists.txt

在功能包下的 CMakeLists.txt 中,添加 C++ 编译配置,可以添加在 find_package(ament_cmake REQUIRED) 下面:

# 找到此包需要的其他包
find_package(rclcpp REQUIRED)

# 添加可执行文件
add_executable(hello_node src/hello_world.cpp)

# 目标链接库
ament_target_dependencies(hello_node rclcpp)

# 安装可执行文件
install(TARGETS
  hello_node
  DESTINATION lib/${PROJECT_NAME}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

节点的名称是 hello_node ,和 C++ 代码中对应。

一个功能包下是可以创建多个节点的,创建多个 C++ 文件即可,如果创建多个节点,那么在 CMakeLists.txt 中配置如下:

# 找到此包需要的其他包
find_package(rclcpp REQUIRED)

# 添加可执行文件,指向 hello_world.cpp
add_executable(hello_node src/hello_world.cpp)
ament_target_dependencies(hello_node rclcpp)

# 添加可执行文件,指向 print_word.cpp
add_executable(print_node src/print_word.cpp)
ament_target_dependencies(print_node rclcpp)

# 安装可执行文件
install(TARGETS
  hello_node
  print_node
  DESTINATION lib/${PROJECT_NAME}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

上面配置了 hello_nodeprint_node 两个节点。

# 3 构建项目

现在代码已经编写完成了,需要构建项目。

在工作空间下执行如下命令:

colcon build
1

构建完成,会在 install 目录下生成文件。

也可以使用如下命令,单独编译一个功能包:

colcon build --packages-select hello_pkg_cpp
1

# 4 运行节点

首先执行 source 命令,在工作空间下执行:

source install/local_setup.sh
1

如果 ROS 还找不到你的功能包和节点,你就 source 一下,如果找得到就不用了。

然后使用如下命令启动节点:

ros2 run hello_pkg_cpp hello_node
1
  • ros2 run :这个是 ros2 中运行节点的命令;
  • hello_pkg_cpp :是功能包的名字;
  • hello_node :是节点的名称。

执行效果也是每秒打印 Hello World!

# 3.5 节点代码优化

上面在使用 Python 实现节点的时候,使用的是面向过程的编码方式,在实际的开发中,现在更推荐使用面向对象的编码方式。

# 1 优化Python代码

我们可以创建一个类继承 Node 类,然后类中实现我们的功能。

修改上面的 Python 代码如下:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import rclpy        # ROS2 Python接口库
from rclpy.node import Node     # ROS2节点类
import time     # 线程用来休眠

# 创建节点类,实现节点,具体的功能在类中实现
class HelloWorldNode(Node):
    def __init__(self):
        super().__init__('hello_node')
        
    def print_hello_world(self):
        while rclpy.ok():
            self.get_logger().info("Hello World!")  # ROS2打印日志
            time.sleep(1)  # 休眠1秒

def main(args=None):
    # 初始化和创建节点
    rclpy.init(args=args)
    node = HelloWorldNode()

    # 调用打印
    node.print_hello_world()
    
    # 销毁
    node.destroy_node()
    rclpy.shutdown()
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

上面的代码使用面向对象的方式,将节点实现的具体功能放到类中。

# 2 优化C++代码

同样,也是用面向对象的编码方式优化 C++ 代码:

#include <rclcpp/rclcpp.hpp>      // 引入 ROS2 C++ 接口库
#include <chrono>                 // 引入 chrono 库,用于时间处理
#include <thread>                 // 引入 thread 库,用于休眠线程

// 定义一个 HelloWorldNode 类,继承自 rclcpp::Node 类
class HelloWorldNode : public rclcpp::Node
{
public:
    // 构造函数,初始化节点名称为 "hello_node"
    HelloWorldNode() : Node("hello_node") {}

    // 定义一个函数,用于每秒打印一次 "Hello World!"
    void print_hello_world()
    {
        // 使用 while 循环,检查节点是否正常运行
        while (rclcpp::ok()) 
        {
            // 打印 "Hello World!" 消息到终端,带有日志信息
            RCLCPP_INFO(this->get_logger(), "Hello World!");

            // 休眠 1 秒钟,类似于 Python 的 time.sleep(1)
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    }
};

// main 函数是程序的入口点
int main(int argc, char * argv[])
{
    // 初始化 ROS2,必须在创建节点之前调用
    rclcpp::init(argc, argv);

    // 创建一个共享指针,指向 HelloWorldNode 节点对象
    auto node = std::make_shared<HelloWorldNode>();

    // 调用节点的 print_hello_world 函数,开始打印 "Hello World!"
    node->print_hello_world(); 

    // 当循环结束时,关闭 ROS2 系统,释放资源
    rclcpp::shutdown(); 

    // 返回 0,表示程序成功退出
    return 0;
}
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