ROS2高效学习第四章 -- ros2 topic 编程之自定义 msg 其二

1 前言和资料

本文是 ros2 topic 编程第二篇,讲解自定义消息,也是第一篇ROS2高效学习第四章 – ros2 topic 编程之收发 string 并使用 ros2 launch的延续。我们将复用第一篇创建的两个软件包,即 pubsub_cpp 和 pubsub_py,在此基础上开发自定义消息样例。
本文参考资料如下:
(1)Custom-ROS2-Interfaces
(2)Single-Package-Define-And-Use-Interface

2 正文

2.1 两种自定义 msg 方式的讨论

(1)自定义 msg:所谓自定义msg,就是用户根据需求自己定义的消息结构体,用于 topic 收发,比如用户定义了描述 student 信息的 msg。自定义的 msg 不属于 ros2 官方提供的,但可以依赖 ros2 官方 msg,后面的样例将对此举例。
(2)msg (包括 srv)支持的原生数据类型:
整数类型:int8,int16,int32,int64;uint8,uint16,uint32,uint64;
浮点数类型:float32,float64
布尔型:bool
字符型:char,byte (实际上是 uint8 的别名)
字符串类型:string
(3)自定义 msg 的两种方式:
第一种:在大型系统中,一般都是将 msg 或者叫通信接口(interface)独立成包,独立维护,并作为上下游节点的依赖模块存在。这对于整个系统模块间的松耦合非常重要,这种方式也是 ros2 推荐的方式。
第二种:当软件规模比较小的时候,比如收发节点都在一个软件包里,直接把自定义 msg 放在一起,肯定更加方便。我们的 ros1 系列博客ROS高效入门系列基本都采用这种方式。
(4)自定义 msg 放在模块包里引发的思考:
ROS2高效学习第三章 – 梳理 ros 编译工具,开始 ros2 编程,第一个 hello ros2 样例这篇博客里,我们讨论了 ros2 的两种软件包的构建区别,一个是纯 cpp,构建类型是 ament_cmake ;另一个是纯 python,构建类型是 ament_python 。
针对用户自定义 msg,ros2 要求必须使用 ament_cmake 构建类型。如果用户想混用 cpp 和 python,比如发送节点是 cpp,接受节点是 python,怎么办?
解决办法是:构建类型选 ament_cmake ,并使用 ros2 提供的 ament_cmake_python 把 python 程序安装出来。这也是 ros2 支持 cpp 和 python 混合编程的唯一方式。

2.2 自定义 msg 独立存在

2.2.1 自定义 msg 包(diy_interface)

(1)创建 diy_interface 软件包和相关文件

cd ~/colcon_ws/src
ros2 pkg create --build-type ament_cmake  --license Apache-2.0 diy_interface
cd diy_interface
mkdir msg
touch msg/Student.msg msg/Sphere.msg

(2)编写 Student.msg

string name
uint8 age

(3)编写 Sphere.msg

geometry_msgs/Point center	# 依赖 ros2 官方的 geometry_msgs/Point
float64 radius

(4)编写 CMakeLists.txt

cmake_minimum_required(VERSION 3.8)
project(diy_interface)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
find_package(geometry_msgs REQUIRED)
// rosidl_default_generators 必须加
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Student.msg"
  "msg/Sphere.msg"
  DEPENDENCIES geometry_msgs
)
ament_package()

(5)添加 package.xml

  <depend>geometry_msgs</depend>
  // 定义 msg , 这三个是刚需
  <buildtool_depend>rosidl_default_generators</buildtool_depend>
  <exec_depend>rosidl_default_runtime</exec_depend>
  <member_of_group>rosidl_interface_packages</member_of_group>

(6)编译并测试

~/colcon_ws
colcon build --packages-select diy_interface
source install/local_setup.bash
ros2 interface show diy_interface/msg/Student
ros2 interface show diy_interface/msg/Sphere

在这里插入图片描述

2.2.2 pubsub_cpp 收发自定义 msg

(1)创建新的 cpp 和 launch 文件

cd ~/colcon_ws/src/pubsub_cpp
touch launch/pubsub_diy_msg_launch.py
touch src/pub_diy_msg.cpp src/sub_diy_msg.cpp

(2)编写 pub_diy_msg.cpp

#include <chrono>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "diy_interface/msg/student.hpp"
#include "diy_interface/msg/sphere.hpp"

