内容参考自Programming Robots with ROS by Morgan Quiley, Brian Gerkey, and William D. Smart (O'Reily). Copyright 2015 Morgan Quiley, Brian Gerkey, and William D. Smart, 978-1-4493-2389-9

将消息发布到话题上

创建话题

如下是声明一个话题并在其上发布消息的基本代码。这个节点以2Hz的速率发送连续的整数到counter节点上。

topic_publisher.py

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python
import rospy
from std_msgs.msg import Int32

rospy.init_node('topic_publisher')
pub = rospy.Publisher('counter', Int32)
rate = rospy.Rate(2)

count = 0
while not rospy.is_shutdown():
pub.publish(count)
count += 1
rate.sleep()

第一行#!/user/bin/env python是所谓的shebang。这句话告诉操作系统这是一个Python文件。由于我们要运行这个文件,所以需要使用chmod命令增加运行权限:

1
chmod u+x topic_publisher.py

初始化节点后,我们通过Publisher声明它:

1
pub = rospy.Publisher('counter', Int32)

至此,我们有一个声明counter话题并在上面发送整数的最小ROS节点。

检查一切是否工作正常

我们可以使用rostopic命令来检查系统中当前所有可见的话题。打开一个终端,运行roscore。我们便可以在另一个终端中使用rostopic list查看系统中的可用话题。

现在,我们要在另一个终端中运行上述节点。确保basic包被放置在一个工作空间中,并且已经导入了这个工作空间的配置(setup)文件。

(可参考前一篇文章创建工作空间和和ROS包)

例如,我的topic_publisher.py位于/home/lai/my_ws1/src/basic/中。

1
rosrun basic topic_publisher.py

这是在另一个终端中查看话题:

1
rostopic list
1
2
3
/counter
/rosout
/rosout_agg

更进一步的,我们可以使用rostopic list查看话题上发布的消息:

1
rostopic echo counter -n 5
1
2
3
4
5
6
7
8
9
---
data: 810
---
data: 811
---
data: 812
---
data: 813
---

-n 5选项告诉rostopic只打印5条消息。如果不加这个选项,它将一直打印下去。

我们也可以使用rostopic来检验它是否按照我们期望的速率发布消息:

1
rostopic hz counter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
subscribed to [/counter]
average rate: 2.001
min: 0.500s max: 0.500s std dev: 0.00000s window: 2
average rate: 2.001
min: 0.499s max: 0.500s std dev: 0.00059s window: 4
average rate: 2.000
min: 0.499s max: 0.501s std dev: 0.00059s window: 6
average rate: 2.000
min: 0.499s max: 0.501s std dev: 0.00062s window: 8
average rate: 2.000
min: 0.499s max: 0.501s std dev: 0.00067s window: 10
average rate: 2.000
min: 0.499s max: 0.501s std dev: 0.00062s window: 12
average rate: 2.000
min: 0.499s max: 0.501s std dev: 0.00059s window: 14
average rate: 2.000
min: 0.499s max: 0.501s std dev: 0.00057s window: 16
average rate: 2.000
min: 0.499s max: 0.501s std dev: 0.00056s window: 18
average rate: 2.000
min: 0.499s max: 0.501s std dev: 0.00053s window: 20
average rate: 2.000
min: 0.499s max: 0.501s std dev: 0.00054s window: 22

类似地,rostopic bw会给出这个话题使用的带宽。

我们也可以使用rostopic info来查看一个已经被声明的话题:

1
rostopic info counter
1
2
3
4
5
6
Type: std_msgs/Int32

Publishers:
* /topic_publisher (http://ubuntu:41967/)

Subscribers: None

这表明counter承载std_msgs/Int32类型的消息,并且由topic_publisher声明,而 且目前没有订阅者。由于多个节点可以发布相同的话题而且多个节点可以订阅同一个话题,所以这个命令可以帮助我们确认连接关系和我们想的一样。在这里, 发布者topic_publisher运行在ubuntu主机上,并且通过TCP端口41967通信。rostopic type的功能很相似, 但是只返回给定话题的消息类型。

最后,我们还可以使用rostopic find命令来发现某种类型消息的所有话题:

1
rostopic find std_msgs/Int32
1
/counter

订阅一个话题

创建订阅

如下是订阅counter话题并打印它接受到的消息的最小节点。

topic_subscriber.py

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env python
import rospy
from std_msgs.msg import Int32

def callback(msg):
print(msg.data)

rospy.init_node('topic_subscriber')
sub = rospy.Subscriber('counter', Int32, callback)
rospy.spin()

检查一切是否工作正常

首先确保发布者节点仍在运行并且仍在counter话题上发布信息。然后,在另一个终端中,启动订阅者节点:

1
rosrun basic topic_subscriber.py
1
2
3
4
5
6
7
8
9
10
11
12
164
165
166
167
168
169
170
171
172
173
174
175

Fantastic! 我们现在已经成功运行了第一个ROS。我们可以使用rqt_graph来可视化这个系统。

我们也可以使用rostopic pub工具发布消息到一个话题上。例如:

1
rostopic pub counter std_msgs/Int32 1000000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
626
627
628
629
630
631
1000000
632
633
634
635
636
637
638
639

锁存话题(latched topics)

ROS中的消息是短暂的。如果一条消息发布的时候我们还没有订阅这个话题,那么我们将错过它。如果发布者的发布频率很高的话,这没有什么影响,因为下一条消息马上就会到来 然而,有些情況下频繁发布消息是很坏的主意。

举个例子, map_server节点在map话题上播地图(nav_msgs/OccupancyGrid类型的 数据)。这种数据表示环境地图,机器人可以使用这些东西来确定自己的位置。通常,这个地图永远不会发生改变,并且只在map_server加载完地图之后发送一次。然而,这意味着如果另一个节点需要这个地图,但是却在map_server发布完成之后启动,它将永远无法获取到地图数据。

锁存节点提供一个简单地解决此类问题的方案。如果一个话题在其声被明时标记为锁存的,订阅者在完成订阅之后将会自动获取话题上发布的最后一个消息,例如:

1
pub = rospy.Publisher('map', nav_msgs/OccupancyGrid, latched=True)

定义自己的消息类型

定义一个新消息

例如,我们想发布随机的复数。

Complex.msg

1
2
float32 real
float32 imaginary

Complex.msg文件位于basic包的msg目录中。它定义了两个值,realimaginary

一旦消息被定义了,我们就需要运行catkin_make命令来产生语言相关的代码。其中包 含了类型的定义,以及序列化和反序列化相关的代码。这将能够使我们在任何ROS支持 的语言中使用这一消息类型,使用一种语言写的节点可以订阅另一种语言写的节点发布 话题。而且这使得我们可以使用这些消息实现计算机和其他平台的无缝通信。

为了让 ROS生成语言相关的消息代码,我们需要确保已经告知构建系统新消息的定义。可以通过向package.xml文件中添加以下代码来实现这一点:

1
2
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

接下来,我们需要更改一下CMakeLists.txt文件。首先,需要在find_package()调用的末尾添加message_generation,这样catkin就知道了去哪里寻找message_generation

1
2
3
4
5
6
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation # Add message_generation here, after the other packages
)

然后,告知catkin我们将在运行时使用消息,即在catkin_package()调用末尾添加message_runtime

1
2
3
catkin_package(
CATKIN_DEPENDS message_runtime # This will not be the only thing here
)

通过在add_message_files()调用的末尾添加消息定义文件来告知catkin我们想要编译它们:

1
2
3
4
add_message_files(
FILES
Complex.msg
)

最后,仍然是在CMakeLists.txt文件中,需要确保去掉了generate_messages()调用的注释, 并已经包含了消息所依赖的所有依赖项:

1
2
3
4
generate messages(
DEPENDENCIES
std_msgs
)

现在,我们已经告知catkin它需要知道的一切,我们可以开始编译它们了。到catkin工作空间的根目录,运行catkin_make。这将产生一个和消息定义文件同名的消息类型。方便起见,ROS的类型都是首字母大写且没有下划线的。

rosmsg命令可以让我们看到某个消息类型的内容:

1
rosmsg show Complex

使用新消息

一旦我们的新消息被定义了,并且被编译了,我们就可以像使用ROS中的其他消息一样来使用它了。

message_publisher.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python
import rospy
from basic.msg import Complex
from random import random

rospy.init_node('message_publisher')
pub = rospy.Publisher('complex', Complex)
rate = rospy.Rate(2)

while not rospy.is_shutdown():
msg = Complex()
msg.real = random()
msg.imaginary = random()

pub.publish(msg)
rate.sleep()

(若提示“[rospack] Error: package 'basic' not found”,可参考https://blog.csdn.net/qq_33973712/article/details/108226729解决)

要使用我们的新消息,可参考如下内容。

message_subscriber.py

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python
import rospy
from basic.msg import Complex

def callback(msg):
print('Real:', msg.real)
print('Imaginary:', msg.imaginary)

rospy.init_node('message_subscriber')
sub = rospy.Subscriber('complex', Complex, callback)
rospy.spin()

分别在不同的终端运行rosrun basic message_publisher.pyrosrun basic message_subscriber.py,我们可以收到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
('Real:', 0.9999882578849792)
('Imaginary:', 0.4572080075740814)
('Real:', 0.8403760194778442)
('Imaginary:', 0.22716167569160461)
('Real:', 0.6950759291648865)
('Imaginary:', 0.9942832589149475)
('Real:', 0.15603911876678467)
('Imaginary:', 0.22643907368183136)
('Real:', 0.07436700910329819)
('Imaginary:', 0.29001638293266296)
('Real:', 0.6325380206108093)
('Imaginary:', 0.9387229084968567)
('Real:', 0.12351877242326736)
('Imaginary:', 0.9426838755607605)
('Real:', 0.5822433233261108)
('Imaginary:', 0.6146134734153748)
('Real:', 0.46813079714775085)
('Imaginary:', 0.9633353352546692)
('Real:', 0.38089749217033386)
('Imaginary:', 0.3922206163406372)
('Real:', 0.10183482617139816)
('Imaginary:', 0.42885103821754456)
('Real:', 0.977981686592102)
('Imaginary:', 0.3764207661151886)
('Real:', 0.358938992023468)
('Imaginary:', 0.08560513705015182)

让发布者和订阅者同时工作

前面的例子显示了单个发布者/单个订阅者的情况,但是一个节点也可以同时是一个发布者和订阅者,或者拥有多个订阅和发布。实际上,ROS节点最常做的事情就是传递消息,并在消息上进行运算。举个例子,一个节点可能会订阅一个包含摄像机图像的节点检测其中的人脸然后在另一个话题上发布这些人脸的位置。

doubler.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python
import rospy
from std_msgs.msg import Int32

rospy.init_node('doubler')

def callback(msg):
doubled = Int32()
doubled.data = msg.data * 2

pub.publish(doubled)

sub = rospy.Subscriber('number', Int32, callback)
pub = rospy.Publisher('doubled', Int32)

rospy.spin()

订阅者和发布者就像前面一样进行配置,但是现在我们在回调函数中发布消息,而不是周期性地发布。这背后的原因就是,我们只想在接收到新的数据之后才发布,因为这个节点的目的就是传递数据 (在本例中,就是把接收到的数字加倍后发送出去) 。