背景和初衷

小弟是University of Colorado at Boulder的一个在读学生.在ARPG(Autonomous Robotics and Perception Group)实验室学习.   自己当初入门ROS的时候还是挺艰难的,因为连c++都不太熟(当然现在也是一知半解).英文资料很全但是并不见得对新手那么友好.中文资料也基本是英文copy过来的.基本限制于写了publisher和subscriber的基本程序(他们是什么后文介绍),写一个roslaunch就结束了.然而现实中的需求是那一两个例子所不能满足的.现在自己稍微有了些经验,便趁空闲时间撸一个详细一点的入门教程,把原来英文版本自己觉得冗余的都删掉,讲一讲自己用过的部分.   除了ROS,也尽量在自己phd结束之前除了什么paper之外多写写其他入门级博客,以分享经验.博主也还有很多不懂的地方,如果有写错的希望指正. 文章不会写什么是ROS,ROS有什么用这类话题,网上能查到很多.直接讲怎么使用.

后续文章内容安排

介绍ROS内基本名词的概念, 在ROS常用C++和python语法(是的,万一有语言不熟悉的小伙伴呢),怎样读懂ROS Wiki里给的资料以及如何使用. 讲解这些的时候当然会有程序辅助. 第一讲的主要内容集中在ROS的publisher和subcriber上.大多数网上的例子publish和subcribe一个string就讲解结束了.我会稍微深入一点,多一些例子. 在介绍如何publish和subscrube的时候大概会有下面这些例子.   1:publish和subcribe一个string 2 : publish和subcribe一个int 3: publish和subcribe一个数组 4: publish和subcribe一个机器人pose 5: 在类中publish和subscribe 6: 使用roslaunch 7: 使用rosbag 8: 使用rviz 9: 学习tf和rviz 10:编写自己的消息种类   希望通过上面的例子和代码(代码会放到我的github中),大家能对publish和subcribe有更多的了解并知道怎么利用ROS wiki的资料. 还会在自己学习过程中陆续添加其他代码. 绝大多数代码是用C++写的.(为什么用C++啊!!为什么不用python!!哎,同志们不要害怕C++, C++是爱你们的.)至于为什么这个系列叫入门到放弃,恩....学不下去就好好休息不要学习了... 简书两边没有广告,看起来比CSDN舒服的.好了,因为打算要写个系列所以废话了这么多,好了,我们开始吧.
ROS常用的概念(一)
下面的概念可能并不是完全准确,但是力求精简便于理解. 他们的具体内容会在程序中更详细讲解   1: message: 即消息.机器人需要传感器,传感器采集到的信息,即这儿的message. 假如我们的GPS采集到机器人位置消息,温度计采集到的温度等. 任何数据都能作为message. 2: topic: 假设我们有两个传感器,GPS和温度计.在ROS中我们得给采集到的消息取个名字用来区分不同的message,这就是topic了. 至于怎么取名字,后面的程序可见. 3: node: 节点. ROS中,通常来讲你写的c++或者python程序主函数所在的程序称为一个节点(并不准确,暂时这么理解). 4: package: 一个ROS package包含了你要完成的一个项目的所有文件. 5: workSpace: ROS workspace,即ROS的工作空间.你当然需要ROS来完成很多不同的项目.你的ROS工作空间是用来存放很多不同package的. 6: publish, subscribe: ROS很大的一个作用就是传递message.为什么要传递消息呢? 打个比方你写了一个程序,用来获得GPS的讯息,写了另外一个程序,用来处理GPS的信息.这时你就是需要把采集到的信息传输到处理用的程序中.信息的传输在ROS中称为publish. 信息的接收(貌似中文ros wiki把这个叫做"订阅",我更喜欢翻译成"接收"了)在ROS中称为subscribe.   好了,有了这几个概念,我们就可以开始撸第一个ROS程序了.(可能大佬们觉得还有好多没讲呢,没关系我们慢慢来.)  

