机器人操作系统(ROS)浅析
学习笔记
至 2018-2-8 已将此教程过了一遍 此文档中为摘抄的笔记,若有不明白的地方,请查看原文档
2018-2-2
关于节点nodes 的操作
1、启动节点master $ roscore
这个定义中的“运行实例”(runninginstance)很重要。如果我们同时执行相同程序的多个副本——注意确保每个副本使用不同的节点名——则每个副本都被当做一个单独的节点。
2、启动节点$ rosrun
rosrun命令有两个参数,其中第一个参数是功能包的名称;第二个参数是该软件包中的可执行文件的名称。
ROS中要求每个节点有不同的名称
可以使用rosrun命令显式设置节点的名称,语法如下:
rosrun package-name executable-name__name:=node-name
rosrun是一个shell脚本,能够理解ROS的文件组织结构,知道到哪里能找到与给定包名称对应的可执行文件。
一旦它找到你要的程序,rosrun就会正常启动节点。
例如,如果你真的想要,你可以像执行任何其他程序一样直接执行turtlesim_node:
/opt/ros/indigo/lib/turtlesim/turtlesim_node
这里还要强调一点,通过节点管理器注册成为ROS节点发生在程序的内部,而不是通过rosrun命令。
3、查看节点要获得特定节点的信息,使用如下命令:
rosnode info node-name
4、终止节点要终止节点,使用如下命令:
rosnode kill node-name
topic
ROS节点之间进行通信所利用的最重要的机制就是消息传递。在ROS中,消息有组织地存放在话题topic里19。
消息传递的理念是:
当一个节点node想要分享信息时,它就会发布(publish)消息到对应的一个或者多个话题;
当一个节点想要接收信息时,它就会订阅(subscribe)它所需要的一个或者多个话题。
ROS节点管理器负责确保发布节点和订阅节点能找到对方;而且消息是直接地从发布节点传递到订阅节点,中间并不经过节点管理器转交。
查看节点构成的计算图
要查看节点之间的连接关系,恐怕将其表示为图形是最便于查看的。
在ROS系统中查看节点之间的发布-订阅关系的最简单方式就是在终端输入如下命令:
rqt_graph
在这个命令中,r代表ROS,qt指的是用来实现这个可视化程序的Qt图形界面(GUI)工具包。输入该命令之后,你将会看到一个图形界面,其中大部分区域用于展示当前系统中的节点。通常情况下,你将会看到如图2.2类似的图形界面。在该图中,
椭圆形表示节点node ,有向边表示其两端节点间的发布-订阅关系。该计算图告诉我们,
/teleop_turtle节点向话题/turtle1/cmd_vel发布消息,而/turtlesim节点订阅了这些消息(“cmd_vel”是“commandvelocity”的缩写。)。
rostopic pub可以把数据发布到当前某个正在广播的话题上。
http://wiki.ros.org/cn/ROS/Tutorials/UnderstandingTopics
节点node 可以发布话题topic,其他的节点node 可以订阅某个topic,当节点订阅某个topic 的时候,可以不用关心整个topic 是由谁发出
turtlesim遥控系统的工作原理有了一定的理解。当你按下一个键时,/teleop_turtle节点会以消息的形式将这些运动控制命令发布到话题/turtle1/cmd_vel;与此同时,因为turtlesim_node订阅了该话题,因此它会接收到这个些消息,控制海龟按照该预定的速度移动。其中的关键点在于:
仿真海龟不关心(或者甚至不知道)哪个程序发布了这些cmd_vel消息。任何向这个话题发布了消息的程序都能控制这个海龟。
远程操作程序不关心(或者甚至不知道)哪个程序订阅了它发布的cmd_vel消息。任何订阅了相关话题的程序都能自主选择是否响应这些命令。
测量发布频率有两个命令可以用来测量消息发布的频率以及这些消息所占用的带宽:
rostopic hz topic-name
rostopic bw topic-name
查看消息类型
要想查看某种消息类型的详情,使用类似下面的命令21,22:
rosmsg show message-type-name
用命令行发布消息
大多数时候,发布消息的工作是由特定的程序完成的*。但是,你会发现有时候手动发布消息是很实用的。要实现该功能,利用rostopic命令行工具23:
rostopicpub –r rate-in-hz topic-name message-type message-content
这条命令重复地按照指定的频率给指定的话题发布指定的消息。
该命令最后的参数message-content应该按顺序提供消息类型中所有域的参数值。
例如:
rostopicpub –r 1 /turtle1/cmd_vel geometry_msgs/Twist ’[2,0,0]’ ’[0,0,0]’
前面三个数字表示期望的位移线速度,后面三个数字表示期望的角速度。用单引号(’…’)和中括号([…])组织这些数值赋给它们对应的两个顶层复合域变量
同样的,类似的命令将会控制小海龟沿着它的z轴(垂直于
电脑屏幕)旋转。
rostopicpub –r 1 /turtle1/cmd_vel geometry_msgs/Twist ’[0,0,0]’ ’[0,0,1]’
注意:如何去确定 message-type
上述语法有一个明显的缺点,那就是你必须记住消息类型里所有的域以及这些域出现的顺序
此处展示的命令利用了-r来指定话题以频率模式发布消息,即以一定的时间周期发布消息。这条命令同样支持一次性发布的模式(-1“虚线后为数字1”)和特别的锁存模式(-l“虚线后为字母L的小写”),锁存模式虽然也只是发布一次消息,但是会确保该话题的新订阅者也会收到消息。实际上,锁存模式是默认的模式。
理解消息类型的命名
和ROS里其他的程序一样,每条消息类型都属于一个特定的包。消息类型名总会包含一个斜杠,斜杠前面的名字是包含它的包:
package-name/type-name
例如,turtlesim/Color消息类型按如下方式分解:
turtlesim (包名)+color (类型名称) --》消息类型 turtlesim/Color
这种分解消息类型名的方法有如下几个目的:
最直接地,把包的名字包含在消息类型名里能避免命名冲突。例如,geometry_msgs/Pose和turtlesim/Pose是有区别的消息类型,它们包含了不同的(但概念上是类似的)数据。
正如我们将在第3章看到的那样,当我们编写ROS程序的时候,如果也用到了其他包的消息类型,那么我们需要声明对它们的依赖关系。把功能包的名称和消息类型名一起写出来会使得这些依赖关系看上去更明朗。
最后一点,包名和其含有的消息类型放在一起将有助于猜测它的含义。例如,消息类型ModelState单独出现可能会让人产生迷惑,但是以gazebo/ModelState的形式出现后,就会指明这个消息类型是Gazebo仿真器中的一部分,而且很有可能包含了这个仿真器中某个模型的状态信息。
rosrun命令中的__name参数
rosrun turtlesim turtlesim_node __name:=A
这些参数覆盖了每个节点赋予自己的默认名称。覆盖是必要的,因为ROS节点管理器不允许多个节点拥有相同的名称。
话题通信的多对多机制
基于话题和消息的通信机制是多对多的,即多个发布者和多个订阅者可以共享同一个
话题。
节点之间的松耦合关系
节点之间——更一般的,对于绝大多数设计精巧的ROS节点——是松耦合的。
每个节点都不需要显式知道其他节点的存在与否;
它们的唯一交互方式是间接地发生在基于话题和消息的通信层。
这种节点之间的独立性,以及其支持的任务可分解特性(即复杂任务分解成可重用的小模块),是ROS最关键的设计特性之一。
“生产”消息的程序(例如turtle_teleop_key)只管发布该消息,而不用关心该消息是如何被“消费”的。
“消费”消息的程序(例如turtlesim_node)只管订阅该话题或者它所需要消息的所有话题,而不用关心这些消息数据是如何“生产”的。
是否有必要建立多个工作区?
对于许多用户来说,确实没有必要使用多个ROS工作区。但是,ROS的catkin编译系统(我们将在3.2.2节中介绍),试图一次性编译同一个工作区中的所有功能包。因此,如果你的工作涉及大量的功能包,或者涉及几个相互独立的的项目,则维护数个独立的工作区可能是有帮助的。
所以,如果你的程序包很多,你可以建立多个workspace
ros 中的程序分析
1// This is a ROS version of the standard "hello , world"
2// program.
3
4// This header defines the standard ROS classes .
5#include
6
7int main ( int argc , char ** argv ) {
8// Initialize the ROS system .
9ros::init ( argc , argv , " hello _ros " ) ;
10
11// Establ ish this program as a ROS node .
12ros::NodeHandle nh ;
13
14// Send some output as a log message .
15ROS_INFO_STREAM( " Hello , ␣ROS! " ) ;
16}
头文件ros/ros.h包含了标准ROS类的声明,你将会在每一个你写的ROS程序中包含它。
ros::init函数初始化ROS客户端库。请在你程序的起始处调用一次该函数3。函数最后的参数是一个包含节点默认名的字符串。
ros::NodeHandle(节点句柄)对象是你的程序用于和ROS系统交互的主要机制4。创建此对象会将你的程序注册为ROS节点管理器的节点。最简单的方法就是在整个程序中只创建一个NodeHandle对象。
编译程序时的内部操作
声明依赖库首先,我们需要声明程序所依赖的其他功能包。对于c++程序而言,此步骤是必要的,以确保catkin能够向c++编译器提供合适的标记来定位编译功能包所需的头文件和链接库。
为了给出依赖库,编辑包目录下的CMakeLists.txt文件。该文件的默认版本含有如下行:
find_package(catkin REQUIRED)
所依赖的其他catkin包可以添加到这一行的COMPONENTS关键字后面,如下所示:
find_package(catkinREQUIRED COMPONENTS package-names)
我们需要添加名为roscpp的依赖库,它提供了ROS的C++客户端库。因此,修改后的find_package行如下所示:
find_package(catkin REQUIRED COMPONENTSroscpp)
我们同样需要在包的清单文件中列出依赖库,通过使用build_depend(编译依赖)和run_depend(运行依赖)两个关键字实现:
在我们的例程中,hello程序在编译时和运行时都需要roscpp库,因此清单文件需要包括:
声明可执行文件接下来,我们需要在CMakeLists.txt中添加两行,来声明我们需要创建的可执行文件。其一般形式是:
add_executable(executable-namesource-files)
target_link_libraries(executable-name${catkin_LIBRARIES})
第一行声明了我们想要的可执行文件的文件名,以及生成此可执行文件所需的源文件列表。如果你有多个源文件,把它们列在此处,并用空格将其区分开。第二行告诉Cmake当链接此可执行文件时需要链接哪些库(在上面的find_package中定义)。如果你的包中包括多个可执行文件,为每一个可执行文件复制和修改上述两行代码。
在我们的例程中,我们需要一个名为hello的可执行文件,它通过名为hello.cpp的源文件编译而来。所以我们需要添加如下几行代码到CMakeLists.txt中:
add_executable(hello hello.cpp)
target_link_libraries(hello${catkin_LIBRARIES})
##Declare a cpp executable
add_executable(position_plan_nodesrc/position_plan.cpp)
在执行自己编写的一个cpp 程序或者节点node 前,要注意先启动节点管理器rosrun
这个程序是一个节点,节点需要一个节点管理器才可以正常运行
重点 关于c++ 语法编程说明
包含消息类型声明你应该还能回忆起我们在2.7.2节中谈到过每一个ROS话题都与一个消息类型相关联。
每一个消息类型都有一个相对应C++头文件。你需要在你的程序中为每一个用到的消息类型包含这个头文件,代码如下所示:
#include
这里需要注意的是,功能包名应该是定义消息类型的包的名称,而不一定是你自己的包的名称。在pubvel程序中,
我们想发布一条类型为geometry_msgs/Twist的消息(名为geometry_msgs的包所拥有的类型为Twist的消息),我们应该这样:
#include
这个头文件的目的是定义一个C++类,此类和给定的消息类型含有相同的数据类型成员。
这个类定义在以包名命名的域名空间中。这样命名的实际影响是当引用C++代码中的消息类时,
你将会使用双分号(::)来区分开包名和类型名,双分号也称为范围解析运算符。在我们的pubvel例程中,头文件定义了一个名为geometry_msgs::Twist的类。+
很常用的就是ros::
详见《机器人操作系统(ROS)浅析》
使用套路如下:
添加头文件:
#include
定义类:
ros::NodeHandle nh
使用类:
ros::Publisher pub = nh . advertise
头文件在哪里?
创建发布者对象
发布消息的实际工作是由类名为ros::Publisher的一个对象来完成的7。类似下面这行的代码创建了我们需要的对象:
ros::Publisher pub =node_handle.advertise
让我们看下这一行代码的每一部分。
node_handle是ros::NodeHandle类的一个对象,是你在程序的开始处创建的。我们将调用这个对象的advertise方法。
在尖括号中的message_type部分,其正式名称为模板参数,是我们要发布的消息的数据类型。这个应该是上面讨论过的头文件中定义的类名。在例程中,我们使用geometry_msgs::Twist类。
topic_name是一个字符串,它包含了我们想发布的话题的名称。它应该和rostopic list或者rqt_graph中展示的话题名称一致,但通常没有前斜杠(/)。我们丢掉前斜杠使话题名为一个相对名称;第5章解释了相对名称的机制和目的。在此例程中,话题名为turtle1/cmd_vel。
请注意话题名和消息类型的区别
advertise最后的参数是一个整数,表示这个发布者发布的消息序列的大小。在大多数情况下,一个相对比较大的值,比方说1000,是合适的。如果你的程序迅速发布比队列可以容纳的更多的消息,最早进入队列的未发送的消息将被丢弃。
设定节点的循环频率
19// Loop at 2Hz until the node is shut down.
20ros::Raterate ( 2 ) ;
从同一个节点发布关于多个话题的消息,你需要为每个话题创建一个独立的ros::Publisher对象。
创建一个发布者是一个很耗时的操作,所以每当你想发布一个消息就去创建一个新的ros::Publisher对象是很不明智的。
建议为每一个话题创建一个发布者,并且在你程序执行的全过程中一直使用那个发布者。
在pubvel中,我们通过在while循环外面声明发布者来达到这个目的。
如何确定包中有几个类?确定方法的使用标准?
编译前的注意事项p63
如何声明对包的依赖
声明消息类型依赖库
因为pubvel使用了来自geometry_msgs包的消息类型,我们必须声明对这个包的依赖关系,这和3.2.2节中讨论的roscpp依赖库的形式相同。具体而言,除了roscpp,我们必须修改CMakeLists.txt文件的find_package行来声明geometry_msgs:
我们是修改已有的find_package行,而不是新建一行。在package.xml文件中,我们添加新的依赖项:
在编译ROS程序时当你看到找不到头文件的错误,最好确认一下你的功能包的依赖关系。
编写回调函数p65
发布和订阅消息的一个重要的区别是订阅者节点无法知道消息什么时候到达。为了应对这一事实,我们必须把响应收到消息事件的代码放到回调函数里,ROS每接收到一个新的消息将调用一次这个函数。
创建订阅者对象
为了订阅一个话题,我们需要创建一个ros::Subscriber对象12:
ros::Subscriber sub =node_handle.subscribe (topic_name,queue_size, pointer_to_callback_function);
可以通过对函数名使用符号运算符(&,“取址”)来获得函数的指针。
ros::Subscribersub = nh.subscribe ( " turtle1/pose " ,1000, &poseMessageReceived) ;
node_handle 从哪里来?
ros::NodeHandlenode_handle;
这个构造函数有三个形参,其中大部分与ros::Publisher声明中的类似:
node_handle与我们之前多次见到的节点句柄对象是相同的。
topic_name是我们想要订阅的话题的名称,以字符串的形式表示。本例程中是"turtle1/pose"。再次强调,我们忽略了前斜线使其成为相对名称。
queue_size是本订阅者接收消息的队列大小,是一个整数。通常,你可以使用一个较大的整数,例如1000,而不用太多关心队列处理过程。
回调函数的作用
当新的消息到达时,它们会被保存在一个队列中,直到ROS有机会去执行相应的回调函数
可以通过如下两个方法减少订阅者队列溢出的可能性:(1)通过调用ros::spin或者ros:spinOnce确保允许回调发生;(2)减少每个回调函数的计算时间。
给ROS控制权
ros::spinOnce();
这个代码要求ROS去执行所有挂起的回调函数,然后将控制权限返回给我们。另一个方法如下所示:
ros::spin();
这个方法要求ROS等待并且执行回调函数,直到这个节点关机。换句话说,ros::spin()大体等于这样一个循环:
while(ros::ok( ))
{
ros::spinOnce();
}
使用ros::spinOnce()还是使用ros::spin() p77
建议如下:你的程序除了响应回调函数,还有其他重复性工作要做吗?
如果答案是“否”,那么使用ros::spin()---无其他事情要做;
否则,合理的选择是写一个循环,做其他需要做的事情,并且周期性地调用ros::spinOnce()来处理回调。表3.5使用ros::spin(),因为程序唯一的工作就是接收和打印接收到的位姿消息。
计算图源名称
节点、话题、服务和参数统称为计算图源,
每个计算图源由一个叫计算图源名称(graph resource name)的短字符串标识。事实上,计算图源名称在ROS命令行和代码中是广泛存在的,前面我们已经多次接触它们。例如,前面学习的rosnodeinfo命令行工具和ros::init函数都将节点名作为其参数,
rostopic echo命令行工具和ros::Publisher构造函数则都要求提供话题名作为参数,这些都是计算图源名称的具体实例。
这些计算图源名称都属于全局名称,之所以叫做全局名称因为它们在任何地方
是一个全局名称的几个组成部分:p101
前斜杠“/”表明这个名称为全局名称。
由斜杠分开的一系列命名空间(namespace),每个斜杠代表一级命名空间。你可能已经猜到了,命名空间用于将相关的计算图源归类在一起。上述名称例子包含了两个显式的命名空间,分别为turtle1和count_and_log。
ROS允许多层次的命名空间,所以下面这个包含了11个嵌套名称空间的名称也是有效的全局名称,虽然看起来不太可能有人这么用。
/a/b/c/d/e/f/g/h/i/j/k/l
如果没有显式提及所属的命名空间,包括上述三个例子在内,则对应的计算图源名称是归在全局命名空间中的。
描述资源本身的基本名称(base name)。上述例子中的基本名称分别为:teleop_turle、cmd_vel、pose、run_id和
相对名称
使用全局名称时,为了指明一个计算图源,需要完整列出其所属的命名空间
让ROS为计算图源提供一个默认的命名空间,具有此特征的名称叫做相对计算图源名称(ralative graph resource name),或简称为相对名称(relative name)。相对名称的典型特征是它缺少全局名称带有的前斜杠“/”
解析相对名称
将相对名称转化为全局名称的过程相当简单。ROS将当前默认的命名空间的名称加在相对名称的前面,从而将相对名解析为全局名称。
相对名称也可以以一系列的命名空间开始,这些命名空间被看作是默认命名空间中的嵌套空间
举个例子,如果我们在默认命名空间为/a/b/c/d/e/f的地方使用相对空间g/h/i/j/k,ROS将会将其进行下面的组合:
/a/b/c/d/e/f/g/h/i/j/k
设置默认命名空间
理解相对名称的目的
如何在ros 中一次启动多个节点?
基本思想是在一个XML格式的文件内将需要同时启动的一组节点罗列出来
http://wiki.ros.org/roslaunch/XML
当一个节点内的计算图源全部使用相对名称时,这本质上给用户提供了一种非常简单的移植手段,即用户能方便地将此节点和话题移植到其他的(比如用户自己程序的)命名空间,而节点的原设计者并不一定参与这个过程。
这种灵活性可以使得一个系统的组织结构更清晰,更重要的是能够防止在整合来自不同来源的节点发生名称冲突。p104
启动文件 p108
同时启动节点管理器(master)和多个节点
用 launch 批量启动 node
rosrun一次只能启动一个节点,而roslaunch可以同时启动多个节点。
ROS也允许使用不归属于任何功能包的启动文件,此时需要向roslaunch提供启动文件的路径,而不像上面的例子需要功能包名参数
roslaunch有一个可以请求输出详细信息的选项:
roslaunch–v package-name launch-file-name
该信息可以用来观察roslaunch如何解释你的启动文件,有时在调试的时候会有所帮助。
launch文件的语法结构
可以仿照 demo.launch 写 launch文件,内含调用函数库的说明,是否连接real robot 实体,是否loadurdf 文件
代码重映射 p122
用来对现有的节点信息进行更改,使其变成新的翻转或者逆向的指令
在启动文件中包含其他启动文件的内容(包括所有的节点和参数),可以使用包含(include)元素
此处file属性的值应该是我们想包含的文件的完整路径。由于直接输入路径信息很繁琐且容易出错,大多数包含元素都使用查找(find)命令搜索功能包的位置来替代直接输入路径:
但是这种方法在普通的 cpp 程序中不好使
launch 中的启动参数
不太明白到底有啥用处
参数服务器
参数服务器(parameterserver)维护一个变量集的值,包括整数、浮点数、字符串以及其他数据类型,每一个变量用一个较短的字符串标识1,2。由于允许节点主动查询其感兴趣的参数的值,它们适用于配置那些不会随时间频繁变更的信息。
$ rosparam list
参数服务器是节点管理器的一部分,因此,它总是通过roscore或者roslaunch自动启动。在所有情况下,参数服务器都能在后台正常工作,因此没有必要去花心思考虑它。然而,需要铭记的是,所有的参数都属于参数服务器而不是任何特定的节点。这意味着参数——即使是由节点创建的——在节点终止时仍将继续存在。
查询参数 rosparam get parameter_name
使用C++获取参数
void ros::param::set(parameter_name,input_value);
boolros::param::get(parameter_name, output_value);
参数名是一个字符串,它可以是全局的、相对的或者是私有的。set函数中的输入值input_value可以是std::string、bool、int或double类型;get函数的输出值应该是上述某个类型的变量(通过引用传递)。如果参数值读取成功,则get函数返回true;如果出现了问题,通常表示所请求的参数还没有被指定一个值,则该函数返回false。
16 // overriding the default bluecolor .
17 ros::param::set("background_r" , 255) ;
服务调用与消息的区别主要体现在两个方面
服务调用是双向的,一个节点给另一个节点发送信息并等待响应,因此信息流是双向的。作为对比,当消息发布后,并没有响应的概念,甚至不能保证系统内有节点订阅了这些消息。
服务调用实现的是一对一通信。每一个服务由一个节点发起,对这个服务的响应返回同一个节点。另一方面,每一个消息都和一个话题相关,这个话题可能有很多的发布者和订阅者。
列出所有服务通过下面这条指令,可以获取目前活跃的所有服务1:
rosservicelist
当服务调用失败的时候,可以调用ROS_ERROR_STREAM输出错误信息。
声明依赖
前面已经介绍完所有客户端代码相关的知识点。
但是要使catkin_make正确编译一个客户端程序,必须保证程序的功能包声明了对定义服务类型的功能包的依赖。这种依赖,同消息类型的使用一样(参照3.3.3节),
需要编辑CMakeLists.txt和清单文件packag.xml。为了编译例子中的程序,我们必须保证CMakeLists.txt中的find_package行涉及了turtlesim功能包,如下所示:
find_package(catkinREQUIRED COMPONENTS roscpp turtlesim)
在package.xml中,我们应当确保build_depend和run_depend元素中存在相同名称的包,即:
完成这些修改后,多次使用的catkin_make命令应该就能完成程序的编译了。
回调函数的功能及意义 p161
编写服务的回调函数如同订阅一样,节点提供的每一个服务必须关联一个回调函数,服务的回调函数原型如下:
boolfunction_name(
package_name::service_type::Request&req,
package_name::service_type::Response&resp)
{
…
}
节点每次接收到一个服务请求,ROS就执行一次回调函数。参数Request中包含了来自于客户端的数据。回调函数的工作是给Response对象的数据成员赋值。Request和Response数据类型与上面在客户端使用的一致,因此需要相同的头文件和相同的包依赖关系来编译。回调函数返回true表明成功,返回false则表示失败。
录制与回放包文件
rosbag record -a 记录所有信息可能会造成大量的信息,包的存储空间占用很大
rosbag record -j 可以在记录的同时对数据进行压缩
rosbag play
rosbag info
至此 已将此教程过了一遍 2018-2-8