引言
目前很多编程语言都提供了 ROS 库 (library) 用来编写 ros node 文件,官方语言是 C++ 和 Python。在我们的项目中,如果对实时性要求比较高,还会用到 Julia,它也有自己的 ROS library —— RobotOS 。
一般来说,如果程序不是太复杂,处理时间可以忍受,我都首选 Python 编写,一方面是语法比较简单,另外一方面,也是很重要的一点,不需要设置额外的 CMakeList, package 等文件用于编译过程。通常只需要一个 .py 文件就可以创建一个简单的 ros node 文件,设置成可执行文件,运行即可。
虽然我用 python 写过不少 ros node 文件,但是如果让我在一个空白的文件中逐字逐句的编写一个完整的 node,不借助任何参考,我可能很难写出来。
就像很多写学术论文的人都在用 latex,但是几乎没有人是从无到有的写出一个 latex 文件。一般都要基于一些的模板,修改其中的 tilte, abstract, ref 等内容。
本文主要目的是通过例子展示常见的 python 编写 ros node 的格式,总结基本框架,作为以后的参考模板。
本文中有些例子来自 ros wiki ,我会加上尽量详细的注释。另外的例子则包含了我比较常用的编程格式。
例1: Publisher node
功能:创建一个 ros node,以每秒 10 次的频率发布 “hello world +时间” 信息。
#!/usr/bin/env python
# 上边一行称为 shebang line, 指定运行该文件的程序。
# 此处是说用从 PATH 环境变量中找到的第一个 python 程序执行该文件。
# 如果要更加具体地指定执行文件,可以用 #!<path>/python
# 指明用该路径下的 python 程序执行该文件。
import rospy
# 调用 rospy module,这是所有 python node 需要加载的一个 module
from std_msgs.msg import String
# 由于后边在构造 Publish message 时用到了 std_msgs/String 类型的 ROS 数据
# 所以要先调用相关的 module 中的 class,即 std_msgs.msgs.String 类。
# 在构造 msg 时,可以用 msg=String() 的方式初始化对象
# 注意:用 python 编写的 node 程序本质上依然是 python 程序文件
# 里面用到的 import 等调用命令还是要去 PYTHONPATH 指定的路径寻找相应的 module。
# 所以 ROS 的各种 module 必须在 PYTHONPATH 的路径中才行。
# 其实在 source ROS 的各种 .bash 文件或者 install 各种 ROS package 的时候
# 就已经将这些 module 放到 PYTHONPATH 中了。
def talker():
pub = rospy.Publisher('chatter', String, queue_size=10)
# 创建一个 pub 对象,pub 对象有个 method —— pub.publish() 可以被用来
# 向 chatter 这个 topic 上发送类型为 String 的数据。
# 这里就用到了 String class,所以需要在开头的时候调用相应的 module
# 我感觉创建 pub 的这句命令放到后边更合适,将创建 node 的 init_node 命令做为最早的命令
rospy.init_node('talker', anonymous=True)
# 初始化 node,名字为 talker,有了名字,node 才开始与 master 以及其他 node 通信
# ROS graph 中可能存在多个重名的 node,例如多个 turtle node,当令 anonymous=True 时,
# 重名 node 会自动在名字后边加上随机数以示区别。
rate = rospy.Rate(10)
# 产生一个 rospy.Rate Class 的对象 rate,它有一个 method —— rate.sleep()
# 可以控制 loop 的频率。
# 比如每秒钟 loop 10次,如果一次 loop 小于 1/10 秒,则 sleep 等待,确保 1秒 完成10个 loop。
# 很显然这里的前提是,每个 loop 不能超过 1/10 秒,否则单靠 sleep 是无法达到期望的 loop 频率的
while not rospy.is_shutdown():
# 监控着是否程序是否被关闭,例如 ctrl + c 等
hello_str = "hello world %s" % rospy.get_time()
# 这里的字符串就是 std_msgs/String 类型的。rospy.get_time() 可以返回当前时间
rospy.loginfo(hello_str)
# 默认情况下 info 会被写到三个地方:1.屏幕,2.log 文件,3.rosout topic
pub.publish(hello_str)
rate.sleep()
if __name__ == '__main__':
# 当该程序作为主程序运行时,满足此条件。
# 如果该程序作为 module 被另一个程序调用,则 if 条件不成立,后边的都不会执行。
try:
talker()
except rospy.ROSInterruptException:
pass
# handle error, 看 talker() 程序是否能够正常运行,如果可以正常运行,则按照 talker() 中的命令运行;
# 如果运行 talker() 程序报错,则执行 except block 中的命令,即 pass,
# 实际上是什么都不做,结束程序,此时不会出现 error 提示信息。
# 如果要求不这么严谨,也可以不用 try ... except 形式,直接调用 talker() 就行。
例2: Subscriber node
作用:创建一个 node,收听信息,然后显示出来。
#!/usr/bin/env python
# 第一句依然是 shebang line,指定运行该文件的程序
import rospy
from std_msgs.msg import String
def callback(data):
# 尽管在 Subscriber 语句中没有给 callback 加参数
# 但是在执行中默认将接受到的 msg 作为参数传递过来
rospy.loginfo(rospy.get_caller_id() + "I heard %s", data.data)
# std_msgs/String 类型的变量结构为 `string: data`,
# 即 msg 的值是存储在 string 基本类型的 data field 中。
def listener():
rospy.init_node('listener', anonymous=True)
# 初始化 node,名为 listener,如果有重名的就在后边加随机数,以示区分
rospy.Subscriber("chatter", String, callback)
# 监听 chatter topic 上的 std_msgs/String 类型的数据,一旦收到数据,就调用 callback 函数,
# 其中函数的第一个 argument 总是接受到的数据。
rospy.spin()
# 在 C++ 中,spin() 的作用是不断的循环,每次循环结束就令 callback 函数处理数据。
# 但在 python 中 spin() simply keeps python from exiting until this node is stopped。
# callback functions 不受 spin() 的影响,它们有自己的处理进程
if __name__ == '__main__':
listener()
例3: 用类与对象的形式编写 Publisher node
作用:依然是上边的 publisher node,但用 class 的方式书写。
我认为用 class 有两个好处:
- 变量共享,在 class 的一个函数中修改的变量可以通过 self 带到另一个函数中。这样就避免了定义全局变量。
- ros node 本身与 class, object 的概念很相符。一个 node 就是一个 object,它有自己的变量,也可以产生一些行为。从 class 和 object 的角度去构造 node,很自然也很直观。
该程序的语句几乎与例 1 相同,不需要再注释了。
#!/usr/bin/env python
import rospy
from std_msgs.msg import String
class Talker():
def __init__(self):
self.pub = rospy.Publisher('chatter', String, queue_size=10)
rate = rospy.Rate(10)
msg = 'Msg from talker'
while not rospy.is_shutdown():
self.pub.publish(msg)
rate.sleep()
if __name__ == '__main__':
rospy.init_node('talker')
try:
Talker()
except rospy.ROSInterruptException:
pass
例4: 用类与对象的形式编写 interpreter node
作用:结合了 sub 与 pub,从 chatter 这个 topic 接受数据,然后 pub 到 final/chatter 这个 topic 上。
这里需要注意的是,在调用 callback 时并没有指明送入什么 argument,但是默认 self 为第一个 argument,subscriber 接收到的 msg 是第二个 argument.
#!/usr/bin/env python
import rospy
from std_msgs.msg import String
class Interpreter():
def __init__(self):
rospy.Subscriber('chatter', String, self.callback)
self.pub = rospy.Publisher('final/chatter', String, queue_size=10)
# 把后边可能用到的 sub, pub 在初始化函数中定义好
def callback(self, data):
msg = 'I have received : (%s), and I interpret it to (Hello World)' \
% data.data
self.pub.publish(msg)
if __name__ == '__main__':
rospy.init_node('interpreter')
try:
Interpreter()
except rospy.ROSInterruptException:
pass
rospy.spin()