ROS第一坑之基本操作

前言

作为自动化专业的学生,机器人操作系统ROS的大名如雷贯耳。

ROS 是一个适用于机器人的开源的元操作系统。它提供了操作系统应有的服务,包括硬件抽象,底层设备控制,常用函数的实现,进程间消息传递,以及包管理。它也提供用于获取、编译、编写、和跨计算机运行代码所需的工具和库函数。在某些方面ROS相当于一种“机器人框架(robot frameworks)”——ROS Wiki

早在今年年初,我与同学一起参观华强北的优艾智合公司的时候,为我们介绍公司的人(似乎是我们的师兄)就向刚结束大一上学期,尚不明所以的我们介绍了机器人学习之路,其中就提到了非常重要的ROS。虽然当时ROS在我心目中留下了深刻的印象,但是我并没有时间也没有机会去学习。

直到小学期,我深感自己在本专业的自我学习上完全不够,所以一时心血来潮安装了ROS,可惜知道小学期结束,我连打开都未曾打开过。
所幸暑期在越疆实习的时候,可以使用ROS操纵他们的机械臂。因而我便趁此机会借用公司的机械臂和其官网上的demo开始真正接触ROS。

然而我并不想写关于ROS的教程,该方面的书籍资料已经够多了,即使那些资料并非十分完美,但至少会比我自己写的要有用的多。所以在这篇文章中我也只想谈一谈我在学习ROS初级教程中所遇到的坑。

ROS Wiki中文 从安装到入门

ROS_INFO不输出

我从越疆官网上下载下来的demo是将所有的程序都写在一个main函数中,这使得稍微有点强迫症的我手动将main函数里的程序封装在不同的函数中。然而作为一个懵懂无知的新手,我的“莽撞”行为导致我在后期调试的时候,在跳出第一个调用的函数后,ROS_INFO都输出不了。我上网搜索原因,可似乎没有人遇到与我相同的问题,所以就暂且搁置。后来我一句一句的测试,发现我将第一个句柄的创建写在main函数中的这个函数调用前问题就解决了。
原因目前暂且不明,但是当我后来知道ROS节点是在第一个句柄被创建后才开始的,我就怀疑应该是这个原因了。
无论如何以后的ROS程序,句柄一定要写在初始化语句的后面。
当然,如果我们也可以选择使用ros::start()和ros::shutdown来启动和关闭节点。

1
2
3
4
5
6
int main(int argc, char **argv)
{
ros::init(argc, argv, "Quit");//初始化节点
ros::NodeHandle n;//创建句柄,节点开始
return 0;
}

消息与服务

关于消息与服务的理解,srv文件与msg文件的创建,订阅器与发布器的编写,服务端和客户端的编写在官网上都写得非常清楚,这里我也就不再赘述。
服务端和客户端的编写我倒是没遇到什么大的问题,毕竟demo中有此两者的源代码。遇到问题也不过是我没有按照流程一步一步来。
但对于消息的订阅器与发布器编写我倒是经常遇到的坑,虽然大多由于我理解有误。

  1. 订阅器与发布器的注册
    编写订阅器和发布器的时候首先要向节点管理器(ros Master)进行注册,这是毋庸置疑的。但是要值得注意的是,要在main函数中启动节点后就注册,尤其是发布器,不能够在单独的函数中进行注册,要不然会无法发布消息。

    1
    2
    ros::Publisher pub=n.advertise<dobot::GetCtrl_msg>("GetCtrl_msg",1000);//注册发布器
    ros::Subscriber sub = n.subscribe<dobot::GetCtrl_msg>("GetCtrl_msg", 1000,messageCallback);//注册订阅器
  2. ros::spin()与ros::spinOnce()
    对于这两个函数的详解,网上资料也有很多,像是这个博客
    简而言之,就是在你订阅消息的时候,只有遇到这两条语句的时候才会调用回调函数。
    使用ros::spin()会进入自循环,也就是不断的调用自己,以使一接收到消息就进入回调函数。使用此语句,该语句下面的程序都不会再被执行。
    而ros::spinOnce()只会调用一次回调函数,可以实现接下来的程序。

  3. 消息的发布订阅机制
    消息会发布到注册的话题上,然后订阅器实时的订阅话题上的消息。这就等同于一个节点实时接受另一个节点的指令,而并非信息的传递,订阅器是无法接受之前发布的消息的。
    举个例子:
    我之前想让一个节点记录我机械臂末端的坐标,并发布到话题上。然后我开启另一个节点,先接受话题上的坐标信息,在执行操作。
    然而实际上这么做是不可行的,当我开启另一个节点的时候,之前发布到话题上的消息已经过期了,新节点是无法订阅到坐标消息的。
    所以后来我转变了思路,由第一个节点发布指令消息,第二个节点根据接受到的指令消息作出相应的操作,这样一来第二个节点就可以一接收到第一个节点发布的消息就执行操作或者记录节点。执行完后继续等待指令消息。

