ROS-roscpp
📖 许多工具本质上都是基于ROS的客户端库(Client Libarary)实现的,所谓客户端库,简单的理解就是一套接口,ROS为我们机器人开发者提供了不同语言的接口,比如roscpp是C++语言ROS接口,rospy是python语言的ROS接口,我们直接调用它所提供的函数就可以实现topic、service等通信功能。
1. Client Library与roscpp
1.1. Client Library简介
ROS为机器人开发者们提供了不同语言的编程接口,比如C++接口叫做roscpp,Python接口叫做rospy,Java接口叫做rosjava。尽管语言不通,但这些接口都可以用来创建topic、service、param,实现ROS的通信功能。Clinet Lirary有点类似开发中的Helper Class,把一些常用的基本功能做了封装。
目前ROS支持的Clinet Library包括:
Client Library | 介绍 |
---|---|
roscpp | ROS的C++库,是目前最广泛应用的ROS客户端库,执行效率高 |
rospy | ROS的Python库,开发效率高,通常用在对运行时间没有太大要求的场合,例如配置、初始化等操作 |
roslisp | ROS的LISP库 |
roscs | Mono/.NET.库,可用任何Mono/.NET语言,包括C#,Iron Python, Iron Ruby等 |
rosgo | ROS Go语言库 |
rosjava | ROS Java语言库 |
rosnodejs | Javascript客户端库 |
… | … |
目前最常用的只有roscpp和rospy,而其余的语言版本基本都还是测试版。
从开发客户端库的角度看,一个客户端库,至少需要能够包括master注册、名称管理、消息收发等功能。这样才能给开发者提供对ROS通信架构进行配置的方法。
整个ROS包括的packages如下,你可以看到roscpp、rospy处于什么位置。
1.2. roscpp
roscpp位于/opt/ros/kinetic
之下,用C实现了ROS通信。在ROS中,C的代码是通过catkin这个编译系统(扩展的CMake)来进行编译构建的。所以简单地理解,你也可以把roscpp就当作为一个C++的库,我们创建一个CMake工程,在其中include了roscpp等ROS的libraries,这样就可以在工程中使用ROS提供的函数了。
通常我们要调用ROS的C++接口,首先就需要#include
。
roscpp的主要部分包括:
- ros::init() : 解析传入的ROS参数,创建node第一步需要用到的函数
- ros::NodeHandle : 和topic、service、param等交互的公共接口
- ros::master : 包含从master查询信息的函数
- ros::this_node:包含查询这个进程(node)的函数
- ros::service:包含查询服务的函数
- ros::param:包含查询参数服务器的函数,而不需要用到NodeHandle
- ros::names:包含处理ROS图资源名称的函数
具体可见:http://docs.ros.org/api/roscpp/html/index.html
以上功能可以分为以下几类:
- Initialization and Shutdown 初始与关闭
- Topics 话题
- Services 服务
- Parameter Server 参数服务器
- Timers 定时器
- NodeHandles 节点句柄
- Callbacks and Spinning 回调和自旋(或者翻译叫轮询?)
- Logging 日志
- Names and Node Information 名称管理
- Time 时钟
- Exception 异常
看到这么多接口,千万别觉得复杂,我们日常开发并不会用到所有的功能,你只需对要有一些印象,掌握几个比较常见和重要的用法就足够了。下面我们来介绍关键的用法。
2. 节点初始、关闭以及NodeHandle
当执行一个ROS程序,就被加载到了内存中,就成为了一个进程,在ROS里叫做节点。每一个ROS的节点尽管功能不同,但都有必不可少的一些步骤,比如初始化、销毁,需要通行的场景通常都还需要节点的句柄。 这一节我们来学习Node最基本的一些操作。
2.1. 初始化节点
对于一个C写的ROS程序,之所以它区别于普通C程序,是因为代码中做了两层工作:
- 调用了
ros::init()
函数,从而初始化节点的名称和其他信息,一般我们ROS程序一开始都会以这种方式开始。 - 创建
ros::NodeHandle
对象,也就是节点的句柄,它可以用来创建Publisher、Subscriber以及做其他事情。
句柄(Handle)这个概念可以理解为一个“把手”,你握住了门把手,就可以很容易把整扇门拉开,而不必关心门是什么样子。NodeHandle就是对节点资源的描述,有了它你就可以操作这个节点了,比如为程序提供服务、监听某个topic上的消息、访问和修改param等等。
2.2. 关闭节点
通常我们要关闭一个节点可以直接在终端上按Ctrl
+C
,系统会自动触发SIGINT句柄来关闭这个进程。 你也可以通过调用ros::shutdown()
来手动关闭节点,但通常我们很少这样做。
以下是一个节点初始化、关闭的例子。
1 | #include<ros/ros.h> |
这段代码是最常见的一个ROS程序的执行步骤,通常要启动节点,获取句柄,而关闭的工作系统自动帮我们完成,如果有特殊需要你也可以自定义。你可能很关心句柄可以用来做些什么,接下来我们来看看NodeHandle常用的成员函数。
2.3. NodeHandle常用成员函数
NodeHandle是Node的句柄,用来对当前节点进行各种操作。在ROS中,NodeHandle是一个定义好的类,通过include
,我们可以创建这个类,以及使用它的成员函数。
NodeHandle常用成员函数包括:
1 | //创建话题的publisher |
可以看出,NodeHandle对象在ROS C++程序里非常重要,各种类型的通信都需要用NodeHandle来创建完成。 下面我们具体来看topic、service和param这三种基本通信方式的写法。
3. topic in roscpp
3.1. Topic通信
Topic是ROS里一种异步通信的模型,一般是节点间分工明确,有的只负责发送,有的只负责接收处理。对于绝大多数的机器人应用场景,比如传感器数据收发,速度控制指令的收发,Topic模型是最适合的通信方式。
为了讲明白topic通信的编程思路,我们首先来看topic_demo
中的代码,这个程序是一个消息收发的例子:自定义一个类型为gps的消息(包括位置x,y和工作状态state信息),一个node以一定频率发布模拟的gps消息,另一个node接收并处理,算出到原点的距离。 源代码见ROS-Academy-for-Beginners/topic_demo
3.2. 创建gps消息
在代码中,我们会用到自定义类型的gps消息,因此就需要来自定义gps消息,在msg路径下创建gps.msg
: 见topic_demo/msg/gps.msg
1 | string state #工作状态 |
以上就定义了一个gps类型的消息,你可以把它理解成一个C语言中的结构体,类似于
1 | struct gps |
在程序中对一个gps消息进行创建修改的方法和对结构体的操作一样。
当你创建完了msg文件,记得修改CMakeLists.txt
和package.xml
,从而让系统能够编译自定义消息。 在CMakeLists.txt
中需要改动
1 | find_package(catkin REQUIRED COMPONENTS |
package.xml
中需要的改动
1 | <build_depend>message_generation</build_depend> |
当你完成了以上所有工作,就可以回到工作空间,然后编译了。编译完成之后会在devel
路径下生成gps.msg
对应的头文件,头文件按照C++的语法规则定义了topic_demo::gps
类型的数据。
要在代码中使用自定义消息类型,只要#include
,然后声明,按照对结构体操作的方式修改内容即可。
1 | topic_demo::gps mygpsmsg; |
3.3. 消息发布节点
定义完了消息,就可以开始写ROS代码了。通常我们会把消息收发的两端分成两个节点来写,一个节点就是一个完整的C++程序。
见topic_demo/src/talker.cpp
1 |
|
机器人上几乎所有的传感器,几乎都是按照固定频率发布消息这种通信方式来传输数据,只是发布频率和数据类型的区别。
3.4. 消息接收节点
见topic_demo/src/listener.cpp
1 |
|
在topic接收方,有一个比较重要的概念,就是回调(CallBack),在本例中,回调就是预先给gps_info
话题传来的消息准备一个回调函数,你事先定义好回调函数的操作,本例中是计算到原点的距离。只有当有消息来时,回调函数才会被触发执行。具体去触发的命令就是ros::spin()
,它会反复的查看有没有消息来,如果有就会让回调函数去处理。
因此千万不要认为,只要指定了回调函数,系统就回去自动触发,你必须ros::spin()
或者ros::spinOnce()
才能真正使回调函数生效。
3.5. CMakeLists.txt文件修改
在CMakeLists.txt
添加以下内容,生成可执行文件
1 | add_executable(talker src/talker.cpp) #生成可执行文件talker |
以上cmake语句告诉catkin编译系统如何去编译生成我们的程序。这些命令都是标准的cmake命令,如果不理解,请查阅cmake教程。
之后经过catkin_make
,一个自定义消息+发布接收的基本模型就完成了。
3.6. 扩展:回调函数与spin()方法
回调函数在编程中是一种重要的方法,在维基百科上的解释是:
1 | In computer programming, a callback is any executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at a given time. |
回调函数作为参数被传入到了另一个函数中(在本例中传递的是函数指针),在未来某个时刻(当有新的message到达),就会立即执行。Subscriber接收到消息,实际上是先把消息放到一个队列中去,如图所示。队列的长度在Subscriber构建的时候设置好了。当有spin函数执行,就会去处理消息队列中队首的消息。
spin具体处理的方法又可分为阻塞/非阻塞,单线程/多线程,在ROS函数接口层面我们有4种spin的方式:
spin方法 | 阻塞 | 线程 |
---|---|---|
ros::spin() |
阻塞 | 单线程 |
ros::spinOnce() |
非阻塞 | 单线程 |
ros::MultiThreadedSpin() |
阻塞 | 多线程 |
ros::AsyncMultiThreadedSpin() |
非阻塞 | 多线程 |
阻塞与非阻塞的区别我们已经讲了,下面来看看单线程与多线程的区别:
我们常用的spin()
、spinOnce()
是单个线程逐个处理回调队列里的数据。有些场合需要用到多线程分别处理,则可以用到MultiThreadedSpin()
、AsyncMultiThreadedSpin()
。
4. service in roscpp
4.1. Service通信
Service是一种请求-反馈的通信机制。请求的一方通常被称为客户端,提供服务的一方叫做服务器端。Service机制相比于Topic的不同之处在于:
- 消息的传输是双向的,有反馈的,而不是单一的流向。
- 消息往往不会以固定频率传输,不连续,而是在需要时才会向服务器发起请求。
在ROS中如何请求或者提供一个服务,我们来看service_demo
的代码:一个节点发出服务请求(姓名,年龄),另一个节点进行服务响应,答复请求。
4.2. 创建Greeting服务
创建service_demo/Greeting.srv
文件,内容包括:
1 | string name #短横线上边部分是服务请求的数据 |
srv格式的文件创建后,也需要修改CMakeLissts.txt
,在其中加入
1 | add_service_files(FILES Greeting.srv) |
其余与添加msg的改动一样。然后进行catkin_make
,系统就会生成在代码中可用的Greeting类型。在代码中使用,只需要#include
,然后即可创建该类型的srv。
1 | service_demo::Greeting grt; //grt分为grt.request和grt.response两部分 |
新生成的Greeting类型的服务,其结构体的风格更为明显,可以这么理解,一个Greeting服务结构体中嵌套了两个结构体,分别是请求和响应:
1 | struct Greeting |
4.3. 创建提供服务节点(server)
service_demo/srv/server.cpp
内容如下:
1 |
|
在以上代码中,服务的处理操作都写在handle_function()
中,它的输入参数就是Greeting的Request和Response两部分,而非整个Greeting对象。通常在处理函数中,我们对Requst数据进行需要的操作,将结果写入到Response中。在roscpp中,处理函数返回值是bool型,也就是服务是否成功执行。不要理解成输入Request,返回Response,在rospy中是这样的。
4.4. 创建服务请求节点(client)
service_demo/srv/client.cpp
内容如下:
1 |
|
以上代码比较关键的地方有两处,一个是建立一个ServiceClient,另一个是开始调用服务。建立client需要用nh.serviceClient("greetings")
,指明服务的类型和服务的名称。而调用时可以直接用client.call(srv)
,返回结果不是response,而是是否成功调用远程服务。
CMakeLists.txt
和pacakge.xml
修改方法和topic_demo
修改方法类似,不再赘述。
5. param in roscpp
5.1. Parameter Server
严格来说,param并不能称作一种通信方式,因为它往往只是用来存储一些静态的设置,而不是动态变化的。所以关于param的操作非常轻巧,非常简单。 关于param的API,roscpp为我们提供了两套,一套是放在ros::param
namespace下,另一套是在ros::NodeHandle
下,这两套API的操作完全一样,用哪一个取决于你的习惯。
5.2. param_demo
我们来看看在C++中如何进行param_demo的操作,param_demo/param.cpp
文件,内容包括:
1 |
|
以上是roscpp中对param进行增删改查所有操作的方法,非常直观。
5.3. param_demo中的launch文件
实际项目中我们对参数进行设置,尤其是添加参数,一般都不是在程序中,而是在launch文件中。因为launch文件可以方便的修改参数,而写成代码之后,修改参数必须重新编译。 因此我们会在launch文件中将param都定义好,比如这个demo正确的打开方式应该是roslaunch param_demo param_demo_cpp.launch
param_demo/launch/param_demo_cpp.launch
内容为:
1 | <launch> |
通过和两个标签我们设置好了5个param,从而在之前的代码中进行增删改查的操作。
5.4. 命名空间对param的影响
在实际的项目中,实例化句柄时,经常会看到两种不同的写法 ros::NodeHandle n; ros::NodeHandle nh("~");` 这两种写法有什么不同呢?以本教学报的name_demo为例。在本节launch文件夹的demo.launch定义两个参数,一个全局serial 他的数值是5,一个是局部的serial,他的数值是10.
1 | <launch> |
在name_demo.cpp中,我们分别尝试了,利用全局命名空间句柄提取全局的param和局部的param,以及在局部命名空间下的句柄提取全局的param和局部的param,详细的代码如下:
1 |
|
最后的结果
1 | [ INFO] [1525095241.802257811]: global namespace |
6. 时钟
6.1. Time 与 Duration
ROS里经常用到的一个功能就是时钟,比如计算机器人移动距离、设定一些程序的等待时间、设定计时器等等。roscpp同样给我们提供了时钟方面的操作。
具体来说,roscpp里有两种时间的表示方法,一种是时刻(ros::Time),一种是时长(ros::Duration)。无论是Time还是Duration都具有相同的表示方法:
···
int32 sec
int32 nsec
···
Time/Duration都由秒和纳秒组成。 要使用Time和Duration,需要#include
和#include
1 | ros::Time begin = ros::Time::now(); //获取当前时间 |
Time和Duration表示的概念并不相同,Time指的是某个时刻,而Duration指的是某个时段,尽管他们的数据结构都相同,但是用在不同的场景下。 ROS为我们重载了Time、Duration类型之间的加减运算,比如:
1 | ros::Time t1 = ros::Time::now() - ros::Duration(5.5); //t1是5.5s前的时刻,Time加减Duration返回都是Time |
以上是Time、Duration之间的加减运算,要注意没有Time+Time的做法。
6.2. sleep
通常在机器人任务执行中可能有需要等待的场景,这时就要用到sleep功能,roscpp中提供了两种sleep的方法:
1 | ros::Duration(0.5).sleep(); //用Duration对象的sleep方法休眠 |
6.3. Timer
Rate的功能是指定一个频率,让某些动作按照这个频率来循环执行。与之类似的是ROS中的定时器Timer,它是通过设定回调函数和触发时间来实现某些动作的反复执行,创建方法和topic中的subscriber很像。
1 | void callback1(const ros::TimerEvent&) |