欢迎来到最后一讲。关于自定义消息类型网上已经有一些文章了,写地也明了,不过这这里会补充的几点就是你自定义了一个新的消息类型之后在不同的package和不同的workspace中如何使用,当然我们仍然从基本的讲起。

定制基本消息类型

当ROS提供的消息类型不满足你的需求时,你就需要考虑制作自己的消息类型了。比如你想要发布一则消息,这则消息包含一个double类型的向量,一个整数,一个字符串,他们都是基本的消息类型(属于std_msgs里的)。 下面我们来看步骤 1:在pub_sub_test这个package下建立一个新的文件夹,文件夹的名字叫msg 2:在msg里创建一个新的文件,名字叫MyBasicMessage.msg。 3:打开MyBasicMessage.msg,并在其中输入下面内容
 
string message_id
int64 message_data1
float64[] message_data2
  为什么是上面这个格式定义message内部呢?回想我们在第二讲里讲到,如果我们想发布某种类型的消息,我们需要了解这个消息包含哪些东西,这样我们才好为它赋值等。比如你想发布Float64,我们已经知道这是属于std_msgs这个命名空间下的消息类型,打开下面这个网页http://docs.ros.org/api/std_msgs/html/msg/Float64.html你能看到Float64怎么定义的。     1  
从这个页面我们知道Float64这个消息包含一个消息类型是float64名字叫data的成员。于是我们在pub_float64.cpp代码里类似如下使用  
...
double abc = 123.456;
std_msgs::Float64 msg; //定义Float64对象msg
msg.data = abc;//为类成员data赋值,赋值类型为double,即float64
...
  当我们要自定义消息时,我们要做的就是模仿就行了。在.msg文件夹里,模仿上面的定义方式即可  
成员类型 自定义成员名字
  那么我们怎么知道成员类型是什么呢?比如字符串是string,这个到简单,double变成float64,double类型的向量竟然是float64[]。其实这个我们同样在第一二讲讲了,ROS把这些基本类型重新定义了一番,具体可见http://wiki.ros.org/msg。在Built-in types下就写明了ROS自身的基本消息类型和C++,python中消息类型的对应关系,比如C++里的double在ROS中对应的是float64。所以我们会在msg文件里使用float64 name这种方式定义一个数据成员。同样在该页面中Array handling部分我们可以看到ROS对某种数据类型的数组的定义方式就是在基本类型后面加了个[]符号。比如bool[]对应c++的std::vector<uint8_t>. 现在我们知道如何定义以及为什么如此定义基本类型了,在msg文件写好后,我们需要让我们的package知道我们新定义了消息类型,接着上面如下做 4:打开pub_sub_test的CMakeLists.txt 找到  
find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  geometry_msgs
)
  添加一行  
find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  geometry_msgs
  message_generation
)
  依赖项 message_generation是要自定义message所必须添加的。 找到  
# add_message_files(
#   FILES
#   Message1.msg
#   Message2.msg
# )
  改为  
add_message_files(
  FILES
  TestBasicMessage.msg
)
  这相当于让这个package知道我们定义了新的message了。 找到  
# generate_messages(
#   DEPENDENCIES
#   geometry_msgs#   std_msgs#  
# )
  改为  
generate_messages(
  DEPENDENCIES
  std_msgs
)
  这是让package知道我们定义的消息是依赖于std_msgs的。因为我们上面定义的消息类型都属于基础消息类型。我们得指明这一点。 关闭保存CMakeLists.txt。打开pub_sub_test的package.xml。 找到  
<!--   <build_depend>message_generation</build_depend> -->
...
<!--   <build_export_depend>message_generation</build_export_depend> -->
...
<!--   <exec_depend>message_runtime</exec_depend> -->
  把注释都去掉,变为  
<build_depend>message_generation</build_depend> 
  ...
<build_export_depend>message_generation</build_export_depend>
...
<exec_depend>message_runtime</exec_depend>
  保存并关闭package.xml。这些都是死步骤,需要自定义message这么做即可。 之后使用catkin_make编译。你自定义的消息类型就已经产生了。如何使用呢?和一般消息类型使用没有差别。 我们在pub_sub_test/src里新创建一个cpp文件,名字叫pub_my_basic_message.cpp,把pub_string.cpp里或者之前写的其他基础pub程序赋值进去,改成下面的样子.  