第一个ROS程序

第一个程序还是会用ros自己的tutorial里的,毕竟大家都是那儿起步的.下载和安装ROS我也没办法提供什么特殊办法了,这个得按照官网来.ROS分为很多版本,目前用的最广泛的是kinetic(不是最新的).笔者使用ubuntu16.04安装的ROS kinetic. 根据链接里的内容一步步安装. [link about the installation of ROS] (http://wiki.ros.org/kinetic/Installation/Ubuntu)

创建一个workspace

安装好ROS后,我们就可以创建第一个ROS的工作空间用来放置我们后面会用到的不同的package了.打开一个terminal, 输入下面内容:
source /opt/ros/kinetic/setup.bash
mkdir -p ~/catkin_ws/src
cd ~/catkin_ws/
catkin_make
 
第一行source命令的作用是使电脑进入ROS环境. 使用了那项命令后ros相关的特殊命令才好执行(这么说好像挺外行= =). kinetic是ROS的版本名称,你如果使用的其他版本就把那儿换成其他版本名称.   第二行命令创建一个名字叫catkin_ws的ROS工作空间,里面包含一个叫src的文件夹用来存放源程序. 注意不是所有的ROS工作空间都得叫catkin_ws,那可以是任何名字. ROS官网一般使用catkin_ws这个工作空间名字做教程.但是src文件夹都得有的.   第三行命令进入工作空间文件夹   第四行命令.ROS怎么知道你创建的文件夹是ROS的workspace不是杂七杂八用的文件夹? 这就靠第四行命令了,经过第四行命令之后你会发现你的workspace多了两个文件夹,一个叫舒克,一个叫贝塔,咳咳跑偏了.一个叫build,一个叫devel. 这时你的第一个ROS workspace就已经创建好了.
 

创建一个package

建立好第一个workspace之后,你就要开始创建你的第一个ROS项目啦.打开一个terminal,键入下面的命令.  
cd ~/catkin_ws/src
catkin_create_pkg pub_sub_test std_msgs rospy roscpp
cd ..
catkin_make
 
第一行进入workspace的src文件夹,所有package都要在这里面创建. 编译的时候ROS才能找得到.   第二行catkin_create_pkg是ROS自带的命令,表示要创建一个package了. pub_sub_test是这个package的名称,当然你也可以取任何名字. ROS的package命名习惯是不出现大写字母.后面的内容表示你这个ROS package的依赖项.可以这么理解,这就好像写c++你需要include,写python你需要import. 当然ROS不是一个语言,不过由于它内容庞杂,你最好在一开始指定你需要包含哪些ROS已有的软件包(也可以后添加).std_msgs全称standard_message,即标准消息.如果你的message是一些比较常见的量比如float, int, bool, array等,你就需要包含这个软件包.ROS并不是默认就能使用python和c++的,所以你需要添加rospy表示你需要ROS能识别并使用python文件,roscpp表示你需要ROS能识别使用并编译c++文件,所以这三个一般我们在创建新的package时都是需要的.   第三行重新回到workspace文件夹,编译命令catkin_make需要在这里执行.catkin_make在前面我们使用过,它集成了cmake,make等一系列命令.执行了这条命令,ROS会自动找到该workspace中的未编译的文件进行编译.创建了一个新的package之后我们都需要使用catkin_make命令使ROS意识到我又有一个新的package添加到我的workspace中了.   完成上述命令后进入到catkin_ws/src文件夹中你会发现多了一个pub_sub_test文件夹,这就是你的新package了.

写一个ROS程序

写发布消息的程序(发布器)   我们说ROS最基本的功能就是发布和接收消息(publish and subcribe message). 第一个ROS程序就是关于这个的. 打开一个terminal键入下面命令
 
cd ~/catkin_ws/src/pub_sub_test/src
touch pub_string.cpp
 
第一个行命令进入你新package的src文件夹(在你之前catkin_make的时候就自动创建了),我们一般在这个文件夹写c++的源代码.   第二行命令我们创建一个叫pub_string的c++文件.   以后的例子我们在慢慢放飞.第一个例子我们还是会采用官网文档的例子(http://wiki.ros.org/cn/ROS/Tutorials/WritingPublisherSubscriber%28c%2B%2B%29).
 
#include "ros/ros.h"
#include "std_msgs/String.h"

#include <sstream>

/**
 * This tutorial demonstrates simple sending of messages over the ROS system.
 */
int main(int argc, char **argv)
{
  ros::init(argc, argv, "talker");

  ros::NodeHandle n;

  ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);

  ros::Rate loop_rate(10);

  int count = 0;
  while (ros::ok())
  {
    std_msgs::String msg;

    std::stringstream ss;
    ss << "hello world " << count;
    msg.data = ss.str();

    ROS_INFO("%s", msg.data.c_str());

    chatter_pub.publish(msg);

    ros::spinOnce();

    loop_rate.sleep();
    ++count;
  }

  return 0;
}
 
这儿我只是把代码搬运过来,把英文注释全部去掉了.英文还不错的同学强烈建议把原英文注释撸一遍.不想看英文注释(以后还是得看的,毕竟ROS资料绝大多数英文)的同学把链接中0.0.2代码说明通读一遍,代码说明会针对每一行代码进行说明.在看完那个之后,再看我下面针对每一行进行的一些补充.   1: #include "ros/ros.h" ,你的节点(包含main函数的那个程序)如果要使用ROS都得包含这个头文件 2: #include "std_msgs/String.h",我们这个程序是用来发布一个String消息的,ROS针对每一个类型消息都有相应的头文件,如果要发布那个类型的消息就必须包含相应的头文件.以后我们发布不同的消息的时候你就会看到不同点. 3:#include<sstream>,sstream是c++自带的头文件,可以实现利用输入输出流的方式往string里写东西,并且还可以拼接string和其他类型的变量.代码中的
std::stringstream ss;
ss<<"hello world"<<count;
 
实现了string hello world和int变量 count的拼接,形成一个新的string.即如果count是1,那么helloworld1会作为string被存放在ss当中.怎么调用这个string呢? ss.str()就可以了.   4: ros::init(argc, argv, "talker"),从名字很好理解,在程序初始化ros,这行代码也是你几乎所有节点都需要的.talker就是node(节点)的名字.即你这段程序在ROS当中的名字叫talker.   5: ros::NodeHandle n,官方的解释是nodeHandle是和ROS系统通讯的重要工具。咱们可以暂时这么记着两个东西,4,5都是在程序中初始化ROS节点所必须的。   另外有一点需要提醒的是命名问题,有些刚入门的孩子不敢大胆的命名,比如节点名字talkerNodeHandle的对象名n,其实你可以取任何名字,只要你觉得这个名字方便以后读懂程序。节点名一般要显示这个节点的作用。NodeHandle的对象名按照习惯一般取为n,nh什么的。其实个人不太建议对象名为n,这样的单个字母很容易不经意间和某个变量或者其他什么重复了。   6:ros::Publisher 这一行定义你要publish的信息和信息的名字了。n.advertise通过NodeHandle的对象n告诉ROS系统我要创建一个可以发布信息的对象了。这个信息是什么类型的呢?<std_msgs::String>告诉ROS我要发布的是标准信息中的String类型。那些信息叫啥名字呢?名字叫chatter。这个chatter就是我们之前提到的topic!(topic名和节点名不要搞混,topic名字代表的是你要发布的信息,节点的名字代表你这个程序)。   1000这个数字的意思是要缓冲的信息数量。为什么需要缓冲呢?我们说这是个发布信息的程序,我们还需要写一个接收信息的程序。这发布和接收之间并不是瞬间进行的。我记得刚学ROS的时候老师让我们测试过发布消息和接收到消息之间的时间差。既然有时间差,打个比方1ms接收一条信息,如果我的信息产生地很快,是0.1ms发布一条信息,那么接收方反应不过来了。这时候咋办?ROS会把发布的信息都写进缓冲区,先存个1000条,然后接收的程序慢慢从缓冲区里读,只要信息不超过1000条,总归是可以慢慢读完的。那么超过1000条了呢?最早的信息就直接丢掉了。缓冲区接收最新的信息放到信息序列的最后。即缓冲区的信息的数据结构是queue。第一条来的信息在序列满了的情况下会被第一个丢弃。   7:ros::Rate loop_rate(10) 这个很好理解,我的程序如果在不断地发布信息,那么有时候我会想控制发布的信息的快慢,这行表示你希望你发布信息的速度为10Hz。这个函数要和loop_rate.sleep()配合使用才能达到控制速度目的。   8:while(ros::ok()) 要是ros不OK,程序就退出了。什么时候ROS不OK呢?上面提供的中文链接中说了四种可能。最常见的就是你按下ctrl+c或者在程序遇到ros::shutdown()这行命令。   9: std_msgs::String msg定义了std_msgs::String的对象msg. 这是我们要发布的信息。可能有些同学会奇怪,我们要发布的是一个string类型的变量。C++中string类型的变量,在我们包含了头文件<string>之后,应该用类似于std::string st的命令来建立一个string类下的对象st。为什么ROS不直接发布那样一个对象却要发布一个"ROS里的string"呢?其实ROS为了管理方便,把所有可能发布的消息都归类到ROS中了。比如你想发布8位的整型变量,你不能直接定义一个int a然后发布a。你得用std_msgs::Int8 a发布这个a才行。那我怎么知道我想发布的变量应该怎么定义呢? 比如float, double 甚至机器人的pose,一张图片? 这个我们必须得从ROS wiki中找到我们想使用的变量该怎么定义。下一章我们会讲解怎么使用ROS wiki找到这些信息。另外如果你想发布的信息类型ROS里面没有,你可以自己定义,这个可能以后会讲到。   10:msg.data = ss.str()这一行的前面两行我们在3中已经讲了。这一行把ss.str()赋值给msg.data。天,这两个都是什么鬼?前面我们说了ssstringstream的对象,它现在储存了hello world 0这个string类型的变量(假设此时count = 0)。提取stringstreamstring的方法就是ss.str()。那么你可以猜得到,msg.data也是一个string类型的量。即msg包含一个成员data,这个成员在std_msgs::String这个类中应该是被这样定义了的   std::string data。那我怎么知道std_msgs::String包含一个成员变量data呢?这个同样需要查询ROS wiki才能得知。下一章讲。总之这行把我们想要发布的std::string搬运到了std_msgs::String中,这样我们才能发布这条消息。   10: ROS_INFO()这一行就可以理解为ROS里的printf()就可以了。   11: chatter_pub.publish(msg) 关键了。前面我们定义了一个chatter_pub。用来发布信息。在我们完成了把std::string放到std_msgs::Stringmsg之后,我们就可以发布这个信息了。方法就是这个直观的名字pusblish().   12: ros::spinOnce()关于这个函数和将出现的ros::spin()我们会专门用一节讲。简单来说这个函数是用于接收器的,必须要有spinOnce或者spin,ROS才会检测是不是接收到信息。spinOnce就是检测一次,spin()就是一直检测。但是我们这儿是发布器呀,为什么有它?所以它在这儿没用= = 。。。我也不知道官网为什么放进去。不过第一个例子我还是不改官网的了。   那么总结来说这个程序就是以10Hz的速度不断发布
 
hello world 0
hello world 1
hello world 2
  等直到你按下ctrl+c退出程序。   写接收消息的程序(接收器)   既然发布器写完,我们就要写接收器了。打开一个terminal,输入下面命令  
cd ~/catkin_ws/src/pub_sub_test/src
touch sub_string.cpp
 
通过这两行命令,我们进入和pub_string.cpp相同的文件夹写sub_string.cpp程序。我们说过一个package的源文件一般都要写到它的src文件夹里。   打开sub_string.cpp我们把之前链接中代码复制进去,同样我也把注释去掉了。
#include "ros/ros.h"
#include "std_msgs/String.h"

/**
 * This tutorial demonstrates simple receipt of messages over the ROS system.
 */
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
  ROS_INFO("I heard: [%s]", msg->data.c_str());
}

