《机器人操作系统(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地址和当前时间生成
  1. 查看roscore的输出结果:setting /run_id to run_id
  2. 向节点管理器查询: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
    • 启动文件内声明时赋值
    1. <arg name="arg-name" default="arg-value"/>  //可被命令行参数覆盖
    2. <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拒绝接受私有名称,即以波浪号~开头的名称
  • 可以利用节点自身的缺省命名空间创建私有名称服务
  1. ros::NodeHandle nhPrivate("~");  //发送给此节点句柄的任意局部名称的缺省命名空间与节点名称一致 (此句柄+相对名称 效果等价于 私有名称)
  2. ros::ServiceServer server=nhPrivate.advertiseService("baz", Callback);  //与广播名为~baz的服务效果相同

注:本例使用ros::spinOnce()而非ros::spin(),原因在于没有服务调用时还需要执行其他工作,即发布速度指令  //与第三章 表3.5 进行对比

(3)解决ros::spinOnce() + sleep()引起的延迟问题

  1. 采用双线程:一个负责发布消息,一个负责处理服务器回调
  2. 采用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仿真:高保真的机器人仿真器

 

posted @ 2019-02-24 17:20  从头再来,不要慌  阅读(2978)  评论(0编辑  收藏  举报