ROS小结_II

Client Library与roscpp

  • roscpp ROS的C++库,是目前最广泛应用的ROS客户端库,执行效率高
  • rospy ROS的Python库,开发效率高,通常用在对运行时间没有太大要求的场合,例如配置、初始化等操作

从开发客户端库的角度看,一个客户端库,至少需要能够包括master注册、名称管理、消息收发等功能。这样才能给开发者提供对ROS通信架构进行配置的方法。 整个ROS包括的packages如下,你可以看到roscpp、rospy处于什么位置。

roscpp

roscpp位于/opt/ros/kinetic之下,用C++实现了ROS通信。在ROS中,C++的代码是通过catkin这个编译系统(扩展的CMake)来进行编译构建的。把roscpp就当作为一个C++的库,我们创建一个CMake工程,在其中include了roscpp等ROS的libraries,这样就可以在工程中使用ROS提供的函数了。

调用ROS的C++接口,首先就需要#include <ros/ros.h>

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图资源名称的函数

以上功能可以分为以下几类:

  • Initialization and Shutdown 初始与关闭
  • Topics 话题
  • Services 服务
  • Parameter Server 参数服务器
  • Timers 定时器
  • NodeHandles 节点句柄
  • Callbacks and Spinning 回调和自旋(或者翻译叫轮询?)
  • Logging 日志
  • Names and Node Information 名称管理
  • Time 时钟
  • Exception 异常

节点初始 关闭及NodeHandle

当执行一个ROS程序,就被加载到了内存中,就成为了一个进程,在ROS里叫做节点。每一个ROS的节点尽管功能不同,但都有必不可少的一些步骤,比如初始化、销毁,需要通行的场景通常都还需要节点的句柄。

初始化节点

对于一个C++写的ROS程序,之所以它区别于普通C++程序,是因为代码中做了两层工作:

  1. 调用了ros::init()函数,从而初始化节点的名称和其他信息,一般我们ROS程序一开始都会以这种方式开始。
  2. 创建ros::NodeHandle对象,也就是节点的句柄,它可以用来创建Publisher、Subscriber以及做其他事情。 句柄(Handle)这个概念可以理解为一个“把手”,你握住了门把手,就可以很容易把整扇门拉开,而不必关心门是什么样子。NodeHandle就是对节点资源的描述,有了它你就可以操作这个节点了,比如为程序提供服务、监听某个topic上的消息、访问和修改param等等。

关闭节点

要关闭一个节点可以直接在终端上按Ctrl+C,系统会自动触发SIGINT句柄来关闭这个进程。 也可以通过ros::shutdown()来手动关闭节点.

节点初始化、关闭的例子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include<ros/ros.h>
int main(int argc, char** argv)
{
    ros::init(argc, argv, "your_node_name"); 
    ros::NodeHandle nh;
    //....节点功能
    //....
    ros::spin();//用于触发topic、service的响应队列
    return 0;
}

一个ROS程序的执行步骤,通常要启动节点,获取句柄,而关闭的工作系统自动完成.

NodeHandle常用成员函数

NodeHandle是Node的句柄,用来对当前节点进行各种操作。在ROS中,NodeHandle是一个定义好的类,通过include<ros/ros.h>,我们可以创建这个类,以及使用它的成员函数。

NodeHandle常用成员函数包括:

 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
27
28
29
30
31
32
33
//创建话题的publisher 
ros::Publisher advertise(const string &topic, uint32_t queue_size, bool latch=false); 
//第一个参数为发布话题的名称
//第二个是消息队列的最大长度,如果发布的消息超过这个长度而没有被接收,那么就的消息就会出队。通常设为一个较小的数即可。
//第三个参数是是否锁存。某些话题并不是会以某个频率发布,比如/map这个topic,只有在初次订阅或者地图更新这两种情况下,/map才会发布消息。这里就用到了锁存。

//创建话题的subscriber
ros::Subscriber subscribe(const string &topic, uint32_t queue_size, void(*)(M));
//第一个参数是订阅话题的名称
//第二个参数是订阅队列的长度,如果受到的消息都没来得及处理,那么新消息入队,就消息就会出队
//第三个参数是回调函数指针,指向回调函数来处理接收到的消息

