ros2 topic 编程之自定义 msg
- 1 前言和资料
- 2 正文
- 2.1 两种自定义 msg 方式的讨论
- 2.2 自定义 msg 独立存在
- 2.2.1 自定义 msg 包(diy_interface)
- 2.2.2 pubsub_cpp 收发自定义 msg
- 2.2.3 pubsub_py 收发自定义 msg
- 2.3 自定义 msg 放在模块包里(pubsub_mix )
- 总结
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 的两种方式:
第一种:在大型系统中,一般都是将 msg 或者叫通信接口(interface)独立成包,独立维护,并作为上下游节点的依赖模块存在。这对于整个系统模块间的松耦合非常重要,这种方式也是 ros2 推荐的方式。
第二种:当软件规模比较小的时候,比如收发节点都在一个软件包里,直接把自定义 msg 放在一起,肯定更加方便。我们的 ros1 系列博客ROS高效入门系列基本都采用这种方式。
(3)自定义 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 Nodedef 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(TARGETStalkerlistenertalker_diylistener_diyDESTINATION 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 Nodefrom diy_interface.msg import Student
from diy_interface.msg import Sphereclass 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 = 0self._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 += 1def 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 Nodefrom diy_interface.msg import Student
from diy_interface.msg import Sphereclass 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 Nodedef 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 AddressBookclass 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=2string 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(TARGETSpublish_address_bookDESTINATION lib/${PROJECT_NAME})install(DIRECTORYlaunchDESTINATION share/${PROJECT_NAME}
)
// 使用install把python程序安装出来
install(PROGRAMSscripts/subscribe_address_book.pyDESTINATION 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 Nodedef 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_cpp,pubsub_py,pubsub_mix