【ROS2】Beginner: Client libraries - 创建自定义ROS 2接口 msg / src (专用接口功能包) + 实现或者自定义接口

一、创建自定义ROS 2接口(msg与srv文件)

目标:定义自定义接口文件(.msg.srv),并在Python和C++节点中使用它们。

教程级别:初学者
预计时间:20分钟

目录

  • 背景
  • 前提条件
  • 任务
    1. 创建新功能包
    2. 定义自定义接口
    3. 配置CMakeLists.txt
    4. 配置package.xml
    5. 构建tutorial_interfaces功能包
    6. 验证msg和srv文件创建结果
    7. 测试新接口
  • 总结

背景

在之前的教程中,你通过消息(message)和服务(service)接口学习了话题(topics)、服务(services),以及简单的发布者/订阅者(C++/Python)和服务端/客户端(C++/Python)节点。这些教程中使用的接口都是预先定义好的。

虽然使用预定义接口是推荐做法,但有时你仍需定义自己的消息和服务。本教程将介绍创建自定义接口定义的最简方法。

前提条件

  • 已创建ROS 2工作空间(如ros2_ws)。
  • 已完成发布者/订阅者(C++和Python)、服务端/客户端(C++和Python)教程,并保留了当时创建的功能包(用于测试新自定义接口)。

任务

1. 创建新功能包

本教程中,自定义.msg.srv文件需放在独立的功能包中,再在其他功能包中调用。两个功能包需位于同一工作空间。

由于将使用之前教程创建的发布/订阅、服务/客户端功能包,需确保当前处于这些功能包所在的工作空间(ros2_ws/src),执行以下命令创建新功能包:

ros2 pkg create --build-type ament_cmake --license Apache-2.0 tutorial_interfaces
  • tutorial_interfaces是新功能包的名称。注意,该功能包必须是ament_cmake类型,但这并不限制其接口的使用场景——你可以在ament_cmake功能包中创建自定义接口,再在C++或Python节点中使用(最后一节将介绍具体方法)。

.msg.srv文件需分别放在名为msgsrv的目录中。在ros2_ws/src/tutorial_interfaces目录下创建这两个目录:

mkdir msg srv

2. 定义自定义接口

2.1 定义msg文件

在刚创建的tutorial_interfaces/msg目录下,新建名为Num.msg的文件,添加以下代码定义其数据结构:

int64 num

这是一个自定义消息,用于传输一个名为num的64位整数。

同样在tutorial_interfaces/msg目录下,新建名为Sphere.msg的文件,添加以下内容:

geometry_msgs/Point center
float64 radius

该自定义消息引用了其他消息功能包中的类型(此处为geometry_msgs/Point)。

2.2 定义srv文件

tutorial_interfaces/srv目录下,新建名为AddThreeInts.srv的文件,定义请求和响应结构:

int64 a
int64 b
int64 c
---
int64 sum

这是一个自定义服务:请求包含三个名为abc的整数,响应返回一个名为sum的整数(即三个请求整数的和)。

3. 配置CMakeLists.txt

为将你定义的接口转换为特定语言的代码(如C++和Python)以便在对应语言中使用,需在CMakeLists.txt中添加以下代码:

find_package(geometry_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Num.msg"
  "msg/Sphere.msg"
  "srv/AddThreeInts.srv"
  DEPENDENCIES geometry_msgs  # 声明上述消息依赖的功能包(此处Sphere.msg依赖geometry_msgs)
)

注意rosidl_generate_interfaces中的第一个参数(库名称)必须以功能包名称开头,例如直接使用${PROJECT_NAME}${PROJECT_NAME}_suffix。详情可参考rosidl官方说明

4. 配置package.xml

由于接口需要依赖rosidl_default_generators生成特定语言的代码,需声明其为构建工具依赖;rosidl_default_runtime是运行时依赖,用于后续调用接口;rosidl_interface_packages是依赖组名称,需通过<member_of_group>标签将你的功能包tutorial_interfaces关联到该组。

package.xml<package>元素内添加以下代码:

<depend>geometry_msgs</depend>
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

