经过一段时间ROS的学习,对ROS有了一些了解。
ROS的核心是节点(node)间的通信,节点是一种使用ROS的中间件进行通信的ROS程序,一个节点可以独立于其他节点启动,
多个节点也可以任何顺序启动,多个节点可以运行在同一台计算机中,或者分布于一个计算机网络中,但一个节点只有当与其他
节点通信并最终与传感器和执行器连接时,它才是有用的。
节点之间的通信涉及了message、topic、roscore、publisher和subscriber(service同样有用,而且这些与发布器和订阅器紧密相关),
节点间所有通信都是系列化的网络通信。
一个节点既可以是订阅器,也可以是发布器。例如,一个控制节点既是一个需要接收传感器信号的订阅器,同样也是一个发布控制指令的发布器,
这只需要在同一个节点内同时实例化订阅器对象和发布器对象即可,将消息管道化有利于进行顺序处理。例如,底层图像处理程序(比如边缘检测)
会订阅原始相机数据并发布底层的处理后的图像,更高层的一个节点会订阅这些处理后的边缘图像,寻找这个图像中的特定形状,并发布它的结果
(比如识别出来的形状)以便给更高层的处理程序做进一步的处理,执行连续层级处理的一组节点序列可以一次更换一个节点进行增量式修改,新节点
只需继续使用同样的输入和输出话题名和消息类型,就可以替换点整个链条中的一个环节,尽管在修改过的节点中的算法会有显著不同,但链条中的其他节点不会受影响。
下面我将详细介绍最小发布器和最小订阅器的设计,以及如何通过关联文件package.xml和CMakeLists.txt中的指令进行编译和代码链接。
最小发布器文件名为:minimal_publisher.cpp 代码如下:
1 #include <ros/ros.h>
2 #include <std_msgs/Float64.h>
3
4 int main(int argc, char **argv) {
5 ros::init(argc, argv, "minimal_publisher"); // name of this node will be "minimal_publisher"
6 ros::NodeHandle n; // two lines to create a publisher object that can talk to ROS
7 ros::Publisher my_publisher_object = n.advertise<std_msgs::Float64>("topic1", 1);
8 //"topic1" is the name of the topic to which we will publish
9 // the "1" argument says to use a buffer size of 1; could make larger, if expect network backups
10
11 std_msgs::Float64 input_float; //create a variable of type "Float64",
12 // as defined in: /opt/ros/indigo/share/std_msgs
13 // any message published on a ROS topic must have a pre-defined format,
14 // so subscribers know how to interpret the serialized data transmission
15
16 input_float.data = 0.0;
17
18
19 // do work here in infinite loop (desired for this example), but terminate if detect ROS has faulted
20 while (ros::ok())
21 {
22 // this loop has no sleep timer, and thus it will consume excessive CPU time
23 // expect one core to be 100% dedicated (waste fully) to this small task
24 input_float.data = input_float.data + 0.001; //increment by 0.001 each iteration
25 my_publisher_object.publish(input_float); // publish the value--of type Float64--
26 //to the topic "topic1"
27 }
28 }
29
分析一下代码:
#include<ros/ros.h>
引入核心ROS库的头文件。
#include<std_msgs/Float64.h>
引入描述对象类型std_msgs::Float64的头文件,它是我们在这个示例代码中会用到的消息类型,
随着加入使用更多的ROS消息类型或ROS库,你需要将他们相关的头文件包含代码中。
int main(int argc, char **argv)
声明了一个main函数,对于所有C++编写的ROS节点,每个节点必须且只能有一个main()函数
ros::init(argc, argv, "minimal_publisher")
引用了在ROS库中定义的函数和对象,在ROS系统中,新节点会被使用参数minimal_publisher作为自己的新名字在启动中被注册,
节点必须要有名字,系统中的每个节点的名字也必须不一样。
ros::NodeHandle n;
声明实例化了一个ROS NodeHandle对象 ,需要NodeHandle建立节点间的网络通信,NodeHandle的名字n可以随意命名。
ros::Publisher my_publisher_object = n.advertise<std_msgs::Float64>("topic1", 1);
实例化名为my_publisher_objest的对象,在实例化对象中,ROS系统收到通知,当前节点(这里称之为minimal_publisher)打算在名为topic1的话题上发布
std_msgs::Float64类型的消息。
std_msgs::Float64 input_float;
创建了一个std_msgs::Float64类型对象,并命名为input_float,需要在std_msgs中查阅消息定义才能了解如何使用这个对象。
input_float.data = 0.0;
程序将input_float的data成员初始化为0.0值。
my_publisher_object.publish(input_float);
它在之前已经建立(来自类ros::Pulisher的对象my_publisher_objest的实例化),my_publisher_object会把std_msgs::Float64
类型的消息发布到名为topic1的话题上,发布器对象my_publisher_objest有成员函数publish来调用发布,发布器期望一个兼容类型
的参数,因为对象input_float属于类型std_msgs::Float64,而且由于发布器对象实例化成该类型的参数,所以publish函数调用是一致的。sh
规划节点时间
带计时的最小发布器:sleep_minimal_publisher.cpp
1 #include <ros/ros.h>
2 #include <std_msgs/Float64.h>
3
4 int main(int argc, char **argv) {
5 ros::init(argc, argv, "minimal_publisher2"); // name of this node will be "minim al_publisher2"
6 ros::NodeHandle n; // two lines to create a publisher object that can talk to RO S
7 ros::Publisher my_publisher_object = n.advertise<std_msgs::Float64>("topic1", 1) ;
8 //"topic1" is the name of the topic to which we will publish
9 // the "1" argument says to use a buffer size of 1; could make larger, if expect network backups
10
11 std_msgs::Float64 input_float; //create a variable of type "Float64",
12 // as defined in: /opt/ros/indigo/share/std_msgs
13 // any message published on a ROS topic must have a pre-defined format,
14 // so subscribers know how to interpret the serialized data transmission
15
16 ros::Rate naptime(1.0); //create a ros object from the ros “Rate” class;
17 //set the sleep timer for 1Hz repetition rate (arg is in units of Hz)
18
19 input_float.data = 0.0;
20
21 // do work here in infinite loop (desired for this example), but terminate if de tect ROS has faulted
22 while (ros::ok())
23 {
24 // this loop has no sleep timer, and thus it will consume excessive CPU time
25 // expect one core to be 100% dedicated (wastefully) to this small task
26 input_float.data = input_float.data + 0.001; //increment by 0.001 each itera tion
27 my_publisher_object.publish(input_float); // publish the value--of type Floa t64--
28 //to the topic "topic1"
29 //the next line will cause the loop to sleep for the balance of the desired peri od
30 // to achieve the specified loop frequency
31 naptime.sleep();
32 }
33 }
以上程序只有两行是新的代码:
ros::Rate naptime(1);
naptime.sleep();
调用ROS的类Rate创建一个名为naptime的Rate对象,这样做,naptime被初始化为“1”,它是期望频率的规格(1hz).
创建这个对象以后,它会用于while循环,并调用成员函数sleep()。该函数会导致节点的挂起(从而停止消耗CPU时间),
直到等待时间(1秒)已经完成。、
在重新编译已修正的代码后(用catkin_make),我们可以运行它(用rosrun)并通过运行一个新的终端。如下:
输出结果表明,topic1正以1hz的速率更新,并且精度好,更好的是,观察系统监视器可以发现,
修改后的发布器节点几乎不消耗cpu时间。
最小ROS订阅器
发布器的补集是订阅器,文件:minimal_subscriber.cpp
1 #include<ros/ros.h>
2 #include<std_msgs/Float64.h>
3 void myCallback(const std_msgs::Float64& message_holder)
4 {
5 // the real work is done in this callback function
6 // it wakes up every time a new message is published on "topic1"
7 // Since this function is prompted by a message event,
8 //it does not consume CPU time polling for new data
9 // the ROS_INFO() function is like a printf() function, except
10 // it publishes its output to the default rosout topic, which prevents
11 // slowing down this function for display calls, and it makes the
12 // data available for viewing and logging purposes
13 ROS_INFO("received value is: %f",message_holder.data);
14 //really could do something interesting here with the received data...but all we d o is print it
15 }
16
17 int main(int argc, char **argv)
18 {
19 ros::init(argc,argv,"minimal_subscriber"); //name this node
20 // when this compiled code is run, ROS will recognize it as a node called "minimal _subscriber"
21 ros::NodeHandle n; // need this to establish communications with our new node
22 //create a Subscriber object and have it subscribe to the topic "topic1"
23 // the function "myCallback" will wake up whenever a new message is published to t opic1
24 // the real work is done inside the callback function
25
26 ros::Subscriber my_subscriber_object= n.subscribe("topic1",1,myCallback);
27
28 ros::spin(); //this is essentially a "while(1)" statement, except it
29 // forces refreshing wakeups upon new data arrival
30 // main program essentially hangs here, but it must stay alive to keep the callba ck function alive
31 return 0; // should never get here, unless roscore dies
32 }
最小订阅器中的大多数代码与最小发布器中的相同,有几行重要的新代码行需要注意。
最重要的是,订阅器要比发布器更复杂,因为它需要一个callback.
void myCallback(const std_msgs::Float64 & message_holder)
这个函数(callback)有一个指向std_msgs::Float类型对象的引用指针参数(由&符号标识)。这是与topic1关联的消息类型,由最小发布器发布。
回调函数(callback) 函数的重要性在于,当在它的关联话题(本例中设定为topic1)上有新数据可用时,它会被唤醒,当新数据发布到关联话题是,
回调函数开始运行,同时已经发布的数据会出现在参数message_holder中,(该信息持有者必须是与发布在兴趣话题上的消息类型相兼容的类型,例如:std_msgs::Float64)
ROS_INFO("received value is : %f",message_holder.data);
回调函数做的唯一的事情就是显示接收到的数据。
ros::Subscriber my_subscriber_object= n.subscribe("topic1",1,myCallback);
ros::Subscriber的用法和之前的ros::Publisher的用法相同,一个Subsciber类型的对象被实例化,Subscriber是存在与ROS版本中的一个类,
有三个参数用于实例化订阅器对象,第一个是话题名,最小发布器发布数据到话题topic1。
第二个参数是队列大小,如果回调函数无法跟上发布数据的频率,数据就需要排队,在本例中,队列大小设置为1,如果回调函数不能与发布保持一致,
消息将会被新消息覆盖而丢失。对于控制来说,通常只有最近发布的传感器数值才有意义,如果传感器发布的速度快于回调函数响应的速度,那么删除
信息并没有什么坏处,因为只有最近的消息才是所需要的,在大多数情况下,只需要一个消息的队列大小。
第三个参数是回调函数名,它用于接收topic1数据,这个参数已经设置为myCallback,是我们的回调函数名,通过这行代码,我们可以把回调函数和
发布到topic1的消息关联到一起。
ros::spin();
引入了一个重要的ROS概念,虽然不明显,但必不可少,每当topic1上有新消息时,回调函数就应该被唤醒,然而,
主函数必须为回调函数的响应提供一些时间,这通过spin()命令完成。在当前案例中,虽然spin可以让主程序挂起,
但回调函数依旧可以工作,如果主函数运行结束,回调函数就不再对新消息做出反应,spin()命令不需要消耗大量
CPU时间就可以保持main()的运行,因此最小订阅器是非常有效的。
package.xml文件 如下:
1 <?xml version="1.0"?>
2 <package>
3 <name>minimal_nodes</name>
4 <version>0.0.0</version>
5 <description>The minimal_nodes package</description>
6
7 <!-- One maintainer tag required, multiple allowed, one person per tag -->
8 <!-- Example: -->
9 <!-- <maintainer email="jane.doe@example.com">Jane Doe</maintainer> -->
10 <maintainer email="wyatt@todo.todo">wyatt</maintainer>
11
12
13 <!-- One license tag required, multiple allowed, one license per tag -->
14 <!-- Commonly used license strings: -->
15 <!-- BSD, MIT, Boost Software License, GPLv2, GPLv3, LGPLv2.1, LGPLv3 -->
16 <license>TODO</license>
17
18
19 <!-- Url tags are optional, but mutiple are allowed, one per tag -->
20 <!-- Optional attribute type can be: website, bugtracker, or repository -->
21 <!-- Example: -->
22 <!-- <url type="website">http://wiki.ros.org/minimal_nodes</url> -->
23
24
25 <!-- Author tags are optional, mutiple are allowed, one per tag -->
26 <!-- Authors do not have to be maintianers, but could be -->
27 <!-- Example: -->
28 <!-- <author email="jane.doe@example.com">Jane Doe</author> -->
29
30
31 <!-- The *_depend tags are used to specify dependencies -->
32 <!-- Dependencies can be catkin packages or system dependencies -->
33 <!-- Examples: -->
34 <!-- Use build_depend for packages you need at compile time: -->
35 <!-- <build_depend>message_generation</build_depend> -->
36 <!-- Use buildtool_depend for build tool packages: -->
37 <!-- <buildtool_depend>catkin</buildtool_depend> -->
38 <!-- Use run_depend for packages you need at runtime: -->
39 <!-- <run_depend>message_runtime</run_depend> -->
40 <!-- Use test_depend for packages you need only for testing: -->
41 <!-- <test_depend>gtest</test_depend> -->
42 <buildtool_depend>catkin</buildtool_depend>
43 <build_depend>roscpp</build_depend>
44 <build_depend>std_msgs</build_depend>
45 <run_depend>roscpp</run_depend>
46 <run_depend>std_msgs</run_depend>
47
48
49 <!-- The export tag contains other, unspecified, tags -->
50 <export>
51 <!-- Other tools can request additional information be placed here -->
52
53 </export>
54 </package>
package.xml文件具有特定的结构,可以命名程序包,并列出它的依赖项,没有对应的依赖,程序是无法正常运行的。
编辑程序包中的package.xml文件,模仿已有的roscpp和std_msgs依赖声明,在build_depend和run_depend行中添加需要利用的新程序包。
CMakeLists.txt文件 如下:
1 cmake_minimum_required(VERSION 2.8.3)
2 project(minimal_nodes)
3
4 ## Find catkin macros and libraries
5 ## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz)
6 ## is used, also find other catkin packages
7 find_package(catkin REQUIRED COMPONENTS
8 roscpp
9 std_msgs
10 )
11
95 include_directories(
96 ${catkin_INCLUDE_DIRS}
97 )
98
99 ## Declare a cpp library
100 # add_library(minimal_nodes
101 # src/${PROJECT_NAME}/minimal_nodes.cpp
102 # )
103
104 ## Declare a cpp executable
105 add_executable(minimal_publisher src/minimal_publisher.cpp)
106 add_executable(sleepy_minimal_publisher src/sleepy_minimal_publisher.cpp)
107 add_executable(minimal_subscriber src/minimal_subscriber.cpp)
108 add_executable(minimal_simulator src/minimal_simulator.cpp)
109 add_executable(minimal_controller src/minimal_controller.cpp)
110 ## Add cmake target dependencies of the executable/library
111 ## as an example, message headers may need to be generated before nodes
112 # add_dependencies(minimal_nodes_node minimal_nodes_generate_messages_cpp)
113
114 ## Specify libraries to link a library or executable target against
115 target_link_libraries(minimal_publisher ${catkin_LIBRARIES} )
116 target_link_libraries(sleepy_minimal_publisher ${catkin_LIBRARIES} )
117 target_link_libraries(minimal_subscriber ${catkin_LIBRARIES} )
118 target_link_libraries(minimal_simulator ${catkin_LIBRARIES} )
119 target_link_libraries(minimal_controller ${catkin_LIBRARIES} )
120 #############
解析代码
add_executable(minimal_publisher src/minimal_publisher.cpp)
参数是期望的可执行文件名(选定为minimal_publisher),以及到源代码的相对路径(src/minimal_publisher.cpp)。
target_link_libraries(minimal_publisher ${catkin_LIBRARIES} )
用于告知编译器把新的可执行文件和声明过的库链接起来。
总结最小订阅器和发布器节点
我们已经知道创建自己的发布器和订阅器节点的基本要素,一个单节点可以通过复制相应的代码来创建额外的订阅器对象和回调函数,
以此订阅多个话题,相似地,一个节点也可以通过实例化多个发布器对象来发布多个话题,一个节点可以既是订阅器,也是发布器。
在ROS控制的机器人系统中,自定义设备驱动节点必须设计成能读取和发布传感器信息,以及设计一个或更多的节点来订阅执行器
(或控件设定值)命令并把他们强加在执行器上。幸运的是,对于常见传感器已经有大量现成的ROS驱动,包括LIDAR、相机、
kinect传感器、惯性测量单元、编码器等,他们可以作为程序包导入,在系统中直接使用(也可能需要调整引用系统中的一些
描述),也有用于驱动一些伺服系统的程序包,还有一些用于工业机器人的RoS工业接口,他们只需要发布和订阅机器人话题。