ROS面试题汇总

1、ROS中订阅(Subscribe)最新消息以及消息队列相关问题
机器人应用中难免会遇到运算起来很费时间的操作,比如图像的特征提取、点云的匹配等等。有时候,不可避免地,我们需要在ROS的Subscriber的Callback回调函数中进行这些费时的操作。Subscriber所订阅的消息的发布频率可能是很高的,而这些操作的运算速度肯定达不到消息发布的速度。所以,如果我们要是没有取舍的对于每个消息都调用一次回调函数,那么势必会导致计算越来越不实时,很有可能当下在处理的还是几十秒以前的数据。所以,我们希望每次回调函数都处理当前时刻最新的一个消息,这就是我们的目标。

    要达到这个目标有三点,第一点是要设置Publisher的queue_size等于1;第二点是要设置Subscriber的queue_size(消息队列大小)等于1;第三点非常重要,要设置Subscriber的buff_size(缓冲区大小)足够大,大于一个消息的大小。像这样:

ROS Python

pcdpub = rospy.Publisher(“lidardata”, PointCloud, queue_size=1)
rospy.Subscriber(“lidardata”, PointCloud, self.pcd_resolve_callback,queue_size=1,buff_size=52428800)
1.1 Subscriber和Publisher的消息队列起什么作用,队列的大小有什么影响?
简单描述一下,Publisher的消息队列是为了缓存发布节点发布的消息,一旦队列中消息的数量超过了queue_size,那么最先进入队列的(最老的)消息被舍弃。Subscriber的消息队列是为了缓存节点接收到的信息,一旦自己处理的速度过慢,接收到的消息数量超过了queue_size,那么最先进入队列的(最老的)消息会被舍弃。所以,我们想只处理最新的消息,实际上只需要把两个queue_size都设置成1,那么系统不会缓存数据,自然处理的就是最新的消息。

1.2 Subscriber有消息队列缓存消息了,为什么Publisher还要有消息队列?
在我看来,Publisher的消息队列是一定要有的,因为ROS中发布节点往外发送消息是基于Topic发送,而不是直接向Subscriber订阅者发送,所以必须要有一个消息队列来存放发布的消息,以供订阅者来获取。而且这个消息队列的好处是在网络差、带宽小、延时高的时候,保证数据不容易丢失。

1.3 既然Publisher有消息队列了,为什么Subscriber还要有消息队列?
这个问题比较难一点。我的理解是,由于ROS毕竟是分布式系统,Publisher和Subscriber不一定在同一台主机上,因此消息需要通过网络来交换。但是网络的性能时好时坏,如果Subscriber没有消息队列,那么每次运行Callback函数前都要先通过网络取回消息,然后才能处理。当网络很差时,就会让系统堵塞。而有消息队列的话,Subscriber就可以一边处理队列中的消息,一边通过网络缓存新的消息,而不用每次处理消息前都要临时去读一个回来。这样就增加了系统的可靠性。

1.4 为什么要设置缓冲区的大小?
这个缓冲区的大小是指消息队列使用的缓冲区物理内存空间大小。如果这个空间小于一个消息所需要的空间,比如消息是一副图片或者一帧点云,数据量超过了缓冲区的大小。这个时候为了保证通信不发生错误,就会触发网络通信的保护机制,TCP的Buffer会为你缓存消息。这种机制就会导致每一帧消息都被完整的缓存下来,没有消息被丢弃,感觉上就像queue_size被设置成了无穷大。详细说明请参考:
Ros subscriber not up to date
rospy subscriber queue_size acting strange when buff_size is too small

1.5 消息队列的运行机制
这里只能说是我的理解了,因为没有看过源代码。
首先,发布节点把消息发布,消息进入Publisher的消息队列,同时通知订阅了该话题消息的Subscriber来取消息。
其次,Subscriber来Publisher的消息队列里取消息,但取走的也是最老的消息,因为毕竟这是先入先出的队列。这也是为什么Publisher的消息队列的大小也要设置为1。
最后,被取走的消息存放入了Subscriber的消息队列中,等待被Callback执行。如果Callback执行很慢,消息越堆越多,最老的消息会逐渐被顶替。
当然,这里究竟是Subscriber来取消息,还是Publisher直接把消息推给Subscriber,我只是猜测,反正这里交换的消息肯定不是最新的消息,而是队列里最老的消息。
2、launch文件的写法

<!-- 启动mbot -->
<node name="robot_bringup" pkg="robot_bringup" type="robot_bringup" output="screen" />

