The ROS API
ROS 包含许多软件库,这些库提供了在构建机器人应用程序时有用的各种功能。您需要的库取决于项目的细节。在本节中,我们将介绍在使用 ROS 进行开发时可能经常使用的两个核心库:
rclpy
: Python client libraryrclcpp
: C++ client library
ROS 客户端库 提供数据结构、函数和语法糖,方便使用特定编程语言进行开发。这里我们只介绍 Python 和 C++ 库,因为它们使用最广泛。但您可以找到许多其他语言的 ROS 客户端库,从 Ada 到 JavaScript 再到 Rust 等等。
使用 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 应用程序时,这些技术将为您提供很好的帮助,尤其是当您想要与其他人(无论是在您的团队中还是在世界范围内)共享您的代码时。