#include "ros/ros.h"
#include "pub_sub_test/MyBasicMessage.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<pub_sub_test::MyBasicMessage>("chatter", 1000); //ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);

  ros::Rate loop_rate(10);

  int count = 0;
  double data2 = 1;
  while (ros::ok())
  {
    pub_sub_test::MyBasicMessage msg; //std_msgs::String msg;

    // std::stringstream ss;
    // ss << "hello world " << count;
    msg.message_id = "1";
    msg.message_data1 = count;
    msg.message_data2.push_back(data2);


    ROS_INFO("%ld", msg.message_data1); //ROS_INFO("%f", msg.data.c_str())

    chatter_pub.publish(msg);

    ros::spinOnce();

    loop_rate.sleep();
    ++count;
  }


  return 0;
}
  变化 1: #include "std_msgs/String.h"变成了#include "pub_sub_test/MyBasicMessage.h",即包含package名/msg文件名.h 2:在advertise函数使用的地方 <std_msgs::String>变成了<pub_sub_test::MyBasicMessage> 3:std_msgs::String msg 变成了pub_sub_test::MyBasicMessage msg; 4:为我们定义的消息类型赋值 在MyBasicMessage.msg里我们定义了类型为string名字叫message_id的成员,所以我们使用msg.message_id,并赋值为字符串“1”。定义了float64(std::vector<double>)类型的成员message_data2,使用vector的函数push_back传入一个float64的变量。ROS_INFO就print出mesage_data1大家感受一下就是了。 其余没变。总的来说程序中pub你自定义的消息和你想pub任何ROS自带的消息的步骤一样。 你把这个cpp写入CMakeLists里编译即可。同样使用rosrun可以跑这个程序。sub文件就不再写了,很类似地改。不再赘述。

定制高级消息

所谓高级消息,即是想PoseStamped那样的东西。其实步骤也一模一样,我们在msg文件夹再创建一个MyAdvancedMessage.msg。并在其中写入下面内容。  
geometry_msgs/Inertia SiHuan
geometry_msgs/Pose WuHuan
std_msgs/Header LiuHuan
 
2
  注意Compact Message Definition下面变量的定义方式,模仿就是了。你可以自己组建任何类型的消息。所以我把上面三种消息类型组合在了一起。其实没有什么大的意义hhh。至于Inertia类型的消息是什么怎么用,我想如果你看了前三讲这应该不是问题。 之后我们需要进一步修改pub_sub_test的CMakeLists。打开CMakeLists.txt,在    
add_message_files(
  FILES
  MyBasicMessage.msg
)
  中添加一行  
add_message_files(
  FILES
  MyBasicMessage.msg
  MyAdvancedMessage.msg
)
  即我们刚刚新建立的message的名字。 在  
generate_messages(
  DEPENDENCIES
  std_msgs
)
  中添加一行  
generate_messages(
  DEPENDENCIES
  std_msgs
  geometry_msgs
)
由于我们新建立的消息类型不仅有来自于std_msgs的,还有来自于geometry_msgs的,所以我们需要把这个包添加到消息的dependency里。保存退出,使用catkin_make编译即可。 之后你如果想发布这个类型的消息,写一个pub_my_advanced_message.cpp之类的文件,包含#include "pub_sub_test/MyAdvancedMessage.h",定义变量,赋值等,和前面一样。至于如何为Pose等类型的成员Wuhuan之类的赋值,我想看过第三讲应该不会有问题。

在packageA中使用packageB的中定义的消息类型

如果我们在pub_sub_test里定义的MyBasicMessage想在另一个包,比如我们的在讲如何使用roslaunch时建立的read_param_test这个package里,使用,应该怎么办呢? 我们可以先试一下,在read_param_test/src中我们写了一个show_param.cpp文件。我们可以先试一下添加头文件看成功否。打开show_param.cpp文件,在头文件那几行添加一行#include "pub_sub_test/MyBasicMessage.h",保存退出,使用catkin_make编译。编译不成功,显示找不到头文件。
3
那么如何找到它呢?如下
1:打开read_param_test里的CMakeLists.txt,在  
find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  geometry_msgs
)
    中添加一行    
find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  geometry_msgs
  pub_sub_test
)
   
是的,我们自己定义的包pub_sub_test也成了依赖项了。 之后在我们定义的可执行文件  
add_executable(read_param src/show_param.cpp)
target_link_libraries(read_param ${catkin_LIBRARIES})
  后添加一行  
add_executable(read_param src/show_param.cpp)
target_link_libraries(read_param ${catkin_LIBRARIES})
add_dependencies(read_param ${catkin_EXPORTED_TARGETS})
即添加add_dependencies(可执行文件名 ${catkin_EXPORTED_TARGETS})。关闭保存CMakeLists.txt。打开read_param_test的package.xml,在<build_depend>roscpp</build_depend>周围的位置添加  
<build_depend>pub_sub_test</build_depend>
  <exec_depend>pub_sub_test</exec_depend>
其实和任何在find_package里添加的依赖项一样,我们也需要在xml中指明添加的依赖项。保存xml,使用catkin_make编译。现在就不会报错了。现在你成功包含头文件,要定义发布MyBasicMessage消息之类的事情就随意拉。

把自定义的消息类型单独制成一个package/在不同workspace使用自定义的消息

现在我们能在同一个workspace(catkin_ws)的不同package里使用我们在pub_sub_test中定义的消息了,但是如果以后我们建立了一个其他的workspace我们还想用自定义的消息MyBasicMessage类型怎么办呢? 方法1:把pub_sub_test这个package直接复制到新的workspace(比如名字叫my_ws)my_ws/src里,使用catkin_make编译之后,pub_sub_test就在你的新的workspace里了,这时候你可以根据上面在packageA中使用pakcageB中定义的消息类型中的内容在新的workspace的不同package里使用自定义的消息了。 而且这个方法有一个bug,是ROS自己的问题。比如当我们把pub_sub_test移动到另一个新建的test_ws/src之后,使用catkin_make会编译失败,提示找不到MyBasicMessage.h。  
4
error_msg2.png
  因为首先得编译成功新的消息类型,才可以使用。现在这个workspace里并没有记载有这个类型的消息,然而我们的cpp文件
add_executable(pub_my_basic_message src/pub_my_basic_message.cpp)
target_link_libraries(pub_my_basic_message ${catkin_LIBRARIES})
已经包含了头文件并且试图编译使用了。这时候我们需要先把这两行文件注释,就能编译成功,编译成功后,新的消息类型在workspace里有了记录,在去掉那两行的注释,再编译,就能成功了= =.... 方法2:方法1的弊端是我们只是想使用自定义的消息,却把整个package都复制过去了,那个package里所有内容(cpp文件什么的)都用不到呢,非常'划不来'。当你意识到你自定义的消息类型需要被很多不同的workspace里的很多不同package使用时,把它单独制成一个package。这个package里没有任何的cpp文件或者python文件,只有msg的定义。这样你把这个package复制到各个不同workspace,将不会有任何多余的累赘复制过去,其实本质上和方法1是一样的。我们可以简单地试一下,cd 到catkin_ws/src,建立一个新的package,假设我们现在自定义的消息同样只包含std_msgs中的内容
 
catkin_create_package my_custom_message std_msgs message_generation message_runtime
由于我们并不会写任何执行文件,所以连roscpp和rospy这两个元老都省了。 这时候你新创建了一个pakcage,在pakcage中新建一个叫msg的文件夹。在文件夹中新建一个MyNewMsg.msg,在其中随便写点内容,如下
float64 data
string id
  之后打开该pakcage的CMakeLists.txt,和前面的内容类似了
# add_message_files(
#   FILES
#   Message1.msg
#   Message2.msg
# )
  去掉注释改为  
add_message_files(
  FILES
  MyNewMsg.msg
)
  另外  
# generate_messages(
#   DEPENDENCIES
#   std_msgs
# )
    去掉注释,改为  
generate_messages(
  DEPENDENCIES
  std_msgs
)
由于我们一开始就添加了依赖项message_generation和message_runtime所以我们不需要早find_packge()中做修改了,也不需要修改package.xml了。保存CMakeLists.txt关闭。之后CMakeLists.txt编译。编译成功之后你就拥有了一个只为定义消息而生的pakcage,这时候结合在packageA中使用packageB的中定义的消息类型和方法1,你可以把它复制到任何workspace给任何package使用了。同时复制到新的workspace也编译也不会像方法1出错,因为这个pakcage里没有任何可执行文件使用了自定义的消息。

系列总结

这一讲之后马上就结束ROS系列的讲解了。还有很多大课题都没讲到,比如ros service之类,还有许多细节没有讲到,比如关于ros spin,同步接收消息等等等等。不过我还是打算这一讲之后的就结束了。ROS只是作为一个工具帮助你实现一些东西,当你理解了前三章的东西,对发布订阅消息这个机制了解了之后,可以说所有东西你都可以google到了。基础的内容已经说地很详细,剩下的就是根据你在project中自己学习了。所以我也不打算再继续了。以后如果关于ROS有些新的记录和感想我会时不时记录,但不会系统地写了。 接下来的内容是关于SLAM的毕竟这才是我专业hhhh。 以后的文章主题,会讲解古老的EKF-SLAM,现在使用的graphSLAM以及基于graph-SLAM的各种应用单目slam以及visual-inertial SLAM。从2D到3D,从理论到程序每一个都详细的讲解出来,内容质量应该会比ROS系列更高。一方面为contribute for the community,另一方面讲解的过程当中自己也可以学到不少东西。所有SLAM程序也都会在ROS的大环境下完成这样你也可以了解到稍微大型的程序中ROS如何发挥作用。 不过更新肯定很慢要保证质量地用业余时间完成这些课题,想ROS的tutorial时不时写一点儿都用了快半年...如果是想现在做SLAM的tutorial还想把各种SLAM都详细地讲一遍加上程序,估计得一两年hhhh。所以主要面向未来读者吧hhhh。如果你需要现在就学习,可以找我私信推荐一些别人写的文章。