机器人操作系统(ROS)浅析
这是看了《A Gentle Introduction to ROS 》这本书后记的笔记,网上刚好找得到中文版的,就看中文版了,欢迎大佬批评指正,如需书的pdf可留下邮箱。
1.文件架构
工作空间
- 顾名思义就是进行操作的地方,放置有编译空间,
- 初始化工作空间:在src文件夹下创建了一个 CMakeLists.txt 的文件,目的是告诉系统,这个是ROS的工作空间
为了可以正常使用它,我们需要设置环境变量,让环境变量在所有终端都可以使用
- src文件下放置你所需要的功能包
功能包
- 功能包的创建
- 会自动生成src文件和CMakeLists.txt、package.xml文件
catkin_creat_package [功能包名字] [依赖]
依赖也可以在CMakeLists.txt、package.xml文件中进行添加
- package.xml
- 我们可以在这里添加我们需要的运行依赖、编译依赖
- CMakeLists.txt
- 规定了依赖哪些功能包,编译生成什么文件,如何编译等流程
- 当我们在工作空间目录下进行编译时,catkin编译系统首先会找到每个package下的 CMakeLists.txt ,然后按照规则来编译构建。
2.节点
2.1节点管理器ROS Master
作用
- ROS的通讯是基于节点来实现的,而ROSMaster起到了统一管理的作用
- 节点之间需要能够互相通信,而实现这一步最关键的就是ROSMaster
使用
- 必须roscore启动节点管理器
- 使用rosrun启动节点只能开启一个节点,后面将会介绍roslaunch来启动节点,与rosrun不同的是,launch文件可以启动同时启动多个节点,并且会开启节点管理器
2.2节点
ROSwiki:一旦启动roscore后,便可以运行ROS程序了。ROS程序的运行
实例被称为节点(node)
使用rosrun命令来启动节点
rosrun [功能包名称] [节点名称(一般是生成的可执行文件)]
- 如果该节点是发布消息的,那么就需要定义好消息的话题,数据类型
- 如果该节点是接受消息的,那么就需要写明要接受的话题
查看节点
查看节点列表 rosnode list
查看特定节点 rosnode info node-name
终止节点 rosnode kill node-name
查看节点构成图
rqt_graph
在这个命令中,r 代表 ROS,qt 指的是用来实现这个可视化程序的 Qt 图形界面(GUI)工具包。输入该命令之后,你将会看到一个图形界面,其中大部分区域用于展示当前系统中的节点。
- 注意:rqt_graph本身就是一个节点
3.话题和消息
节点管理器的作用的地方就是节点与节点之间的话题和消息
- 获取当前活跃话题:rostopic list
- 为 了 查 看 某 个 话 题 上 发 布 的 消 息 :rostopic echo topic-name
- 查看消息类型:rosmsg show message-type-name
4.编写ROS程序
1.Hello_ROS
1.创建工作空间和功能包
2.在工作空间下创建src文件来放置功能包
3.按照自己需要的功能编写代码,注意:
- 头文件需要包含ros/ros.h
- 如果包含了消息类型就需要包含指定的头文件
4.代码完成后开始配置文件
- CMakeLists.txt
- 需要写明文件包含了哪些头文件
- 添加文件编译选项
- package.xml
- 添加文件的编译运行依赖
5.进行编译
6.运行
-
roscore启动节点管理器
-
rosrun启动节点
贴上代码
// This is a ROS version of the standard "hello , world"
// program.
// This header defines the standard ROS classes .
#include <ros/ros.h>
int main ( int argc , char **argv ) {
// Initialize the ROS system .
ros::init ( argc , argv , "hello_ros" ) ;
// Establ ish this program as a ROS node .
ros::NodeHandle nh ;
// Send some output as a log message .
ROS_INFO_STREAM( "Hello,ROS!" ) ;
}
2.发布者程序
2.1代码部分
- 因为该程序中包含了消息类型,因此我们需要添加头文件
- 用双分号(::)来区分开包名和类型名,双分号也称为范围解析运算符
- main函数中首先进行初始化,并定义节点名称
- 接着创建句柄
- 句柄的作用是为了更方便的开启和关闭节点
- 创建发布者对象
- ros::Publisher pub = node_handle.advertise<message_type>(
topic_name, queue_size); - 队列长度不是指存放那个多少字节,而是发布消息序列的大小
- ros::Publisher pub = node_handle.advertise<message_type>(
- 进入循环( ros::ok () 检测的是程序作为 ROS 节点是否仍处于运行良好的状态。它会一直返回 true)
- 创建消息对象
- 发布消息
- 定义消息发布格式
2.2编译部分
修改两个文件中的声明
贴上代码
// This program publishes randomly−generated velocity
// messages for turtlesim .
#include <ros/ros.h>
#include <geometry_msgs/Twist.h> // For geometry_msgs:: Twist
#include <stdlib.h> // For rand() and RAND_MAX
int main ( int argc , char **argv ) {
// Initialize the ROS system and become a node .
ros::init ( argc , argv , "publish_velocity" ) ;
ros::NodeHandle nh ;
// Create a publisher obj ect .
ros::Publisher pub = nh.advertise <geometry_msgs::Twist >(
"turtle1/cmd_vel" , 1000) ;
// Seed the random number generator .
srand ( time ( 0 ) ) ;
// Loop at 2Hz until the node is shut down.
ros::Rate rate(2);
while ( ros::ok () ) {
// Create and fill in the message . The other four
// fields , which are ignored by turtl esim , default to 0.
geometry_msgs::Twist msg;
msg.linear.x = double (rand())/double(RAND_MAX) ;
msg.angular.z = 2*double(rand())/double(RAND_MAX)-1;
// Publish the message .
pub.publish(msg);
// Send a message to rosout with the details .
ROS_INFO_STREAM( "Sending random velocity command : "
<< " linear=" << msg.linear.x
<< " angular=" << msg.angular.z) ;
// Wait untilit's time for another iteration .
rate.sleep ( ) ;
}
}
3.订阅者程序
与发布者差不多,下边介绍发布者中没有的内容
- 回调函数:订阅者功能实现的地方
给ROS控制权
- ros::spinOnce():要求ROS执行所有挂起的回调函数,然后返回控制权
ros::spin():要求 ROS 等待并且执行回调函数,直到这个节点关机。
换句话说,ros::spin()大体等于这样一个循环:
while(ros::ok( ))
{
ros::spinOnce();
} - 使用 ros::spinOnce()还是使用 ros::spin()的建议如下:你的程
序除了响应回调函数,还有其他重复性工作要做吗?如果答案是
“否”,那么使用 ros::spin();否则,合理的选择是写一个循环,做
其他需要做的事情,并且周期性地调用 ros::spinOnce()来处理回调。
贴上代码
// This program subscribes to turtle1/pose and shows its
// messages on the screen .
#include <ros/ros.h>
#include <turtlesim/Pose.h>
#include <iomanip> // for std::setprecision and std::fixed
// A callback function . Executed each time a new pose
// message arrives .
void poseMessageReceived (const turtlesim::Pose &msg ) {
ROS_INFO_STREAM( std::setprecision(2) << std::fixed
<< "position =("<< msg.x << "," << msg.y << ")"
<< "*direction=" << msg.theta ) ;
}
int main ( int argc , char **argv ) {
// Initialize the ROS system and become a node .
ros::init ( argc , argv , "subscribe_to_pose" ) ;
ros::NodeHandle nh ;
// Create a subscri ber object .
ros::Subscriber sub = nh.subscribe ("turtle1/pose" ,1000,
&poseMessageReceived ) ;
// Let ROS take over .
ros::spin ( ) ;
}
5.日志消息
1.级别
DEBUG、INFO、WARN、ERROR、FATAL
- 级别旨在提供一种区分和管理日志消息的全局方法
2.生成日志消息
-
生成简单的日志消息
#这些都是宏 ROS_DEBUG_STREAM(message); ROS_INFO_STREAM(message); ROS_WARN_STREAM(message); ROS_ERROR_STREAM(message); ROS_FATAL_STREAM(message);
-
生成一次性日志消息
ROS_DEBUG_STREAM_ONCE(message); ROS_INFO_STREAM_ONCE (message); ROS_WARN_STREAM_ONCE (message); ROS_ERROR_STREAM_ONCE (message); ROS_FATAL_STREAM_ONCE (message);
3.查看日志消息
rqt_console:显示所有结点的日志消息
4.检查和清除日志文件
-
查看当前账户中被ROS日志消耗的硬盘空间 :rosclean check
-
如果日志正在消耗过多的硬盘空间,可以通过下面的命令删除所有已经存在的日志:rosclean purge
6.launch文件
launch文件的作用
-
同时启动节点管理器和多个节点
-
启动方式
roslaunch package-name launch-file-name
如何编写launch文件
1.根元素
<launch>
...
<launch>
2.写节点
<node
pkg=package-name”
type=executable-name”
name=node-name”
/>
注意:如果有remap或在param元素这样的子节点,就需要加上</node>标签,称为显式结束标签
<node pkg=”...” type=”...” name=”...”></node>
3.节点中的元素
- pkg、type就不在这多讲了
- name给节点指派了名称,可以覆盖其他赋予节点的名称
- output=screen” 可以在控制台中输出信息(等校于roslaunch –screen package-name launch-file-name)
- respawn=“true” 请求复位,可以在节点停止的时候重启该节点,常常用于必要节点
- required=”true”在一个必要节点关闭的时候可以关闭其他节点
launch文件的缺点:对于launch文件启动的节点是共用一个终端的,如果有需要控制台控制的节点则建议使用rosrun单独启动,例如键盘控制节点
-
节点元素使用启动前缀(launch-prefix)属性:launch-prefix=“command-prefix”
-
xterm–e rosrun turtlesim turtle_teleop_key,xterm-e会开启一个终端窗口来运行rosrun的代码
4.包含其他launch文件
<include file="path-to-launch-file”>(这里路径需要完整路径,建议使用下边这种方法)
或者
<include file=$(find package-name)/launch-file-name”>
5.启动参数(这个目前没怎么用过但还是记一下笔记吧)
- 我们可以在roslaunch命令行中修改参数,但是只能改default的值,改不了value的值,因此有了下边这种写法
<incluce file=”path–to-launch-file”>
<arg name=”arg-name” value=”arg-value”/>
...
</include>
6.创建组
- 可以将许多节点放在 同一命名空间下,写法为
<group ns=”namespace”/>
...
</group>
- 且组可以有条件的使能或者禁用一个节点
<group if=”0 or 1”/>
</group>
在这里,如果if的属性值是1,那么该标签内元素就正常包含,反之会被忽略
unless意思相反
注意:1.一般不会直接设置为0或1,灵活运用启动参数,丰富启动文件的可配置性
2.组不是必须的,当然我们也可以单独为每个节点配置if,ns等属性
3.有些属性不能通过组来传递,例如output=”screen”,我们需要单独为节点设置该属性
- csdn上看到这边笔记也很棒:launch文件
7.参数
1.基础操作
- 查看参数列表:rosparam list
- 查询某个参数的值:rosparam get /参数
- 例如:rosparam get /rosistro 得到的是ros的操作版本
- 设置参数:rosparam set 参数名 值
同时设置一个命名空间中的几个参数
rosparam set /duck_colors "huey: red
dewey: blue
louie: green
webby: pink"
注意:第一个引号告诉bash 命令尚未完成,因此按下回车时终端将会插入一个换行符而不是执行命令
- 创建参数文件:rosparam dump filename namespace
- 加载参数文件:rosparam load filename namespace(namespace默认是全局命名空间/)
2.案例
- 设置背景颜色后,只有调用turtlesim_node的/clear服务后才会从参数服务器读取参数的值
- 调用该服务:rosservice call /clear
3.使用ROS参数的C++接口
void ros::param::set(parameter_name, input_value);
bool ros::param::get(parameter_name, output_value);
4.启动文件中设置参数
<node
pkg="agitr"
type="pubvel_with_max"
name="publish_velocity"
/>
<param name="max_vel" value="3" />
</node>
8.服务
- 服务调用是双向的,节点消息发出去后会等待响应
- 实现的是一对一的通讯,每个服务由一个节点发起,响应也返回同一个节点。每一个消息都和一个话题相关,该话题可能有很多个发布者和订阅者
1.概念
客户端(client):发送请求到服务器节点等待响应
服务端(server):接受到请求后作出响应,响应可以是配置,改变某些行为
请求和响应携带的内容由数据类型决定,数据类型分为两部分:客户端和服务端
2.服务查询
rosservice list:查询活跃的服务,输出的服务名称是全局名称
rosnode info node-name:查看某个节点的服务类型
rosservice node service-name:查看服务是由哪个节点发出的
rosservice info service-name:查看服务的数据类型
roscrv show service-date-type-name:查看服务数据类型的详情(即发布和响应的数据字段,字段可以同时为空)
-
注意区分rosservice和rossrv的区别
Topics Services active things rostopic rosservice date types rosmsg rossrv
3.两类服务类型
- 一些服务:由特定节点获取或传递信息,例如get_loggers 和 set_logger_level
- 其他服务:不针对某些特定节点的服务,我们调用该服务时只关心服务的结果,而不关心服务是由哪个节点产生的
4.从命令行调用服务
-
rosservice call service-name request-content
例如:rosservice call /spawn 3 3 0 Mikey 这条服务调用的效果是在现有仿真器中,位置 ( x , y ) = (3,3) 处创建一个名为“Mikey”的新海龟,其朝向角度 θ = 0 。这些新的资源都在新的命名空间Mikey中,很好的阐明了命名空间可以有效的阻止命名冲突
5.客户端程序
1.声明请求和响应的类型
#include <package_name/type_name.h>
2.创建客户端对象
首先ros::init和创建NodeHandle句柄
ros::ServiceClient client = node_handle.serviceClient<service_type>(
service_name);
- node_handle 是常用的 ros::NodeHandle 对象,这里我们将调用它的 serviceClient 方法
- service_type 是我们在上面头文件中定义的服务对象的数据类型
- service_name 是一个字符串,说明了我们想要调用的服务名称。再次强调,这应当是一个相对名称,虽然也可以声明为全局名称。
与发布者不同的是,服务不需要设置队列大小,服务调用直到响应后才会返回,所以不用维持发送状态
3.创建请求和响应对象
定义请求和响应的类,命名为Request和Response
4.声明依赖
编辑 CMakeLists.txt 和清单文件 packag.xml
贴上代码
// This program spawns a new turtlesim turtle by calling
// the appropriate service .
#include <ros/ros.h>
// The srv class for the service .
#include <turtlesim/Spawn.h>
int main( int argc , char ** argv ) {
ros::init( argc , argv , "spawn_turtle") ;
ros::NodeHandle nh ;
// Create a client object for the spawn service . This
// needs to know the data type of the service and its
// name.
ros::ServiceClient spawnClient
= nh.serviceClient <turtlesim::Spawn>("spawn") ;
// Create the request and response objects.
turtlesim::Spawn::Request req ;
turtlesim::Spawn::Response resp ;
// Fill in the request data members.
// 不能在这里给Response对象赋值
req.x = 2;
req.y = 3;
req.theta = M_PI/2;
req.name = "Leo" ;
// Actually c a l l the service. This won't return until
// the service is complete .
// 这一步是在调用服务
bool success = spawnClient.call( req , resp ) ;
// Check for success and use the response .核对服务调用的返回值
i f ( success ) {
ROS_INFO_STREAM("Spawned␣ a␣ turtle␣ named␣ "
<< resp.name) ;
} else {
ROS_ERROR_STREAM("Failed␣ to␣ spawn.") ;
}
}
6.服务器程序
- 当接受到请求时,进入回调函数,Request来自于客户端的数据
1.编写服务的回调函数
bool function_name(
package_name::service_type::Request &req),
package_name::service_type::Response &resp)
) {
...
}
当回调函数返回true表明成功,false表明失败
2.创建对象
ros::ServiceServer server = node_handle.advertiseService(
service_name,pointer_to_callback_function);
最后一个参数是指向回调函数的指针
3.注意
因为用到了回调函数,所以我们需要提供ROS控制权
贴上代码
// This program t oggles between rotation and translation
// commands, based on calls to a service .
#include <ros/ros.h>
#include <std_srvs/Empty.h>
#include <geometry_msgs/Twist.h>
bool forward = true ;
bool toggleForward (
std_srvs::Empty::Request &req ,
std_srvs::Empty::Response &resp
) {
forward = !forward ;
ROS_INFO_STREAM("Now␣ sending␣ " << ( forward ?
"forward" : " rotate ") << "␣ commands.") ;
return true ;
}
int main( int argc , char ** argv ) {
ros::i n i t ( argc , argv , "pubvel_toggle") ;
ros::NodeHandle nh ;
// Register our service with the master .
ros::ServiceServer server = nh.advertiseService (
"toggle_forward" , &toggleForward ) ;
// Publish commands, using the latest value for forward ,
// until the node shuts down.
ros::Publisher pub = nh.advertise <geometry_msgs::Twist>(
"turtle1/cmd_vel" , 1000) ;
ros::Rate rate (2) ;
while ( ros::ok () ) {
geometry_msgs::Twist msg ;
msg.linear.x = forward ? 1.0 : 0.0 ;
msg.angular.z = forward ? 0.0 : 1.0 ;
pub.publish (msg) ;
ros::spinOnce () ;
rate.sleep () ;
}
}
缺陷分析
- 代码中有延迟,原因是sleep系统会以较低的频率进行循环,大部分时间处于休眠,大部分服务调用在sleep执行时到达,等待后再执行
解决方案
- 使用两个线程:一个发布消息,一个处理服务回调
- 使用ros::spin来代替sleep/ros::spinOnce循环,并且利用计数器回调函数(timer callback) 来发布消息
9.消息的录制与回放
使用包(bag)文件来录制和回放信息
通过 rosbag,我们能够将发布在一个或者多个话题上的消息录制到一个包文件中,然后可以回放这些消息,重现相似的运行过程。将这两种能力结合,便形成了测试某些机器人软件的有效方式:我们可以偶尔运行机器人,运行过程中录制关注的话题,然后多次回放与这些话题相关的消息,同时使用处理这些数据的软件进行实验。
1.录制和回放包文件
-
录制包文件:rosbag record -O filename.bag topic-names(如果不指定文件名,就会基于日期和时间自动生成文件名)
-
也可以使用rosbag record -a记录当前所有话题的消息
-
Ctrl+C停止rosbag
-
回放包文件:rosbag play filename.bag
-
检查文件包:rosbag info filename.bag
2.注意
rosbag如果没有记录初始状态,则在回放时会在结束时刻的位置开始回放
3.放入launch文件
方法:包括适当的节点元素
-
录制节点
<node pkg="rosbag" name="record" type="record" args="-O filename.bag topic-names" />
-
回放节点
<node pkg="rosbag" name="play" type="play" args="filename.bag" />