【ROS2指南-15】创建自定义消息统一管理包
目标:了解更多在 ROS 2 中实现自定义接口的方法
教程级别:初学者
时间: 15分钟
内容
-
背景
-
先决条件
-
任务
-
1 创建一个包
-
2 创建消息文件
-
3 使用来自同一个包的接口
-
4 尝试一下
-
5(额外)使用现有的接口定义
-
-
概括
-
下一步
-
相关内容
背景
在之前的教程中,您学习了如何创建自定义 msg 和 srv 接口。
虽然最佳实践是在专用接口包中声明接口,但有时在一个包中声明、创建和使用一个接口会很方便。
回想一下,接口目前只能在 CMake 包中定义。但是,可以在 CMake 包中包含 Python 库和节点(使用ament_cmake_python),因此您可以在一个包中一起定义接口和 Python 节点。为了简单起见,我们将在此处使用 CMake 包和 C++ 节点。
本教程将重点介绍 msg 接口类型,但此处的步骤适用于所有接口类型。
先决条件
我们假设您在完成本教程之前已经查看了创建自定义 ROS 2 msg 和 srv 文件教程中的基础知识。
您应该安装了 ROS 2、一个工作区,并且了解创建包。
与往常一样,不要忘记在您打开的每个新终端中获取 ROS 2。
任务
1 创建一个包
在您的工作区src
目录中,创建一个包more_interfaces
并在其中为 msg 文件创建一个文件夹:
ros2 pkg create --build-type ament_cmake more_interfaces mkdir more_interfaces/msg
2 创建消息文件
在里面more_interfaces/msg
,创建一个新文件AddressBook.msg
粘贴以下代码以创建一条消息,用于携带有关个人的信息:
bool FEMALE=true bool MALE=false string first_name string last_name bool gender uint8 age string address
此消息由 5 个字段组成:
-
first_name:字符串类型
-
last_name:字符串类型
-
性别:bool 类型,可以是 MALE 或 FEMALE
-
年龄:uint8类型
-
地址:字符串类型
请注意,可以为消息定义中的字段设置默认值。有关自定义界面的更多方法,请参阅关于 ROS 2 界面。
接下来,我们需要确保将 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 步中一起完成。
2.2 (Extra) 设置多个接口
笔记
您可以使用set
来整齐地列出所有接口:
set(msg_files "msg/Message1.msg" "msg/Message2.msg" # etc ) set(srv_files "srv/Service1.srv" "srv/Service2.srv" # etc )
并像这样一次生成所有列表:
rosidl_generate_interfaces(${PROJECT_NAME} ${msg_files} ${srv_files} )
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.age = 30; message.gender = message.MALE; message.address = "unknown"; 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 代码解释
#include "more_interfaces/msg/address_book.hpp"
包括我们新创建的AddressBook.msg
.
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");
创建节点和AddressBook
发布者。
auto publish_msg = [this]() -> void {
创建回调以定期发布消息。
auto message = more_interfaces::msg::AddressBook();
创建AddressBook
我们稍后将发布的消息实例。
message.first_name = "John"; message.last_name = "Doe"; message.age = 30; message.gender = message.MALE; message.address = "unknown";
填充AddressBook
字段。
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);
publish_msg
创建一个 1 秒计时器以每秒调用我们的函数。
3.2 构建发布者
我们需要在以下位置为此节点创建一个新目标CMakeLists.txt
:
find_package(rclcpp REQUIRED) add_executable(publish_address_book src/publish_address_book.cpp ) ament_target_dependencies(publish_address_book "rclcpp" ) install(TARGETS publish_address_book DESTINATION lib/${PROJECT_NAME})
3.3 链接对接口
为了使用在同一个包中生成的消息,我们需要使用以下 CMake 代码:
rosidl_target_interfaces(publish_address_book ${PROJECT_NAME} "rosidl_typesupport_cpp")
这会从中找到相关生成的 C++ 代码,AddressBook.msg
并允许您的目标链接到它。
您可能已经注意到,当使用的接口来自单独构建的包时,此步骤不是必需的。仅当您想要在与使用它们的包相同的包中使用接口时,才需要此 CMake 代码。
4 尝试一下
返回到工作区的根目录来构建包:
cd ~/dev_ws colcon build --packages-up-to more_interfaces
然后获取工作区并运行发布者:
. install/local_setup.bash ros2 run more_interfaces publish_address_book
您应该看到发布者转发您定义的消息,包括您在 中设置的值publish_address_book.cpp
。
要确认正在该address_book
主题上发布消息,请打开另一个终端,找到工作区,然后调用:topic echo
. install/setup.bash ros2 topic echo /address_book
我们不会在本教程中创建订阅者,但您可以尝试自己编写一个用于练习(使用编写简单的发布者和订阅者 (C++)来提供帮助)。
5(额外)使用现有的接口定义
笔记
您可以在新的接口定义中使用现有的接口定义。例如,假设有一条名为 的消息Contact.msg
属于现有的名为 的 ROS 2 包rosidl_tutorials_msgs
。AddressBook.msg
假设它的定义与我们之前定制的接口相同。
AddressBook.msg
在那种情况下,您可以将(包中与节点的接口)定义为类型(单独Contact
包中的接口)。您甚至可以定义为type 的数组,如下所示:AddressBook.msg
Contact
rosidl_tutorials_msgs/Contact[] address_book
要生成此消息,您需要声明对Contact.msg's
package, rosidl_tutorials_msgs
, in 的依赖package.xml
:
<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
,以便能够添加contacts
到您的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.age = 30; contact.gender = contact.MALE; contact.address = "unknown"; msg->address_book.push_back(contact); } { rosidl_tutorials_msgs::msg::Contact contact; contact.first_name = "Jane"; contact.last_name = "Doe"; contact.age = 20; contact.gender = contact.FEMALE; contact.address = "unknown"; 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); };
构建并运行这些更改将显示按预期定义的消息,以及上面定义的消息数组。
概括
在本教程中,您尝试了不同的字段类型来定义接口,然后在使用它的同一个包中构建了一个接口。
您还学习了如何将另一个接口用作字段类型,以及使用该功能所需的package.xml
、CMakeLists.txt
和语句。#include
下一步
接下来,您将创建一个简单的 ROS 2 程序包,其中包含您将学习如何从启动文件设置的自定义参数。同样,您可以选择用C++或Python编写它。