ROS面试题汇总

编程入门 行业动态 更新时间:2024-10-22 23:39:00

ROS<a href=https://www.elefans.com/category/jswz/34/1769418.html style=面试题汇总"/>

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文件的写法

<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" /></launch>

3、ros::spin()和ros::spinonce()的区别

        以ros消息发布器和订阅器的教程: (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 <sstream>int main(int argc, char **argv)
{ros::init(argc, argv, "talker");ros::NodeHandle n;ros::Publisher chatter_pub = n.advertise<std_msgs::String>("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");
  • 这使得使用该句柄的任何相对名字都是相对<node_namespace>/my_namespace,而不只是相对<node_namespace>。
  • 也可以指定父NodeHandle和命名空间:
ros::NodeHandle nh1("ns1");
ros::NodeHandle nh2(nh1, "ns2");
  • 这个放nh2 进入<node_namespace>/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",....);

以上例子会订阅<node_name>/my_private_namespace/my_private_topic === (node_namespace/nodename/my_private_namespace/my_private_topic)

注意:理解的重点上文中红色标注的部分,node_namespacenode_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);            //定义循环频率 10HZwhile(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文件

 

 

更多推荐

ROS面试题汇总

本文发布于:2024-02-26 14:11:53,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1702783.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:面试题   ROS

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!