【ROS2】初级:客户端-实现自定义接口

目标:学习更多在 ROS 2 中实现自定义接口的方法。

 教程级别:初学者

 时间:15 分钟

 目录

  •  背景

  •  先决条件

  •  任务

    • 1. 创建一个包

    • 2. 创建一个 msg 文件

    • 3. 使用来自同一包的接口

    • 4. 尝试一下

    • 5. 使用现有的接口定义

  •  摘要

  •  下一步

  •  相关内容

 背景

在之前的教程中,您学习了如何创建自定义 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
cxy@ubuntu2404-cxy:~/ros2_ws/src$ ros2 pkg create --build-type ament_cmake --license Apache-2.0 more_interfaces
going to create a new package
package name: more_interfaces
destination directory: /home/cxy/ros2_ws/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['cxy <cxy@todo.todo>']
licenses: ['Apache-2.0']
build type: ament_cmake
dependencies: []
creating folder ./more_interfaces
creating ./more_interfaces/package.xml
creating source and include folder
creating folder ./more_interfaces/src
creating folder ./more_interfaces/include/more_interfaces
creating ./more_interfaces/CMakeLists.txt

b85d0a3942ded544e26ed260514f050a.png

2. 创建一个 msg 文件

在 more_interfaces/msg 中,创建一个新文件 AddressBook.msg ,并粘贴以下代码以创建旨在传达个人信息的消息:

cxy@ubuntu2404-cxy:~/ros2_ws/src$ cd more_interfaces/msg
cxy@ubuntu2404-cxy:~/ros2_ws/src/more_interfaces/msg$ gedit 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姓氏:字符串类型

  • last姓氏:字符串类型

  • 电话号码:字符串类型

  • 电话类型:类型为 uint8,定义了几个命名的常量值

请注意,在消息定义中可以为字段设置默认值。有关自定义接口的更多方式,请参见接口 https://docs.ros.org/en/jazzy/Concepts/Basic/About-Interfaces.html 。

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

建立消息文件

打开 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>
  • <buildtool_depend>rosidl_default_generators</buildtool_depend>: 这是构建工具依赖,用于生成ROS接口定义语言(IDL)文件的代码。

  • <exec_depend>rosidl_default_runtime</exec_depend>: 这是执行时依赖,提供了运行时需要的ROS IDL代码。

  • <member_of_group>rosidl_interface_packages</member_of_group>: 这表明该包是ROS IDL接口包的一部分。

请注意,在构建时,我们需要 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)

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

2.2(额外)设置多个接口

 便条

您可以在 CMakeLists.txt 中使用 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 的文件,并粘贴以下代码:

8a610a338ebc4b882cbb8f707d979486.png

# 包含chrono和memory两个库
#include <chrono>
#include <memory>


# 包含rclcpp库和自定义的AddressBook消息类型
#include "rclcpp/rclcpp.hpp"
#include "more_interfaces/msg/address_book.hpp"


# 使用std::chrono_literals命名空间中的字面量
using namespace std::chrono_literals;


# 定义一个AddressBookPublisher类,继承自rclcpp::Node
class AddressBookPublisher : public rclcpp::Node
{
public:
  # 构造函数
  AddressBookPublisher()
  : Node("address_book_publisher")
  {
    # 创建一个发布者,发布AddressBook类型的消息到"address_book"话题,队列大小为10
    address_book_publisher_ =
      this->create_publisher<more_interfaces::msg::AddressBook>("address_book", 10);


    # 定义一个lambda函数,用于发布消息
    auto publish_msg = [this]() -> void {
        # 创建一个AddressBook消息实例
        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);
      };
    # 创建一个定时器,每秒调用一次publish_msg函数
    timer_ = this->create_wall_timer(1s, publish_msg);
  }


private:
  # 私有成员变量:一个发布者指针和一个定时器指针
  rclcpp::Publisher<more_interfaces::msg::AddressBook>::SharedPtr address_book_publisher_;
  rclcpp::TimerBase::SharedPtr timer_;
};


# main函数
int main(int argc, char * argv[])
{
  # 初始化rclcpp库
  rclcpp::init(argc, argv);
  # 开始循环执行AddressBookPublisher节点
  rclcpp::spin(std::make_shared<AddressBookPublisher>());
  # 关闭rclcpp库
  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 代码

# 指定CMake的最低版本要求
cmake_minimum_required(VERSION 3.8)
# 定义项目名称
project(more_interfaces)


# 如果使用的是GNU编译器或者Clang编译器,添加编译选项
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()


# 查找依赖包
find_package(ament_cmake REQUIRED)
# 如果需要手动填写更多依赖项,请取消下面一节的注释。
# find_package(<dependency> REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(rclcpp REQUIRED)


# 设置消息文件
set(msg_files
  "msg/AddressBook.msg"
)


# 生成接口
rosidl_generate_interfaces(${PROJECT_NAME}
  ${msg_files}
)


# 导出依赖项
ament_export_dependencies(rosidl_default_runtime)


# 添加可执行文件
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})


# 获取类型支持目标
rosidl_get_typesupport_target(cpp_typesupport_target
  ${PROJECT_NAME} rosidl_typesupport_cpp)


# 链接库
target_link_libraries(publish_address_book "${cpp_typesupport_target}")


# 如果构建测试,则查找测试依赖项
if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # 下面这行代码跳过了检查版权的linter,
  # 当所有源文件添加了版权和许可时,注释掉这行代码
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()


# 打包
ament_package()

6cc3030e28ef554422c43ee3d2a36b93.png

4. 尝试一下

返回工作区的根目录来构建包:

cd ~/ros2_ws
colcon build --packages-up-to more_interfaces
cxy@ubuntu2404-cxy:~/ros2_ws$ colcon build --packages-up-to more_interfaces
Starting >>> more_interfaces
[Processing: more_interfaces]                             
Finished <<< more_interfaces [35.1s]                       


Summary: 1 package finished [45.7s]

然后初始化工作空间并运行发布者:

source install/local_setup.bash
ros2 run more_interfaces publish_address_book
cxy@ubuntu2404-cxy:~/ros2_ws$ ros2 run more_interfaces publish_address_book
Publishing Contact
First:John  Last:Doe
Publishing Contact
First:John  Last:Doe
Publishing Contact
First:John  Last:Doe

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

要确认消息是否已发布在 address_book 主题上,请打开另一个终端,加载工作区,并调用 topic echo :

source install/setup.bash
ros2 topic echo /address_book
cxy@ubuntu2404-cxy:~/ros2_ws$ source install/setup.bash
cxy@ubuntu2404-cxy:~/ros2_ws$ ros2 topic echo /address_book
first_name: John
last_name: Doe
phone_number: '1234567890'
phone_type: 2
---

在本教程中,我们不会创建一个订阅者,但你可以尝试自己写一个来练习(使用“编写一个简单的发布者和订阅者(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's 包 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 的头文件,以便能够将 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.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,以及上面定义的 msg 数组。

 摘要

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

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

下一步

接下来,您将创建一个简单的 ROS 2 包,其中包含一个自定义参数,您将学习如何从启动文件中设置它。同样,您可以选择用 C++ https://docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Using-Parameters-In-A-Class-CPP.html 或 Python  https://docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Using-Parameters-In-A-Class-Python.html 编写。

 相关内容 

有关 ROS 2 接口和 IDL(接口定义语言)的几篇设计文章。https://design.ros2.org/#interfaces 

  • 29
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值