int main(int argc, char **argv)
{
  ros::init(argc, argv, "listener");

  ros::NodeHandle n;

  ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);

  ros::spin();

  return 0;
}
 
1: include的两个头文件在发布器中也包含了的。使用了ROS自然要包含ros/ros.h,同样,作为接收器要接收string类型的消息,你也得包含ROS中的string消息的定义std_msgs/String.h   2: 先讲main函数。我们说过节点就是main函数在的那个程序。而节点都得初始化ros和nodeHandle。main函数的头两行,和发布器的头两行一样的。除了节点的名字换成了listener。名字自然是要换的,节点的名字得unique。如果ROS遇到了相同的节点名字那么他会停止掉旧节点的名字然后使用新节点的那个程序(这时候旧的节点如果 有ros::ok(),那么他会就变得不OK了 = = 。这是ros::ok()返回false的第三种情况)   3: ros::Subscriber这一行是定义接收器的方法,对比发布器中定义ros::Publisher的方法,发现十分类似。Publisher使用'n.advertise',这儿使用n.subscriber表示定义接收器, chatter即前面publisher的topic的名字。注意node的名字得独一无二但是topic的名字得和你想接收的信息的topic一样! 这个很好理解,ROS怎么知道你想接收什么信息呢?如果你有两个发布器,都发布std_msgs::String类型的消息,接收器通过找谁的topic和自己一样就接收谁的信息。我们发现这儿没有publisher中类似于<std_msgs::String>的东西来定义要接收的数据类型。其实是有的,它藏在了第三个参数chatterCallback里。我们注意到chatterCallback和main函数之前定义那个返回值为空的函数名字一样。chatterCallback称为回调函数,接收器每一次接收到消息,就会调用名字为它的函数。chatterCallback这个名字也是可以随意定义的,只要和前面那个返回值为空的函数有一样的名字就可以了,但一般命名为...Callback这样一看就知道这是ROS使用得回调函数。ROS的回调函数返回值只能为空。如果你想在接收到消息处理后返回什么有用的值怎么办呢?自然是写到类中了(当然也可以用全局变量)。这个以后讲。   4: 我们看回调函数。不熟悉C++的同学可能要会想为什么用这个函数的参数长这个样子。 (const std_msgs::String::ConstPtr& msg)什么鬼? 首先我得说,这个模式还是比较固定的,如果你要接受的是Int8类型的消息,那么参数会变成(const std_msgs::Int8::ConstPtr& msg)ConstPtr代表一个指针。所以你目前要知道的是msg这个参数是你接收到的消息的指针就行了,同样这个名字你也随意改,但一般是..msg。所以你看到printf(ROS_INFO)中有msg->data这种调用方式。(在Publisher中我们定义了std_msgs::String对象msg,类包含数据成员data,调用方式为msg.data。如果类的指针叫msg,那么调用该成员的方式是msg->data)。所以现在msg->data就是一个std::string类型的量,假设有string类型的变量st要想print出来,代码就是printf("%s,", st.c_str() )(不能直接print st)。使用ROS_INFO其内部完全一样。   5: ros::spin()会使程序在循环中,一直检测有没有接收到新的消息。其终止方式和使ros::ok()返回false方式一样。 总结来说,这个接收器的程序会接收发布器发布的hello world 0这种消息并print在屏幕上。

