在同一功能包中定义并使用接口
目标: 学习更多在 ROS 2 中实现自定义接口的方法。
教程级别: 初级
预计时间: 15 分钟
在上一篇教程中,你学习了如何创建自定义 msg 和 srv 接口。
虽然最佳实践是在专门的接口功能包中声明接口,但有时在同一个功能包中同时声明、创建和使用接口会更加方便。
需要注意的是,接口目前只能在 CMake 功能包中定义。不过,CMake 功能包中也可以包含 Python 库和节点(通过 ament_cmake_python),因此你完全可以在同一个功能包中同时定义接口和 Python 节点。为简单起见,本教程使用 CMake 功能包和 C++ 节点。
本教程以 msg 接口类型为重点,但其中的步骤适用于所有接口类型。
我们假设你在学习本教程之前已经掌握了自定义 ROS 2 接口教程中的基础知识。
你应该已经安装了 ROS 2,拥有一个工作空间,并了解如何创建功能包。
一如既往,不要忘记在打开的每个新终端中 source ROS 2。
1 创建功能包
Section titled “1 创建功能包”在工作空间 src 目录中,创建一个名为 more_interfaces 的功能包,并在其中创建一个 msg 文件目录:
ros2 pkg create --build-type ament_cmake --license Apache-2.0 more_interfacesmkdir more_interfaces/msg2 创建 msg 文件
Section titled “2 创建 msg 文件”在 more_interfaces/msg 中,创建一个新文件 AddressBook.msg,粘贴以下代码来定义一个用于携带个人信息的消息:
uint8 PHONE_TYPE_HOME=0uint8 PHONE_TYPE_WORK=1uint8 PHONE_TYPE_MOBILE=2
string first_namestring last_namestring phone_numberuint8 phone_type此消息由以下字段组成:
- first_name:string 类型
- last_name:string 类型
- phone_number:string 类型
- phone_type:uint8 类型,定义了几个命名常量值
需要注意的是,可以在消息定义中为字段设置默认值。有关自定义接口的更多方法,请参阅接口文档。
接下来,需要确保 msg 文件被转换为 C++、Python 等语言的源代码。
2.1 构建 msg 文件
Section titled “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 在同一个功能包中使用接口
Section titled “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 代码解析
Section titled “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 构建发布者
Section titled “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 链接接口
Section titled “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 代码仅在你需要在定义接口的同一个功能包中使用该接口时才需要。
回到工作空间根目录构建功能包:
Linux/macOS:
cd ~/ros2_wscolcon build --packages-up-to more_interfacesWindows:
cd /ros2_wscolcon build --merge-install --packages-up-to more_interfaces然后 source 工作空间并运行发布者:
Linux:
source install/local_setup.bashros2 run more_interfaces publish_address_bookmacOS:
. install/local_setup.bashros2 run more_interfaces publish_address_bookWindows:
call install/local_setup.batros2 run more_interfaces publish_address_book或者使用 PowerShell:
install/local_setup.ps1ros2 run more_interfaces publish_address_book你应该会看到发布者正在发布你定义的 msg,其中包含了你在 publish_address_book.cpp 中设置的值。
要确认消息正在 address_book 话题上发布,打开另一个终端,source 工作空间后调用 topic echo:
Linux:
source install/setup.bashros2 topic echo /address_bookmacOS:
. install/setup.bashros2 topic echo /address_bookWindows:
call install/setup.batros2 topic echo /address_book或者使用 PowerShell:
install/setup.ps1ros2 topic echo /address_book本教程中我们不会创建订阅者,但你可以尝试自己编写一个来练习(可以参考编写简单的 C++ 发布者和订阅者教程来获取帮助)。
5(附加)使用现有接口定义
Section titled “5(附加)使用现有接口定义”注意:你可以在新的接口定义中引用现有的接口定义。例如,假设有一个名为
Contact.msg的消息,属于一个名为rosidl_tutorials_msgs的现有 ROS 2 功能包,其定义与我们之前自定义的AddressBook.msg接口相同。
在这种情况下,你可以将 AddressBook.msg(定义在包含节点的功能包中)声明为 Contact 类型(定义在另一个独立功能包中)。甚至可以将 AddressBook.msg 定义为 Contact 类型的数组,如下所示:
rosidl_tutorials_msgs/Contact[] address_book要生成此消息,需要在 package.xml 中声明对 rosidl_tutorials_msgs 功能包(即 Contact.msg 所在功能包)的依赖:
<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); };构建并运行后,你会看到按预期定义的消息,以及上面定义的消息数组。
在本教程中,你尝试使用不同的字段类型来定义接口,然后在同一个功能包中构建并使用了该接口。
你还学习了如何将另一个接口作为字段类型来引用,以及实现该功能所需的 package.xml、CMakeLists.txt 和 #include 语句。
接下来,你将创建一个带有自定义参数的简单 ROS 2 功能包,并学习如何从 launch 文件中设置参数。同样,可以选择用 C++ 或 Python 来编写。
关于 ROS 2 接口和 IDL(接口定义语言),有若干设计文章可供参考。