//创建服务的server,提供服务
ros::ServiceServer advertiseService(const string &service, bool(*srv_func)(Mreq &, Mres &)); 
//第一个参数是service名称
//第二个参数是服务函数的指针,指向服务函数。指向的函数应该有两个参数,分别接受请求和响应。

//创建服务的client
ros::ServiceClient serviceClient(const string &service_name, bool persistent=false); 
//第一个函数式service名称
//第二个参数用于设置服务的连接是否持续,如果为true,client将会保持与远程主机的连接,这样后续的请求会快一些。通常我们设为flase

//查询某个参数的值
bool getParam(const string &key, std::string &s); 
bool getParam (const std::string &key, double &d) const
bool getParam (const std::string &key, int &i) const
//从参数服务器上获取key对应的值,已重载了多个类型

//给参数赋值
void setParam (const std::string &key, const std::string &s) const
void setParam (const std::string &key, const char *s) const;
void setParam (const std::string &key, int i) const;
//给key对应的val赋值,重载了多个类型的val

NodeHandle对象在ROS C++程序里非常重要,各种类型的通信都需要用NodeHandle来创建完成。


回调函数与spin()方法

回调函数在编程中是一种重要的方法,在维基百科上的解释是: 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()。


roscpp里有两种时间的表示方法,一种是时刻(ros::Time),一种是时长(ros::Duration)

int32 sec
int32 nsec

Time/Duration都由秒和纳秒组成。 要使用Time和Duration,需要#include <ros/time.h>#include <ros/duration.h>

1
2
3
4
5
6
7
ros::Time begin = ros::Time::now(); //获取当前时间
ros::Time at_some_time1(5,20000000);  //5.2s
ros::Time at_some_time2(5.2) //同上,重载了float类型和两个uint类型的构造函数
ros::Duration one_hour(60*60,0); //1h

double secs1 = at_some_time1.toSec();//将Time转为double型时间
double secs2 = one_hour.toSec();//将Duration转为double型时间

Time和Duration表示的概念并不相同,Time指的是某个时刻,而Duration指的是某个时段,尽管他们的数据结构都相同,但是用在不同的场景下。 ROS重载了Time、Duration类型之间的加减运算,比如:

1
2
3
4
ros::Time t1 = ros::Time::now() - ros::Duration(5.5); //t1是5.5s前的时刻,Time加减Duration返回都是Time
ros::Time t2 = ros::Time::now() + ros::Duration(3.3);//t2是当前时刻往后推3.3s的时刻
ros::Duration d1 = t2 - t1;//从t1到t2的时长,两个Time相减返回Duration类型
ros::Duration d2 = d1 -ros::Duration(0,300);//两个Duration相减,还是Duration

没有Time+Time的做法。


等待场景sleep

1
2
3
4
5
6
7
8
ros::Duration(0.5).sleep(); //用Duration对象的sleep方法休眠

ros::Rate r(10); //10HZ
while(ros::ok())
{
    r.sleep();     
    //定义好sleep的频率,Rate对象会自动让整个循环以10hz休眠,即使有任务执行占用了时间
}

Rate的功能是指定一个频率,让某些动作按照这个频率来循环执行。与之类似的是ROS中的定时器Timer,它是通过设定回调函数和触发时间来实现某些动作的反复执行,创建方法和topic中的subscriber很像。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void callback1(const ros::TimerEvent&)
{
  ROS_INFO("Callback 1 triggered");
}

void callback2(const ros::TimerEvent&)
{
  ROS_INFO("Callback 2 triggered");
}

int main(int argc, char **argv)
{
  ros::init(argc, argv, "talker");
  ros::NodeHandle n;

  ros::Timer timer1 = n.createTimer(ros::Duration(0.1), callback1);  //timer1每0.1s触发一次callback1函数
  ros::Timer timer2 = n.createTimer(ros::Duration(1.0), callback2);  //timer2每1.0s触发一次callback2函数

  ros::spin();  //千万别忘了spin,只有spin了才能真正去触发回调函数

  return 0;
}


Exception

roscpp中有两种异常类型,当有以下两种错误时,就会抛出异常:

ros::InvalidNodeNameException 当无效的基础名称传给ros::init(),通常是名称中有/,就会触发

ros::InvalidNameExcaption 当无效名称传给了roscpp

updatedupdated2020-08-112020-08-11