编译你的ROS程序

写完你的程序后自然就要编译了.ROS中的程序要先写在CMakeLists中再使用catkin_make编译(也可以使用catkin tool编译,不过catkin_tool我没用过).什么?没怎么用过CMakeLists??不要着急,这讲我们直接讲你需要添加什么内容,以后再抽出一讲来讲CMakeLists的细节以及ROS中的CMakeLists有什么特别之处.打开你的terminal,首先进入你的package的CMakeLists所在的文件夹.
cd ~/catkin_ws/src/pub_sub_test/
gedit CMakeLists.txt
 
gedit是一个文本编写软件,一般Linux会自带,你要是没有的话就直接进入文件夹,双击CMakeLists.txt打开也可以.gedit ABC表示用gedit打开ABC,如果没有ABC就会创建一个.总之打开了CMakeLists之后里面有一堆东西,你先不用管.你找到里面有一行写着##Declare a C++ executable,在这一行前面或者后面添加如下内容
第一行表示我们要编译add_executable表示我们要添加一个可执行文件,pub_string是这个可执行文件的名字(并不是非得和pub_string.cpp中的pub_string一样,不过建议用一样的),src/pub_string.cpp指定要编译的源文件的位置.   第二行target_link_libraries表示我们要将可执行文件链接到一个库,我们要使用ROS当然是要链接到ROS的库了,括号里pub_string指定要链接可执行文件的名字,后面是指定要链接的库的名字.三四行类似,作用是编译sub_string.cpp文件.   添加完上述内容后我们保存并退出CMakeLists.txt文件.
 
