ROS话题通信,最详细最易懂(从文件构建到执行原理)

Package.xml和CMakelist.txt文件的功能

编译规则的“制定者”——CMakelist.txt文件

我们知道 Linux 平台编译 C++ 文件,需要编译器和链接器,其中编译器是将源代码编译成目标代码,链接器是将目标代码链接到可执行文件。若是编译单个文件,用 g++ 即可;若是编译一个 C++ 工程,则需要批处理编译工具,如 make,通过设定规则文件 makefile 调用 g++ 等编译工具进行批量编译。但 makefile 的编写十分复杂,便诞生了 CMake,通过编写简单的 CMakeLists.txt 规则文件,就能自动生成 makefile 文件,并且 CMake 是跨平台的,十分强大。ROS 的编译器便是 CMake,为了更加人性化,ROS 在 CMake 基础上封装了 catkin 命令,用 cmake 命令创建功能包时,会自动生成 CMakeList.txt 文件,已配置了多数编译选项,且包含详细的注释,只需稍作修改便可编译自己的文件。

这个文件直接规定了这个package要依赖哪些package,要编译生成哪些目标,如何编译等等流程。所以 CMakeLists.txt 非常重要,它指定了由源码到目标文件的规则,catkin编译系统在 工作时首先会找到每个package下的 CMakeLists.txt ,然后按照规则来编译构建。

我们细心就会发现有两种CMakelist.txt文件,一个是针对所有package的,一个是针对于某一个特性的package的。这两种不同使用范围的CMake文件回答了:“如何将所有package功能包生成的可执行文件组成我们想要的目标文件——宏观把握“‘以及”如何将某一个特定的package功能包中的源代码生成可执行目标文件——细节掌控“。

Package功能包的身份证——package.xml文件

pacakge.xml 包含了package的名称、版本号、内容描述、维护人员、软件许可、编译构建工具、编译依赖、运行依赖等信息。上面的CMakelist.txt回答了“如何编译,编译规则是什么“,这个package.xml文件则回答了”你用什么编译,编译环境的构建以及你是谁“的问题。

Launch文件中三个核心参数

Pkg:功能包名称;

Type(executable_name):实际运行的.cpp文件的名称(一个节点就是 ROS 功能包中的一个可执行文件);

Name:给上述type对应的节点起的别名(运行名)。一般情况下使用与type相同的名称,但可以根据需要,在运行时更改名称。

节点的类型就是“package_name+cpp_name“,但是节点名称一开始是我们init节点初始化时指定的名称,但是在.launch文件中指定的节点名称可以覆盖我们源文件中使用init定义的节点名称。

Package,node,.可执行源文件对应关系

功能包是ROS中的基本单元,包含src、include等文件夹一个功能包里可以有多个节点,在节点中进行发布和订阅话题;

一个节点就是 ROS 功能包中的一个可执行文件(.cpp、.py可执行源文件),节点之间可以通过 ROS 客户端库(如roscpp 、 rospy )相互通信。一个机器人控制系统由许多节点组成,这些节点各司其职,如,一个节点控制激光距离传感器,一个节点控制轮子的马达,一个节点执行定位,一个节点执行路径规划,一个节点提供系统的整个视图等。因此executable_name就是说的.cpp/.py源文件名称。

我们的.cpp源文件中的函数init指定的节点名称仅仅在计算图中表明节点之间的信息传递关系,其他的并没有用,有用的是以下两个名字:

1. 节点的executable_name:节点所在.cpp源文件的文件名;

2. 包的名称,即package_name:ROS项目文件夹下src目录中子文件夹的名称。

Cmake文件中起别名又是怎么回事?

在Cmake文件CMakeLists.txt中有一个add_executable宏定义,add_executable(demo01_pub src/hello_vscode_c.cpp)表示”将.cpp源文件映射为demo01_pub相当于给.cpp源文件取了个别名方便链接使用“。

target_link_libraries(demo01_pub, ${catkin_LIBRARIES})同理也是一样的,就是给别名对应的源文件添加链接库,以便编译源代码。

rosrun命令的形式

rosrun的使用格式一般为:rosrun [package_name] [node_name]。其中node_name就是我们源文件中init函数值指定的ROS节点的名称。

创建ROS项目的流程

1. 使用CTRL+SHIFT+T打开终端,然后使用“mkdir -p XXX/Project_Name/src“在指定路径下创建项目文件夹(XXX/Project_Name可以自定义但是其后一定要跟一个“/src”,如果文件夹下没有src子文件夹,你调用catkin_make进行编译时会报错);

2. 使用”cd XXX的绝对地址”跳转至我们的项目文件所在路径中,然后使用”catkin_make”对其进行编译生成具有ROS项目文件初始架构的项目文件;

3. 使用”code .”-使用vscode软件打开ROS项目文件;

4. 使用CRTL+SHITF+B进行编译,此时会让你设置编译工具,我们选择build进行配置,将一下代码粘贴进文件当中进行替换原内容:

{  
// 有关 tasks.json 格式的文档,请参见  
    // https://go.microsoft.com/fwlink/?LinkId=733558  
    "version": "2.0.0",  
    "tasks": [  
        {  
            "label": "catkin_make:debug", //代表提示的描述性信息  
            "type": "shell",  //可以选择shell或者process,如果是shell代码是在shell里面运行一个命令,如果是process代表作为一个进程来运行  
            "command": "catkin_make",//这个是我们需要运行的命令  
            "args": [],//如果需要在命令后面加一些后缀,可以写在这里,比如-DCATKIN_WHITELIST_PACKAGES=“pac1;pac2”  
            "group": {"kind":"build","isDefault":true},  
            "presentation": {  
                "reveal": "always"//可选always或者silence,代表是否输出信息  
            },  
            "problemMatcher": "$msCompile"  
        }  
    ]  
} 

5. 在src目录下创建文件夹,再在文件夹中的src文件夹下创建.cpp源文件,.cpp源文件名就是你的节点的可执行名这和节点名并不一样;

6. 打开package中的CMakelist.txt,修改如下代码:

add_executable(节点可执行名称  
  src/C++源文件名.cpp  
)  
target_link_libraries(节点可执行名称  
  ${catkin_LIBRARIES}  
) 

这里设定的节点可执行名称就是src/C++源文件名.cpp这段冗长的文字的替代,我们一般将其选取为C++源文件名。其实我们一般说的节点类型(type)也就是我们这里的executable_name了。如下图所示:

ROS中服务通信原理

通信基本框架

话题通信步骤如下:

① talker,也就是发布端先在master中注册自身的信息,包括自身的RPC地址以及自身所发布的话题(基于RPC协议);

② listener,也就是订阅方也在master中注册自身的信息,这里的master只需要知道listener的订阅话题即可(基于RPC协议);

③ 一旦master匹配到有相同话题的talker和listener就会有建立连接的趋势,即master先将talker发布端的RPC远程调用地址发给listener订阅端(基于RPC协议);

④ Listener 接收到 Master 发回的 Talker 地址信息,尝试通过 RPC 向 Talker 发送连接请求,传输订阅的话题名、消息类型以及通信协议(TCP/UDP)(基于RPC协议);

⑤ Talker 接收到 listener 发送的连接请求后,继续通过 RPC 向 Listener 确认链接信息,其中包含自身的 TCP 地址信息。(基于RPC协议);

⑥ listener再通过TCP地址在TCP网络中找到talker并使用 TCP 尝试与 Talker 建立网络连接(基于TCP/IP协议);

⑦ talker通过TCP网络向listener发送信息(基于TCP/IP协议)。

通信协议之间的转换

发布端-订阅端网络组成

在一个网络中,一个发布端只能发布一个话题,但是一个订阅端可以订阅一个或多个话题,例如:红外相机和雷达共同采集到数据分别通过topic1和topic2送给需要这些数据的node1,则就是多对一,红外相机和雷达都分别可以发布topic1和topic2主题的消息,node1可以接受不同主题的消息。示例如下:

① 订阅端代码:

#include <ros/ros.h>    
#include "std_msgs/String.h"  
#include <boost/thread.hpp>  
#include <sstream>  
  
class SubscribeAndPublish    
{    
public:    
  SubscribeAndPublish()    
  {    
    count = 0;  
    //Topic you want to publish    
    pub_ = n_.advertise<std_msgs::String>("/chatter_pub", 1000);    
  
    //Topic1 you want to subscribe    
    sub_ = n_.subscribe("chatter1", 10, &SubscribeAndPublish::callback1, this);   
    //Topic2 you want to subscribe    
    sub2_ = n_.subscribe("chatter2", 10, &SubscribeAndPublish::callback2, this);    
  }    
  
  void callback1(const std_msgs::String::ConstPtr& msg1)    
  {    
    ROS_INFO("I heard: [%s]", msg1->data.c_str());   
    //.... do something with the input and generate the output...  
    std::stringstream ss;  
    ss << "Pub: hello world " << count;  
    output.data = ss.str();  
    pub_.publish(output);  
    ROS_INFO("%s", output.data.c_str());   
    ++count;   
  }    
  void callback2(const std_msgs::String::ConstPtr& msg2);   
  
private:    
  ros::NodeHandle n_;     
  ros::Publisher pub_;    
  ros::Subscriber sub_;  
  ros::Subscriber sub2_;   
  std_msgs::String output;  
  int count;   
  
};//End of class SubscribeAndPublish    
  
int main(int argc, char **argv)    
{    
  //Initiate ROS    
  ros::init(argc, argv, "subscribe_and_publish");    
  
  //Create an object of class SubscribeAndPublish that will take care of everything    
  SubscribeAndPublish test;    
  //ros::spin();  
  ros::MultiThreadedSpinner s(2);  //多线程  
  ros::spin(s);    
  
  return 0;    
}    
  
void SubscribeAndPublish::callback2(const std_msgs::String::ConstPtr& msg2)  
{  
  ROS_INFO("I heard: [%s]", msg2->data.c_str());  
  ros::Rate loop_rate(0.5);//block chatterCallback2() 0.5Hz  
  loop_rate.sleep();  
} 

② 话题为chatter1的发布端:

#include "ros/ros.h"  
#include "std_msgs/String.h"  
#include <sstream>  
  
int main(int argc, char **argv)  
{  
  ros::init(argc, argv, "talker1");  
  
  ros::NodeHandle n;  
  ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter1", 1000);  
  
  ros::Rate loop_rate(10);  
  int count = 0;  
  
  while (ros::ok())  
  {  
    std_msgs::String msg;  
    std::stringstream ss;  
    ss << "chatter1: hello world " << count;  
    msg.data = ss.str();  
    ROS_INFO("%s", msg.data.c_str());  
    chatter_pub.publish(msg);  
  
    ros::spinOnce();  
    loop_rate.sleep();  
    ++count;  
  }  
  return 0;  
} 

③ 话题为chatter2的发布者

#include "ros/ros.h"  
#include "std_msgs/String.h"  
#include <sstream>  
  
int main(int argc, char **argv)  
{  
  ros::init(argc, argv, "talker2");  
  
  ros::NodeHandle n;  
  ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter2", 1000);  
  
  ros::Rate loop_rate(10);  
  int count = 0;  
  
  while (ros::ok())  
  {  
    std_msgs::String msg;  
    std::stringstream ss;  
    ss << "chatter2: hello world " << count;  
    msg.data = ss.str();  
    ROS_INFO("%s", msg.data.c_str());  
    chatter_pub.publish(msg);  
  
    ros::spinOnce();  
    loop_rate.sleep();  
    ++count;  
  }  
  return 0;  
} 

此外在一个网络中master可以接收各种各样talker的信息注册,然后listener可以从master中通过设置话题可以订阅任意一个talker发出的消息,这样就组成了一个复杂的网络,也就是多个发布端发出数据,多个订阅端订阅数据的通信架构。

ROS发布方与订阅方交互数据的实现步骤

项目结构

发布方源文件程序实现(hello_vscode_c.app)

#include "ros/ros.h"  
#include "std_msgs/String.h"  
#include "sstream"  
  
int main(int argc, char* argv[])  
{  
    // set local language  
    setlocale(LC_ALL,"");  
    // initial ros node  
    ros::init(argc,argv,"talker");  
    // set node handler  
    ros::NodeHandle nh;  
    // create publishing object  
    ros::Publisher pub = nh.advertise<std_msgs::String>("topic",10);  
    // organize message data  
    std_msgs::String msg;  
    std::stringstream ss;  
    int count = 0; // message number counter  
    // set sending frequency  
    ros::Rate SendedRate(10);  
    while(ros::ok())  
    {  
        ss<<"hello "<<count;  
        msg.data = ss.str();  
        // publish message information  
        pub.publish(msg);  
        // printf ROS information on the worktable  
        ROS_INFO("sended message is %s",ss.str().c_str());  
        count++;  
        SendedRate.sleep(); // come into sleep for the rest of time after sending message per interval  
    }  
    return 0;  
}  

订阅者源程序实现(demo02.cpp)

#include "ros/ros.h"  
#include "std_msgs/String.h"  
#include "sstream"  
  
void TopicCallback(const std_msgs::String::ConstPtr& topic_msg)  
{  
    ROS_INFO("subscribing information: %s",topic_msg->data.c_str());  
}  
  
int main(int argc,char* argv[])  
{  
    // set local language  
    setlocale(LC_ALL,"");  
    // initial ros node  
    ros::init(argc,argv,"listener");  
    // create node handler to communicate with master  
    ros::NodeHandle nh;  
    // create subscribing object  
    ros::Subscriber sub = nh.subscribe<std_msgs::String>("topic",10,TopicCallback);  
    // blocking function - looping to take information  
    ros::spin();  
    return 0;  
}  

修改CMakelist.txt文件中的链接指令

add_executable(hello_vscode_c src/hello_vscode_c.cpp)  
add_executable(demo02 src/demo02.cpp)  
  
target_link_libraries(hello_vscode_c  
  ${catkin_LIBRARIES}  
)  
target_link_libraries(demo02  
  ${catkin_LIBRARIES}  
) 

ros::spinOnce()和ros::spin ()的异同点

这两个函数都是干啥的?

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

这两个函数有啥不一样?

阻塞函数(ros::spin())和非阻塞函数(ros::spinOnce())

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

其实看函数名也能理解个差不多,一个是一直调用;另一个是只调用一次,如果想要调用ros::spinOnce()实现ros::spin()的效果,那就需要加上循环了(用while(ros::ok()){…}循环)。

使用的注意事项

这里一定要记住,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批量处理一次数据,人家发两次我处理一次。

如何解决非阻塞函数(ros::spinOnce())的缺点?

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

相较于非阻塞函数(ros::spinOnce()),阻塞函数(ros::spin())又有什么优点?

阻塞函数(ros::spin())没有放在while循环中,这也就不会有“ros::Rate loop_rate(5)“这条代码,也就是说ros::spin()并不会按照一定频率执行,而是一直执行阻塞了代码执行的通道,也就是说卡在这里了,卡在这里等待着话题接受由发布者发布的新的数据,来一个我就处理一个。也就是说非阻塞函数(ros::spinOnce())更像异步,跟不上趟的那部分数据用缓冲队列进行存储等待下一次一起处理掉,而阻塞函数(ros::spin())更像同步,那边发一个,我这边处理一个。

发布者节点的注册和信息的发布

// create ROS Nodehandler
ros::NodeHandle nh;
// create publisher object
ros::Publisher pub = nh.advertise<hello_ros::Person>("topic_person",10);

发布者节点的注册就是由上述代码完成的,当你阅读publish的源码实现你就会知道,只有在“master节点完成了publisher信息的注册(话题,缓冲队列的数量)”并且“publisher和subscriber之间完成了TCP/IP网络通信的连接”之后,advertise函数才会返回一个publisher对象供我们调用。这个publisher对象的通信模式(publisherßàsubscriber)不同于advertise的通信方式(publisherßàmaster),前者用TCP/IP协议后者用RPC协议(Remote Procedure Call)。我们前面提到过发布订阅模式的如下通信原理图:

因此我们以后可以知道:publisher与master相互通信的函数是通过Nodehandler节点句柄来实现的,也就是说publisher可以通过Nodehandler.advertise函数向master递交“publisher的注册信息,例如RPC地址、publisher的消息主题、publisher的缓冲区大小等信息”。,但是当publisher与subscriber之间通过TCP/IP协议建立通信链路之后,就不需要master的参与了,此时我们换了另一个函数进行publisher和subscriber两者之间的通信,即publisher_object.publish(message_object)。

设置当前程序使用的本地化信息——setlocale函数

Setlocale()是一个计算机函数,功能是用来配置地域的信息,设置当前程序使用的本地化信息。函数原型如下:

char *setlocale(int category, const char *locale) ;  

函数的参数和返回值如下所示:

②.参数:  
category  这是一个已命名的常量,指定了受区域设置影响的函数类别。  
LC_ALL  包括下面的所有选项。  
LC_COLLATE  字符串比较。参见 strcoll()。  
LC_CTYPE  字符分类和转换。例如 strtoupper()。  
LC_MONETARY  货币格式,针对 localeconv()。  
LC_NUMERIC  小数点分隔符,针对 localeconv()。  
LC_TIME  日期和时间格式,针对 strftime()。  
LC_MESSAGES  系统响应。  
locale  如果 locale 是 NULL 或空字符串 "",则区域名称将根据环境变量值来设置,其名称与上述的类别名称相同。  
③.返回值:  
如果成功调用 setlocale(),则返回一个对应于区域设置的不透明的字符串。如果请求无效,则返回值是 NULL。 

为什么发布方和订阅方都有缓冲区队列?

发布方中的队列:传感器采集数据太快了,我还没来得及发布呢信息就已经从传感器传过来了,我上传的给话题的数据不能够丢失,那我就自己弄一个缓冲区队列吧,以协调传感器数据发送速度和发布方接受传感器数据速度;

订阅方中的队列:发布方传数据穿的忒快了,我还没来得及处理呢他那边的数据就又发过来了,我订阅的数据一个也不能丢并且由于对方数据太快我根本不能接受一个处理一个,那就建个缓冲区吧,让我每间隔一段时间下一子处理一堆数据,用来协调发布方发送数据的速度和接收订阅方接受数据的速度。

订阅方与发布方信息收发数量不一致

为何发布了数据之后,我们订阅到的数据丢失了前几条呢?

在我们publisher发布数据时,publisher与master的数据链还未建立,即master中还没有publisher的信息,也就是说此时listener还不知道有相同话题的publisher的存在。这就导致我们的publisher一边发送数据,master一边注册publisher的信息与listener进行匹配,这就不可避免地导致listener丢失几条数据,但由于注册时间很短因此丢失数据不会太多。

如何解决发布方和订阅方收发数据数量不一致的问题呢?

让publisher发布的第一条信息延迟3秒发布出去,对应代码如下:

在while循环上面添加一句延迟发送的数据,我们验证过publisher发送第三条数据时subscriber才接收数据,那就说明“master注册publisher信息到publisher和subscriber之间的通信信道建立“一共耗时”publisher发布三条数据所耗费的时间,即3/frequency(publisher)“。我们最少延迟3/frequency(publisher)秒才可以使得收发双方建立信道传输数据,我们这里延迟了3秒大大的大于所需的最少时间,即3秒延迟后收发双方的信息传输通道一定已经建立完成。

订阅过程中回调函数被调用的时机

我们从上图中看到了:回调函数的调用是有条件的。

自定义话题通信的数据类型

发布者(publisher)的.cpp源代码

#include "ros/ros.h"  
#include "hello_ros/Person.h"  
  
int main(int argc,char* argv[])  
{  
    // localization  
    setlocale(LC_ALL,"");  
    // create ROS node  
    ros::init(argc,argv,"talker_person");  
    // create ROS Nodehandler  
    ros::NodeHandle nh;  
    // create publisher object  
    ros::Publisher pub = nh.advertise<hello_ros::Person>("topic_person",10);  
    // set publishing frequency  
    ros::Rate rate(10);  
    // delay publishing  
    ros::Duration(3).sleep();  
    // define person-type object  
    hello_ros::Person person_object;  
    person_object.age = 10;  
    person_object.height = 1.73;  
    person_object.name = "father";  
    // publishing information for cycle  
    while(ros::ok())  
    {  
        pub.publish(person_object);  
        ROS_INFO("name:%s;height:%f;age:%d\n\t",person_object.name,person_object.height,person_object.age);  
        rate.sleep();  
    }  
} 

其中,我们输出时加上了一句ROS_INFO日志输出命令,是我们输出的数据更加有条理:

  1. name: "father"  
  2. age: 10  
  3. height: 1.73 

发布者源代码运行步骤

1. 跳转至ROS项目文件下的工作空间(cd XXX);

2. 运行roscore启动ROS核心;

3. 使用rosrun hello_ros pub_person运行发布者所在的源文件。

按如上步骤进行完毕后,你会看到如下输出:

订阅者(subscriber)的.cpp源代码

#include "ros/ros.h"  
#include "hello_ros/Person.h"  
  
void Topic_personCallbackFunc(const hello_ros::Person::ConstPtr& msg)  
{  
    ROS_INFO("subscribing information:name:%s;age:%d;height:%f\n\t",msg->name,msg->age,msg->height);  
}  
  
int main(int argc,char* argv[])  
{  
    // localization  
    setlocale(LC_ALL,"");  
    // create ROS node  
    ros::init(argc,argv,"lisener_person");  
    // create ROS Nodehandler  
    ros::NodeHandle nh;  
    // create publisher object  
    ros::Subscriber sub = nh.subscribe<hello_ros::Person>("topic_person",10,Topic_personCallbackFunc);  
    // receive subscribing information and callback funcion  
    ros::spin();  
    return 0;  
}

订阅者源代码运行步骤

1. 跳转至ROS项目文件下的工作空间(cd XXX);

2. 运行roscore启动ROS核心;

3. 使用rosrun hello_ros sub_person运行订阅者所在的源文件。

执行完上面的步骤,你就会看到如下输出的信息:

让系统知道你自定义的.msg消息文件在哪里?

add_message_files(  
  FILES  
  Person.msg  
) 

上述代码告知了编译器:“Person.msg就是用户自定义的.msg消息文件你以后编译含有消息的文件时就找它就行“。

运行代码前的准备工作

CMakelist.txt文件中的修改

.cpp源代码链接库的设定

target_link_libraries(hello_vscode_c  
  ${catkin_LIBRARIES}  
)  
target_link_libraries(demo02  
  ${catkin_LIBRARIES}  
)  
target_link_libraries(pub_person  
  ${catkin_LIBRARIES}  
)  
target_link_libraries(sub_person  
  ${catkin_LIBRARIES}  
)

.cpp源码文件的可执行名称命名(节点的可执行名称设定)

add_executable(hello_vscode_c src/hello_vscode_c.cpp)  
add_executable(demo02 src/demo02.cpp)  
add_executable(pub_person src/pub_person.cpp)  
add_executable(sub_person src/sub_person.cpp)  

编译.cpp源代码的功能包设置

find_package(catkin REQUIRED COMPONENTS  
  roscpp  
  rospy  
  std_msgs  
  message_generation  
) 

运行.cpp源代码的功能包设置

catkin_package(  
#  INCLUDE_DIRS include  
#  LIBRARIES hello_ros  
 CATKIN_DEPENDS roscpp rospy std_msgs message_runtime  
#  DEPENDS system_lib  
) 

指定处理.msg/.srv消息文件所需的ROS官方库文件

generate_messages(  
  DEPENDENCIES  
  std_msgs  
) 

在msg文件夹中指定我们所需编译的消息文件

add_message_files(  
  FILES  
  Person.msg  
) 

注意:这里在ROS项目中新建的文件夹一定要以msg命名!

指定编译.msg文件的功能包——generate_message宏命令

generate_messages(    
  DEPENDENCIES    
  std_msgs    
)

上述代码就说明了“如果我要编译我们自定义的.msg文件就必须使用官方的std_msgs功能包,即我们的所有自定义文件是以官方文件为基础建立起来的,要将我们自定义的文件编译成ROS标准文件就必须依赖官方文件”。最终在devel/include文件夹下,使用std_msgs编译生成的符合ROS执行标准的.h/.py文件。

在vscode编译文件(c_cpp_properties.json)中添加消息头文件所在目录

{  
  "configurations": [  
    {  
      "browse": {  
        "databaseFilename": "",  
        "limitSymbolsToIncludedHeaders": true  
      },  
      "includePath": [  
        "/opt/ros/noetic/include/**",  
        "/usr/include/**",  
        "/home/rosnetic/RosProject/devel/include/**"  
  
      ],  
      "name": "ROS",  
      "intelliSenseMode": "gcc-x64",  
      "compilerPath": "/usr/bin/gcc",  
      "cStandard": "gnu17",  
      "cppStandard": "c++17"  
    }  
  ],  
  "version": 4  
}

如何查询到./devel/person.h的所在目录呢?

在文件目录中,devel/include文件夹上右键可以见到“在工作空间中执行”,点击之后输入“pwd”命令,工作空间的命令行回出线person.h所在的路径,我们只需复制一下粘贴至vscode编译配置.json文件即可,千万别忘了按照人家includePath中的格式书写:

"/home/rosnetic/RosProject/devel/include/**"  

在发布者源文件和订阅者源文件中添加自定义消息类型头文件

订阅者

#include "ros/ros.h"  
#include "hello_ros/Person.h"  
  
void Topic_personCallbackFunc(const hello_ros::Person::ConstPtr& msg)  
{  
    ROS_INFO("subscribing information:name:%s;age:%d;height:%f\n\t",msg->name,msg->age,msg->height);  
}  
  
int main(int argc,char* argv[])  
{  
    // localization  
    setlocale(LC_ALL,"");  
    // create ROS node  
    ros::init(argc,argv,"lisener_person");  
    // create ROS Nodehandler  
    ros::NodeHandle nh;  
    // create publisher object  
    ros::Subscriber sub = nh.subscribe<hello_ros::Person>("topic_person",10,Topic_personCallbackFunc);  
    // receive subscribing information and callback funcion  
    ros::spin();  
    return 0;  
} 

发布者

#include "ros/ros.h"  
#include "hello_ros/Person.h"  
  
