平台
Ubuntu16.04
ROS Kinetic Kame
环境配置
具体详见该博客
创建工作空间Work Space
首先创建ROS的工作空间,一般创建在Home目录下,使用命令行或者右击创建文件夹均可。例如,我在Home文件夹下创建一个名为cxx_ws(也可以是其他名字)的工作空间。
然后创建src文件夹,并初始化。1
2
3mkdir src
cd src
catkin_init_workspace
然后回到工作空间,并编译。1
2cd ..
catkin_make
编译完成后,工作空间会新增2个文件夹,build和devel。其中build文件夹为编译空间Build Space,devel为开发空间Development Space。
注:catkin_make含义:CMake(cross platform make)是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程),能够输出各种各样的makefile或者project文件。而catkin_make是cmake的升级版,可以认为是对cmake进一步封装的高级命令。
附:ROS工程文件结构:
添加环境变量1
2
3
4source devel/setup.bash # 该文件定义了工作空间所需要的环境变量
gedit ~/.bashrc # .bashrc类似于windows下的环境变量
# source ~/cxx_ws/devel/setup.bash # 在打开的文件中添加该命令
source ~/.bashrc
创建功能包package
ROS功能包package指的是一种特定的文件结构和文件夹组成。工程文件夹下的src文件夹由一个个package功能包组成,一个功能包中可以包含一个或多个节点的文件。
创建package
创建功能包的指令为:1
catkin_create_pkg <package_name> [depend1] [depend2]
package_name为包名,depend为依赖包名。
进入src文件夹,创建hello功能包。1
2cd src
catkin_create_pkg hello std_msgs roscpp
回到工作空间并编译:1
2cd ..
catkin_make

注:同一个工作空间下,不允许存在同名功能包,不同工作空间下,允许存在同名功能包。功能包名字只能使用小写字母、数字和下划线,首字符必须是小写字母。
std_msgs包含了常见消息类型。roscpp使用C++实现ROS的各种功能,提供了一个客户端库。
创建完成的功能包一般会自带include、src、CMakeList.txt和package.xml文件。
package详解
通常,一个完整的package由以下几个部分组成:
- CMakeLists.txt:定义
package的包名、依赖、源文件、目标文件等编译规则。 - package.xml:描述
package的包名、版本号、作者、依赖等信息。 - src:存放
ROS的源代码,包括.cpp和.py。 - include:存放
C++源代码对应的头文件。 - scirpts:存放可执行脚本文件。
- msg:存放自定义格式的消息(.msg)。
- srv:存放自定义格式的服务(.srv)。
- models:存放机器人或仿真场景的3D模型(.sda,.stl,.dae)。
- urdf:存放机器人的模型描述(.urdf,.xacro)。
- launch:存放launch文件(.launch,.xml)。
通常除了CMakeLists.txt和package.xml是必须的,其他根据需求添加,实际建立工程文件时,都要遵守以上的命名规则。
package.xml
package.xml包含了package的名称、版本号、内容描述、维护人员、软件许可、编译构建工具、编译依赖、运行依赖等信息。文件写法遵循XML标签文本的写法,目前存在两种格式,但内容大致一样。(红色字体的是必备标签)
CMakeLists.txt
CMakeList.txt原本是Cmake编译系统的规则文件,ROS的构建系统catkin基本上使用CMake,只是针对ROS工程添加了一些宏定义。因此在写法上CMakeList.txt和Cmake基本一致。CMakeList.txt文件规定了catkin的编译规则,直接规定这个包需要依赖那些package,要编译生成那些目标,如何编译等等流程写法。该文件的基本语法都是按照CMake,只是在其基础上添加了一些宏。
package编译过程
功能包的编译过程也就是catkin的编译过程。
- 首先在工作空间的
src目录下递归的查找每一个ros的package。 - 因为每一个
package中都有package.xml和CMakeList.txt文件,所以catkin编译系统依据CMakeList.txt文件,生成makefile文件,放在工作空间的build文件夹中。 - 然后
make刚刚生成的makefiles等文件,编译链接生成可执行文件,放在工作空间下的devel文件夹中。
简而言之,catkin就是将camke和make指令做了一个封装从而完成整个编译过程的工具。
创建ROS节点
一个节点就是ROS下面一个可执行程序,使用ROS可以与其他节点进行通信。
编写Node
Node文件通常放在package下的src文件夹中。
例如在本例中,进入hello文件夹的src文件夹,右击创建一个cpp文件:hello_node.cpp。
打开hello_node.cpp文件,并输入代码:1
2
3
4
5
6
7
8
int main (int argc, char **argv)
{
ros::init(argc, argv, "hello_node") ;
ros::NodeHandle nh;
ROS_INFO_STREAM("Hello, ROS!") ;
}
#include <ros/ros.h>为声明ROS的标准库。
ros::init(argc, argv, "hello_node")为ROS初始化节点函数,其调用形式一般为:ros::init(argc, argv, "my_node_name");。本例中,将node_name取名为hello_node,当然也可以是其他名字。
ros::NodeHandle nh;为ROS启动节点函数。一个ROS的node只有一个NodeHandle,它提供这个node的对于topic的收发功能。
ROS_INFO_STREAM("Hello, ROS!") ;打印日志信息,相对于print。
编译Node
声明依赖库
依赖库的声明在package.xml中,打开该文件,检查是否所有的依赖库都已安装。(通常,创建package时都会声明好的)
声明可执行文件
打开CMakeLists.txt文件,找到注释掉的Declare a C++ executable声明C++可执行文件这一行,在这一段的最后,按照注释添加声明,在本例中,添加如下命令:(hello_node为可执行文件名字,可以换成其他的,下同)1
add_executable(hello_node src/hello_node.cpp)
再往下找到注释掉的Specify libraries to link a library or executable target aginst指定链接库这一行,在这一段的最后,按照注释添加声明,在本例中,添加如下指令:1
target_link_libraries(hello_node ${catkin_LIBRARIES})
修改完成后,回到工作空间文件夹,重新catkin_make编译即可。
编译完成后,会在devel/lib/hello文件夹下生成hello_node可执行文件。
注:这里的hello_node也就是后面rosrun命令中调用的节点名称,也就是CMakeLists文件中 add_executable和target_link_libraries中的名字(这2个名字必须一致),但和hello_node.cpp文件中定义的节点名称可以不一致。
运行Node
运行指令为:rosrun package-name executable-name,其中package-name为功能包名称,本例中为hello,executable-name为可执行文件名称,即在上文声明可执行文件中的可执行文件名字,在本例中为hello_node,也可以在lib文件夹中找到可执行文件名字(可以反过来使用这个规则,即当程序报错找不到节点时,可以去lib文件夹看一下有没有生成的可执行文件)。
Node节点不能单独单独运行,需要一个节点管理器才能正常运行。
首先在终端输入roscore:启动节点管理器。
然后再打开一个终端,输入运行节点命令:rosrun hello hello_node即可看到打印出来的信息。
创建launch文件
launch文件即启动文件,可以编写很多节点一起启动(rosrun一次只能启动一个节点),而且也会自动启动roscore命令,在大型工程中使用起来非常方便。
编写launch文件
launch文件一般放在包文件下的launch文件夹下(如没有则新建),在本例中,在hello包文件夹下新建一个launch文件夹,然后进入该文件夹,新建hello_launch.launch文件。打开并输入以下内容:1
2
3<launch>
<node pkg="hello" type="hello_node" name="hello_launch" output="screen"/>
</launch>

pkg为package name,即功能包名,本例中为hello,type为executable name,即指向节点的可执行文件的名称,本例中为hello_node。这两个参数也相当于rosrun命令中的2个参数。name为node name,即节点运行的名字,这里会覆盖节点定义中的init()的节点的名称。本例中定义为hello_launch,当然也可以是其他名字。
运行launch文件
启动命令为:roslaunch package-name launch-name,其中package-name为功能包名称,本例中为hello,launch-name为launch文件名称(包括后缀),在本例中为hello_launch.launch。
打开终端输入rosluanch hello hello_launch.launch即可。
可以看到输出的日志信息。
launch文件解读
launch文件中常用的参数为:
参考博客:解析 roslaunch 文件
ROS中的通讯
ROS是以节点的形式开发的,而节点是根据其目的细分的可执行程序的最小单位。节点通过消息(message)与其他的节点交换数据,最终成为一个大型的程序。这里的关键概念是节点之间的消息通信,它分为三种。单向消息发送/接收方式的话题(topic);双向消息请求/响应方式的服务(service);双向消息目标(goal)/结果(result)/反馈(feedback)方式的动作(action)。
Topic in roscpp
Topic(话题)是ROS里一种异步通信的模型,节点间分工明确,有的只负责发送,有的只负责接收处理。对于绝大多数的机器人应用场景,比如传感器数据收发,速度控制指令的收发,Topic模型是最适合的通信方式。ROS中的通信方式中,topic是常用的一种。对于实时性、周期性的消息,使用topic来传输是最佳的选择。topic是一种点对点的单向通信方式,这里的“点”指的是node,也就是说node之间可以通过topic方式来传递信息。
topic要经历下面几步的初始化过程:首先,publisher节点和subscriber节点都要到节点管理器进行注册,然后publisher会发布topic,subscriber在master的指挥下会订阅该topic,从而建立起sub-pub之间的通信。注意整个过程是单向的。
创建Topic消息类型
ros中自带很多种topic类型(可以在/ros/kinect/share中带_msg的文件夹中查看,例如查看标准消息类型,找到ros/kinect/share/std_msg/msg,里面定义了大量的topic类型数据。),也可以自己新建topic类型。例如在工程文件夹下的功能包hello下面,创建msg文件夹,并在新建hello_msg.msg:1
2string state
int32 num