<!-- 加载机器人模型参数 -->
<param name="robot_description" command="$(find xacro)/xacro --inorder '$(find robot_description)/xacro/robot_with_laser.xacro'" />

<!-- 运行joint_state_publisher节点,发布机器人的关节状态 -->
<node name="joint_state_publisher" pkg="joint_state_publisher" type="joint_state_publisher" />
<!-- 运行robot_state_publisher节点,发布tf -->
<node name="state_publisher" pkg="robot_state_publisher" type="robot_state_publisher">
    <param name="publish_frequency" type="double" value="50.0" />
</node>

<!-- 运行激光雷达驱动 -->
<include file="$(find robot_bringup)/launch/rplidar.launch" />


3、ros::spin()和ros::spinonce()的区别
以ros消息发布器和订阅器的教程: http://wiki.ros.org/ROS/Tutorials/WritingPublisherSubscriber(c++)为例。

    消息发布器(ros::Publisher)在一个while循环里一直循环发布消息(“hello world”)到话题(“chatter”)上。

    消息订阅器(ros::Subscriber)一直监视话题,一旦知道话题上有数据,就会将该话题上的数据(message)作为参数传入到对应的回调函数(callback)中,但是这时候回调函数还没有被执行,而是把callback函数放到了回调函数队列中。所以当消息发布器不断发送message到该话题上时,就会有相应的callback函数存入队列中,它们函数名一样,只是实参内容不一样。(其实,是将传入的message存入到队列中,这里说是将callback函数存入到队列中,应该是为了便于理解)。

    那什么时候会执行callback函数呢?那就是ros::spin()和ros::spinOnce()的事情了。

    当spinOnce()函数被调用时,spinOnce()会调用回调函数队列中第一个callback函数,此时callback函数才被执行,然后等到下次spinOnce函数又被调用时,回调函数队列中第二个callback函数就会被调用,以此类推。

    所以,这会有一个问题。因为回调函数队列的长度是有限的,如果发布器发送数据的速度太快,spinOnce函数调用的频率太少,就会导致队列溢出,一些callback函数就会被挤掉,导致没被执行到。

    而对于spin函数,一旦进入spin函数,它就不会返回了,相当于它在自己的函数里面死循环了。只要回调函数队列里面有callback函数在,它就会马上去执行callback函数。如果没有的话,它就会阻塞,不会占用CPU。

3.1 spin()和spinOnce()函数意义
首先要知道,这俩兄弟学名叫ROS消息回调处理函数。它俩通常会出现在ROS的主循环中,程序需要不断调用ros::spin() 或ros::spinOnce(),两者区别在于前者调用后不会再返回,也就是你的主程序到这儿就不往下执行了,而后者在调用后还可以继续执行之后的程序。

如果你的程序写了相关的消息订阅函数,那么程序在执行过程中,除了主程序以外,ROS还会自动在后台按照你规定的格式,接受订阅的消息,但是所接到的消息并不是立刻就被处理,而是必须要等到ros::spin()或ros::spinOnce()执行的时候才被调用,这就是消息回调函数的原理。

3.2 spin()和spinOnce()的区别
就像上面说的,ros::spin() 在调用后不会再返回,也就是你的主程序到这儿就不往下执行了,而 ros::spinOnce() 后者在调用后还可以继续执行之后的程序。

    其实看函数名也能理解个差不多,一个是一直调用;另一个是只调用一次,如果还想再调用,就需要加上循环了。

这里一定要记住,

ros::spin()函数一般不会出现在循环中,因为程序执行到spin()后就不调用其他语句了,也就是说该循环没有任何意义。
spin()函数后面一定不能有其他语句(return 0 除外),有也是白搭,不会执行的。
ros::spinOnce()的用法相对来说很灵活,但往往需要考虑调用消息的时机,调用频率,以及消息池的大小,这些都要根据现实情况协调好,不然会造成数据丢包或者延迟的错误。
3.3 spin()和spinOnce()常见使用方法
如果你的程序写了相关的消息订阅函数,那千万千万千万不要忘了在相应位置加上ros::spin()或者ros::spinOnce()函数,不然你是永远都得不到另一边发出的数据或消息的。

3.3.1 ros::spin()
ros::spin()函数用起来比较简单,一般都在主程序的最后,加入该语句就可。例子如下:

// 发送端

include “ros/ros.h”

include “std_msgs/String.h”

include