cd ~/catkin_ws/
catkin_make
 

执行你的ROS程序

万众瞩目的时刻终于到了,下面要开始执行程序了!为了防止干扰,为了避免你之前打开的terminal的影响,建议先关掉你之前的terminal.打开三个terminal.   第一个terminal中输入
 
roscore
 
cd ~/catkin_ws/
source devel/setup.bash
rosrun pub_sub_test sub_string
 
第二行命令是source是使计算机进入ROS环境,说得不准确但是直白些,source了之后计算机才能识别你很多的ROS专属命令.   第三行命令是跑ROS程序的.rosrun代表你要跑一个ROS节点了.第二个输入的是节点所在的package的名字,第三个自然就是节点自己的名字了.   输入完上面的命令之后,你的第二个terminal中的ROS接收程序就已经跑起来了.但是因为现在没有消息发布,所以你什么也看不到.这时候在第三个terminal中输入下面内容:
 
cd ~/catkin_ws/
source devel/setup.bash
rosrun pub_sub_test pub_string
  这时候你应该会在第二个和第三个terminal中看到类似于下面的内容  

helloWorld_string.png

  图片坐标是接收消息,右边是在发布消息.至此你的第一个ROS程序就编译并完成了.两个程序都是无限循环的,需要在terminal中按ctrl+c退出.但在这之前你可以在打开一个terminal,输入下面的东西
source ~/catkin_ws/devel/setup.bash
rqt_graph
  这时候你应该能看到下面的图片   rqt_graph.png
  这个图片很有意思了,你会发现talker就是你发布信息的程序中取的节点的名字,listener就是你接收信息的程序中取的节点的名字,chatter就是你在程序中取的topic的名字.这副图传递的信息是   节点talker通过topicchatter向节点listener发布消息 rqt_graph命令简洁地表现除了node和node之间的关系,当你以后有很多节点很多topic时,可以看看这个图像理清思维.

第一讲总结

第一讲总结

第一讲主要给大家讲述了几个需要知道的ROS的基本概念和写了第一个小程序.   程序可以直接移步到我的github下载 https://github.com/zhaozhongch/ros_tutorial   下载解压后把名字叫pub_sub_test的文件夹复制到你的catkin_ws/src下在cd ..回到catkin_ws文件夹中使用catkin_make编译即可.   以后的内容我也都会放到github里方便大家下载.下一讲讲怎么发布接收int, vector 和 pose类型的消息以及如何读懂ROS wiki中的一些资料.不定期更新.谢谢观看.