class Publisher : public rclcpp::Node 
{
public:
    Publisher() : Node("test_pub_diy_msg"), count_(0) {
        pub_student_ = this->create_publisher<diy_interface::msg::Student>("student_topic", 10);
        pub_sphere_ = this->create_publisher<diy_interface::msg::Sphere>("sphere_topic", 10);
        timer_ = this->create_wall_timer(std::chrono::seconds(1), std::bind(&Publisher::timer_callback, this));
    }

private:
    void timer_callback() {
        auto stu_msg = diy_interface::msg::Student();
        stu_msg.name = students_[count_ % students_.size()].first;
        stu_msg.age = students_[count_ % students_.size()].second;
        RCLCPP_INFO(this->get_logger(), "publishing student in cpp: %s, %d", stu_msg.name.c_str(), stu_msg.age);
        pub_student_->publish(stu_msg);

        auto sphere_msg = diy_interface::msg::Sphere();
        sphere_msg.center.x = std::get<0>(spheres_[count_ % spheres_.size()].first);
        sphere_msg.center.y = std::get<1>(spheres_[count_ % spheres_.size()].first);
        sphere_msg.center.z = std::get<2>(spheres_[count_ % spheres_.size()].first);
        sphere_msg.radius = spheres_[count_ % spheres_.size()].second;
        RCLCPP_INFO(this->get_logger(), "publishing sphere in cpp: (%f, %f, %f), %f", 
            sphere_msg.center.x, sphere_msg.center.y, sphere_msg.center.z, sphere_msg.radius);
        pub_sphere_->publish(sphere_msg);

        count_++;
    }

private:
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher<diy_interface::msg::Student>::SharedPtr pub_student_;
    rclcpp::Publisher<diy_interface::msg::Sphere>::SharedPtr pub_sphere_;
    size_t count_;
    std::vector<std::pair<std::string, int>> students_ = 
        {{"yi", 32}, {"miao", 18}, {"bao", 3}};
    std::vector<std::pair<std::tuple<float, float, float>, float>> spheres_ = 
        {{{1.0f, 2.0f, 3.0f}, 4.0f}, {{1.1f, 2.1f, 3.1f}, 4.1f}};
};

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

(3)编写 sub_diy_msg.cpp

#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "diy_interface/msg/student.hpp"
#include "diy_interface/msg/sphere.hpp"

class Subscriber : public rclcpp::Node 
{
public:
    Subscriber() : Node("test_sub_diy_msg") {
        sub_student_ = this->create_subscription<diy_interface::msg::Student>(
            "student_topic", 10, std::bind(&Subscriber::student_topic_callback, this, std::placeholders::_1));
        sub_sphere_ = this->create_subscription<diy_interface::msg::Sphere>(
            "sphere_topic", 10, std::bind(&Subscriber::sphere_topic_callback, this, std::placeholders::_1));
    }

private:
    void student_topic_callback(const diy_interface::msg::Student &msg) const {
        RCLCPP_INFO(this->get_logger(), "i received student in cpp: %s, %d", msg.name.c_str(), msg.age);
    }
    void sphere_topic_callback(const diy_interface::msg::Sphere &msg) const {
        RCLCPP_INFO(this->get_logger(), "i received sphere in cpp: (%f, %f, %f), %f", 
            msg.center.x, msg.center.y, msg.center.z, msg.radius);
    }

private:
    rclcpp::Subscription<diy_interface::msg::Student>::SharedPtr sub_student_;
    rclcpp::Subscription<diy_interface::msg::Sphere>::SharedPtr sub_sphere_;
};

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

(4)编写 pubsub_diy_msg_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='pubsub_cpp',
            namespace='cpp',
            executable='talker_diy',
            name='talker_diy'
        ),
        Node(
            package='pubsub_cpp',
            namespace='cpp',
            executable='listener_diy',
            name='listener_diy'
        )
    ])

(5)添加 CMakeLists.txt

find_package(diy_interface REQUIRED)
add_executable(talker_diy src/pub_diy_msg.cpp)
ament_target_dependencies(talker_diy rclcpp diy_interface)
add_executable(listener_diy src/sub_diy_msg.cpp)
ament_target_dependencies(listener_diy rclcpp diy_interface)
install(TARGETS
  talker
  listener
  talker_diy
  listener_diy
  DESTINATION lib/${PROJECT_NAME})

(6)添加 package.xml

  <depend>diy_interface</depend>

(7)编译并运行

~/colcon_ws
colcon build --packages-select diy_interface pubsub_cpp
source install/local_setup.bash
ros2 launch pubsub_cpp pubsub_diy_msg_launch.py

在这里插入图片描述

2.2.3 pubsub_py 收发自定义 msg

(1)创建新的 python 和 launch 文件

cd ~/colcon_ws/src/pubsub_py
touch launch/pubsub_diy_msg_launch.py
touch pubsub_py/pub_diy_msg.py pubsub_py/sub_diy_msg.py

(2)编写 pub_diy_msg.py

