乐于分享
好东西不私藏

机器人系统——ROS2文档—实现自定义接口

机器人系统——ROS2文档—实现自定义接口

目标: 学习在 ROS 2 中实现自定义接口的更多方法。
教程级别: 初学者
时间: 15 分钟

目录
背景
前置条件
任务
o1 创建包
o2 创建 msg 文件
o3 从同一包中使用接口
o4 尝试运行
o5(额外)使用现有的接口定义
总结
后续步骤
相关资料


背景

在之前的教程中,您学习了如何创建自定义 msg 和 srv 接口。

虽然最佳实践是在专用的接口包中声明接口,但有时在一个包中声明、创建和使用接口会更加方便。

回想一下,接口目前只能在 CMake 包中定义。但是,可以在 CMake 包中包含 Python 库和节点(使用 ament_cmake_python),因此您可以在一个包中同时定义接口和 Python 节点。为简单起见,我们在这里使用 CMake 包和 C++ 节点。

本教程将重点介绍 msg 接口类型,但这里的步骤适用于所有接口类型。

前置条件

我们假设您在学习本教程之前已经复习了“创建自定义 msg 和 srv 文件”教程中的基础知识。

您应该已经安装了 ROS 2、有一个工作空间,并了解如何创建包。

和往常一样,别忘了在每个新终端中加载 ROS 2。

任务

1 创建包

在您的工作空间 src 目录中,创建一个名为 more_interfaces 的包,并在其中创建一个用于存放 msg 文件的目录:

ros2 pkg create --build-type ament_cmake --license Apache-2.0 more_interfaces
mkdir more_interfaces/msg

2 创建 msg 文件

在 more_interfaces/msg 内部,创建一个新文件 AddressBook.msg,并粘贴以下代码以创建一个用于传递个人信息的消息:

uint8 PHONE_TYPE_HOME=0
uint8 PHONE_TYPE_WORK=1
uint8 PHONE_TYPE_MOBILE=2

string first_name
string last_name
string phone_number
uint8 phone_type

此消息由以下字段组成:

  • first_name:类型为 string
  • last_name:类型为 string
  • phone_number:类型为 string
  • phone_type:类型为 uint8,定义了多个命名常量值

请注意,可以在消息定义中为字段设置默认值。请参阅“接口”概念文档以了解自定义接口的更多方法。

接下来,我们需要确保将 msg 文件转换为 C++、Python 和其他语言的源代码。

2.1 构建 msg 文件

打开 package.xml 并添加以下行:

<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

请注意,在构建时我们需要 rosidl_default_generators,而在运行时我们只需要 rosidl_default_runtime

打开 CMakeLists.txt 并添加以下行:

查找从 msg/srv 文件生成消息代码的包:

find_package(rosidl_default_generators REQUIRED)

声明要生成的消息列表:

set(msg_files "msg/AddressBook.msg")

通过手动添加 .msg 文件,我们确保 CMake 知道在您添加其他 .msg 文件后何时需要重新配置项目。

生成消息:

rosidl_generate_interfaces(${PROJECT_NAME} ${msg_files})

还要确保导出消息运行时依赖项:

ament_export_dependencies(rosidl_default_runtime)

现在您已准备好从 msg 定义生成源文件。我们暂时跳过编译步骤,将在下面的步骤 4 中一起完成。

3 从同一包中使用接口

现在我们可以开始编写使用此消息的代码了。

在 more_interfaces/src 中创建一个名为 publish_address_book.cpp 的文件,并粘贴以下代码:

#include <chrono>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "more_interfaces/msg/address_book.hpp"

using namespace std::chrono_literals;

class AddressBookPublisher : public rclcpp::Node
{
public:
AddressBookPublisher() : Node("address_book_publisher")
{
address_book_publisher_ =
this->create_publisher<more_interfaces::msg::AddressBook>("address_book", 10);

auto publish_msg = [this]() -> void {
auto message = more_interfaces::msg::AddressBook();

message.first_name = "John";
message.last_name = "Doe";
message.phone_number = "1234567890";
message.phone_type = message.PHONE_TYPE_MOBILE;

std::cout << "Publishing Contact\nFirst:" << message.first_name << " Last:" << message.last_name << std::endl;

this->address_book_publisher_->publish(message);
};
timer_ = this->create_wall_timer(1s, publish_msg);
}

private:
rclcpp::Publisher<more_interfaces::msg::AddressBook>::SharedPtr address_book_publisher_;
rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<AddressBookPublisher>());
rclcpp::shutdown();

return 0;
}

3.1 代码解释

包含我们新创建的 AddressBook.msg 的头文件:

#include "more_interfaces/msg/address_book.hpp"

创建一个节点和一个 AddressBook 发布者:

using namespace std::chrono_literals;

class AddressBookPublisher : public rclcpp::Node
{
public:
AddressBookPublisher() : Node("address_book_publisher")
{
address_book_publisher_ =
this->create_publisher<more_interfaces::msg::AddressBook>("address_book");

创建一个回调以定期发布消息:

auto publish_msg = [this]() -> void {

创建一个稍后将发布的 AddressBook 消息实例:

auto message = more_interfaces::msg::AddressBook();

填充 AddressBook 字段:

message.first_name = "John";
message.last_name = "Doe";
message.phone_number = "1234567890";
message.phone_type = message.PHONE_TYPE_MOBILE;

最后定期发送消息:

std::cout << "Publishing Contact\nFirst:" << message.first_name << " Last:" << message.last_name << std::endl;

this->address_book_publisher_->publish(message);

创建一个 1 秒定时器,每秒调用一次 publish_msg 函数:

timer_ = this->create_wall_timer(1s, publish_msg);

3.2 构建发布者

我们需要在 CMakeLists.txt 中为此节点创建一个新目标:

find_package(rclcpp REQUIRED)

add_executable(publish_address_book src/publish_address_book.cpp)
target_link_libraries(publish_address_book rclcpp::rclcpp)

install(TARGETS publish_address_book DESTINATION lib/${PROJECT_NAME})

3.3 链接接口

为了使用在同一包中生成的消息,我们需要使用以下 CMake 代码:

rosidl_get_typesupport_target(cpp_typesupport_target ${PROJECT_NAME} rosidl_typesupport_cpp)
target_link_libraries(publish_address_book "${cpp_typesupport_target}")

这会从 AddressBook.msg 中查找相关生成的 C++ 代码,并允许您的目标链接到它。

您可能已经注意到,当所使用的接口来自独立构建的不同包时,不需要此步骤。此 CMake 代码仅在您想要在定义接口的同一包中使用接口时才需要。

4 尝试运行

返回到工作空间的根目录以构建包:

cd ~/ros2_ws
colcon build --packages-up-to more_interfaces

然后加载工作空间并运行发布者:

source install/local_setup.bash
ros2 run more_interfaces publish_address_book

您应该会看到发布者转发您定义的 msg,包括您在 publish_address_book.cpp 中设置的值。

要确认消息正在 address_book 话题上发布,请打开另一个终端,加载工作空间,然后调用 topic echo

source install/setup.bash
ros2 topic echo /address_book

我们不会在本教程中创建订阅者,但您可以尝试自己编写一个来练习(参考“编写简单的发布者和订阅者(C++)”教程)。

5(额外)使用现有的接口定义

注意: 您可以在新的接口定义中使用现有的接口定义。例如,假设有一个名为 Contact.msg 的消息,它属于一个名为 rosidl_tutorials_msgs 的现有 ROS 2 包。假设它的定义与我们之前自定义的 AddressBook.msg 接口相同。

在这种情况下,您可以将 AddressBook.msg(位于包含您的节点的包中的接口)定义为类型 Contact(位于单独包中的接口)。您甚至可以将 AddressBook.msg 定义为 Contact 类型的数组,如下所示:

rosidl_tutorials_msgs/Contact[] address_book

要生成此消息,您需要在 package.xml 中声明对 Contact.msg 的包 rosidl_tutorials_msgs 的依赖:

<build_depend>rosidl_tutorials_msgs</build_depend>
<exec_depend>rosidl_tutorials_msgs</exec_depend>

并在 CMakeLists.txt 中:

find_package(rosidl_tutorials_msgs REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME} ${msg_files} DEPENDENCIES rosidl_tutorials_msgs)

您还需要在发布者节点中包含 Contact.msg 的头文件,以便能够将联系人添加到 address_book 中:

#include "rosidl_tutorials_msgs/msg/contact.hpp"

您可以将回调更改为类似以下内容:

auto publish_msg = [this]() -> void {
auto msg = std::make_shared<more_interfaces::msg::AddressBook>();
{
rosidl_tutorials_msgs::msg::Contact contact;
contact.first_name = "John";
contact.last_name = "Doe";
contact.phone_number = "1234567890";
contact.phone_type = contact.PHONE_TYPE_MOBILE;
msg->address_book.push_back(contact);
}
{
rosidl_tutorials_msgs::msg::Contact contact;
contact.first_name = "Jane";
contact.last_name = "Doe";
contact.phone_number = "4254242424";
contact.phone_type = contact.PHONE_TYPE_HOME;
msg->address_book.push_back(contact);
}

std::cout << "Publishing address book:" << std::endl;
for (auto contact : msg->address_book) {
std::cout << "First:" << contact.first_name << " Last:" << contact.last_name << std::endl;
}

address_book_publisher_->publish(*msg);
};

构建并运行这些更改将按预期显示定义的 msg,以及上面定义的 msg 数组。

总结

在本教程中,您尝试了定义接口的不同字段类型,然后在定义接口的同一包中构建了一个接口。

您还学习了如何使用另一个接口作为字段类型,以及使用该功能所需的 package.xmlCMakeLists.txt 和 #include 语句。

后续步骤

接下来,您将创建一个带有自定义参数的简单 ROS 2 包,并学习如何从启动文件中设置它。同样,您可以选择用 C++ 或 Python 编写。

相关资料

有关 ROS 2 接口和 IDL(接口定义语言)的设计文章可供参考。

本文档完整翻译自 ROS 2 Lyrical 官方文档,仅用于学习交流。