回调函数传递多个参数

在ROS的节点程序中,经常会使用到许多回调函数,像是订阅到消息 的时候会自己调用回调函数。但这种时候传递到回调函数里的参数是固定的,所以我们需要使用boost::bind函数来使我们可以自由的传递参数。

boost::bind是标准库函数std::bind1st和std::bind2nd的一种泛化形式。其可以支持函数对象、函数、函数指针、成员函数指针,并且绑定任意参数到某个指定值上或者将输入参数传入任意位置。

boost::bind(f,1,2) —> f(1,2)
boost::bind(f,_1,1) —> f(x,1)
具体用法参见此博客

此处举两个例子

  1. 订阅消息
    1
    2
    ros::Subscriber sub = n.subscribe<dobot::GetCtrl_msg>("GetCtrl_msg", 1000,messageCallback);//单参数
    ros::Subscriber sub = n.subscribe<dobot::GetCtrl_msg>("GetCtrl_msg", 1000,boost::bind(&messageCallback,\_1,point);多参数

其中_1是占位符,代表回调函数中原先的参数const dobot::GetCtrl_msg::ConstPtr &msg

  1. 多线程
    1
    2
    boost::thread t(keboard);//单参数
    boost::thread t1(boost::bind(&keyboard,boost::ref(pub)));//多参数

其中boost::ref(x)是向回调函数传递一个变量

退出节点

在未使用launch文件启动的情况下,每一个节点都是一个终端,所以通常只要按Ctrl+C即可取消命令,程序会调用ros::shutdown()来关闭节点。
但是有时候我们会需要在退出节点的时候进行一些处理,例如需要清空指令队列使机器人停下或者传递消息之类的。这时候我们就需要signal函数来覆盖原先的Ctrl+C,使得当用户按下Ctrl+C的时候,会调用相应的回调函数,作最后的处理。
这里是对信号处理函数的比较好的介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "ros/ros.h"
#include "signal.h"
void MySigintHandler(int sig)
{
//这里主要进行退出前的数据保存、内存清理、告知其他节点等工作
ROS_INFO("shutting down!");
/*自己封装的函数
StopQueueCmd();
ClearQueueCmd();
*/
ros::shutdown();
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "Quit");//初始化
ros::NodeHandle n;//创建句柄
signal(SIGINT, MySigintHandler);//第一个参数是指Ctrl+C的指令,第二个参数是调用的函数
ros::spinOnce();
return 0;
}

总结

其实还有好多的东西没有写,像是我封装函数到服务端,launch文件启动,用键盘键入字符,多线程等,只是这些大多是基础教程或是与ros关系不大的知识,所以也就没写出来。
这几天用ros实现我所想要的功能时,也的的确确学到了不少东西,至少ros的初级用法都已明晰。虽然还有许多有待提升的地方,像是暂停功能、紧急停止功能、用栈优化指令队列之类的,不过意义不大,也就没有继续写下去了。