int main(int argc, char **argv)
{
ros::init(argc, argv, “talker”);
ros::NodeHandle n;
ros::Publisher chatter_pub = n.advertise(“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());

    /**
     * 向 Topic: chatter 发送消息, 发送频率为10Hz(1秒发10次);消息池最大容量1000。
     */
    chatter_pub.publish(msg);

    loop_rate.sleep();
    ++count;
}
return 0;

}
接收端代码中用到spin()函数:

// 接收端

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() 将会进入循环, 一直调用回调函数chatterCallback(),每次调用1000个数据。
 * 当用户输入Ctrl+C或者ROS主进程关闭时退出,
 */
ros::spin();
return 0;

}
3.3.2 ros::spinOnce()
对于ros::spinOnce()的使用,虽说比ros::spin()更自由,可以出现在程序的各个部位,但是需要注意的因素也更多。比如:

    1. 对于有些传输特别快的消息,尤其需要注意合理控制消息池大小和ros::spinOnce()执行频率; 比如消息送达频率为10Hz, ros::spinOnce()的调用频率为5Hz,那么消息池的大小就一定要大于2,才能保证数据不丢失,无延迟。

// 接收端

include “ros/ros.h”

include “std_msgs/String.h”

void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
/…TODO…/
}

int main(int argc, char **argv)
{
ros::init(argc, argv, “listener”);
ros::NodeHandle n;
ros::Subscriber sub = n.subscribe(“chatter”, 2, chatterCallback);

ros::Rate loop_rate(5);
while (ros::ok())
{
    /*...TODO...*/ 

    ros::spinOnce();
    loop_rate.sleep();
}
return 0;

}
2. ros::spinOnce()用法很灵活,也很广泛,具体情况需要具体分析。但是对于用户自定义的周期性的函数,最好和ros::spinOnce并列执行,不太建议放在回调函数中;

/…TODO…/
ros::Rate loop_rate(100);

while (ros::ok())
{
/…TODO…/
user_handle_events_timeout(…);

ros::spinOnce();                 
loop_rate.sleep();

}
4、ros命名空间NodeHandle相关问题
4.1 NodeHandle的定义
NodeHandles节点类,ros::NodeHandle类有两个作用:

首先,在roscpp程序的内部,它提供了RAII方式启动和关闭。
其次,它提供了一个额外的层命令空间解决方案,可以使组件更容易写。
4.2 NodeHandle的自动启动节点和关闭节点
自动启动和关闭

ros::NodeHandle管理内部的引用计数
开始节点:
ros::NodeHandle nh;
创建时候,如果内部节点没有开始,ros::NodeHandle会开始节点,ros::NodeHandle实例销毁,节点就会关闭。
4.3 NodeHandle的命名空间
1、句柄可以让你通过构造函数指定命名空间

查阅ROS命名空间文档
NodeHandle可以指定命名空间给构造器:
ros::NodeHandle nh(“my_namespace”);
这使得使用该句柄的任何相对名字都是相对/my_namespace,而不只是相对。
也可以指定父NodeHandle和命名空间:
ros::NodeHandle nh1(“ns1”);
ros::NodeHandle nh2(nh1, “ns2”);
这个放nh2 进入/ns1/ns2 命名空间。
2、也可以指定全局名字

ros::NodeHandle nh(“/my_global_namespace”);
这种做法并不推荐,因为这样会使得节点无法被放入别的命名空间。只是有时在代码中使用全局名字有用。

你可以指定全局名,如:
ros::NodeHandle nh(“/my_global_namespace”);
这通常不鼓励,因为它阻止了节点被推为命名空间(如roslaunch)。然而,有时在代码中使用全局名称可能是有用的。
3、私有名字

    使用私有名字比直接调用有私有名的句柄方法更有技巧,你可以在一个私有命名空间中直接创建一个新的句柄。

ros::NodeHandle nh(“~my_private_namespace”); ros::Subscriber sub = nh.subscribe(“my_private_topic”,….);
以上例子会订阅/my_private_namespace/my_private_topic === (node_namespace/nodename/my_private_namespace/my_private_topic)

注意:理解的重点上文中红色标注的部分,node_namespace和node_name是两回事!

node_name = node_namespace + node name

// launch 文件中 ns==”node_namespace”
ros::init(argc, argv, “node_name”); // node name

ros::NodeHandle n; //n 命名空间为/node_namespace

ros::NodeHandle n1(“sub”); // n1命名空间为/node_namespace/sub
ros::NodeHandle n2(n1,”sub2″);// n2命名空间为/node_namespace/sub/sub2

