2.2 话题通信
场景
话题通信是ROS中使用频率最高的一种通信模式,话题通信是基于发布订阅模式的,也即:一个节点发布消息,另一个节点订阅该消息。话题通信的应用场景也极其广泛,比如如下场景:
机器人在执行导航功能,使用的传感器是激光雷达,机器人会采集激光雷达感知到的信息并计算,然后生成运动控制信息驱动机器人底盘运动。
在该场景中,就不止一次使用到了话题通信。
以激光雷达信息的采集处理为例,在ROS中有一个节点需要时时的发布当前雷达采集到的数据,导航模块中也有节点会订阅并解析雷达数据。
再以运动消息的发布为例,导航模块会综合多方面数据实时计算出运动控制信息并发布给底盘驱动模块,底盘驱动有一个节点订阅运动信息并将其转换成控制电机的脉冲信号。
以此类推,像雷达、摄像头、GPS.... 等等一些传感器数据的采集,也都是使用了话题通信,话题通信适用于不断更新的数据传输相关的应用场景。
概念
话题通信是一种以发布订阅的方式实现不同节点之间数据传输的通信模型。数据发布对象称为发布方,数据订阅对象称之为订阅方,发布方和订阅方通过话题相关联,发布方将消息发布在话题上,订阅方则从该话题订阅消息,消息的流向是单向的。

话题通信的发布方与订阅方是一种多对多的关系,也即,同一话题下可以存在多个发布方,也可以存在多个订阅方,这意味着数据会出现交叉传输的情况,当然如果没有订阅方,数据传输也会出现丢失的情况。

作用
话题通信一般应用于不断更新的、少逻辑处理的数据传输场景。
关于消息接口
关于消息接口的使用有多种方式:
在ROS2中通过std_msgs包封装了一些原生的数据类型,比如:String、Int8、Int16、Int32、Int64、Float32、Float64、Char、Bool、Empty.... 这些原生数据类型也可以作为话题通信的载体,不过这些数据一般只包含一个 data 字段,而std_msgs包中其他的接口文件也比较简单,结构的单一意味着功能上的局限性,当传输一些结构复杂的数据时,就显得力不从心了;
在ROS2中还预定义了许多标准话题消息接口,这在实际工作中有着广泛的应用,比如:sensor_msgs包中定义了许多关于传感器消息的接口(雷达、摄像头、点云......),geometry_msgs包中则定义了许多几何消息相关的接口(坐标点、坐标系、速度指令......);
如果上述接口文件都不能满足我们的需求,那么就可以自定义接口消息;
具体如何选型,大家可以根据具体情况具体分析。
2.2.1 案例以及案例分析
1.案例需求
需求1:编写话题通信实现,发布方以某个频率发布一段文本,订阅方订阅消息,并输出在终端。

需求2:编写话题通信实现,发布方以某个频率发布自定义接口消息,订阅方订阅消息,并输出在终端。

2.案例分析
在上述案例中,需要关注的要素有三个:
发布方;
订阅方;
消息载体。
案例1和案例2的主要区别在于消息载体,前者可以使用原生的数据类型,后者需要自定义接口消息。
3.流程简介
案例2需要先自定义接口消息,除此之外的实现流程与案例1一致,主要步骤如下:
编写发布方实现;
编写订阅方实现;
编辑配置文件;
编译;
执行。
案例我们会采用C++和Python分别实现,二者都遵循上述实现流程。
4.准备工作
终端下进入工作空间的src目录,调用如下两条命令分别创建C++功能包和Python功能包。
ros2 pkg create cpp01_topic --build-type ament_cmake --dependencies rclcpp std_msgs base_interfaces_demo ros2 pkg create py01_topic --build-type ament_python --dependencies rclpy std_msgs base_interfaces_demo
2.2.2 话题通信之原生消息(C++)
1.发布方实现
功能包cpp01_topic的src目录下,新建C++文件demo01_talker_str.cpp,并编辑文件,输入如下内容:

2.订阅方实现
功能包cpp01_topic的src目录下,新建C++文件demo02_listener_str.cpp,并编辑文件,输入如下内容:
/*
需求:订阅发布方发布的消息,并输出到终端。
步骤:
1.包含头文件;
2.初始化 ROS2 客户端;
3.定义节点类;
3-1.创建订阅方;
3-2.处理订阅到的消息。
4.调用spin函数,并传入节点对象指针;
5.释放资源。
*/
// 1.包含头文件;
using std::placeholders::_1;
// 3.定义节点类;
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
// 3-1.创建订阅方;
subscription_ = this->create_subscription<std_msgs::msg::String>("topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
private:
// 3-2.处理订阅到的消息;
void topic_callback(const std_msgs::msg::String & msg) const
{
RCLCPP_INFO(this->get_logger(), "订阅的消息: '%s'", msg.data.c_str()); }
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char * argv[])
{
// 2.初始化 ROS2 客户端;
rclcpp::init(argc, argv);
// 4.调用spin函数,并传入节点对象指针。
rclcpp::spin(std::make_shared<MinimalSubscriber>());
// 5.释放资源;
rclcpp::shutdown();
return 0;
}
3.编辑配置文件
在C++功能包中,配置文件主要关注package.xml与CMakeLists.txt。
1.package.xml
在创建功能包时,所依赖的功能包已经自动配置了,配置内容如下:
<depend>rclcpp</depend><depend>std_msgs</depend><depend>base_interfaces_demo</depend>
需要说明的是<depend>base_interfaces_demo</depend>
在本案例中不是必须的。
2.CMakeLists.txt
CMakeLists.txt中发布和订阅程序核心配置如下:
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(base_interfaces_demo REQUIRED)
add_executable(demo01_talker_str src/demo01_talker_str.cpp)
ament_target_dependencies(
demo01_talker_str
"rclcpp"
"std_msgs"
)
add_executable(demo02_listener_str src/demo02_listener_str.cpp)
ament_target_dependencies(
demo02_listener_str
"rclcpp"
"std_msgs"
)
install(TARGETS
demo01_talker_str
demo02_listener_str
DESTINATION lib/${PROJECT_NAME})
4.编译
终端中进入当前工作空间,编译功能包:
colcon build --packages-select cpp01_topic
5.执行
当前工作空间下,启动两个终端,终端1执行发布程序,终端2执行订阅程序。
终端1输入如下指令:
. install/setup.bash
ros2 run cpp01_topic demo01_talker_str
终端2输入如下指令:
. install/setup.bash
ros2 run cpp01_topic demo02_listener_str
最终运行结果与案例1类似。
2.2.3 话题通信之原生消息(Python)
1.发布方实现
功能包py01_topic的py01_topic目录下,新建Python文件demo01_talker_str_py.py,并编辑文件,输入如下内容:
"""
需求:以某个固定频率发送文本“hello world!”,文本后缀编号,每发送一条消息,编号递增1。
步骤:
1.导包;
2.初始化 ROS2 客户端;
3.定义节点类;
3-1.创建发布方;
3-2.创建定时器;
3-3.组织消息并发布。
4.调用spin函数,并传入节点对象;
5.释放资源。
"""
# 1.导包;
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
# 3.定义节点类;
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher_py')
# 3-1.创建发布方;
self.publisher_ = self.create_publisher(String, 'topic', 10)
# 3-2.创建定时器;
timer_period = 0.5
self.timer = self.create_timer(timer_period, self.timer_callback) self.i = 0
# 3-3.组织消息并发布。
def timer_callback(self):
msg = String()
msg.data = 'Hello World(py): %d' % self.i
self.publisher_.publish(msg)
self.get_logger().info('发布的消息: "%s"' % msg.data)
self.i += 1
def main(args=None):
# 2.初始化 ROS2 客户端;
rclpy.init(args=args)
# 4.调用spin函数,并传入节点对象;
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
# 5.释放资源。
rclpy.shutdown()
if __name__ == '__main__':
main()
2.订阅方实现
功能包py01_topic的py01_topic目录下,新建Python文件demo02_listener_str_py.py,并编辑文件,输入如下内容:

3.编辑配置文件
在Python功能包中,配置文件主要关注package.xml与setup.py。
1.package.xml
在创建功能包时,所依赖的功能包已经自动配置了,配置内容如下:
<depend>rclcpp</depend><depend>std_msgs</depend><depend>base_interfaces_demo</depend>
需要说明的是和上一节C++实现一样<depend>base_interfaces_demo</depend>
在本案例中不是必须的。
2.setup.py
entry_points
字段的console_scripts
中添加如下内容:
entry_points={ 'console_scripts': [ 'demo01_talker_str_py = py01_topic.demo01_talker_str_py:main', 'demo02_listener_str_py = py01_topic.demo02_listener_str_py:main'
],
},
4.编译
终端中进入当前工作空间,编译功能包:
colcon build --packages-select py01_topic
5.执行
当前工作空间下,启动两个终端,终端1执行发布程序,终端2执行订阅程序。
终端1输入如下指令:
. install/setup.bash
ros2 run py01_topic demo01_talker_str_py
终端2输入如下指令:
. install/setup.bash
ros2 run py01_topic demo02_listener_str_py
最终运行结果与案例1类似。
2.2.4 话题通信自定义接口消息
自定义接口消息的流程与在功能包中编写可执行程序的流程类似,主要步骤如下:
创建并编辑
.msg
文件;编辑配置文件;
编译;
测试。
接下来,我们可以参考案例2编译一个msg文件,该文件中包含学生的姓名、年龄、身高等字段。
1.创建并编辑 .msg 文件
功能包base_interfaces_demo下新建 msg 文件夹,msg文件夹下新建Student.msg文件,文件中输入如下内容:
string name
int32 age
float64 height
2.编辑配置文件
1.package.xml文件
在package.xml中需要添加一些依赖包,具体内容如下:
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
2.CMakeLists.txt文件
为了将.msg
文件转换成对应的C++和Python代码,还需要在CMakeLists.txt中添加如下配置:
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/Student.msg"
)
3.编译
终端中进入当前工作空间,编译功能包:
colcon build --packages-select base_interfaces_demo
4.测试
编译完成之后,在工作空间下的install目录下将生成Student.msg
文件对应的C++和Python文件,我们也可以在终端下进入工作空间,通过如下命令查看文件定义以及编译是否正常:
. install/setup.bash
ros2 interface show base_interfaces_demo/msg/Student
正常情况下,终端将会输出与Student.msg
文件一致的内容。
2.2.5 话题通信之自定义消息(C++)
准备
C++文件中包含自定义消息相关头文件时,可能会抛出异常,可以配置VSCode中c_cpp_properties.json文件,在文件中的 includePath属性下添加一行:"${workspaceFolder}/install/base_interfaces_demo/include/**"
添加完毕后,包含相关头文件时,就不会抛出异常了,其他接口文件或接口包的使用也与此同理。
1.发布方实现
功能包cpp01_topic的src目录下,新建C++文件demo01_talker_stu.cpp,并编辑文件,输入如下内容:
/*
需求:以某个固定频率发送文本学生信息,包含学生的姓名、年龄、身高等数据。
*/
// 1.包含头文件;
using namespace std::chrono_literals;
using base_interfaces_demo::msg::Student;
// 3.定义节点类;
class MinimalPublisher : public rclcpp::Node
{ public:
MinimalPublisher()
: Node("student_publisher"), count_(0)
{
// 3-1.创建发布方;
publisher_ = this->create_publisher<Student>("topic_stu", 10);
// 3-2.创建定时器;
timer_ = this->create_wall_timer(500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
private:
void timer_callback()
{
// 3-3.组织消息并发布。
auto stu = Student();
stu.name = "张三";
stu.age = count_++;
stu.height = 1.65;
RCLCPP_INFO(this->get_logger(), "学生信息:name=%s,age=%d,height=%.2f", stu.name.c_str(),stu.age,stu.height);
publisher_->publish(stu);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<Student>::SharedPtr publisher_;
size_t count_;
};
int main(int argc, char * argv[])
{
// 2.初始化 ROS2 客户端;
rclcpp::init(argc, argv);
// 4.调用spin函数,并传入节点对象指针。
rclcpp::spin(std::make_shared<MinimalPublisher>());
// 5.释放资源;
rclcpp::shutdown(); return 0;
}
2.订阅方实现
功能包cpp01_topic的src目录下,新建C++文件demo04_listener_stu.cpp,并编辑文件,输入如下内容:
功能包cpp01_topic的src目录下,新建C++文件demo04_listener_stu.cpp,并编辑文件,输入如下内容:
/*
需求:订阅发布方发布的学生消息,并输出到终端。
*/
// 1.包含头文件;
using std::placeholders::_1;
using base_interfaces_demo::msg::Student;
// 3.定义节点类;
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("student_subscriber")
{
// 3-1.创建订阅方;
subscription_ = this->create_subscription<Student>("topic_stu", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
private:
// 3-2.处理订阅到的消息;
void topic_callback(const Student & msg) const
{
RCLCPP_INFO(this->get_logger(), "订阅的学生消息:name=%s,age=%d,height=%.2f", msg.name.c_str(),msg.age, msg.height);
}
rclcpp::Subscription<Student>::SharedPtr subscription_;
};
int main(int argc, char * argv[])
{
// 2.初始化 ROS2 客户端;
rclcpp::init(argc, argv);
// 4.调用spin函数,并传入节点对象指针。
rclcpp::spin(std::make_shared<MinimalSubscriber>());
// 5.释放资源;
rclcpp::shutdown();
return 0;
}
3.编辑配置文件
package.xml无需修改,CMakeLists.txt文件需要添加如下内容:
add_executable(demo03_talker_stu src/demo03_talker_stu.cpp)
ament_target_dependencies(
demo03_talker_stu
"rclcpp"
"std_msgs"
"base_interfaces_demo"
)
add_executable(demo04_listener_stu src/demo04_listener_stu.cpp)
ament_target_dependencies(
demo04_listener_stu
"rclcpp"
"std_msgs"
"base_interfaces_demo"
)
文件中install修改为如下内容:
install(TARGETS
demo01_talker_str
demo02_listener_str
demo03_talker_stu
demo04_listener_stu
DESTINATION lib/${PROJECT_NAME})
4.编译
终端中进入当前工作空间,编译功能包:
colcon build --packages-select cpp01_topic
5.执行
当前工作空间下,启动两个终端,终端1执行发布程序,终端2执行订阅程序。
终端1输入如下指令:
. install/setup.bash
ros2 run cpp01_topic demo03_talker_stu
终端2输入如下指令:
. install/setup.bash
ros2 run cpp01_topic demo04_listener_stu
最终运行结果与案例2类似。
2.2.6 话题通信之自定义消息(Python)
准备
Python文件中导入自定义消息相关的包时,为了方便使用,可以配置VSCode中settings.json文件,在文件中的python.autoComplete.extraPaths和python.analysis.extraPaths属性下添加一行:"${workspaceFolder}/install/base_interfaces_demo/local/lib/python3.10/dist-packages"
添加完毕后,代码可以高亮显示且可以自动补齐,其他接口文件或接口包的使用也与此同理。
1.发布方实现
功能包py01_topic的py01_topic目录下,新建Python文件demo03_talker_stu_py.py,并编辑文件,输入如下内容:
"""
需求:以某个固定频率发送文本学生信息,包含学生的姓名、年龄、身高等数据。
"""
# 1.导包;
import rclpy
from rclpy.node import Node
from base_interfaces_demo.msg import Student
# 3.定义节点类;
class MinimalPublisher(Node):
def __init__(self):
super().__init__('stu_publisher_py')
# 3-1.创建发布方;
self.publisher_ = self.create_publisher(Student, 'topic_stu', 10)
# 3-2.创建定时器;
timer_period = 0.5
self.timer = self.create_timer(timer_period, self.timer_callback) self.i = 0
# 3-3.组织消息并发布。
def timer_callback(self):
stu = Student()
stu.name = "李四"
stu.age = self.i
stu.height = 1.70
self.publisher_.publish(stu)
self.get_logger().info('发布的学生消息(py): name=%s,age=%d,height=%.2f' % (stu.name, stu.age, stu.height))
self.i += 1
def main(args=None):
# 2.初始化 ROS2 客户端;
rclpy.init(args=args)
# 4.调用spin函数,并传入节点对象;
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
# 5.释放资源。
rclpy.shutdown()
if __name__ == '__main__':
main()
2.订阅方实现
功能包py01_topic的py01_topic目录下,新建Python文件demo04_listener_stu_py.py,并编辑文件,输入如下内容:
"""
需求:订阅发布方发布的学生消息,并输出到终端。
"""
# 1.导包;
import rclpy
from rclpy.node import Node
from base_interfaces_demo.msg import Student#
3.定义节点类;
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('stu_subscriber_py')
# 3-1.创建订阅方;
self.subscription = self.create_subscription(
Student,
'topic_stu',
self.listener_callback,
10)
self.subscription
# 3-2.处理订阅到的消息。
def listener_callback(self, stu):
self.get_logger().info('订阅的消息(py): name=%s,age=%d,height=%.2f' % (stu.name, stu.age, stu.height))
def main(args=None):
# 2.初始化 ROS2 客户端;
rclpy.init(args=args)
# 4.调用spin函数,并传入节点对象;
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
# 5.释放资源。
rclpy.shutdown()
if __name__ == '__main__':
main()
3.编辑配置文件
package.xml无需修改,需要修改setup.py文件,entry_points
字段的console_scripts
中修改为如下内容:
entry_points={
'console_scripts': [
'demo01_talker_str_py = py01_topic.demo01_talker_str_py:main', 'demo02_listener_str_py = py01_topic.demo02_listener_str_py:main', 'demo03_talker_stu_py = py01_topic.demo03_talker_stu_py:main', 'demo04_listener_stu_py = py01_topic.demo04_listener_stu_py:main'
],
},
4.编译
终端中进入当前工作空间,编译功能包:
colcon build --packages-select py01_topic
5.执行
当前工作空间下,启动两个终端,终端1执行发布程序,终端2执行订阅程序。
终端1输入如下指令:
. install/setup.bash
ros2 run py01_topic demo03_talker_stu_py
终端2输入如下指令:
. install/setup.bash
ros2 run py01_topic demo04_listener_stu_py
最终运行结果与案例2类似。

B站有完整的ros系列教程视频,可以观看完整内容ros课程ROS2理论与实践
更多内容将在猛狮知识星球社区更新最新课程,后续将推出更多优质内容——详情可关注猛狮集训营公众号和猛狮集训营官方网站。