4.ROS基础编程(2.基本数据结构或API分析)
✅博客主页:爆打维c-CSDN博客 🐾
🔹分享自己学习AI/编程知识的过程 🐾
🔹我的GitHub代码仓库 https://github.com/lyy-0118
该文章是对手把手教你学ROS(万字详解)这篇博客的续写,将补全之前没讲到的知识,想系统了解ROS知识的可以去看我的ROS学习专栏,持续更新中~~~
目录
3.2 基本数据结构或API分析
3.2.1 ros::NodeHandle的用法
1. 自动启动和结束
2. 命名空间(Nnamespaces)
3. 全局域名(Global Names)
4. 私有域名(Private Names)
5. 实例讲解
3.2.2 ros::spinOnce()和ros::spin ()的异同点
1. 这两个函数都是干啥的?
2. 这两个函数有啥不一样?
3. 使用的注意事项
4. 如何解决非阻塞函数(ros::spinOnce())的缺点?
5. 相较于非阻塞函数(ros::spinOnce()),阻塞函数(ros::spin())又有什么优点
3.2基本数据结构或API分析
3.2.1 ros::NodeHandle的用法
ros::NodeHandle对象,也就是节点的句柄,它可以用来创建Publisher、Subscriber以及做其他事情。一个节点(Node)可以有多个节点句柄(NodeHandle),可以是一个,可以是两个,可以是三个,甚至更多,但是他们都指向同一个节点。
句柄(Handle)这个概念可以理解为一个“把手”,你握住了门把手,就可以很容易把整扇门拉开,而不必关心门是什么样子。NodeHandle就是对节点资源的描述,有了它你就可以操作这个节点了,比如为程序提供服务、监听某个topic上的消息、访问和修改param等等。
1. 自动启动和结束
ros::NodeHandle管理着一个内部引用数,使得开启和结束一个节点(node)可以简单地按照下面一行代码完成:
ros::NodeHandle nh;
2. 命名空间(Nnamespaces)
NodeHandles可以指定一个命名空间给它的构造函数:
ros::NodeHandle nh("my_namespace");
这行代码将创建一个相对于NodeHandle的域名,<node_namespace>/my_namespace,而不是直接地表示为<node_namespace>。
你也可以指定一个父节点的NodeHandle和命名空间,后面可以跟子节点的NodeHandle和命名空间:
ros::NodeHandle nh1("ns1");
ros::NodeHandle nh2(nh1, "ns2");
这将会把nh2放在<node_namespace>/ns1/ns2的命名空间下。
3.全局域名(Global Names)
如果确实需要,你可以指定一个全局域名:
ros::NodeHandle nh("/my_global_namespace");
这样的方式通常不是很推荐,因为它会阻止节点被放进命名空间(如使用roslaunch)。但是,有时候,在代码中使用全局命名空间也会很有用。
4.私有域名(Private Names)
使用私有域名比起直接调用一个带有私有域名("~name")的NodeHandle函数要麻烦一点。相反,你必须创建一个新的位于私有命名空间的NodeHandle:
ros::NodeHandle nh("~my_private_namespace");
ros::Subscriber sub = nh.subscribe("my_private_topic", ...);
上面这个例子将会订阅话题<node_name>/my_private_namespace/my_private_topic。
5.实例讲解
- 全局命名空间:ros::NodeHandle n;--名字以“/”开始
- 局部命名空间:ros::NodeHandle nh("~");--名字以“/节点名”开始
- 局部命名空间:ros::NodeHandle nh("名字空间");--名字以“/名字空间”开始
因为话题和服务通常是多个节点共用的,所以一般用普通的初始化方式,这样A节点定义的话题,能够被B节点看到。而私有节点句柄通常将节点名称(用~表示)作为一个子命名空间(私有可以理解为:make it private to this node),这样便于管理参数,因为参数通常和节点是配套使用的,使用私有节点句柄可以避免名称冲突。
在本节launch文件夹的demo.launch定义两个参数,一个全局serial 他的数值是5,一个是局部的serial,他的数值是10.
<launch> <!--全局参数serial--> <param name="serial" value="5" /> <node name="name_demo" pkg="name_demo" type="name_demo" output="screen"> <!--局部参数serial--> <param name="serial" value="10" /> </node> </launch>#include <ros/ros.h> int main(int argc, char* argv[]) { int serial_number = -1; //serial_number初始化 ros::init(argc, argv, "name_demo"); //node初始化 /*创建命名空间*/ //n 是全局命名空间 ros::NodeHandle nh; //nh 是局部命名空间 ros::NodeHandle pnh("~"); /*全局命名空间下的Param*/ ROS_INFO("global namespace"); //提取全局命名空间下的参数serial nh.getParam("serial", serial_number); ROS_INFO("global_Serial was %d", serial_number); //提取局部命名空间下的参数serial nh.getParam("name_demo/serial", serial_number);//在全局命名空间要提取局部命名空间下的参数,需要添加node name ROS_INFO("global_to_local_Serial was %d", serial_number); /*局部命名空间下的Param*/ ROS_INFO("local namespace"); //提取局部命名空间下的参数serial pnh.getParam("serial", serial_number); ROS_INFO("local_Serial was %d", serial_number); //提取全局命名空间下的参数serial pnh.getParam("/serial", serial_number);//在局部命名空间下要提取全局命名空间下的参数,要添加“/” ROS_INFO("local_to_global_Serial was %d", serial_number); ros::spin(); return 0; }3.2.2 ros::spinOnce()和ros::spin ()的异同点
1.这两个函数都是干啥的?
如果你的程序写了相关的消息订阅函数,那么程序在执行过程中,除了主程序以外,ROS还会自动在后台按照你规定的格式,接受订阅的消息,但是所接到的消息并不是立刻就被处理,而是必须要等到ros::spin()或ros::spinOnce()执行的时候才被调用,这就是消息回到函数的原理。
2.这两个函数有啥不一样?
阻塞函数(ros::spin())和非阻塞函数(ros::spinOnce())
spin/spinOnce是ROS消息回调处理函数。它俩通常会出现在ROS的主循环中,程序需要不断调用ros::spin() 或 ros::spinOnce(),两者区别在于:前者调用后不会再返回,也就是你的主程序到这儿就不往下执行了,而后者在调用后还可以继续执行之后的程序。
其实看函数名也能理解个差不多,一个是一直调用;另一个是只调用一次,如果想要调用ros::spinOnce()实现ros::spin()的效果,那就需要加上循环了(用while(ros::ok()){…}循环)。
3.使用的注意事项
这里一定要记住,ros::spin()函数一般不会出现在循环中,因为程序执行到spin()后就不调用其他语句了,也就是说该循环没有任何意义,还有就是spin()函数后面一定不能有其他语句(return 0 除外),有也是白搭,不会执行的。
ros::spinOnce()的用法相对来说很灵活,但往往需要考虑调用消息的时机,调用频率,以及消息池的大小,这些都要根据现实情况协调好,不然会造成数据丢包或者延迟的错误。就比如:
#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, "listener2"); ros::NodeHandle n; ros::Subscriber sub = n.subscribe("chatter", 2, chatterCallback); ros::Rate loop_rate(5); // 设置了每个循环所耗费的时间为1/5s while (ros::ok()) { /*...TODO...*/ ros::spinOnce(); loop_rate.sleep(); // 在执行周期剩余时间内系统休眠 } return 0; }由于程序中设定了每次循环执行所花费时间为1/5s,如果程序还未到点就已经执行完成了一次循环那么就自动进入休眠状态,这就导致休眠时间段内如果有消息传来订阅方并不能及时接收消息,导致数据丢失。
[ INFO] [1599531419.076638802]: I heard: [hello world 7123]
[ INFO] [1599531419.076741485]: I heard: [hello world 7124]
[ INFO] [1599531419.276584099]: I heard: [hello world 7127]
[ INFO] [1599531419.276618426]: I heard: [hello world 7128]
[ INFO] [1599531419.476654136]: I heard: [hello world 7131]
[ INFO] [1599531419.476744663]: I heard: [hello world 7132]
[ INFO] [1599531419.676645419]: I heard: [hello world 7135]
上述代码就演示了“frequency(publisher)=10Hz,frequency(subscriber)=5Hz“时,数据丢失了,即人家0.1S发一次数据,而我0.2S批量处理一次数据,人家发两次我处理一次。
4.如何解决非阻塞函数(ros::spinOnce())的缺点?
对于有些传输特别快的消息,尤其需要注意合理控制消息池大小和ros::spinOnce()执行频率; 比如消息送达频率为10Hz, ros::spinOnce()的调用频率为5Hz,那么消息池的大小就一定要大于2,才能保证数据不丢失,无延迟。
5.相较于非阻塞函数(ros::spinOnce()),阻塞函数(ros::spin())又有什么优点?
阻塞函数(ros::spin())没有放在while循环中,这也就不会有“ros::Rate loop_rate(5)“这条代码,也就是说ros::spin()并不会按照一定频率执行,而是一直执行阻塞了代码执行的通道,也就是说卡在这里了,卡在这里等待着话题接受由发布者发布的新的数据,来一个我就处理一个。也就是说非阻塞函数(ros::spinOnce())更像异步,跟不上趟的那部分数据用缓冲队列进行存储等待下一次一起处理掉,而阻塞函数(ros::spin())更像同步,那边发一个,我这边处理一个。
