实现自定义接口
目标: 了解更多在 ROS 2 中实现自定义接口的方法。
教程级别: 初学者
时间: 15 分钟
背景
在 上一个教程 中,您学习了如何创建自定义 msg 和 srv 接口。
虽然最佳实践是在专用接口包中声明接口,但有时在一个包中声明、创建和使用接口会很方便。
回想一下,接口目前只能在 CMake 包中定义。 但是,可以在 CMake 包中拥有 Python 库和节点(使用 ament_cmake_python),因此您可以在一个包中一起定义接口和 Python 节点。 为了简单起见,我们将在这里使用 CMake 包和 C++ 节点。
本教程将重点介绍 msg 接口类型,但此处的步骤适用于所有接口类型。
先决条件
我们假设您在完成本教程之前已经查看了 创建自定义 msg 和 srv 文件 教程中的基础知识。
与往常一样,不要忘记在您打开的每个新终端中 source 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:字符串类型
last_name:字符串类型
phone_number:字符串类型
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)
ament_target_dependencies(publish_address_book 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
cd ~/ros2_ws
colcon build --packages-up-to more_interfaces
cd /ros2_ws
colcon build --merge-install --packages-up-to more_interfaces
然后获取工作区并运行发布者:
source install/local_setup.bash
ros2 run more_interfaces publish_address_book
. install/local_setup.bash
ros2 run more_interfaces publish_address_book
call install/local_setup.bat
ros2 run more_interfaces publish_address_book
Or using Powershell:
install/local_setup.ps1
ros2 run more_interfaces publish_address_book
您应该看到发布者转发您定义的消息,包括您在“publish_address_book.cpp”中设置的值。
要确认消息正在“address_book”主题上发布,请打开另一个终端,获取工作区,然后调用“topic echo”:
source install/setup.bash
ros2 topic echo /address_book
. install/setup.bash
ros2 topic echo /address_book
call install/setup.bat
ros2 topic echo /address_book
Or using Powershell:
install/setup.ps1
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>And in
CMakeLists.txt
:find_package(rosidl_tutorials_msgs REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} ${msg_files} DEPENDENCIES rosidl_tutorials_msgs )您还需要在发布者节点中包含“Contact.msg”的标题,以便能够将“联系人”添加到您的“地址簿”。
#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 = message.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 = message.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,以及上面定义的 msgs 数组。
摘要
在本教程中,您尝试了不同的字段类型来定义接口,然后在使用它的同一包中构建了一个接口。
您还学习了如何使用另一个接口作为字段类型,以及使用该功能所需的 package.xml
、CMakeLists.txt
和 #include
语句。
下一步
接下来,您将创建一个带有自定义参数的简单 ROS 2 包,您将学习如何从启动文件中设置该参数。 同样,您可以选择在 C++ 或 Python 中编写它。
相关内容
关于 ROS 2 接口和 IDL(接口定义语言),有`几篇设计文章<https://design.ros2.org/#interfaces>`_。