ros::NodeHandle pn1(“~”); //pn1 命名空间为/node_namespace/node_name
ros::NodeHandle pn2(“~sub”); //pn2 命名空间为/node_namespace/node_name/sub
ros::NodeHandle pn3(“~/sub”); //pn3 命名空间为/node_namespace/node_name/sub
ros::NodeHandle gn(“/global”); // gn 命名空间为/global
综上,只要不是明确指定,就会用节点名node_name来指定命名空间。

5、ros1通信模型
talker注册
listener注册
ROS Master进行信息匹配
listener 发送链接请求
talker确认请求
建立连接
talker给listener发数据

6、ROS基础

6.1 ROS的总体设计目标

6.2 ROS的应用框架是怎样的?

总结:ROS就是一个为了提高机器人开发的软件复用率,开发的一款具有独特的通信机制、丰富的开发工具、海量的应用功能、良好的生态系统集的工具。

6.3 如何新建一个工作空间?
最基础的功能包都不会写,怎么意思说学过ROS:

cd ~/catkin_test_ws/src #进入存放功能包的地方
catkin_create_pkg test_package std_msgs roscpp rospy #创建名字为test_package的功能包,添加std_msgs roscpp rospy依赖
cd ~/catkin_test_ws
catkin_make #编译
source devel/setup.bash #新建功能包刷新环境变量,才能找到功能包
rospack profile #如果还是找不到功能包,使用该命令刷新功能包路径
需要注意一点,同一个工作空间下不能存在同名功能包,不同的工作空间下可以存在同名功能包。但是要注意一个问题,ROS运行时会优先选择最前端工作空间的同名功能包。为了避免出现想不到的问题,所有工作空间尽量不使用同名功能包。

    功能包的名称尽量按照a_b_c的格式书写,否则,编译过程将会出现警告。

6.4 如何新建一个功能包?
这也是需要每个人都要会的,连个最基础的功能包都不会写,怎么好意思说学过ROS?

cd ~/catkin_test_ws/src #进入存放功能包的地方
catkin_create_pkg test_package std_msgs roscpp rospy #创建名字为test_package的功能包,添加std_msgs roscpp rospy依赖
cd ~/catkin_test_ws
catkin_make #编译
source devel/setup.bash #新建功能包刷新环境变量,才能找到功能包
rospack profile #如果还是找不到功能包,使用该命令刷新功能包路径
这里需要注意一点,同一个工作空间下不能存在同名功能包,不同的工作空间下可以存在同名功能包。但是要注意一个问题,ROS运行时会优先选择最前端工作空间的同名功能包。为了避免出现意想不到的问题,所以工作空间尽量不使用同名功能包。

    功能包的名称尽量按照a_b_c的格式书写,否则,编译过程将会出现警告。

6.5 如何新建一个ROS节点?
接着上面的测试功能包,我们在功能包的src文件下新建test.cpp文件:

cd ~/catkin_test_ws/src/test_package/src/
vim test.cpp
然后就是ROS节点的模板程序,都是在模板程序之上做的修改。

include “ros/ros.h” //引入ROS头文件

int main(int argc,char **argv)
{
ros::init(argc,argv,”test”); //初始化ROS节点

ros::NodeHandle nh;                 //创建ROS句柄

ros::Rate loop_rate(10);            //定义循环频率 10HZ

while(ros::ok())
{
    std::cout<< "hello ros" <<std::endl;
    loop_rate.sleep();             //按照循环速率延时
}
return 0;

}

    到这里,然后编译就可以了?这个功能包的编译还需要修改对应的CMakeLIsts.txt文件,一般为三步。

    设置需要编译的代码和生成的可执行文件;

    设置链接库;

    设置依赖;

    所以我们修改功能包下的CMakeLIsts.txt文件,一般功能包会自动生成CMakeLists.txt文件内容,我们进行修改即可。