#! /usr/bin/env python3
# -*- coding: utf-8 -*-

import rclpy
from rclpy.node import Node

from diy_interface.msg import Student
from diy_interface.msg import Sphere

class Publisher(Node):
    def __init__(self):
        super().__init__('test_pub_diy_msg')
        self._pub_student = self.create_publisher(Student, "student_topic", 10)
        self._pub_sphere = self.create_publisher(Sphere, "Sphere_topic", 10)
        self._timer = self.create_timer(0.5, self.timer_callback)
        self._i = 0
        self._students = [
            ("yi", 32),
            ("miao", 18),
            ("bao", 3)
        ]
        self._spheres = [
            ((1.0, 2.0, 3.0), 4.0),
            ((1.1, 2.1, 3.1), 4.1)
        ]

    def timer_callback(self):
        stu_msg = Student()
        stu_msg.name = self._students[self._i % len(self._students)][0]
        stu_msg.age = self._students[self._i % len(self._students)][1]
        self.get_logger().info("Publishing student in python: {0}, {1}".format(stu_msg.name, stu_msg.age))
        self._pub_student.publish(stu_msg)

        sph_msg = Sphere()
        sph_msg.center.x = self._spheres[self._i % len(self._spheres)][0][0]
        sph_msg.center.y = self._spheres[self._i % len(self._spheres)][0][1]
        sph_msg.center.z = self._spheres[self._i % len(self._spheres)][0][2]
        sph_msg.radius = self._spheres[self._i % len(self._spheres)][1]
        self.get_logger().info("Publishing sphere in python: ({0}, {1}, {2}), {3}".format(sph_msg.center.x, sph_msg.center.y, sph_msg.center.z, sph_msg.radius))
        self._pub_sphere.publish(sph_msg)
        self._i += 1

def main(args=None):
    rclpy.init(args=args)
    pub_node = Publisher()
    rclpy.spin(pub_node)
    pub_node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

(3)编写 sub_diy_msg.py

#! /usr/bin/env python3
# -*- coding: utf-8 -*-

import rclpy
from rclpy.node import Node

from diy_interface.msg import Student
from diy_interface.msg import Sphere

class Subscriber(Node):
    def __init__(self):
        super().__init__('test_sub_diy_msg')
        self._sub_student = self.create_subscription(Student, "student_topic", self.student_topic_callback, 10)
        self._sub_sphere = self.create_subscription(Sphere, "sphere_topic", self.sphere_topic_callback, 10)

    def student_topic_callback(self, msg):
        self.get_logger().info("i received student in python: {0}, {1}".format(msg.name, msg.age))

    def sphere_topic_callback(self, msg):
        self.get_logger().info("i received sphere in python: ({0}, {1}, {2}), {3}".format(msg.center.x, msg.center.y, msg.center.z, msg.radius))

def main(args=None):
    rclpy.init(args=args)
    sub_node = Subscriber()
    rclpy.spin(sub_node)
    sub_node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

(4)编写 pubsub_diy_msg_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='pubsub_py',
            namespace='python',
            executable='talker_diy',
            name='talker_diy'
        ),
        Node(
            package='pubsub_py',
            namespace='python',
            executable='listener_diy',
            name='listener_diy'
        )
    ])

(5)添加 package.xml

  <exec_depend>diy_interfaces</exec_depend>

(6)添加 setup.py

    entry_points={
        'console_scripts': [
            'talker = pubsub_py.publisher:main',
            'talker_diy = pubsub_py.pub_diy_msg:main',
            'listener = pubsub_py.subscriber:main',
            'listener_diy = pubsub_py.sub_diy_msg:main'
        ],
    },

(7)编译并运行

~/colcon_ws
colcon build --packages-select diy_interface pubsub_py
source install/local_setup.bash
ros2 launch pubsub_py pubsub_diy_msg_launch.py

在这里插入图片描述

2.3 自定义 msg 放在模块包里(pubsub_mix )

(1)创建 pubsub_mix 软件包和相关文件

cd ~/colcon_ws/src
ros2 pkg create --build-type ament_cmake  --license Apache-2.0 pubsub_mix
cd pubsub_mix
mkdir launch msg scripts
touch launch/pubsub_mix_launch.py
touch msg/AddressBook.msg
touch src/publish_address_book.cpp scripts/subscribe_address_book.py

(2)编写 publish_address_book.cpp

#include <chrono>
#include <functional>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "pubsub_mix/msg/address_book.hpp"

class Publisher : public rclcpp::Node 
{
public:
    Publisher() : Node("address_book_publisher"), count_(0) {
        publisher_ = this->create_publisher<pubsub_mix::msg::AddressBook>("address_book", 10);
        timer_ = this->create_wall_timer(std::chrono::seconds(1), std::bind(&Publisher::timer_callback, this));
    }

private:
    void timer_callback() {
        auto msg = pubsub_mix::msg::AddressBook();
        msg.first_name = "yi";
        msg.last_name = "cao";
        msg.phone_number = "123456789";
        msg.phone_type = msg.PHONE_TYPE_MOBILE;
        RCLCPP_INFO(this->get_logger(), "publishing in cpp: %s %s %s %d", 
            msg.first_name.c_str(), msg.last_name.c_str(), msg.phone_number.c_str(), msg.phone_type);
        publisher_->publish(msg);
    }

private:
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher<pubsub_mix::msg::AddressBook>::SharedPtr publisher_;
    size_t count_;
};

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

(3)编写 subscribe_address_book.py

#! /usr/bin/env python3
# -*- coding: utf-8 -*-

import rclpy
from rclpy.node import Node
from pubsub_mix.msg import AddressBook

class Subscriber(Node):
    def __init__(self):
        super().__init__('address_book_subscriber')
        self._subscriber = self.create_subscription(AddressBook, "address_book", self.topic_callback, 10)

    def topic_callback(self, msg):
        self.get_logger().info("subscribe in python: {0}, {1}, {2}, {3}".format(
                msg.first_name, msg.last_name, msg.phone_number, msg.phone_type))

def main(args=None):
    rclpy.init(args=args)
    sub_node = Subscriber()
    rclpy.spin(sub_node)
    sub_node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

(4)编写 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

(5)编写 CMakeLists.txt

cmake_minimum_required(VERSION 3.8)
project(pubsub_mix)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(rclcpp REQUIRED)
find_package(ament_cmake_python 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)
// 因为自定义消息在软件包里,这句就要加
rosidl_get_typesupport_target(cpp_typesupport_target
  ${PROJECT_NAME} rosidl_typesupport_cpp)
target_link_libraries(publish_address_book "${cpp_typesupport_target}")

install(TARGETS
  publish_address_book
  DESTINATION lib/${PROJECT_NAME})

install(DIRECTORY
  launch
  DESTINATION share/${PROJECT_NAME}
)
// 使用install把python程序安装出来
install(PROGRAMS
  scripts/subscribe_address_book.py
  DESTINATION lib/${PROJECT_NAME}
)
ament_package()

(6)添加 package.xml

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

(7)编写 pubsub_mix_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='pubsub_mix',
            namespace='mix',
            executable='publish_address_book',
            name='publish_address_book'
        ),
        Node(
            package='pubsub_mix',
            namespace='mix',
            executable='subscribe_address_book.py',
            name='subscribe_address_book'
        )
    ])

(8)编译并运行

~/colcon_ws
colcon build --packages-select pubsub_mix
source install/local_setup.bash
ros2 launch pubsub_mix pubsub_mix_launch.py

在这里插入图片描述

总结

本文的代码托管在本人的github上:pubsub_cpppubsub_pypubsub_mix

  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
ROS 2中,创建自定义消息需要以下步骤: 1. 创建.msg文件:在ROS 2中,消息定义使用.msg文件。创建一个新的.msg文件,其中包含自定义消息的字段和类型。例如,如果您想要创建一个名为“my_message”的自定义消息,可以创建一个名为“my_message.msg”的文件,并在其中定义消息的字段和类型。 2. 编译消息:要使用自定义消息,您需要将其编译成ROS 2可用的代码。可以使用ROS 2的消息构建工具Colcon来完成此操作。首先,在您的工作空间中创建一个“msg”文件夹,并将您的.msg文件放在其中。然后,在终端中运行以下命令: ``` colcon build --packages-select my_package ``` 这将编译您的自定义消息并生成ROS 2可用的代码。 3. 使用自定义消息:一旦您的自定义消息已经编译,您可以在ROS 2中使用它。在您的ROS 2节点或包中,包含您的自定义消息的头文件,并使用该消息的类型来定义变量。例如,如果您想要在ROS 2节点中发布一个名为“my_topic”的主题,其中包含您的自定义消息类型,可以执行以下操作: ```c++ #include "my_package/my_message.hpp" ... rclcpp::Node::SharedPtr node = rclcpp::Node::make_shared("my_node"); auto publisher = node->create_publisher<my_package::msg::MyMessage>("my_topic", 10); my_package::msg::MyMessage message; message.field1 = 1; message.field2 = "hello"; publisher->publish(message); ``` 这将创建一个名为“my_topic”的主题,并使用您的自定义消息类型来发布名为“message”的消息。 总之,以上是在ROS 2中创建自定义消息的基本步骤。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值