《机器人操作系统(ROS)浅析》肖军浩译
PDF下载地址:
链接:https://pan.baidu.com/s/1l_FAbn_g04q-Z0lKSxx8tw
提取码:8lbp
注:英文原文为《A Gentle Introduction to ROS》 【美】Jason M. O'Kane著
一、绪论
二、入门概述
1. 安装:参考 http://wiki.ros.org/cn
(1)sudo rosdep init 初始化rosdep,rosdep用于检查和安装软件包的依赖
(2)rosdep update 初始化rosdep,在根目录下保存一些文件,文件夹名为.ros
(3)source /opt/ros/ros版本/setup.bash
- 设置ROS_PACKAGE_PATH等环境变量,ROS根据这些环境变量来定位文件 // 输入 export | grep ROS 判断是否配置完成
- setup.bash还定义了一些ros系统的bash函数,如roscd和rosls //这些函数定义在rosbash软件包中
(3)测试ROS是否正确安装
- roscore
- rosrun turtlesim turtlesim_node
- rosrun turtlesim turtle_teleop_key
注:三个终端分别执行指令
2. 常用术语和命令
(1)ROS功能包/软件包:一组用于实现特定功能的相关文件的集合,包括可执行文件和其他支持文件 //任何能找到且包含package.xml文件的目录
注:使用catkin编译生成的可执行文件放在外部标准目录devel中(源码外编译)
(2)节点node:ROS程序的运行实例 running instance
(3)rosrun命令:启动ROS节点 rosrun 功能包名 可执行文件名 //节点名不一定与可执行文件名相同,可以使用 __name:=节点名 显式设置节点名称
- rosrun本质上是一个shell脚本,可以根据ROS的文件组织结构自动找到相应可执行文件 //可以直接输入可执行文件的路径启动节点(等价的普通程序执行方式)
- 通过节点管理器master注册成为ROS节点发生在程序的源码内部,而不是通过rosrun命令
注:节点名称必须唯一,不可多个节点拥有相同名称
(4)消息传递机制:节点管理器负责确保发布节点和订阅节点能找到对方,随后消息从发布节点直接传递到订阅节点,不经过节点管理器转交
注:节点只管发,不管收,有助于减少节点之间的耦合度;话题可共享,话题和消息的通信机制是多对多的;服务是一对一的通信机制
(5)rosmsg show 消息类型名 //输出消息类型的详细信息
(6)rostopic pub 话题名 消息类型 消息内容 //命令行手动发布消息
注:消息内容可以按照rosmsg show命令的显示结果进行输入,也可以以YAML字典的形式输入(显式指明结构域中变量名和值的映射关系)
(7)每个消息类型均属于一个特定的包,如turtlesim/Color属于turtlesim包 //消息类型斜杠/前的是包名,斜杠/后是类型名
注:把包名包含在消息类型中的目的:
- 能避免命名冲突 // geometry_msgs/Pose 和 turtlesim/Pose
- 在用到其他包的消息类型时,更容易表明包之间的依赖关系
- 有助于理解消息类型的含义
三、编写ROS程序
1. catkin编译系统试图一次性编译同一个工作区中的所有功能包
2. 创建功能包:catkin_create_pkg 包名 //包名只允许使用小写字母、数字和下划线,且首字符必须为字母
3. 清单文件package.xml中声明的依赖库并不会在编译过程中进行检查,但是会在将包发布给他人时,他人会因为没有安装依赖库而编译报错
4. CMakeLists.txt中的${catkin_LIBRARIES}变量由find_package(catkin REQUIRED COMPONENTS ...)语句定义
5. source devel/setup.bash //专门为当前的工作区进行环境变量设置
6. 每个话题都有一个消息类型,而每个消息类型都有相应的C++头文件 //包含消息类型头文件 #include<包名/类型名.h> 如#include<geometry_msgs/Twist.h>
注:头文件定义了一个与消息类型对应的C++类,且该类定义在以包名命名的域名空间中,如geometry_msgs::Twist类
7. ros::NodeHandle类维护一个引用计数,仅在第一个NodeHandle对象创建时才会在节点管理器注册新的节点 //使用标准的roscpp接口,在单个程序中无法运行多个节点
8. 创建发布者ros::Publisher属于耗时操作,应该每个话题创建一个发布者,在程序执行过程中一直使用相应发布者 //发布者创建不要放在循环内,避免为每条消息生成新的发布者
9. ros::ok()返回false条件:
- 使用rosnode kill命令终止节点
- 使用Ctrl+C发送终止信号 //需要采用rosnode cleanup从节点管理器的列表中删除节点
- 调用ros::shutdown() //在代码中显式表明节点工作已完成
- 以相同节点名启动新的节点 //新的节点将正常运行,而旧节点会被终止
10. 订阅者回调函数:void function_name(const package_name::type_name &msg){ ... }
- 调用回调函数是ROS执行,返回值也交给ROS,用户程序无法获得返回值,因此设置为void
- 回调函数指针为 &function_name,如程序中的 &poseMessageReceived // 取地址符&是可选的,但建议使用,编译器会根据函数名后面是否有括号,自动判断是指针还是函数调用
11. 显式调用 ros::spin()或ros::spinonce()让ROS执行回调函数
四、日志消息 //ROS使用log4cxx库实现日志功能
1. ROS日志系统的核心思想:使程序生成一些简短的文本字符流,即日志消息
2. 严重级别(递增):DEBUG、INFO、WARN、ERROR、FATAL
3. 产生日志消息的基本C++宏
- ROS_DEBUG_STREAM(message)
- ROS_INFO_STREAM(message)
- ROS_WARN_STREAM(message)
- ROS_ERROR_STREAM(message)
- ROS_FATAL_STREAM(message)
注:日志系统是面向行的,调用任意宏会生成完整的一行日志消息 //无需使用std::endl
4. 日志消息的输出
(1)控制台
- DEBUG和INFO消息被送至标准输出,WARN、ERROR和FATAL消息被送至标准错误
- 设置ROSCONSOLE_FORMAT环境变量调整日志消息打印到控制台的格式
- roslaunch工具默认不会将节点的标准输出和标准错误导入至自己的输入流,需显式使用 output="screen"属性或者--screen参数
(2)/rosout 话题:消息类型为rosgraph_msgs/Log
- 包含系统中所有节点的日志消息
- 查看消息内容:rostopic echo /rosout 或者 rqt_console
- rqt_console节点订阅/rosout_agg话题 //后缀_agg表示消息是经过rosout节点聚合的结果
- /rosout话题发布的消息都通过rosout节点输出到/rosout_agg话题
- rosout是唯一一个订阅/rosout话题的节点,也是/rosout_agg话题唯一的发布者 //减少调试代价
注:rosout即表示话题,也表示节点
(3)日志文件:作为/rosout话题回调函数的一部分,由rosout节点生成
- 文件名类似于:~/.ros/log/run_id/rosout.log //纯文本文件,用less、head、tail等命令查看
- 查看运行标识码run_id //节点管理器开始运行时基于MAC地址和当前时间生成
- 查看roscore的输出结果:setting /run_id to run_id
- 向节点管理器查询:rosparam get /run_id //run_id存放在参数服务器中
检查日志文件大小:rosclean check
删除日志文件:rosclean purge
5. 设置日志级别
(1)通过命令行设置:rosservice call /node-name /set_logger_level ros.包名 level // level参数包括DEBUG、INFO、WARN、ERROR、FATAL
注:参数ros.包名指定期望配置的日志记录器logger名称
(2)通过图形界面设置:rqt_logger_level
(3)通过C++代码设置:调用ROS实现日志功能的log4cxx提供的接口 #include<log4cxx/logger.h>
五、计算图源命名
1. 计算图源graph resource:节点、话题、服务和参数的统称,由短字符串表示的计算图源名称进行标识
2. 全局名称:/turtle1/cmd_vel //优点:任何地方都可以使用;缺点:需要完整列出其所属的命名空间
- 由前斜杠"/"作为首字符
- 由斜杠分开一系列命名空间,每个斜杠代表一级命名空间,如turtle1命名空间
- 命名空间用于将相关的计算图源归类在一起,ROS允许多层命名空间嵌套
- 最末为基本名称base name,如cmd_vel
3. 相对名称 //利用ROS提供的默认命名空间
(1)典型特征:缺少全局名称带有的前斜杠"/"
(2)解析相对名称:将当前的默认命名空间名称加在相对名称之前,生成全局名称,如 /turtle1 + cmd_vel => /turtle1/cmd_vel
(3)设置默认命名空间:单独为每个节点设置 //不设置,则使用全局命名空间"/"作为默认命名空间
- 在启动文件中使用命名空间ns属性
- 利用命令行参数 __ns:=default-namespace
- 利用环境变量设置shell:export ROS_NAMESPACE=default-namespace //仅当未设置__ns参数时才有效
优点:避免在移植和整合来自不同来源的节点时发生名称冲突
4. 私有名称 //用于节点内部仅与本节点有关的资源,如参数
(1)典型特征:以一个波浪字符(~)开始
(2)与相对名称一致,不能完全确定自身所在命名空间,需要利用ROS客户端库进行名称解析
(3)不使用当前的默认命名空间,而采用节点名称作为命名空间,如 /sim1/pubvel + ~max_vel => /sim1/pubvel/max_vel
(4)私有名称可用于管理节点的服务,话题不能命名为私有名称
注:私有名称的关键字"private"仅表明不使用所在的命名空间,其他节点可以通过私有名称解析后的全局名称进行访问 //仅在命名空间层面上有意义,注意与其他编程语言的不同
5. 匿名名称:非用户指定的无语义信息的名字
(1)一般用于为节点命名,使节点的命名保存唯一性
(2)调用ros::init方法时请求一个自动分配的唯一名称
- ros::init(argc, argv, base_name, ros::init_options::AnonymousName);
注:ros::init使用处理器时间在节点的基本名称后追加文本,保证名字的唯一性
六、启动文件
1. 目的:利用启动文件一次性配置和运行多个节点
2. 执行启动文件:roslaunch 包名 启动文件名 //如果没有运行roscore,roslaunch会自动启动roscore
(1)如果启动文件不属于任何功能包,则可以直接以启动文件路径启动 roslaunch + 启动文件路径
(2)启动文件内的节点几乎是同一时刻启动,无法确定启动顺序,因此节点之间应尽量保持独立
(3)添加-v选项可以输出详细信息:roslaunch -v 包名 启动文件名
(4)查找启动文件时,roslaunch工具会同时搜索每个功能包目录的子目录
3. 启动文件基本元素
(1)根元素:<launch> ... </launch>
(2)启动节点:
<node
pkg=包名
type=可执行文件名
name=节点名
/>
- 标签末尾的斜杠"/"不可少,另一种方式为 <node pkg="..." type="..." name="..."></node> //显式给出结束标签
- 必需属性pkg,type,name(会覆盖ros::init设置的名称)
- 默认情况下,启动文件启动节点的标准输出被重定向至日志文件~/.ros/log/run_id/node_name-number-stout.log中
- 在节点元素中配置属性output="screen"可以在控制台终端输出信息
- 使用--screen 命令行选项在控制台显式所有节点输出:roslaunch --screen 包名 启动文件名
(3)请求复位:respawn="true" //当节点停止时,roslaunch会重新启动该节点,可用于应对软件崩溃、硬件故障等引起的节点中止
(4)必要节点:required="true" //当必要节点中止时,roslaunch会终止所有其他活跃节点,并退出;与respawn作用相互矛盾,不能同时配置
(5)roslaunch所有节点共享一个终端,针对依赖控制台输入的节点来说,需要维护独立的终端:使用启动前缀属性 launch-prefix="命令行前缀"
- roslaunch启动节点时调用相应的命令行工具rosrun
- 启动前缀相当于在rosrun前添加前缀,即示例中的launch-prefix="xterm -e" 等价于 xterm -e rosrun turtlesim turtle_teleop_key
3. 在命名空间内启动节点 //通过配置节点元素的ns属性,压入命名空间
(1)尽管两个节点相对名称相同,但是由于命名空间不同,所以两个节点相互独立
(2)由于话题名称定义时采用的也是相对名称,所以话题也分别位于独立的命名空间
4. 名称重映射remapping names //从更精细的层面控制节点名称的修改
(1)基于替换的思想:每个重映射包含一个原始名称和一个新名称,每当节点使用原始名称时,ROS客户端库会将它自动替换为新名称
(2)创建重映射
- 命令行启动时,分别给出原始名称和新名称 原始名称:=新名称 //rosrun turtlesim turtlesim_node turtl1/pose:=tim
- 启动文件中,使用重映射元素: <remap from="original-name" to "new-name"/> //若属性作为launch元素的子元素出现在顶层,则应用到所有后续节点
注:重映射元素也可以作为节点的子元素:<node ...> <remap from="original-name" to "new-name"/> </node> 此时只应用于所在节点
注:
- ROS在应用任何重映射之前,所有的名称需要先解析为全局名称
- 应用示例:反向海龟 //将原始速度话题重映射为处理后的反向速度话题
5. 其他元素
(1)启动文件的嵌套:include元素
- <include file="启动文件完整路径"/> //通常使用find命令搜索功能包位置,避免路径输入错误,如<include file="$(find package-name)/launch-file-name"/>
- 支持命名空间属性,可以将包含内容压入指定命名空间:<include file="..." ns="namespace"/>
(2)启动参数argument:便于配置启动文件,类似可执行程序中的局部变量(仅在声明的当前启动文件内有效,不可被包含的启动文件继承)
- 声明参数:<arg name="arg-name"/> //声明是可选的,有助于明确启动文件的参数有哪些
- 参数赋值
- 命令行赋值 roslaunch 包名 启动文件名 arg-name:=arg-value
- 启动文件内声明时赋值
- <arg name="arg-name" default="arg-value"/> //可被命令行参数覆盖
- <arg name="arg-name" value="arg-value"/> //不可被命令行参数覆盖
- 获取参数值:$(arg arg-name)
- 向包含的次级启动文件中发送参数值
- 将arg元素作为include元素的子元素 <include ...> <arg name="arg-name" value="arg-value"/> </include>
- 可使用<arg name="arg-name" value="$(arg arg-name)"/> 保证内外同名参数具有相同的参数值
注:在ROS中,parameter和argument是有区别的
- parameter是ROS系统运行过程中使用的数值,存储在参数服务器parameter server中,可以被节点和用户获取(ros::param::get或rosparam)
- argument只在启动文件内有意义,不能被节点直接获取
(3)创建组group:大型启动文件内管理节点的快捷方式
- 把若干节点放入同一命名空间中:<group ns="namespace"> ... </group>
- 可以有条件地启动或禁用一个节点:<group if="0 or 1"> ... </group> //若属性为1,则正常启动,否则忽略组标签内元素;若 if 改为 unless 则用法相反
注:仅有0和1为合法取值,且不能使用布尔运算;也可以不使用组元素,单独为每个节点设置ns、if和unless
七、参数parameter //配置节点信息的集中式方法
1. 主要思想:使用集中的参数服务器维护一个变量集的值(字典),适用于不会随时间频繁变更的信息
2. 通过命令行获取参数
(1)rosparam list //查看参数列表,输出 /rosdistro 等全局计算图源的名称字符串
(2)rosparam get parameter_name //查询参数,如 rosparam get /rosdistro 得到ROS版本
(3)rosparam get namespace //检索给定命名空间每个参数的值,如 rosparam get / 查询全局命名空间/
(4)rosparam set parameter_name parameter_value //设置参数,修改已有参数的值或创建新参数
(5)rosparam set namespace values //设置同一命名空间的多个参数,其中值values要以YAML字典的形式给出参数和值的对应关系
(6)创建和加载参数文件(YAML形式) //可用于场景复现
- rosparam dump filename namespace //存储命名空间中的所有参数
- rosparam load filename namespace //读取参数至参数服务器
注:命名空间参数可选,默认为全局命名空间/
注:
- 所有的参数都属于参数服务器,而不是任何特定节点,即使节点终止时参数仍然存在
- 更新的参数值不会自动“推送”到节点,节点需要主动显式地向参数服务器查询参数值
3. 使用C++接口获取参数 //参数名可以是全局的、相对的或者私有的
void ros::param::set(parameter_name, input_value);
bool ros::param::get(parameter_name, output_value); //读取成功返回true,否则表明参数还未指定值
注:在命令行中实现私有参数赋值 rosrun agitr pubvel_with_max _max_vel:=1 //参数max_vel前面添加下划线_前缀 _param-name:=param-value
4. 在启动文件中设置参数
(1)<param name="param-name" value="param-value"/> //设置参数,参数名是相对名称
(2)<node ...> <param name="param-name" value="param-value"/> </node> //在节点元素内包含param元素,无论是否以~或/开头,参数均为节点私有参数
(3)<rosparam command="load" file="path-to-param-file"/> //一次性从文件中加载多个参数,等价于rosparam load
注:通常采用 file="$(find package-name)/param-file" 指定文件路径
八、服务service
1. 与话题消息的区别
- 服务调用是双向的
- 服务调用是一对一通信
2. 执行过程:客户端节点发送请求数据到服务器节点,并且等待回应;服务器节点收到请求后,执行相应活动,随后发送响应数据给客户端节点;
3. 与话题内容由消息数据类型确定类似,服务的内容也由服务数据类型决定
4. 从命令行查看和调用服务
(1)rosservice list //列出所有服务
(2)服务一般可以划分为两类:
- 从特定节点获取或向其传递消息 //通常采用节点名作为命名空间以防止命名冲突,如采用私用名称的~get_loggers服务和~set_logger_level服务会被解析为/turtlesim/get_logger和/turtlesim/set_logger_level
- 不针对某些特定节点的服务 //例如/spawn服务用于生成一个新的仿真海龟,尽管由turtlesim节点提供,但是不属于任何特定节点(只需要完成相应功能,而不关心是哪个节点在起作用)
5. rosnode info node-name //查看某个节点的服务类型
6. rosservice node service-name //查找提供服务的节点(反向查询)
7. rosservice info service-name //确定服务的数据类型,通常也是由包名和类型名组成“包名/类型名”,如turtlesim/Spawn
8. rossrv show 服务数据类型名 //获取服务数据类型的详细信息(一系列数据域),注意rosservice和rossrv的区别
注:
- 短横线---之前为请求的数据项,之后的字段为响应项
- 服务的请求和响应字段都可以为空,如turtlesim_node提供的/reset服务的数据类型为std_srvs/Empty,其请求和响应字段均为空
9. rosservice call service-name request-content //调用服务,提供请求域的所有值,如rosservice call /spawn 3 3 0 Mikey 在位置(3, 3)处创建一个名为"Mikey"的新海龟,朝向为0
注:
- 新海龟有自己的资源集,且位于新命名空间Mikey中 //避免命名冲突
- rosservice call的输出为服务器节点的响应数据,如上例为 name: Mikey //调用成功或失败
- rosservice call /clear 从参数服务器读取参数值,刷新当前参数
10. 客户端程序
(1)声明请求和响应类型:#include<turtlesim/Spawn.h> //包含头文件,定义turtlesim::Spawn类(服务数据类型)
(2)创建客户端对象:ros::ServiceClient client = node_handle.serviceClient<service_type>(service_name) //服务名称一般应当为相对名称,如"spawn"
注:与话题发布者不同,无需缓冲队列,会阻塞等待直至服务调用完成
(3)创建请求和响应对象 //上述头文件中定义了请求和响应的类,通过包名和服务类型引用,如turtlesim::Spawn::
- package_name::service_type::Request //请求对象类,应做赋值操作
- package_name::service_type::Response //响应对象类,接收服务器响应消息,无需赋值
注:服务类型的头文件中还定义了一个单独类package_name::service_type,同时拥有Request和Response作为数据成员;可以定义单独对象package_name::service_type srv,而不使用独立的Request和Response对象
(4)调用服务:bool success = service_client.call(request, response)
- 完成定位服务器节点、传输请求数据、等待响应和存储响应数据等一系列工作
- 调用方法返回布尔值来确认调用服务是否成功完成
注:
- 不要忘记对调用返回值进行判断,可通过ROS_ERROR_STREAM输出可能的错误信息
- 默认情况下,调用返回后,会关闭与服务器的连接;
- 可通过ros::ServiceClient client = node_handle.serviceClient<service_type>(service_name, true)建立持续连接的客户端 //提高了节点耦合性,系统鲁棒性变差,不建议
11. 服务器程序
(1)编写回调函数 bool function_name(package_name::service_type::Request &req, package_name::service_type::Response &resp)
注:本例中请求和响应的数据类型均为std_srvs/Empty(空字符串),无需进行数据处理
(2)与话题订阅者类似,需要创建服务器对象ros::ServiceServer server=node_handle.advertiseService(service_name, pointer_to_callback_function);
- service_name 建议使用局部名称,但不严格限制使用全局名称
- ros::NodeHandle::advertiseService拒绝接受私有名称,即以波浪号~开头的名称
- 可以利用节点自身的缺省命名空间创建私有名称服务
- ros::NodeHandle nhPrivate("~"); //发送给此节点句柄的任意局部名称的缺省命名空间与节点名称一致 (此句柄+相对名称 效果等价于 私有名称)
- ros::ServiceServer server=nhPrivate.advertiseService("baz", Callback); //与广播名为~baz的服务效果相同
注:本例使用ros::spinOnce()而非ros::spin(),原因在于没有服务调用时还需要执行其他工作,即发布速度指令 //与第三章 表3.5 进行对比
(3)解决ros::spinOnce() + sleep()引起的延迟问题
- 采用双线程:一个负责发布消息,一个负责处理服务器回调
- 采用ros::spin(),并利用计数器回调函数(timer callback)发布消息 // http://wiki.ros.org/roscpp/Overview/Timers
九、消息录制与回放
1. rosbag:将发布在一个或多个话题上的消息录制到一个包文件中,可用于事后回放
注:术语包文件bag files指用于存储带时间戳的ROS消息的特殊文件格式
2. 命令行工具
(1)录制包文件:rosbag record -O filename.bag topic-names //不指定文件名时,rosbag基于当前的日期和时间自动生成文件名;会创建新节点 /record_...
- rosbag record -a 记录当前发布的所有话题消息
- rosbag record -j 启动包文件的压缩
(2)回放包文件:rosbag play filename.bag //保持与原始发布相同的顺序和时间间隔
注:尽量避免系统中rosbag和“真实”节点向同一个话题发布消息
(3)检查文件包:rosbag info filename.bag //提供包文件的丰富信息
3. rosbag功能包
(1)rosrun rosbag record -O filename.bag topic-names;rosrun rosbag play filename.bag //利用record和play可执行文件
(2)在启动文件中使用包文件
- 录制节点:<node pkg="rosbag" name="record" type="record" args='-O filename.bag topic-names"/>
- 回放节点:<node pkg="rosbag" name="play" type="play" args="filename.bag"/>
注:需要提供参数
第十章 总结
1. 在网络环境中运行ROS:分布式机器人控制模式
(1)网络层配置:确保计算机之间能够互相通信
(2)ROS层配置:确保所有节点都能与节点管理器通信
2. 编写更规范的程序,如使用ros::Timer的回调函数替代ros::Rate对象
3. 使用rviz是数据可视化:订阅用户话题以显示消息
4. 创建消息和服务类型
5. 使用tf工具来管理多个坐标系:帮助节点完成坐标转换
6. 使用Gazebo仿真:高保真的机器人仿真器