ROS是一个适用于机器人编程的框架,这个框架把原本松散的零部件耦合在了一起,为他们提供通信架构,相当于应用程序之间的沟通桥梁。
ROS采用了分布式的框架,通过点对点的设计让机器人的进程可以分别运行。
分布式:将不同功能数据放到不同模块,将相同数据放到不同服务器,他们之间依靠通信网络沟通。
1. ROS文件系统
1.1 Catkin编译
Catkin是基于CMake的编译构建系统,具有以下特点:
- Catkin沿用了包管理的传统像
find_package()
基础结构 - 扩展了CMake,例如:软件包编译后无需安装就可使用;自动生成
find_package()
代码,pkg-config
文件;解决了多个软件包构建顺序问题。
一个Catkin的软件包(package)必须要包括两个文件:
package.xml
:包括了package的描述信息CMakeLists.txt
:构建package所需的CMake文件
Catkin工作流程如下:
- 首先在工作空间
catkin_ws/src/
下递归的查找其中每一个ROS的package。 - package中会有
package.xml
和CMakeLists.txt
文件,Catkin(CMake)编译系统 据CMakeLists.txt
文件,从而生成makefiles(放在catkin_ws/build/
)。 - 然后make刚刚生成的makefiles等文件,编译链接生成可执行文件(放在
catkin_ws/devel
)。
可以看到,Catkin就是将cmake
与make
指令做了一个封装从而完成整个编译过程的工具。
实际的使用过程如下:
cd ~/catkin_ws #回到工作空间,catkin_make必须在工作空间下执行
sudo catkin_make #开始编译
source ~/catkin_ws/devel/setup.bash #刷新坏
注意:catkin编译之前必须要回到工作空间目录(work station),在其他路径下编译不会成功;如果有新的目标文件产生,那么需要刷新环境,是的系统1.2 Package软件包够遭到刚才编译生成的ROS可执行文件。
package是ROS的基本编译单元,任何ROS程序只有组织成package才能编译,所以package也是ROS源代码存放的地方。一个package可以编译出来多个目标文件(ROS可执行程序、动态静态库、头文件等等)。
一个package下常见的文件、路径有:
其中定义package的是CMakeLists.txt和package.xml,这两个文件是package中必不可少的。catkin编译系统在编译前,首先就要解析这两个文件,这两个文件就定义了一个package。
创建package需要在工作空间目录catkin_ws/src
,使用catkin_create_pkg
命令,用法是:
$ catkin_create_pkg package depends
其中package是包名,depends是依赖的包名,可以依赖多个软件包。
例如,新建一个package叫做test_pkg
,依赖roscpp、rospy、std_msgs(常用依赖)。
$ catkin_create_pkg test_pkg roscpp rospy std_msgs
这样就会在当前路径下新建test_pkg软件包,包括:
catkin_create_pkg
帮你完成了软件包的初始化,填充好了CMakeLists.txt
和package.xml
,并且将依赖项填进了这两个文件中。需要注意的是,需要刷新文件才能找到。
1.3 CMakeList写法
思路是:这个package的依赖是什么?要生成哪些目标?如何编译?
以turtlesim小海龟这个pacakge为例,可roscd
到tuetlesim
包下查看,在 turtlesim/CMakeLists.txt
find_package(catkin REQUIRED COMPONENTS ...)
项目依赖的其他软件包,都会自动成为catkin的组件(components)(就CMake而言)。因此可以将这些依赖包指定为catkin的组件,而不必再使用find_package
,这样将会变得简单。
include_directories(include ${catkin_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS})
头文件地址,配合后面的link链接过程,参数由上一步find_package
生成(省略了Boost的find过程)
add_message_files(DIRECTORY msg FILES Color.msg Pose.msg)
在被ROS软件包编译和使用之前,ROS中的消息(.msg)、服务(.srv)和操作(.action)文件需要特殊的预处理器编译步骤。这些宏的要点是生成编程语言特定的文件,以便可以在编程语言中使用消息、服务和操作。编译系统将使用所有可用的生成器(例如gencpp、genpy、genlisp)生成绑定。
提供了三个宏来分别处理消息,服务和操作:
- add_message_files
- add_service_files
- add_action_files
这些宏后面必须调用一个调用生成的宏generate_messages()
(不是message专有,通用,只写一次)
示例的命令,是从目标文件的msg子文件下面,添加文件名(FILES) Color.msg
和Pose.msg
的消息。服务也是同理。
catkin_package(CATKIN_DEPENDS geometry_msgs message_runtime std_msgs std_srvs)
catkin_package() 是catkin支持的 CMake 宏指令。用来向编译系统指明 catkin-specific 的信息,而编译系统来生成 pkg-config和CMake files。必须放在消息、服务和操作之后,链接执行之前。
该函数有5个参数,这里只用了CATKIN_DEPENDS
这一个,在之前的find_package
中就已经找到了
add_executable(turtlesim_node ${turtlesim_node_SRCS} ${turtlesim_node_MOCS})
作用是添加一个可执行文件构建目标,可执行文件名字叫做turtlesim_node
,源代码由set(turtlesim_node_SRCS ...)
设置;mocs是QT相关qt5_wrap_cpp(turtlesim_node_MOCS ${turtlesim_node_HDRS})
后面的target_link_libraries
则是将node和widgets与之前find的catkin和boost相链接;然后再为node添加依赖turtlesim_gencpp
2. ROS通信架构
ROS的通信方式有以下四种:
- Topic 主题
- Service 服务
- Parameter Service 参数服务器
- Actionlib 动作库
2.1 Node & Master
在Ros中最小的进程单元是节点(Node),一个软件包里可以有多个可执行文件,可执行文件在运行之后就成了一个进程(process),这个进程在ROS中就叫做节点。从程序的角度来说,node是一个可执行文件;从功能角度来说,一个node负责某一个单独的功能。
Master是节点管理器,由于实际情况中node数量众多,需要一个管理器在整个网络通信架构里管理各种node。node首先在master处进行注册,之后master会将该node纳入整个ROS程序中。 node之间的通信也是先由master进行“牵线”,才能两两的进行点对点通信。当ROS程序启动 时,第一步首先启动master,由节点管理器处理依次启动node。
当我们要启动ROS时,输入:
$ roscore
此时会启动三个功能:ROS master
,rosout
,parameter server
。这其中rosout
负责日志输出的一个节点,其作用是告知用户当前系统的状态,包括输出系统的error、warning等 ,并且将log记录于日志文件中。parameter server
是参数服务器,它并不是一个node, 而是存储参数配置的一个服务器。
启动master以后,节点管理器就开始按照系统的安排协调进行启动具体的节点,具体启动node的语句是:
$ rosrun pkg_name node_name
rosrun
将会寻找pkg_name
下的名为node_name
的可执行程序。
rosnode的常用命令如下:
2.2 launch文件
通常运行一个机器人系统我们需要启动多个node,为了方便我们用launch的方式来统一启动,命令是:
$ roslaunch pkg_name file_name.launch
roslaunch
命令首先会自动进行检测系统的roscore
有没有运行,也即是确认节点管理器是否在运行状态中,如果master
没有启动,那么roslaunch
就会首先启动master
,然后再按照launch
的规则执行。
launch
文件里已经配置好了启动的规则。所以roslaunch
就像是一个启动工 具,减少我们在终端中一条条输入指令的麻烦。
2.3 Topic
对于实时性、周期性的消息,使用topic来传输是最佳的选择。topic是一种点对点的单向通信方式,这里的“点”指的是node,也就是说node之 间可以通过topic方式来传递信息。
topic的步骤是:
- publisher节点和subscriber节点到节点管理器进行注册
- publisher发布topic
- subscriber在master的指挥下订阅该topic
Subscriber接收消息会进行处理,一般这个过程叫做回调(Callback)。所谓回调就是提前定义好了一个处理函数(写在代码中),当有消息来就会触发这个处理函数,函数会对消息进行处理。
topic通信的特点是:
- 异步通信。发送时调用publish()方法,发送完成立即返回,不用等待反 馈。
- subscriber通过回调函数的方式来处理消息
- topic可以同时有多个subscribers,也可以同时有多个publishers
Msg
topic有很严格的格式要求,比如摄像头拍摄的rgb图像topic,就必然要遵循ROS中 义好的rgb图像格式,这种数据格式就是Message。Message按照定义解释就是topic内容的数据类型,也称之为topic的格式标准,这里和我们平常用到的Massage直观概念有所不同。
基本的msg包括bool、int8、int16、int32等等常用的数据类型,但实际情况中,我们需要重新封装,比如image类型:
std_msg/Header header
uint32 seq
time stamp
string frame_id
uint32 height
uint32 width
string encoding
uint8 is_bigendian
uint32 step
uint8[] data
这种结构类似于C语言结构体,我们可以将msg进一步理解为一个类,我们每次发布的内容可以理解为实例化的对象。
ROS中往往针对不同的传感器、导航系统封装好了许多msg。
2.4 Service
道topic是ROS中的一种单向的异步通信方式。然而有些时候单向的通信满足不了通信要求,比如当一些节点只是临时而 非周期性的需要某些数据,如果用topic通信方式时就会消耗大量不必要的系统资源,造成系统的低效率高功耗。
这种情况下,就需要有另外一种请求-查询式的通信模型。这节我们来介绍ROS通信中的另一 种通信方式——service(服务)。
步骤是:
- 请求方(Client)就会发送一个request,阻塞等待。
- server处理,反馈回一个reply。
NodeB是server(应答方),提供了一个服务的接口,叫做/Service,通过这个接口和Clinet通信。
Service是同步通信方式,所谓同步就是说,此时NodeA发布请求后会在原地等待reply,直到 NodeB处理完了请求并且完成了reply,NodeA才会继续执行。这样避免了频繁的消息传递。
Srv
类似msg文件,srv文件是用来描述service数据类型的, service通信的数据格式定义在 *.srv
中
举个例子msgs_demo/srv/DetectHuman.srv
:
bool start_detect
---
my_pkg/HumanPose[] pose_data
该服务例子取自OpenNI的人体检测ROS软件包。它是用来查 询当前深度摄像头中的人体姿态和关节数的。第一行是请求格式,中间用—-隔开,第三行是应答格式。在本例中,请求为是否开始检测,应答为一个数组,数组的每个元素为某个人的姿态。而对于人的姿态HumanPose
,其实是一个msg,如下。所以srv可以嵌套msg在其中,但它不能嵌套srv。
msgs_demo/msg/HumanPose.msg
:
std_msgs/Header header
string uuid
int32 number_of_joints
my_pkg/JointPose[] joint_data
2.5 Parameter server
除了前文介绍的主题和服务这两种通信方式,另一种特殊的通信方式是参数服务器。于参数服务器是节点存储参数的地方、用于配置参数,全局共享参数。参数服务器使用互联网传输,在节点管理器中运行,实现整个通信过程。参数服务器维护着一个数据字典,字典里存储着各种参数和配置。
每一个key不重复,且每一个key对应着一个value,这就是字典的映射关系。,我们往往将一些不常用到的参数和配置放入参数服务器里的字典里,这样对这些数据进行读写都将方便高效。
2.6 Action
Action是对service的一个改进,适合于长时间通信。假如利用service通信方式,那么publisher会很长时间接受不到反馈的reply,会致使通信受阻。actionlib通信过程可以随时被查看过程进度,也可以终止请求,使得 在一些特别的机制中拥有很高的效率。
通信双方在ROS Action Protocol下进行交流通信是通过接口来实现,类似于TCP下的socket套接字接口。
如上图,客户端会向服务器发送目标指令和取消动作指令,而服务器则可以给客户端发送 实时的状态信息,结果信息,反馈信息等等,从而完成了service没法做到的部分。
动作的内容格式应包含三个部分:目标、结果、反馈
- 目标:机器人执行一个动作,应该有明确的移动目标信息,包括一些参数的设定,方向、角度、速 度等等。从而使机器人完成动作任务。
- 结果:当运动最终完成时,动作服务器把本次运动的结果数据发送给客户端,使客户端得到本次动作的全部信息,例如可能包含机器人的运动时长,最终姿势等等。
- 反馈:在动作进行的过程中,应该有实时的状态信息反馈给服务器的实施者,告诉实施者动作完成的状态,可以使实施者作出准确的判断去修正命令。
Action规范文件的后缀名是.action
,它的内容格式如下:
# Define the goal
uint32 dishwasher_id
--
# Define the result
uint32 total_dishes_cleaned
--
# Define a feedback message
float32 percent_complete
3. roscpp
如roscpp是C++语言ROS接口,我们直接调用它所 提供的函数就可以实现topic、service等通信功能。roscpp位于/opt/ros/kinetic
之下,用C++实现了ROS通信。通常我们要调用ROS的C++接口,首先就需要#include
roscpp的主要部分包括:
ros::init()
解析传入的ROS参数,创建node第一步需要用到的函数ros::NodeHandle
和topic、service、param等交互的公共接口ros::master
包含从master查询信息的函数ros::this_nod
包含查询这个进程(node)的函数ros::service
包含查询服务的函数ros::param
包含查询参数服务器的函数,而不需要用到NodeHandleros::names
包含处理ROS图资源名称的函数
3.1 编写消息发布器和订阅器
创建程序包
首先需要创建Catkin工作空间:
$ mkdir -p ~/catkin_ws/src #创建一个目录,-p表示如果不存在就创建否则不创建
$ cd ~/catkin_ws/
$ catkin_make
在catkin_ws
工作空间内$ cd ~/catkin_ws/src
然后创建一个名为beginner_tutorials
的新程序包,这个包依赖于std_msg、roscpp和rospy:
$ catkin_create_pkg beginner_tutorials std_msgs rospy roscpp
这将会创建一个名为beginner_tutorials
的文件夹,这个文件夹里面包含一个package.xml文件和一个CMakeLists.txt文件,这两个文件都已经自动包含了部分在执行catkin_create_pkg
命令时提供的信息。
发布器节点
首先进入之前创建的 beginner_tutorials package 路径下,在这之中创建一个src文件夹:
cd ~/catkin_ws/src/beginner_tutorials
mkdir -p ~/catkin_ws/src/beginner_tutorials/src
这个文件夹将会用来放置 beginner_tutorials package 的所有源代码。然后创建src/talker.cpp
文件,在其中写发布器源代码,如下:
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
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;
}
下面分块解释:
1.用init
初始化这个节点,唯一命名为talker
,然后为他创建一个句柄n
(这里是自动对应上的,一个一个进程只能有一个节点)
ros::init(argc, argv, "talker");
ros::NodeHandle n;
2.告诉 master
我们将要在 chatter
(话题名) 上发布 std_msgs/String 消息类型的消息。这样 master 就会告诉所有订阅了 chatter
话题的节点,将要有数据发布。第二个参数是发布序列的大小,如果我们发布的消息的频率太高,缓冲区中的消息在大于 1000 个的时候就会开始丢弃先前发布的消息。
ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);
3.ros::Rate
对象可以允许你指定自循环的频率。它会追踪记录自上一次调用 Rate::sleep()
后时间的流逝,并休眠直到一个频率周期的时间。在这个例子中,我们让它以 10Hz 的频率运行。
ros::Rate loop_rate(10);
4.roscpp 会默认生成一个 SIGINT 句柄,它负责处理 Ctrl-C 键盘操作——使得 ros::ok()
返回 false。如果下列条件之一发生,ros::ok()
返回false:
- SIGINT 被触发 (Ctrl+C)
- 被另一同名节点踢出 ROS 网络
ros::shutdown()
被程序的另一部分调用- 节点中的所有
ros::NodeHandles
都已经被销毁
while (ros::ok())
{...}
5.为msg
添加数据成员hello world
;用ROS_INFO
和其他类似的函数可以用来代替 printf
/cout
等函数;在这个例子中 ros::spinOnce()
非必须,因为我们不接受回调。然而,如果程序里包含其他回调函数,加上 ros::spinOnce()
这一语句,否则回调函数就永远也不会被调用了。
这是ROS消息回调处理函数,在ROS的主循环中,程序需要不断调用ros::spin()
或 ros::spinOnce()
,两者区别在于前者调用后不会再返回,也就是你的主程序到这儿就不往下执行了,而后者在调用后还可以继续执行之后的程序。
loop_rate.sleep();
是调用 ros::Rate
对象来休眠一段时间以使得发布频率为 10Hz。
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;
}
订阅器节点
在 beginner_tutorials package
目录下创建 src/listener.cpp
文件。
#include "ros/ros.h"
#include "std_msgs/String.h"
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;
}
这是一个回调函数,当接收到 chatter
话题的时候就会被调用。消息是以 boost shared_ptr 指针的形式传输,这就意味着可以存储它而又不需要复制数据。
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
ROS_INFO("I heard: [%s]", msg->data.c_str());
}
告诉 master 我们要订阅 chatter
话题上的消息。当有消息发布到这个话题时,ROS 就会调用 chatterCallback()
函数。第二个参数是队列大小,以防我们处理消息的速度不够快,当缓存达到 1000 条消息后,再有新的消息到来就将开始丢弃先前接收的消息。
ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);
编译节点并测试
之前创建包时,创建了创建了 package.xml 和 CMakeLists.txt 文件,在生成好的CMakeLists.txt
后面加入
## Build talker and listener
include_directories(include ${catkin_INCLUDE_DIRS})
add_executable(talker src/talker.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
add_dependencies(talker beginner_tutorials_generate_messages_cpp)
add_executable(listener src/listener.cpp)
target_link_libraries(listener ${catkin_LIBRARIES})
add_dependencies(listener beginner_tutorials_generate_messages_cpp)
然后运行catkin_make
。注意:如果你是添加了新的 package,你需要通过 --force-cmake
选项告诉 catkin 进行强制编译
# In your catkin workspace
$ catkin_make
按顺序启动并刷新:
$ roscore
# In your catkin workspace
$ cd ~/catkin_ws
$ source ./devel/setup.bash
注意!roscore,talker,listener需要开三个不同的terminal,talker和listener开启时都需要刷新。
启动发布器:
$ source ./devel/setup.bash
$ rosrun beginner_tutorials talker
启动订阅器:
$ source ./devel/setup.bash
$ rosrun beginner_tutorials listener
3.2 编写简单的服务器和客户端
首先需要创建一个srv,用来描述服务器数据类型。
int64 a
int64 b
---
int64 sum
编写服务器节点
创建add_two_ints_server.cpp
#include "ros/ros.h"
#include "beginner_tutorials/AddTwoInts.h"
bool add(beginner_tutorials::AddTwoInts::Request &req,
beginner_tutorials::AddTwoInts::Response &res)
{
res.sum = req.a + req.b;
ROS_INFO("request: x=%ld, y=%ld", (long int)req.a, (long int)req.b);
ROS_INFO("sending back response: [%ld]", (long int)res.sum);
return true;
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "add_two_ints_server");
ros::NodeHandle n;
ros::ServiceServer service = n.advertiseService("add_two_ints", add);
ROS_INFO("Ready to add two ints.");
ros::spin();
return 0;
}
我们自己创建了一个add
函数,这个函数提供两个int值求和的服务,int值从request里面获取,而返回数据装入response内,这些数据类型都定义在srv文件内部,函数返回一个boolean值。
Request
值从分割线上方获取,Response
值从分割线下方获取
bool add(beginner_tutorials::AddTwoInts::Request &req,
beginner_tutorials::AddTwoInts::Response &res)
{
res.sum = req.a + req.b;
ROS_INFO("request: x=%ld, y=%ld", (long int)req.a, (long int)req.b);
ROS_INFO("sending back response: [%ld]", (long int)res.sum);
return true;
}
通过ros::NodeHandle::advertiseService()
来创建ros::ServiceServer
。 advertiseService()
工作方式类似 subscribe()
函数,提供一个服务名和回调函数。
ros::ServiceServer service = n.advertiseService("add_two_ints", add);
ROS_INFO("Ready to add two ints.");
编写客户端节点
在beginner_tutorials包中创建src/add_two_ints_client.cpp文件,并复制粘贴下面的代码:
#include "ros/ros.h"
#include "beginner_tutorials/AddTwoInts.h"
#include <cstdlib>
int main(int argc, char **argv)
{
ros::init(argc, argv, "add_two_ints_client");
if (argc != 3)
{
ROS_INFO("usage: add_two_ints_client X Y");
return 1;
}
ros::NodeHandle n;
ros::ServiceClient client = n.serviceClient<beginner_tutorials::AddTwoInts>("add_two_ints");
beginner_tutorials::AddTwoInts srv;
srv.request.a = atoll(argv[1]);
srv.request.b = atoll(argv[2]);
if (client.call(srv))
{
ROS_INFO("Sum: %ld", (long int)srv.response.sum);
}
else
{
ROS_ERROR("Failed to call service add_two_ints");
return 1;
}
return 0;
}
add_two_ints
service创建一个client。ros::ServiceClient
对象待会用来调用service。
ros::ServiceClient client = n.serviceClient<beginner_tutorials::AddTwoInts>("add_two_ints");
实例化一个由ROS编译系统自动生成的service类,并给其request成员赋值。一个service类包含两个成员request
和response
。同时也包括两个类定义Request
和Response
。atoll把字符串转换为long long integer
beginner_tutorials::AddTwoInts srv;
srv.request.a = atoll(argv[1]);
srv.request.b = atoll(argv[2]);
代码是在调用service。由于service的调用是模态过程(调用的时候占用进程阻止其他代码的执行),一旦调用完成,将返回调用结果。如果service调用成功,call()
函数将返回true,srv.response
里面的值将是合法的值。如果调用失败,call()
函数将返回false,srv.response
里面的值将是非法的。
if (client.call(srv))
还是老规矩,三个terminal启动,注意刷新,注意客户端需要传参
$ roscore
$ rosrun beginner_tutorials add_two_ints_server
$ rosrun beginner_tutorials add_two_ints_client 1 3
服务器窗口: