ROS2 Jazzy:实现自定义的消息和服务接口(C++)

ROS2 Jazzy:实现自定义的消息和服务接口(C++)

精选文章moguli202025-05-25 20:11:142A+A-

目标

学习在ROS2中实现自定义接口的更多方法。

背景信息

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

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

需要注意的是,虽然接口只能在CMake软件包中定义。但是,在CMake软件包中使用Python的库和节点是可行的(通过使用ament_cmake_python),所以你可以在一个软件包中同时定义接口和Python节点。为了简化内容,本文将使用CMake软件包和C++节点进行讲解。

本文将重点介绍msg接口类型,不过这里的步骤同样适用于所有接口类型。

先决条件

我们假设你在学习本教程之前,已经学习过创建自定义消息msg和服务srv接口文件中的基础知识。

你应该已经安装了ROS2,拥有一个工作空间,并且了解如何创建软件包。

和往常一样,不要忘记在打开的每个新终端中加载ROS2环境。

操作步骤

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:无符号8位整数类型,定义了几个命名常量值

请注意,在消息定义中可以为字段设置默认值。

接下来,我们需要确保将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文件,我们可以确保在添加其他.msg文件后,CMake知道何时需要重新配置项目。

  • 生成消息:
rosidl_generate_interfaces(${PROJECT_NAME}
  ${msg_files}
)

现在,你已经准备好从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

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

source install/local_setup.bash
ros2 run more_interfaces publish_address_book

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

要确认消息是否在address_book话题上发布,打开另一个终端,加载工作空间,并调用topic echo命令:

source install/setup.bash
ros2 topic echo /address_book

在本文中我们不会创建订阅者,但你可以自己尝试编写一个进行练习。

5. (额外内容)使用现有接口定义

注意:你可以在新的接口定义中使用现有的接口定义。例如,假设有一个名为Contact.msg的消息,它属于一个现有的ROS2软件包rosidl_tutorials_msgs。假设它的定义与我们之前自定义的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 = 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);
 };

编译并运行这些更改后,你将看到按预期定义的消息,以及上面定义的消息数组。

总结

在本教程中,你尝试了使用不同的字段类型来定义接口,然后在使用接口的同一软件包中编译了接口。

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


关注【智践行】公众号,发送 【机器人】 获得机器人经典学习资料

点击这里复制本文地址 以上内容由莫古技术网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

莫古技术网 © All Rights Reserved.  滇ICP备2024046894号-2