上述命令相对于创建了一个类似于C语言中的结构体,其中包含了string类型的state变量和int32类型的num变量。
编译工程
打开该功能包下的CMakeLists.txt文件,找到fin_package,add_message_files和generate_messages这几行,添加如下内容:
打开该功能包下的package.xml文件,在里面添加以下内容:
回到工程文件夹目录,进行catkin_make编译,打开工程文件夹下的devel/include/hello文件夹,可以看到新生成的hello_msg.h文件,后续需要该消息类型只需在添加该头文件即可。
编写发送文件
进入功能包下的src文件夹,创建hello_talker.cpp,其内容如下: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
int main(int argc, char **argv)
{
ros::init(argc, argv, "hello_talker"); // 初始化节点,hello_talker为节点名称
ros::NodeHandle nh;
hello::hello_msg msg;
// 初始化消息值
msg.state = "working";
msg.num = 1;
ros::Publisher pub = nh.advertise<hello::hello_msg>("hello_info", 1); // 创建发送者
ros::Rate loop_rate(1); // 定义发布的频率,1HZ
while(ros::ok())
{
ROS_INFO("hello_talker: Hello %d", msg.num); // 打印消息
msg.num += 1; // 消息类型数据处理
pub.publish(msg); // 发布消息
loop_rate.sleep(); // 根据前面的定义的loop_rate,设置1s的暂停
}
return 0;
}
其中最重要的函数为topic消息发布函数advertise(),其调用格式为:ros::Publisher pub = nh.advertise<[msg_type]>([msgName], [msgCountLimit])。在本例中,消息格式为自定义的hello_msg,消息名称为hello_info(也可以是其他名字)。1表示每次只发送一次。在本例中,发送者会一直以1Hz的频率循环+1发送信息。publish()函数会向所有订阅该节点的接收者发送消息。
在ROS中,消息有组织地存放在话题里。topic消息传递的理念是:当一个节点想要分享信息时,它就会发布(publish)消息到对应的一个或者多个话题;当一个节点想要接收信息时,它就会订阅(subscribe)它所需要的一个或者多个话题。ROS节点管理器负责确保发布节点和订阅节点能找到对方;而且消息是直接地从发布节点传递到订阅节点,中间并不经过节点管理器转交。
编写接收文件
进入功能包下的src文件夹,创建hello_listener.cpp,其内容如下: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
void helloCallback(const hello::hello_msg::ConstPtr &msg)
{
std_msgs::Int32 helloNum; // 定义int32消息类型变量
helloNum.data = msg->num + 1; // 注意这里只能是指针引用方式
ROS_INFO("listener: hello %d, state = %s", helloNum.data, msg->state.c_str()); // 打印信息
}
int main(int argc, char ** argv)
{
ros::init(argc, argv, "hello_listener"); // 初始化节点,hello_listener为节点名称
ros::NodeHandle nh;
ros::Subscriber sub = nh.subscribe("hello_info", 1, helloCallback); // 创建接收者
ros::spin(); // ros::spin()用于调用所有可触发的回调函数,将进入循环,不会返回,类似于在循环里反复
// 调用spinOnce()只会发送一次
return 0;
}
其中最重要的函数为topic消息接收函数subscribe(),其调用格式为:ros::Subscriber sub = node_handle.subscribe(topic_name,queue_size, pointer_to_callback_function);。前2个参数和advertise()的参数一致,第三个参数为回调函数的指针(这里是指针,所以只需写函数名即可,不需要加小括号())。当接收者接收到消息时,则会进入到回调函数中处理数据。本例中,接收者会将接收到的消息数值+1并打印输出状态信息。
编译运行
打开功能包下的CMakeLists.txt文件,修改内容如下:
进入工程文件夹下,使用catkin_make编译。编译完成后,可以进入devel/lib/hello文件夹下面看到新生成的hello_talker和hello_listener可执行文件。
首先打开节点管理器roscore,然后分别打开2个终端,运行指令:1
rosrun hello hello_talker
1 | rosrun hello hello_listener |

此时可以再打开1个终端,并输入rqt_graph查看当前的节点及消息。
由图可见,当前共有2个节点和1个消息(椭圆为节点,矩形为消息),即hello_talker和hello_listener通过消息(话题)hello_info来实现通讯。
Service in roscpp
Service是一种请求-反馈的通信机制。请求的一方通常被称为客户端client,提供服务的一方叫做服务器端 server。Service机制相比于Topic的不同之处在于:
- 消息的传输是双向的,有反馈的,而不是单一的流向。
- 消息往往不会以固定频率传输,不连续,而是在需要时才会向服务器发起请求。
创建Service消息类型
和topci消息类型类似,ros也自带很多service消息类型,存放位置和topic一致,只是有的_msg类型不一定有service消息类型的数据。例如std_msg中就没有service类型的,而sensor_msg中存在srv文件夹,里面就有定义好的service消息类型数据。
同样的,本例中也创建一个自己的service消息类型。在工程文件夹下的hello包中创建一个srv文件夹,新建hello_srv.srv:1
2
3
4string name
int32 age
---
string feedback

横线上面的部分为服务请求的数据,即client数据,横向下面是服务器回传的内容,即server数据。类似于topic数据类型,service数据包含了2个结构体数据。
编译工程
打开该功能包下的CMakeLists.txt文件,找到add_service_files这一行,根据注释添加以下内容:
该部分内容一定要在generate_messages这一行之上,否则会编译报错。
package.xml文件的添加内容如topic部分一致(如果在topic部分做过了就不需要再添加了)。
回到工程文件夹目录,进行catkin_make编译,打开工程文件夹下的devel/include/hello文件夹,可以看到新生成的hello_srvRequest.h和hello_srvResponse.h文件,其中Request.h为client的,Response.h为server的。
编写服务器端文件
进入功能包下的src文件夹,创建hello_server.cpp,其内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool handle_function(hello::hello_srv::Request &req, hello::hello_srv::Response &res)
{
ROS_INFO("Request from client %s with age %d", req.name.c_str(),req.age);
res.feedback = "Hi " + req.name + ". I'm server!";
return true;
}
int main(int argc, char **argv){
ros::init(argc, argv, "hello_server");
ros::NodeHandle nh;
ros::ServiceServer service = nh.advertiseService("hello_server", handle_function);
ros::spin();
return 0;
}
编写客户端文件
进入功能包下的src文件夹,创建hello_client.cpp,其内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main(int argc, char **argv){
ros::init(argc, argv, "hello_client");
ros::NodeHandle nh;
ros::ServiceClient client = nh.serviceClient<hello::hello_srv>("hello_server");
hello::hello_srv srv;
srv.request.name = "Cxx";
srv.request.age = 18;
if(client.call(srv))
{
ROS_INFO("Response from service: %s", srv.response.feedback.c_str());
}
else
{
ROS_ERROR("Failed to call service hello_service");
return 1;
}
return 0;
}
编译运行
打开功能包下的CMakeLists.txt文件,修改内容如下:
进入工程文件夹下,使用catkin_make编译。编译完成后,可以进入devel/lib/hello文件夹下面看到新生成的hello_server和hello_client可执行文件。
首先打开节点管理器roscore,然后分别打开2个终端,运行指令:1
rosrun hello hello_server
1 | rosrun hello hello_client |
要先打开server节点程序,然后再打开client节点。
service消息只会在client开启时才会调用,且调用完后立即结束,而topic会一直发送消息。
个人理解
ros中node是最基本也是最小的一个单元,一个node就是一个main()函数,也就是一个可执行文件,而每个package则是由若干个node组成的一个功能包,最后一个个package组成了整个工程文件。因此,实际的工程项目最基础的还是基本功能的编写,ros系统只是一个上层的封装,可以更便于操纵传感器等硬件,是硬件和软件之间的一层系统,所以叫做机器人操作系统,但其本身又不像windows或者Ubuntu那样强大,只是针对机器人硬件进行了封装,所以还必须要依附在Ubuntu上面(据说现在也可以在windows上面安装了,具体还没有试过)。- 创建完任何一个
ROS工程空间,首先要做2件事情,1.编译工作空间;2.添加环境。环境变量添加一次即可,每次修改包里面的内容时都要重新回到工作空间编译,使其生效。编译工作空间的本质就是生成一些库文件、可执行文件,而其工具就是CMake这个交叉编译工具,也就是说通过CMake这个工具(在ros里面就是输入catkin_make),根据一定的规则和说明文件(在ros里面就是CMakeLists.txt文件),将package里面的文件转换为其他文件,例如将src里面的.cpp文件转为.exe文件(因为计算机最终执行的是二进制文件,也就是.exe文件,而.cpp文件属于高级语言,是人看的),将msg里面的文件转为lib里面的文件,等等。在通俗的讲,就是将我们看的东西转换为计算机看的东西。 - 个人觉得
ros中有2个概念非常重要,一个是消息通讯,一个是launch文件,其余的概念接触过C++或者python都可以很快的理解。
参考博客
ROS—catkin编译系统、package.xml和CMakeList.txt文件
ros系统入门笔记(一)
ROS学习笔记三:编写第一个ROS节点程序
ROS 中的 launch 文件
ROS环境下launch文件格式说明
ROS Topic in roscpp 通信(简介+实例+测试)
ROS入门
机器人操作系统——Robot Operating System(ROS)