5. 构建tutorial_interfaces功能包

自定义接口功能包的所有配置已完成,可开始构建。在工作空间根目录(~/ros2_ws)执行以下命令:

Linux/macOS

colcon build --packages-select tutorial_interfaces

Windows

colcon build --merge-install --packages-select tutorial_interfaces

构建完成后,其他ROS 2功能包即可发现并使用这些自定义接口。

6. 验证msg和srv文件创建结果

打开新终端,进入工作空间(ros2_ws)并配置环境:

Linux

source install/setup.bash

macOS

. install/setup.bash

Windows

call install/setup.bat

使用ros2 interface show命令验证接口创建是否成功,终端输出应与以下内容类似:

# 查看Num.msg
ros2 interface show tutorial_interfaces/msg/Num
int64 num

# 查看Sphere.msg
ros2 interface show tutorial_interfaces/msg/Sphere
geometry_msgs/Point center
    float64 x
    float64 y
    float64 z
float64 radius

# 查看AddThreeInts.srv
ros2 interface show tutorial_interfaces/srv/AddThreeInts
int64 a
int64 b
int64 c
---
int64 sum

7. 测试新接口

此步骤将使用之前教程创建的功能包,通过对节点、CMakeLists.txtpackage.xml进行简单修改,即可使用新自定义接口。由于将标准字符串消息改为数值类型,输出结果会与之前略有不同。

7.1 使用Num.msg测试发布/订阅功能

对之前创建的发布者/订阅者功能包(C++或Python)进行如下修改,即可验证Num.msg的功能。

发布者(Publisher)

C++

#include <chrono>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp"                                            // 改动:导入自定义Num.msg

using namespace std::chrono_literals;

class MinimalPublisher : public rclcpp::Node
{
public:
  MinimalPublisher()
  : Node("minimal_publisher"), count_(0)
  {
    // 改动:创建发布者时使用自定义消息类型tutorial_interfaces::msg::Num
    publisher_ = this->create_publisher<tutorial_interfaces::msg::Num>("topic", 10);

    auto timer_callback = [this](){
      // 改动:初始化自定义消息对象
      auto message = tutorial_interfaces::msg::Num();
      // 改动:为消息的num字段赋值
      message.num = this->count_++;
      // 改动:打印发布的数值
      RCLCPP_INFO_STREAM(this->get_logger(), "Publishing: '" << message.num << "'");
      publisher_->publish(message);
    };
    timer_ = this->create_wall_timer(500ms, timer_callback);
  }

private:
  rclcpp::TimerBase::SharedPtr timer_;
  // 改动:发布者类型改为自定义消息类型
  rclcpp::Publisher<tutorial_interfaces::msg::Num>::SharedPtr publisher_;
  size_t count_;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MinimalPublisher>());
  rclcpp::shutdown();
  return 0;
}

Python

import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node

from tutorial_interfaces.msg import Num                            # 改动:导入自定义Num.msg


class MinimalPublisher(Node):
    def __init__(self):
        super().__init__('minimal_publisher')
        # 改动:创建发布者时使用自定义消息类型Num
        self.publisher_ = self.create_publisher(Num, 'topic', 10)
        timer_period = 0.5  # 秒
        self.timer = self.create_timer(timer_period, self.timer_callback)
        self.i = 0

    def timer_callback(self):
        # 改动:初始化自定义消息对象
        msg = Num()
        # 改动:为消息的num字段赋值
        msg.num = self.i
        self.publisher_.publish(msg)
        # 改动:打印发布的数值
        self.get_logger().info('Publishing: "%d"' % msg.num)
        self.i += 1


def main(args=None):
    try:
        with rclpy.init(args=args):
            minimal_publisher = MinimalPublisher()
            rclpy.spin(minimal_publisher)
    except (KeyboardInterrupt, ExternalShutdownException):
        pass


if __name__ == '__main__':
    main()
订阅者(Subscriber)

C++

#include <functional>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp"                                       // 改动:导入自定义Num.msg

using std::placeholders::_1;