add_executable(${PROJECT_NAME}_node src/test.cpp) #这里注意自动生成的源文件名字一般不对,一定要修改成对的名字
target_link_libraries(${PROJECT_NAME}_node ${catkin_LIBRARIES})
add_dependencies(${PROJECT_NAME}_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
这里因为我们没有自定义的消息或者服务类型,所以其他的地方不做更改,如果使用了自定义的消息或者服务类型,注意修改其他的地方。

cd ~/catkin_test_ws
catkin_make #编译
此时~/catkin_test_ws/devel/lib/test_package目录下生成了test_package_node的可执行文件
执行功能包,在使用rosrun执行功能包之前,使用roscore启动rosmaster。启动rosmaster是启动一切ros节点的前提。

roscore #启动rosmaster
打开一个新终端启动,test_package功能包下的test_package_node节点。

rosrun test_package test_package_node
终端循环打印hello ros,一个新的ros节点就创建完成了。

6.6 如何在一个ros节点文件中调用其他的源文件?
这个问题很常见,但是他不应该是个问题,但是发现一些小伙伴还是不太会操作。我们在开发的过程中,随着程序的不断扩大,模块化编程肯定必不可少。有时,我们要调用别人写好的特定功能的源文件,这个时候我们应该怎么做吗?

    首先,将你需要的源文件复制到你需要编写的功能包的src文件夹下,规范地讲,.hpp文件应该放到include文件下,如果文件比较少,也没有必要,和.cpp文件放在一起也可以。

    ros节点源文件只需要添加你复制的头文件下就行了,函数自己调用就行了。

    CMakeLists.txt文件需要做相应的修改,添加你添加的文件名字,放在ros节点文件后面,如下所示:

add_executable(${PROJECT_NAME}_node src/test.cpp src/test2.cpp src/test3.cpp)

这里注意自动生成的源文件名字一般不对,一定要修改成对的名字

然后,编译就行了。

6.7 如何设置两台机器的分布式通信?
ROS是一种分布式软件框架,节点之间通过松耦合的方式进行组合。

    如何设置两台机器的分布式通信呢?

    首先,确保两台机器在同一局域网下,查看各自的ip地址。

ifconfig
然后,各自安装ssh,确保ssh服务,开机自启动。

sudo apt-get install openssh-server #安装ssh服务端
sudo service ssh start #开启ssh服务
ssh服务开机不会自动启动,需要设置开机自启动
在开机自启动脚本/etc/rc.local加入下面这条命令,注意加入位置是在exit 0这一句之前,加上service ssh start
然后,在两台机器的/etc/hosts文件中,添加对方的ip地址,通过ping命令,互相测试是否连通。比如在hosts添加,对方机器IP对方机器名。

192.168.1.5 对方机器名
ping 对方机器名或者对方机器ip #ping测试
然后,在从机端的~/.bashrc文件末尾设置ROS_MASTER_URI,确保从机可以找到主机。

export ROS_MASTER_URI=http://主机名称:11311
最后,在从机端使用ssh用户名@IP地址,输入密码,连接远程终端。

6.8 ROS开发常用的工具


launch文件,可以同时启动多个ros节点,可自动启动rosmaster。关于launch的细节,自己学习就好。

    TF坐标变换,用来描述机器人系统中繁杂的坐标系。

    QT工具箱,用来可视化各个ros节点之间的订阅或者请求关系或者可视化各种话题的数据。常用命令,rqt_graph、rqt_plot,而且还可以动态修改部分参数。

    rqt_reconfigure(不常用)。

    rviz可视化平台,机器人可视化平台,几乎是最常用的工具。使用rviz命令启动。

Gazebo物理仿真环境,可对机器人进行三维物理仿真。需要仿真的同学常用。

6.9 ROS的常用命令

最常用

roscore #启动rosmaster
rosrun pkg_name node_name #启动ros节点
roslaunch pkg_name launch_files_name #启动launch文件
catkin_make #编译工作空间
rospack profile #刷新功能包路径

环境变量

echo $ROS_PACKAGE_PATH #打印ros环境变量
export | grep ROS #确认环境变量已经设置正确
source devel/setup.bash #刷新环境变量
echo “source ~/catkin_test_ws/devel/setup.bash” >> ~/.bashrc #刷新环境变量,永久有效
source ~/.bashrc #生效上一句

功能包

catkin_create_pkg test_package std_msgs roscpp rospy #创建名字为test_package的功能包,添加std_msgs roscpp rospy依赖
rospack list #查看软件包列表
rospack find package-name #定位软件包
roscd package-name #切换到指定功能包目录

话题

rostopic list #输出当前运行的topic列表
rostopic info topic-name #查看话题信息
rostopic echo topic-name #输出话题数据
rostopic hz topic-name #每秒发布的消息数量
rostopic bw topic-name #每秒发布信息所占的字节量

工具

rviz #启动rviz
rqt_graph #可视化节点关系
rqt_plot #可视化话题数据
rosrun rqt_tf_tree rqt_tf_tree #查看tf树

数据记录与播放

rosbag record -a #录制所有topic到bag文件
rosbag play bag_files_name #播放bag文件

Similar Posts

Leave a Reply