int main(int argc,char* argv[])  
{  
    // localization  
    setlocale(LC_ALL,"");  
    // create ROS node  
    ros::init(argc,argv,"talker_person");  
    // create ROS Nodehandler  
    ros::NodeHandle nh;  
    // create publisher object  
    ros::Publisher pub = nh.advertise<hello_ros::Person>("topic_person",10);  
    // set publishing frequency  
    ros::Rate rate(10);  
    // delay publishing  
    ros::Duration(3).sleep();  
    // define person-type object  
    hello_ros::Person person_object;  
    person_object.age = 10;  
    person_object.height = 1.73;  
    person_object.name = "father";  
    // publishing information for cycle  
    while(ros::ok())  
    {  
        pub.publish(person_object);  
        ROS_INFO("name:%s;height:%f;age:%d\n\t",person_object.name,person_object.height,person_object.age);  
        rate.sleep();  
    }  
}  

一定要要注意:person.h文件所在的命名空间,一般是我们自定义功能包的名称,我们也可以打开devel/include/person.h文件一探究竟:

namespace hello_ros  
{  
template <class ContainerAllocator>  
struct Person_  
{  
  typedef Person_<ContainerAllocator> Type;  
  
  Person_()  
    : name()  
    , age(0)  
    , height(0.0)  {  
    }  
  Person_(const ContainerAllocator& _alloc)  
    : name(_alloc)  
    , age(0)  
    , height(0.0)  {  
  (void)_alloc;  
    } 
……
}

我们清楚地看到person.h对外的接口以及成员变量均是在hello_ros命名空间下定义实现的。

Package.xml文件中的修改

由于package.xml是ROS项目编译器的身份证,其中包含了编译器编译和运行时所需要以来的外部官方功能包,即在package.xml中添加编译依赖与执行依赖:

<build_depend>message_generation</build_depend>  
<exec_depend>message_runtime</exec_depend> 

设置依赖项

指定.msg/.srv文件与.cpp文件编译顺序

add_dependencies(pub_person ${PROJECT_NAME}_generate_messages_cpp)  

这句代码的意思是添加依赖项,也就是说你在编译你的.cpp文件之前需要先编译.msg/.srv文件,你就必须规定一下编译的先后顺序,上述代码就表示“先编译.msg/.srv文件在编译.cpp文件”,如果不然你的.cpp文件中引用的”Person.h”消息类型头文件将不会被找到,因为在你.cpp文件编译之前.msg/.srv文件根本没被编译,那你上哪去找他们编译后的生成文件呢!

规定.cpp源码文件的编译的先后顺序

假设我们需要生成一个可执行文件,该文件生成需要链接a.so b.so c.so d.so四个动态库

正常来讲,我们一把只需要以下两条指令即可:

ADD_EXECUTABLE(main main.cpp)  
TARGET_LINK_LIBRARIES(main a.so b.so c.so d.so)  

但是编译的时候报错,一些符号的定义找不到,而这些符号恰恰就在这几个库中,假设在a.so 和 b.so中,在上述两条指令之间加上一条指令即可编译通过: 

ADD_DEPENDENCIES(main a.so b.so)  

原因比较简单,生成main需要依赖a.so和b.so中的符号定义,然而a.so和b.so库的生成是在main编译生产之后的,添加这条语句就是提醒编译器需要先生成main的依赖(a.so,b.so),然后再去生成main.

如何查看publisher发布的数据信息?

1. 执行roscore命令,以便启动ROS系统核心;

2. 新建工作空间,然后使用cd指令跳转至文件目录下的工作空间;

3. 使用source ./devel/setup.bash指令来运行我们的ROS项目启动文件;

4. rosrun package_name executable_node_name,其中executable_node_name就是节点所在.cpp源文件的文件名;

5. 新建工作空间,执行2-3步骤,然后使用rostopic echo topic_name来查看publisher发布的消息。

注意:每次新建的工作空间时,都必须走一下上面的程序,尤其是要进行source运行我们ROS项目的启动文件!要想每次打开新的工作空间系统默认运行ROS项目的启动文件,我们就要添加如下命令:

echo "source ~/catkin_ws/devel/setup.sh" >> ~/.bashrc 

CMakelist.txt文件中catkin_package与find_package的功能

上面介绍完了“如何将消息文件编译成符合ROS标准的文件”,那现在我们就介绍一下“如何将package中的源文件编译成我们的符合ROS标准的文件”。这里先介绍两个定义:

编译依赖项:我们把这个包编译成ROS标准文件需要依赖哪些ROS功能包;

运行依赖项:ROS功能包运行所需的功能包。

自定义功能包/find_package引用的功能包/catkin_package引用的功能包的关系如下:

在 ROS 功能包的 CMakeLists.txt 文件中有这样两个宏,catkin_package 与 find_package。我们如下代码进行示例说明:

find_package(catkin REQUIRED COMPONENTS  
  roscpp  
  rospy  
  std_msgs  
  message_generation  
)  
catkin_package(  
#  INCLUDE_DIRS include  
#  LIBRARIES hello_ros  
 CATKIN_DEPENDS roscpp rospy std_msgs message_runtime  
#  DEPENDS system_lib  
)  

find_package 是用于加载 catkin 宏和指定对其他 ROS 功能包的依赖关系,也就是说我要将将自定义的功能包编译成符合ROS标准的功能包。编译构建此功能包的ROS标准文件需要依赖于如下功能包,即编译依赖项:

roscpp  
rospy  
std_msgs  
message_generation  

catkin_package是将符合ROS标准的功能包编译成符合CMake标准的功能包,即运行依赖项:

CATKIN_DEPENDS roscpp rospy std_msgs message_runtime  

命令的递进关系

上面CMakelist.txt编译规则指定文件中的关于消息的宏命令顺序如下:

find_package(catkin REQUIRED COMPONENTS  
  roscpp  
  rospy  
  std_msgs  
  message_generation  
)  
add_message_files(  
  FILES  
  Person.msg  
)  
generate_messages(  
  DEPENDENCIES  
  std_msgs  
)  
catkin_package(  
#  INCLUDE_DIRS include  
#  LIBRARIES hello_ros  
 CATKIN_DEPENDS roscpp rospy std_msgs message_runtime  
#  DEPENDS system_lib  
) 

我们使用find_package找到我们将整个自定义package功能包编译成ROS的标准文件所需的所有官方功能包,然后我们使用add_message_files告知系统编译器“我自定义的.msg消息文件是XXX.msg“,再然后我们使用generate_messages来告知编译器”我编译XXX.msg文件所需的官方功能包是std_msgs“,最后使用catkin_package命令调用roscpp rospy std_msgs message_runtime这四个功能包将编译好的ROS标准文件进一步编译为可以执行的目标文件。

总之,我们的编译过程可以大致分为三步:

1. 添加将现有文件编译成ROS标准文件的依赖包;

2. 告知编译器用户自定义的.msg消息文件的位置以及所需的编译功能包;

3. 使用官方功能包将初次编译好的ROS标准文件进一步编译成为可执行的目标文件。

Cmakelist.txt与package.xml文件的关系

在package.xml添加了如下两句命令:

构建依赖项:  
<build_depend>roscpp</build_depend>  
<build_depend>rospy</build_depend>  
<build_depend>std_msgs</build_depend>  
<build_depend>message_generation</build_depend>  
执行/运行依赖项:  
<exec_depend>roscpp</exec_depend>  
<exec_depend>rospy</exec_depend>  
<exec_depend>std_msgs</exec_depend>  
<exec_depend>message_runtime</exec_depend>  

这表明我们的编译器加入了“用于构建ROS标准文件的依赖包message_generation”以及“运行ROS标准文件的依赖包message_runtime”。

在CMakelist文件中我们使用这两个依赖项去指定我们的编译规则:

find_package(catkin REQUIRED COMPONENTS  
  roscpp  
  rospy  
  std_msgs  
  message_generation  
)  
catkin_package(  
#  INCLUDE_DIRS include  
#  LIBRARIES hello_ros  
 CATKIN_DEPENDS roscpp rospy std_msgs message_runtime  
#  DEPENDS system_lib  
)  

编译好的消息文件如何使用呢?

编译完成的.msg是如何被.cpp源文件调用的呢?这里的ROS编译器将XXX.msg文件编译生成devel开发空间中include文件夹下的XXX.h头文件,我们如果要在.cpp内使用我们自定义的消息类型,我们只需要包含XXX.h头文件即可使用,相当于一个“名为XXX的结构体,其内部元素就是我们在XXX.msg文件中自定义的变量名称,变量所对应的数据类型就是我们在XXX.msg文件中指定的ROS数据类型所对应的C++数据类型”。

注意:我们以后调用XXX.msg相关文件时都是从devel存放中间文件的文件夹中调用的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

肥肥胖胖是太阳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值