class MinimalSubscriber : public rclcpp::Node
{
public:
  MinimalSubscriber()
  : Node("minimal_subscriber")
  {
    // 改动:回调函数参数类型改为自定义消息类型
    auto topic_callback = [this](const tutorial_interfaces::msg::Num & msg){
      // 改动:打印接收的数值
      RCLCPP_INFO_STREAM(this->get_logger(), "I heard: '" << msg.num << "'");
    };
    // 改动:创建订阅者时使用自定义消息类型
    subscription_ = this->create_subscription<tutorial_interfaces::msg::Num>(
      "topic", 10, topic_callback);
  }

private:
  // 改动:订阅者类型改为自定义消息类型
  rclcpp::Subscription<tutorial_interfaces::msg::Num>::SharedPtr subscription_;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MinimalSubscriber>());
  rclcpp::shutdown();
  return 0;
}

Python

import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node

from tutorial_interfaces.msg import Num                        # 改动:导入自定义Num.msg


class MinimalSubscriber(Node):
    def __init__(self):
        super().__init__('minimal_subscriber')
        self.subscription = self.create_subscription(
            Num,                                               # 改动:使用自定义消息类型Num
            'topic',
            self.listener_callback,
            10)
        self.subscription  # 防止未使用变量警告

    def listener_callback(self, msg):
        # 改动:打印接收的数值
        self.get_logger().info('I heard: "%d"' % msg.num)


def main(args=None):
    try:
        with rclpy.init(args=args):
            minimal_subscriber = MinimalSubscriber()
            rclpy.spin(minimal_subscriber)
    except (KeyboardInterrupt, ExternalShutdownException):
        pass


if __name__ == '__main__':
    main()
配置文件修改

CMakeLists.txt(仅C++)

# ...

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tutorial_interfaces REQUIRED)                      # 改动:添加自定义接口功能包依赖

add_executable(talker src/publisher_member_function.cpp)
# 改动:链接自定义接口目标
target_link_libraries(talker PUBLIC rclcpp::rclcpp ${tutorial_interfaces_TARGETS})

add_executable(listener src/subscriber_member_function.cpp)
# 改动:链接自定义接口目标
target_link_libraries(listener PUBLIC rclcpp::rclcpp ${tutorial_interfaces_TARGETS})

install(TARGETS
  talker
  listener
  DESTINATION lib/${PROJECT_NAME})

ament_package()

package.xml

语言添加内容
C++<depend>tutorial_interfaces</depend>
Python<exec_depend>tutorial_interfaces</exec_depend>
构建与运行

完成上述修改并保存后,构建功能包:

C++

  • Linux/macOS:
    colcon build --packages-select cpp_pubsub
    
  • Windows:
    colcon build --merge-install --packages-select cpp_pubsub
    

Python

  • Linux/macOS:
    colcon build --packages-select py_pubsub
    
  • Windows:
    colcon build --merge-install --packages-select py_pubsub
    

打开两个新终端,在每个终端中配置ros2_ws环境,然后运行:

C++

ros2 run cpp_pubsub talker
ros2 run cpp_pubsub listener

Python

ros2 run py_pubsub talker
ros2 run py_pubsub listener

由于Num.msg仅传输整数,发布者将只发布整数值(而非之前的字符串),示例输出如下:

[INFO] [minimal_publisher]: Publishing: '0'
[INFO] [minimal_publisher]: Publishing: '1'
[INFO] [minimal_publisher]: Publishing: '2'
7.2 使用AddThreeInts.srv测试服务/客户端功能

对之前创建的服务端/客户端功能包(C++或Python)进行如下修改,即可验证AddThreeInts.srv的功能。由于将原本接收两个整数的服务改为接收三个整数,输出结果会与之前略有不同。

服务端(Service)

C++

#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_ints.hpp"                                        // 改动:导入自定义AddThreeInts.srv

#include <memory>

// 改动:请求和响应类型改为自定义服务类型
void add(const std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Request> request,
          std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Response>       response)
{
  // 改动:计算三个整数的和
  response->sum = request->a + request->b + request->c;
  // 改动:打印三个请求整数
  RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld" " c: %ld",
                request->a, request->b, request->c);
  RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);

  // 改动:节点名称改为"add_three_ints_server"
  std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_server");

  // 改动:服务类型和名称改为自定义的AddThreeInts和"add_three_ints"
  rclcpp::Service<tutorial_interfaces::srv::AddThreeInts>::SharedPtr service =
    node->create_service<tutorial_interfaces::srv::AddThreeInts>("add_three_ints",  &add);

  // 改动:日志提示改为"Ready to add three ints."
  RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add three ints.");

  rclcpp::spin(node);
  rclcpp::shutdown();
}

Python

from tutorial_interfaces.srv import AddThreeInts                                                           # 改动:导入自定义AddThreeInts.srv

import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node


class MinimalService(Node):
    def __init__(self):
        super().__init__('minimal_service')
        # 改动:服务类型和名称改为AddThreeInts和"add_three_ints"
        self.srv = self.create_service(AddThreeInts, 'add_three_ints', self.add_three_ints_callback)

    # 改动:回调函数计算三个整数的和,并打印三个请求值
    def add_three_ints_callback(self, request, response):
        response.sum = request.a + request.b + request.c
        self.get_logger().info('Incoming request\na: %d b: %d c: %d' % (request.a, request.b, request.c))
        return response


def main(args=None):
    try:
        with rclpy.init(args=args):
            minimal_service = MinimalService()
            rclpy.spin(minimal_service)
    except (KeyboardInterrupt, ExternalShutdownException):
        pass


if __name__ == '__main__':
    main()
客户端(Client)

C++

#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_ints.hpp"                                       // 改动:导入自定义AddThreeInts.srv

#include <chrono>
#include <cstdlib>
#include <memory>

using namespace std::chrono_literals;

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);

  // 改动:节点名称改为"add_three_ints_client"
  std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_client");
  // 改动:客户端类型和服务名称改为自定义的AddThreeInts和"add_three_ints"
  rclcpp::Client<tutorial_interfaces::srv::AddThreeInts>::SharedPtr client =
    node->create_client<tutorial_interfaces::srv::AddThreeInts>("add_three_ints");

  // 改动:请求对象类型改为自定义服务的Request
  auto request = std::make_shared<tutorial_interfaces::srv::AddThreeInts::Request>();
  request->a = 41;
  request->b = 1;
  request->c = 1;                                                                           // 改动:添加第三个请求参数c

  while (!client->wait_for_service(1s)) {
    if (!rclcpp::ok()) {
      RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
      return 0;
    }
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
  }

  auto result = client->async_send_request(request);
  // 等待结果返回
  if (rclcpp::spin_until_future_complete(node, result) ==
    rclcpp::FutureReturnCode::SUCCESS)
  {
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum);
  } else {
    // 改动:错误提示中的服务名称改为"add_three_ints"
    RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_three_ints");
  }

  rclcpp::shutdown();
  return 0;
}

Python

from tutorial_interfaces.srv import AddThreeInts                            # 改动:导入自定义AddThreeInts.srv

import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node


class MinimalClientAsync(Node):
    def __init__(self):
        super().__init__('minimal_client_async')
        # 改动:客户端类型和服务名称改为AddThreeInts和"add_three_ints"
        self.cli = self.create_client(AddThreeInts, 'add_three_ints')
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        # 改动:请求对象类型改为AddThreeInts.Request
        self.req = AddThreeInts.Request()

    def send_request(self):
        self.req.a = 41
        self.req.b = 1
        self.req.c = 1                                                      # 改动:添加第三个请求参数c
        return self.cli.call_async(self.req)


def main(args=None):
    try:
        with rclpy.init(args=args):
            minimal_client = MinimalClientAsync()
            future = minimal_client.send_request()
            rclpy.spin_until_future_complete(minimal_client, future)
            response = future.result()
            # 改动:日志提示改为打印三个整数的和
            minimal_client.get_logger().info(
                'Result of add_three_ints: for %d + %d + %d = %d' %
                (minimal_client.req.a, minimal_client.req.b, minimal_client.req.c, response.sum))
    except (KeyboardInterrupt, ExternalShutdownException):
        pass


