The ROS API

ROS 包含许多软件库,这些库提供了在构建机器人应用程序时有用的各种功能。您需要的库取决于项目的细节。在本节中,我们将介绍在使用 ROS 进行开发时可能经常使用的两个核心库:

  • rclpy : Python client library
  • rclcpp : C++ client library

ROS 客户端库 提供数据结构、函数和语法糖,方便使用特定编程语言进行开发。这里我们只介绍 Python 和 C++ 库,因为它们使用最广泛。但您可以找到许多其他语言的 ROS 客户端库,从 Ada 到 JavaScript 再到 Rust 等等。

Note: In this section we aim for a gentle and efficient introduction to the ROS API. In service of that goal, we will purposefully ignore and/or violate various conventions and patterns.

使用 Python 发布和订阅主题

使用 ROS 发布数据很容易。这是一个完整的 Python 程序,用于发布字符串消息:

from time import sleep
import rclpy
from std_msgs.msg import String

rclpy.init()
node = rclpy.create_node('my_publisher')
pub = node.create_publisher(String, 'chatter', 10)
msg = String()
i = 0
while rclpy.ok():
    msg.data = f'Hello World: {i}'
    i += 1
    print(f'Publishing: "{msg.data}"')
    pub.publish(msg)
    sleep(0.5)

自己尝试一下。(确保在每个使用的 shell 中都获取了 ROS 设置文件,如我们在上一章中讨论的那样;例如, source/opt/ros/foxy/setup.bash。)将上面的代码块复制到一个文件中,将其命名为 talker.py,然后将其提供给您的 Python3 解释器:

$ python3 talker.py

You should see:

Publishing: "Hello world: 0"
Publishing: "Hello world: 1"
Publishing: "Hello world: 2"

它打印到控制台,但数据会传到任何地方吗?我们可以使用前面介绍的ros2 topic工具检查我们的工作。在另一个 shell 中(让您的 talker 保持运行),运行:

$ ros2 topic echo chatter

您应该会看到以下内容,但数字会根据两个命令之间的时间而有所不同:

data: 'Hello world: 13'
---
data: 'Hello world: 14'
---
data: 'Hello world: 15'

这样我们就有了一个可以工作的 talker。现在我们可以添加自己的监听器来代替ros2 topic。这是一个完整的 Python 程序,它订阅字符串消息并将其打印到控制台:

import rclpy
from std_msgs.msg import String

def cb(msg):
    print(f'I heard: "{msg.data}"')

rclpy.init()
node = rclpy.create_node('my_subscriber')
sub = node.create_subscription(String, 'chatter', cb, 10)
rclpy.spin(node)

自己尝试一下。将上面的代码块复制到一个文件中,并将其命名为listener.py。让您的谈话者仍在一个 shell 中运行,在另一个 shell 中启动您的 监听器:

$ python3 listener.py

您应该会看到(再次强调,数字将根据时间而变化):

I heard: "Hello world: 35"
I heard: "Hello world: 36"
I heard: "Hello world: 37"

深入研究 Python 代码

现在我们知道这些程序可以运行,我们可以深入研究它们的代码。两个程序都以相同的前言开头:

import rclpy
from std_msgs.msg import String

我们需要导入 rclpy 客户端库,它为我们提供了用 Python 编写 ROS 应用程序所需的大部分内容。但我们还需要专门导入我们将使用的 ROS 消息类型。在本例中,我们使用简单的 std_msgs/String 消息,其中包含一个名为 data 的字段,类型为 string。如果我们想使用代表相机图像的 sensor_msgs/Image 消息,那么我们会写 from sensor_msgs.msg import Image

导入后,两个程序都执行共同的初始化:

rclpy.init()
node = rclpy.create_node('my_node_name')

我们初始化 rclpy 库,然后调用它来创建一个 Node 对象,并为其命名。随后我们​​将对该 Node 对象进行操作。

在 talker 中,我们使用 Node 对象创建一个 Publisher 对象:

pub = node.create_publisher(String, 'chatter', 10)

我们声明要发布的数据类型(std_msgs/String)、要发布的主题名称(chatter)以及本地排队的最大出站消息数(10)。当我们发布的速度比订阅者使用数据的速度快时,最后一个参数就会发挥作用。

监听器中的等效步骤是创建一个Subscription对象:

sub = node.create_subscription(String, 'chatter', cb, 10)

类型(String)和主题名称(chatter)参数具有与create_publisher()调用相同的含义,最后一个参数(10)是为入站消息设置类似的最大队列大小。关键的区别是cb参数,它指的是我们在侦听器中定义的这个回调函数:

def cb(msg):
    print(f'I heard: "{msg.data}"')

每当侦听器收到消息时,就会调用该函数,

并且收到的消息将作为参数传递。在这种情况下, 我们只需将内容打印到控制台。

定义回调并创建“订阅”后,侦听器的其余部分就只有一行:

rclpy.spin(node)

此调用将控制权移交给 rclpy,以等待新消息到达(更一般地是等待事件发生)并调用我们的回调。

回到谈话者,我们创建一个简单的循环来使用我们的 Publisher

msg = String()
i = 0
while rclpy.ok():
    msg.data = f'Hello World: {i}'
    i += 1
    print(f'Publishing: "{msg.data}"')
    pub.publish(msg)
    sleep(0.5)

这些步骤非常清楚:我们创建一个消息对象,然后在循环的每次迭代中,我们更新消息内容并发布它,在迭代之间短暂休眠。

使用 C++ 发布和订阅主题

现在我们将用 C++ 编写相同的发送器和监听器对。

这是一个发布字符串消息的完整 C++ 程序:

#include <unistd.h>
#include <iostream>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("minimal_publisher");
  auto pub = node->create_publisher<std_msgs::msg::String>("chatter", 10);
  std_msgs::msg::String message;
  auto i = 0;
  while (rclcpp::ok()) {
    message.data = "Hello world: " + std::to_string(i++);
    std::cout << "Publishing: " << message.data << std::endl;
    pub->publish(message);
    usleep(500000);
  }
  return 0;
}

当然,和所有 C++ 一样,我们需要编译这个程序。管理 C++ 的编译参数很麻烦,所以我们使用 CMake 来帮忙。以下是完整的 CMake 代码,它允许我们构建 talker 示例:

cmake_minimum_required(VERSION 3.5)
project(talker_listener)

find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

add_executable(talker talker.cpp)
target_include_directories(talker PRIVATE ${rclcpp_INCLUDE_DIRS} ${std_msgs_INCLUDE_DIRS})
target_link_libraries(talker ${rclcpp_LIBRARIES} ${std_msgs_LIBRARIES})

自己尝试一下。将 C++ 代码复制到名为“talker.cpp”的文件中 并将 CMake 代码复制到名为“CMakeLists.txt”的文件中。将它们 并排放在一个目录中,然后调用“cmake”,然后调用“make”:

$ cmake .
$ make

你最终应该得到一个名为“talker”的编译可执行文件。运行它:

$ ./talker

You should see:

Publishing: "Hello world: 0"
Publishing: "Hello world: 1"
Publishing: "Hello world: 2"

保持谈话者运行,另一个 shell 尝试“ros2 topic”来监听:

$ ros2 topic echo chatter

您应该会看到(数字将根据两个命令之间的时间而变化):

data: 'Hello world: 13'
---
data: 'Hello world: 14'
---
data: 'Hello world: 15'

现在我们可以编写自己的监听器来代替“ros2 topic”。下面是一个完整的 C++ 程序,它订阅字符串消息并将其打印到控制台:

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

void cb(const std_msgs::msg::String::SharedPtr msg)
{
  std::cout << "I heard: " << msg->data << std::endl;
}

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("my_subscriber");
  auto sub = node->create_subscription<std_msgs::msg::String>("chatter", 10, cb);
  rclcpp::spin(node);
  return 0;
}

将代码块复制到名为“talker.cpp”的文件中。为了安排编译,我们还需要将一些相应的 CMake 代码添加到我们之前的“CMakeLists.txt”文件的底部:

add_executable(listener listener.cpp)
target_include_directories(listener PRIVATE ${rclcpp_INCLUDE_DIRS} ${std_msgs_INCLUDE_DIRS})
target_link_libraries(listener ${rclcpp_LIBRARIES} ${std_msgs_LIBRARIES})

Configure and build again:

$ cmake .
$ make

现在你应该也有一个 listener 可执行文件了。当你的 talker 仍在一个 shell 中运行时,在另一个 shell 中启动你的 listener:

$ ./listener

You should see (again, numbers will vary depending on timing):

I heard: "Hello world: 35"
I heard: "Hello world: 36"
I heard: "Hello world: 37"

深入研究 C++ 代码

现在我们知道这些程序可以运行,我们可以深入研究它们的代码。两个程序都以相同的前言开头:

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

我们总是需要包含 rclcpp 客户端库,它为我们提供了用 C++ 编写 ROS 应用程序所需的大部分内容。但我们还需要专门导入我们将使用的 ROS 消息类型。在本例中,我们使用简单的 std_msgs/String 消息,其中包含一个名为 data 的字段,类型为 string。如果我们想使用代表相机图像的 sensor_msgs/Image 消息,那么我们将 #include "sensor_msgs/msg/image.hpp"

导入后,两个程序都会执行通用初始化:

  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("my_node_name");

我们初始化 rclcpp 库,然后调用它来创建一个 Node 对象,并为其命名。随后我们​​将对该 Node 对象进行操作。

在 talker 中,我们使用 Node 对象创建一个 Publisher 对象:

  auto pub = node->create_publisher<std_msgs::msg::String>("chatter", 10);

我们通过模板声明我们将发布的数据类型(std_msgs/String)、我们将发布的主题名称(chatter)以及本地排队的最大出站消息数(10)。当我们发布的速度比订阅者使用数据的速度快时,最后一个参数就会发挥作用。

监听器中的等效步骤是创建一个Subscription对象:

  auto sub = node->create_subscription<std_msgs::msg::String>("chatter", 10, cb);

类型(String)和主题名称(chatter)参数具有与create_publisher()调用相同的含义,数值参数(10)设置传入消息的类似最大队列大小。关键区别在于cb参数,它引用了我们在侦听器中定义的此回调函数:

void cb(const std_msgs::msg::String::SharedPtr msg)
{
  std::cout << "I heard: " << msg->data << std::endl;
}

每当侦听器收到消息时,就会调用该函数,

并且收到的消息将作为参数传递。在这种情况下, 我们只需将内容打印到控制台。

定义回调并创建“订阅”后,侦听器的其余部分就只有一行:

  rclcpp::spin(node);

此调用将控制权移交给 rclcpp,以等待新消息到达(更一般地是等待事件发生)并调用我们的回调。

回到谈话者,我们创建一个简单的循环来使用我们的 Publisher

  std_msgs::msg::String message;
  auto i = 0;
  while (rclcpp::ok()) {
    message.data = "Hello world: " + std::to_string(i++);
    std::cout << "Publishing: " << message.data << std::endl;
    pub->publish(message);
    usleep(500000);
  }

在这些步骤中,我们创建一个消息对象,然后在循环的每次迭代中更新消息内容并发布它,在迭代之间短暂休眠。

下一步该去哪里

以上只是非常简短的介绍,我们仅涵盖了主题,而不是服务、操作、参数或 ROS 的许多其他方面。幸运的是,在线 ROS 教程 是学习 ROS 其余部分的绝佳资源。我们特别推荐 初学者:客户端库 合集,这是阅读本章后自然而然的下一步。

关于快捷方式

在本节中,我们介绍了我们能想到的最简单、最简短的 ROS 程序示例。此类程序易于理解和学习,因为它们没有不必要的结构或修饰。但作为交换,此类程序不易扩展、不易组合或不易维护。

我们在本节的示例代码中使用的技术对于原型设计和实验(任何优秀机器人项目的重要方面!)很有用,但我们不建议将它们用于严肃的工作。 当您阅读 ROS 教程 并开始阅读现有的 ROS 代码时,您将了解许多概念、模式和约定,例如:

  • 将代码组织到
  • 将包组织到工作区
  • 管理包之间的依赖关系
  • 使用 colcon 工具按依赖顺序在多个包中构建代码
  • 使用 CMakeLists.txt 文件中的 ament 模块
  • 构建代码以允许运行时控制节点如何映射到进程
  • 使用客户端库的控制台日志记录例程将内容输出到屏幕和其他地方

当您开始构建自己的 ROS 应用程序时,这些技术将为您提供很好的帮助,尤其是当您想要与其他人(无论是在您的团队中还是在世界范围内)共享您的代码时。