if __name__ == '__main__':
    main()
配置文件修改

CMakeLists.txt(仅C++)

# ...

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tutorial_interfaces REQUIRED)         # 改动:添加自定义接口功能包依赖

add_executable(server src/add_two_ints_server.cpp)
# 改动:链接自定义接口目标
target_link_libraries(server PUBLIC rclcpp::rclcpp ${tutorial_interfaces_TARGETS})

add_executable(client src/add_two_ints_client.cpp)
target_link_libraries(client PUBLIC rclcpp::rclcpp ${tutorial_interfaces_TARGETS})

install(TARGETS
  server
  client
  DESTINATION lib/${PROJECT_NAME})

ament_package()

package.xml

语言添加内容
C++<depend>tutorial_interfaces</depend>
Python<exec_depend>tutorial_interfaces</exec_depend>
构建与运行

完成上述修改并保存后,构建功能包:

C++

  • Linux/macOS:
    colcon build --packages-select cpp_srvcli
    
  • Windows:
    colcon build --merge-install --packages-select cpp_srvcli
    

Python

  • Linux/macOS:
    colcon build --packages-select py_srvcli
    
  • Windows:
    colcon build --merge-install --packages-select py_srvcli
    

打开两个新终端,在每个终端中配置ros2_ws环境,然后运行:

C++

ros2 run cpp_srvcli server
ros2 run cpp_srvcli client

Python

ros2 run py_srvcli service
ros2 run py_srvcli client

总结

在本教程中,你学习了如何在独立功能包中创建自定义接口,以及如何在其他功能包中调用这些接口。

本教程仅介绍了自定义接口定义的基础内容,更多细节可参考ROS 2接口详解

二、实现自定义接口

目标:学习在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

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:字符串(string)类型
  • last_name:字符串(string)类型
  • phone_number:字符串(string)类型
  • phone_type:无符号8位整数(uint8)类型,包含多个预定义的命名常量值

注意:可以在消息定义中为字段设置默认值。更多接口自定义方式可参考《ROS 2接口详解》。

接下来,需确保msg文件能被转换为C++、Python及其他语言的源代码。

2.1 构建msg文件
  1. 打开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

  2. 打开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}
      )
      
    • 确保导出消息运行时依赖:
      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", 10);
    
  • 创建周期性发布消息的回调函数:

    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)
target_link_libraries(publish_address_book rclcpp::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. 尝试运行

返回工作空间根目录,构建功能包:

Linux/macOS

cd ~/ros2_ws
colcon build --packages-up-to more_interfaces

Windows

cd /ros2_ws
colcon build --merge-install --packages-up-to more_interfaces

配置工作空间环境并运行发布者节点:

Linux

source install/local_setup.bash
ros2 run more_interfaces publish_address_book

macOS

. install/local_setup.bash
ros2 run more_interfaces publish_address_book

Windows(命令提示符)

call install/local_setup.bat
ros2 run more_interfaces publish_address_book

Windows(PowerShell)

install/local_setup.ps1
ros2 run more_interfaces publish_address_book

你将看到发布者节点输出你定义的消息内容,包括在publish_address_book.cpp中设置的值。

为确认消息正在address_book话题上发布,可打开另一个终端,配置工作空间环境后,执行topic echo命令:

Linux

source install/setup.bash
ros2 topic echo /address_book

macOS

. install/setup.bash
ros2 topic echo /address_book

Windows(命令提示符)

call install/setup.bat
ros2 topic echo /address_book

Windows(PowerShell)

install/setup.ps1
ros2 topic echo /address_book

本教程不创建订阅者节点,但你可以尝试自行编写(可参考《编写简单的C++发布者和订阅者》教程)。

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

注意:可以在新的接口定义中使用现有的接口定义。例如,假设存在一个名为Contact.msg的消息,它属于已有的ROS 2功能包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的头文件,才能将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 = 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功能包,并学习如何通过启动文件设置该参数。同样,你可以选择使用C++或Python编写。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ray.so

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值