2.2 信息、类和服务器
2.2.1 自定义消息
(1)定义一条自定义消息
在~/ros_ws/src
目录下利用以下命令创建包:
cs_create_pkg example_ros_msg roscpp std_msgs
在~/ros_ws/src/example_ros_msg
目录下创建一个msg
子目录,该子目录下创建一个ExampleMessage.msg
文件,其内容填入:
Header header
int32 demo_int
float64 demo_double
编辑~/ros_ws/src/example_ros_msg
目录下的package.xml
,将以下两行取消注释(使得通知编译器知道需要生成新的消息头):
<build_depend>message_generation</build_depend>
<run_depend>message_runtime</run_depend>
编辑~/ros_ws/src/example_ros_msg
目录下的CMakeLists.txt
:
cmake_minimum_required(VERSION 2.8.3)
project(example_ros_msg)
find_package(catkin_simple REQUIRED)
catkin_simple()
# Executables
# cs_add_executable(example_ros_message_publisher src/example_ros_message_publisher.cpp)
cs_install()
cs_export()
在~/ros_ws
目录下运行终端,执行以下命令进行编译(生成适合的C++
文件包含相应的头文件):
catkin_make
该文件存放在~/ros_ws/devel/include/example_ros_msg/ExampleMessage.h
下。如果想要使用此新消息类型的节点源代码应依赖于程序包example_ros_msg
,并且使用该消息类型时,在节点的C++
源代码中应该包含以下头文件:
#include <example_ros_msg/ExampleMessage.h>
在~/ros_ws/src/example_ros_msg
目录下创建一个使用自定义消息类型的发布器程序example_ros_message_publisher.cpp
:
// 使用自定义消息类型的发布器:example_ros_message_publisher.cpp
#include <ros/ros.h>
#include <example_ros_msg/ExampleMessage.h> // 导入含有自定义消息类型的头文件
#include <math.h>
int main(int argc, char **argv) {
ros::init(argc, argv, "example_ros_message_publisher"); // 节点名
ros::NodeHandle n; // 建立网络通信
// 初始化话题example_topic,将消息数据发布到话题
ros::Publisher my_publisher_object = n.advertise<example_ros_msg::ExampleMessage>("example_topic", 1);
// 实例化自定义消息类型
example_ros_msg::ExampleMessage my_new_message;
ros::Rate naptime(1.0);
// 填充自定义消息数据的各个字段
my_new_message.header.stamp = ros::Time::now();
my_new_message.header.seq=0;
my_new_message.header.frame_id = "base_frame";
my_new_message.demo_int= 1;
my_new_message.demo_double=100.0;
double sqrt_arg;
while (ros::ok())
{
my_new_message.header.seq++; // 递增序列计数器
my_new_message.header.stamp = ros::Time::now(); // 更新时间戳
// 具体的数据变化:不再做注释
my_new_message.demo_int*=2.0;
sqrt_arg = my_new_message.demo_double;
my_new_message.demo_double = sqrt(sqrt_arg);
// 发布器发布数据
my_publisher_object.publish(my_new_message);
naptime.sleep();
}
}
取消CMakeList.txt
文件中以下注释:
cs_add_executable(example_ros_message_publisher src/example_ros_message_publisher.cpp)
编译后运行(此时应该保证ROS
系统在运行中):
catkin_make
rosrun example_ros_msg example_ros_message_publisher
开启另外一个终端查看运行情况:
rostopic echo example_topic
显示结果如下:
header:
seq: 18
stamp:
secs: 1658328650
nsecs: 275196150
frame_id: "base_frame"
demo_int: 524288
demo_double: 1.0000087837
---
header:
seq: 19
stamp:
secs: 1658328651
nsecs: 275239555
frame_id: "base_frame"
demo_int: 1048576
demo_double: 1.00000439184
---
(2)定义一条变长的信息
在~/ros_ws/src
目录下利用以下命令创建包:
cs_create_pkg custom_msgs roscpp std_msgs
在~/ros_ws/src/custom_msgs
目录下创建一个msg
子目录,该子目录下创建一个VecOfDoubles.msg
文件,其内容填入:
float64[] dbl_vec
编辑~/ros_ws/src/custom_msgs
目录下的package.xml
,将以下两行取消注释(使得通知编译器知道需要生成新的消息头):
<build_depend>message_generation</build_depend>
<run_depend>message_runtime</run_depend>
在~/ros_ws
目录下运行终端,执行以下命令进行编译(生成适合的C++
文件包含相应的头文件):
catkin_make
在~/ros_ws/src/example_ros_msg
目录下(注意此处目录)创建一个使用该变长消息类型的发布器程序vector_publisher.cpp
:
// 使用变长消息类型的发布器程序:vector_publisher.cpp
#include <ros/ros.h>
#include <custom_msgs/VecOfDoubles.h> // 导入新消息类型头文件
int main(int argc, char **argv) {
ros::init(argc, argv, "vector_publisher"); // 节点名
ros::NodeHandle n; // 建立网络通信
// 初始化发布器,将会把数据发到话题vec_topic上
ros::Publisher my_publisher_object = n.advertise<custom_msgs::VecOfDoubles>("vec_topic", 1);
custom_msgs::VecOfDoubles vec_msg; // 实例化新消息类型
double counter=0;
ros::Rate naptime(1.0);
vec_msg.dbl_vec.resize(3); // 修改消息长度
// 赋值
vec_msg.dbl_vec[0]=1.414;
vec_msg.dbl_vec[1]=2.71828;
vec_msg.dbl_vec[2]=3.1416;
vec_msg.dbl_vec.push_back(counter); // 插入新数据
while(ros::ok()) {
counter+=1.0;
vec_msg.dbl_vec.push_back(counter); // 插入新数据
my_publisher_object.publish(vec_msg); // 发布数据
naptime.sleep();
}
}
为了能够在example_ros_msg
中编译新节点,需要对该包中的package.xml
和CMakeLists.txt
进行编辑:
# package.xml文件插入以下行:
<build_depend>custom_msgs</build_depend>
<run_depend>custom_msgs</run_depend>
# CMakeLists.txt文件插入以下行:
cs_add_executable(vector_publisher src/vector_publisher.cpp)
编译节点后运行节点:
catkin_make
rosrun example_ros_msg vector_publisher
观察结果:
rostopic echo vec_topic
显示结果如下:
dbl_vec: [1.414, 2.71828, 3.1416, 0.0, 1.0, 2.0]
---
dbl_vec: [1.414, 2.71828, 3.1416, 0.0, 1.0, 2.0, 3.0]
---
dbl_vec: [1.414, 2.71828, 3.1416, 0.0, 1.0, 2.0, 3.0, 4.0]
---
在~/ros_ws/src/example_ros_msg
目录下(注意此处目录)创建一个使用该变长消息类型的订阅器程序vector_subscriber.cpp
:
// 使用该变长消息类型的订阅器程序:vector_subscriber.cpp
#include<ros/ros.h>
#include <custom_msgs/VecOfDoubles.h>
void myCallback(const custom_msgs::VecOfDoubles& message_holder) // 回调函数
{
std::vector <double> vec_of_doubles = message_holder.dbl_vec; // 将订阅数据存放到vector容器内
int nvals = vec_of_doubles.size(); // 记录容器大小
for (int i=0;i<nvals;i++) {
ROS_INFO("vec[%d] = %f",i,vec_of_doubles[i]); //print out all the values
}
ROS_INFO("\n");
}
int main(int argc, char **argv)
{
ros::init(argc,argv,"vector_subscriber"); // 节点名
ros::NodeHandle n; // 建立网络通信
ros::Subscriber my_subscriber_object= n.subscribe("vec_topic",1,myCallback);
ros::spin();
return 0;
}
同样,为了编译节点,将以下语句插入CMakeLists.txt
文件中:
cs_add_executable(vector_subscriber src/vector_subscriber.cpp)
利用catkin_make
编译。
打开终端,运行ROS
系统:
roscore
打开另外一个终端,运行发布器:
rosrun example_ros_msg vector_publisher
打开另外一个终端,运行订阅器:
rosrun example_ros_msg vector_subscriber
显示结果如下:
[ INFO] [1658384328.515807550]: vec[0] = 1.414000
[ INFO] [1658384328.515938266]: vec[1] = 2.718280
[ INFO] [1658384328.515975837]: vec[2] = 3.141600
[ INFO] [1658384328.516006054]: vec[3] = 0.000000
[ INFO] [1658384328.516034508]: vec[4] = 1.000000
[ INFO] [1658384328.516060837]: vec[5] = 2.000000
[ INFO] [1658384328.516087448]:
[ INFO] [1658384329.515928234]: vec[0] = 1.414000
[ INFO] [1658384329.516032330]: vec[1] = 2.718280
[ INFO] [1658384329.516074560]: vec[2] = 3.141600
[ INFO] [1658384329.516103575]: vec[3] = 0.000000
[ INFO] [1658384329.516131998]: vec[4] = 1.000000
[ INFO] [1658384329.516158618]: vec[5] = 2.000000
[ INFO] [1658384329.516184537]: vec[6] = 3.000000
[ INFO] [1658384329.516210657]:
注:使用这种消息长度可变的数据类型时需要注意以下几点:
(1)确保变长向量不会过大,否则可能会消耗所有内存和通信带宽。
(2)与数组一样,如果访问未分配的内存时,将会导致运行错误(但是编译器并不会报错)。
2.2.2 ROS
服务
目前,前面提到的通信模式中,发布器并不清楚谁是订阅器,而订阅器也不知道是哪个发布器向该话题发布数据。这种通信方式适用于重复的消息,如传感器数值的发布。传感器发布器不需要知道哪些订阅器节点订阅话题,发布器也不会接收用户的任何请求来改变自身发布的数据。
有些情况下,我们需要建立双向、一对一、可靠的通信。客户端向服务器发送请求,服务器向客户端发送响应。
(1)定义服务消息
定义一个自定义服务消息来描述请求和响应的数据类型和字段名称。(通过一个.srv
文件实现)
在~/ros_ws/src
目录下创建一个程序包example_ros_service
:
cs_create_pkg example_ros_service roscpp std_msgs
在~/ros_ws/src/example_ros_service
目录下创建一个子目录srv
,该子目录下创建一个ExampleServiceMsg.srv
文件,编辑文件内容,文件内容如下:
string name
---
bool on_the_list
bool good_guy
int32 age
string nickname
该srv
文件中,“—”上面的行定义了请求结构,该请求有单个组件(name)组成,数据类型是一个字符串;“—”下面的行定义了响应结构,该响应有on_the_list
、good_guy
、age
和nickname
四个字段组成,对应的数据类型为bool
、bool
、int32
和string
。
与发布器和订阅器自定义消息类型一样,我们需要通知编译器通过package.xml
文件生成新的消息头,因此在编译前我们需要编辑修改package.xml
文件:
<build_depend>message_generation</build_depend>
<run_depend>message_runtime</run_depend>
编辑CMakeList.txt
文件如下:
cmake_minimum_required(VERSION 2.8.3)
project(example_ros_service)
find_package(catkin_simple REQUIRED)
catkin_simple()
#cs_add_executable(example_ros_service src/example_ros_service.cpp)
#cs_add_executable(example_ros_client src/example_ros_client.cpp)
cs_install()
cs_export()
在工作空间目录下,编译:
roscd
catkin_make
编译完成后,目录~ros_ws/devel/include/example_ros_service
下生成了一个ExampleServiceMsg.h
头文件。同时,该目录下还生成了两个服务头文件:ExampleServiceMsgRequest.h
和ExampleServiceMsgResponse.h
。
如果想要在一个节点中使用该服务消息类型,则需要利用以下代码包含相关头文件:
#include <example_ros_service/ExampleServiceMsg.h>
注:如果在另外一个单独的包内调用该服务消息类型,则需要注意两点:
(a)同样,在C++
代码中仍需要添加#include <example_ros_service/ExampleServiceMsg.h>
导入相关头文件。
(b)该程序包的package.xml
文件将不再依赖message_generation
和message_runtime
,因为该服务消息类型只需要生成一次,已经在example_ros_service
执行;其次该程序包的package.xml
文件需要新增对example_ros_service
的依赖,即需要加入行<build_depend>example_ros_service</build_depend>
。
(2)ROS
服务节点
在目录~ros_ws/src/example_ros_service
下创建一个服务器程序example_ros_service.cpp
,源代码如下:
// 服务器程序:example_ros_service.cpp
#include <ros/ros.h>
#include <example_ros_service/ExampleServiceMsg.h> // 服务消息类型文件头
#include <iostream>
#include <string>
using namespace std;
bool callback(example_ros_service::ExampleServiceMsgRequest& request, example_ros_service::ExampleServiceMsgResponse& response)
{
ROS_INFO("callback activated");
string in_name(request.name);
response.on_the_list=false;
// 简单的服务器响应
if (in_name.compare("Bob")==0)
{
ROS_INFO("asked about Bob");
response.age = 32;
response.good_guy=false;
response.on_the_list=true;
response.nickname="BobTheTerrible";
}
if (in_name.compare("Ted")==0)
{
ROS_INFO("asked about Ted");
response.age = 21;
response.good_guy=true;
response.on_the_list=true;
response.nickname="Ted the Benevolent";
}
return true;
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "example_ros_service"); // 节点名
ros::NodeHandle n; // 建立通信
// 建立一个服务器响应,等待交互
ros::ServiceServer service = n.advertiseService("lookup_by_name", callback);
ROS_INFO("Ready to look up names.");
ros::spin();
return 0;
}
(3)与ROS
服务器手动交互
编译服务器程序example_ros_service.cpp
,运行该程序:
catkin_make
rosrun example_ros_service example_ros_service
打开另外一个终端,手动与服务器交互:
rosservice call lookup_by_name 'Ted'
显示结果如下:
on_the_list: True
good_guy: True
age: 21
nickname: "Ted the Benevolent"
(4)ROS
客户端
与上面手动与服务器交互不同,一般可以采用客户端程序与服务器交互。
在目录~ros_ws/src/example_ros_service
下创建一个服务器程序example_ros_client.cpp
,源代码如下:
// 客户机程序:example_ros_client.cpp
#include <ros/ros.h>
#include <example_ros_service/ExampleServiceMsg.h> // 服务消息类型文件头
#include <iostream>
#include <string>
using namespace std;
int main(int argc, char **argv) {
ros::init(argc, argv, "example_ros_client"); // 节点名
ros::NodeHandle n;
// 实例化客户client,希望按照example_ros_service::ExampleServiceMsg中定义的方式传达请求和响应,且期望通信对象是名为lookup_by_name的服务
ros::ServiceClient client = n.serviceClient<example_ros_service::ExampleServiceMsg>("lookup_by_name");
// 实例化了一个服务拥有请求和响应的对象,包含一个请求字段和一个响应字段。首先将会填充请求消息,见下面代码“srv.request.name = in_name; ”
example_ros_service::ExampleServiceMsg srv;
bool found_on_list = false;
string in_name;
while (ros::ok()) {
cout<<endl;
cout << "enter a name (x to quit): ";
cin>>in_name;
if (in_name.compare("x")==0)
return 0;
srv.request.name = in_name;
if (client.call(srv)) { // client.call(srv)判断是否调用成功,如果调用成功,srv.response将会是服务器的响应,该响应有定义好的四个可用字段。
if (srv.response.on_the_list) {
cout << srv.request.name << " is known as " << srv.response.nickname << endl;
cout << "He is " << srv.response.age << " years old" << endl;
if (srv.response.good_guy)
cout << "He is reported to be a good guy" << endl;
else
cout << "Avoid him; he is not a good guy" << endl;
} else {
cout << srv.request.name << " is not in my database" << endl;
}
} else {
ROS_ERROR("Failed to call service lookup_by_name");
return 1;
}
}
return 0;
}
(4)运行服务器和客户端
保证ROS
系统运行中,在另外一个终端运行服务器:
rosrun example_ros_service example_ros_service
打开另外一个终端运行客户端:
rosrun example_ros_service example_ros_client
显示结果如下:
enter a name (x to quit): Ted
Ted is known as Ted the Benevolent
He is 21 years old
He is reported to be a good guy
enter a name (x to quit): Hny
Hny is not in my database
enter a name (x to quit): Bob
Bob is known as BobTheTerrible
He is 32 years old
Avoid him; he is not a good guy
注:在运行前,要修改CMakeLists.txt
文件并且编译。后续代码均是如此,写完代码后要保证编辑CMakeLists.txt
并且编译。
2.2.3 ROS
中使用C++
类
C++
类的用法可以使用到ROS
中,以提高代码效率和代码复用。
在目录~ros_ws/src
下创建包example_ros_class
:
cs_create_pkg example_ros_class roscpp std_msgs
(1)在头文件中定义类
类所有成员的原型、私有和公共数据成员、类构造函数的原型。
// 类的头文件:example_ros_class.h
#ifndef EXAMPLE_ROS_CLASS_H_
#define EXAMPLE_ROS_CLASS_H_
#include <math.h>
#include <stdlib.h>
#include <string>
#include <vector>
#include <ros/ros.h>
// 调用使用到的消息数据类型的头文件
#include <std_msgs/Bool.h>
#include <std_msgs/Float32.h>
#include <std_srvs/Trigger.h>
// 定义类ExampleRosClass,包含成员函数和数据成员
class ExampleRosClass
{
public:
ExampleRosClass(ros::NodeHandle* nodehandle); // 构造函数
private: // 私有变量后带下划线_,便于阅读代码
ros::NodeHandle nh_; // 联系主函数和构造函数
// 订阅器、服务器和发布器
ros::Subscriber minimal_subscriber_;
ros::ServiceServer minimal_service_;
ros::Publisher minimal_publisher_;
double val_from_subscriber_;
double val_to_remember_;
void initializeSubscribers();
void initializePublishers();
void initializeServices();
void subscriberCallback(const std_msgs::Float32& message_holder);
bool serviceCallback(std_srvs::TriggerRequest& request, std_srvs::TriggerResponse& response);
};
#endif
(2)编写一个单独的实现文件
包含上面定义的头文件、包含已声明成员函数的工作代码、包含在构造函数中封装必要初始化的代码。
// 类的实现:example_ros_class.cpp:
#include "example_ros_class.h" // 导入声明好的类头文件
// 类的构造函数
ExampleRosClass::ExampleRosClass(ros::NodeHandle* nodehandle):nh_(*nodehandle) //直接赋值,将传入的参数nodehandle复制到私有变量nh_中
{
ROS_INFO("in class constructor of ExampleRosClass");
// 初始化订阅器、发布器和服务
initializeSubscribers();
initializePublishers();
initializeServices();
// 初始化成员变量
val_to_remember_=0.0;
}
void ExampleRosClass::initializeSubscribers()
{
ROS_INFO("Initializing Subscribers");
minimal_subscriber_ = nh_.subscribe("example_class_input_topic", 1, &ExampleRosClass::subscriberCallback,this); // this 关键字作为当前实例
}
void ExampleRosClass::initializeServices()
{
ROS_INFO("Initializing Services");
minimal_service_ = nh_.advertiseService("example_minimal_service",&ExampleRosClass::serviceCallback,this);
}
void ExampleRosClass::initializePublishers()
{
ROS_INFO("Initializing Publishers");
minimal_publisher_ = nh_.advertise<std_msgs::Float32>("example_class_output_topic", 1, true);
}
void ExampleRosClass::subscriberCallback(const std_msgs::Float32& message_holder) {
val_from_subscriber_ = message_holder.data; // 保存数据到私有变量上
ROS_INFO("myCallback activated: received value %f",val_from_subscriber_);
std_msgs::Float32 output_msg;
val_to_remember_ += val_from_subscriber_;
output_msg.data= val_to_remember_;
minimal_publisher_.publish(output_msg); // 发布数据
}
bool ExampleRosClass::serviceCallback(std_srvs::TriggerRequest& request, std_srvs::TriggerResponse& response) {
ROS_INFO("service callback activated");
response.success = true;
response.message = "here is a response string";
return true;
}
int main(int argc, char** argv)
{
ros::init(argc, argv, "exampleRosClass"); // 节点名
ros::NodeHandle nh;
ROS_INFO("main: instantiating an object of type ExampleRosClass");
ExampleRosClass exampleRosClass(&nh); // 通过nh联系
ROS_INFO("main: going into spin; let the callbacks do all the work");
ros::spin();
return 0;
}
在订阅器的回调函数中,当前的订阅数据会保留在类中的私有变量val_from_subscriber_
中,而val_to_remember_
则会保存订阅话题的每一次数据,这些变量在类中相当于全局变量。
在构造函数中,执行订阅器和服务器时,在提供回调函数的同时需要提供一个指针。该关键字this
(指针)告诉编译器正在引用此类的当前示例。
在与服务相关的回调函数使用了一个已经定义好的服务消息:
bool ExampleRosClass::serviceCallback(std_srvs::TriggerRequest& request, std_srvs::TriggerResponse& response)
该消息类型Trigger
在std_srvs
中定义。因此,在编译前,需要在package.xml
文件中增加对std_srvs
的依赖。
保持ROS
系统运行,运行example_ros_class
:
rosrun example_ros_class example_ros_class
运行结果如下:
[ INFO] [1658474040.360780286]: main: instantiating an object of type ExampleRosClass
[ INFO] [1658474040.360880034]: in class constructor of ExampleRosClass
[ INFO] [1658474040.360908378]: Initializing Subscribers
[ INFO] [1658474040.362983130]: Initializing Publishers
[ INFO] [1658474040.363427029]: Initializing Services
[ INFO] [1658474040.363910102]: main: going into spin; let the callbacks do all the work
打开另外一个终端,检查服务对应话题状态:
rosservice call example_minimal_service
该终端显示结果:
success: True
message: "here is a response string"
运行example_ros_class
的终端显示结果:
[ INFO] [1658474296.879997406]: service callback activated
检查订阅器状态:
rostopic pub -r 2 example_class_input_topic std_msgs/Float32 10.0
运行example_ros_class
的终端显示结果:
[ INFO] [1658474645.347071052]: myCallback activated: received value 10.000000
[ INFO] [1658474645.847343847]: myCallback activated: received value 10.000000
[ INFO] [1658474646.346482449]: myCallback activated: received value 10.000000
[ INFO] [1658474646.847432733]: myCallback activated: received value 10.000000
[ INFO] [1658474647.347156673]: myCallback activated: received value 10.000000
[ INFO] [1658474647.847809435]: myCallback activated: received value 10.000000
2.2.4 ROS中创建库模块
源代码变长则需要我们将其分解为较小的模块。为了实现对模块的复用,可以创建一个库。
在目录~ros_ws/src
下创建包creating_a_ros_library
:
cs_create_pkg creating_a_ros_library roscpp std_msgs std_srvs
将example_ros_class.cpp
文件拷贝到包creating_a_ros_library
的子目录src
下,并且删除main
函数和修改头文件的引用:
#include <creating_a_ros_library/example_ros_class.h> // 导入声明好的类头文件
类原型的头文件example_ros_class.h
并不是放在include
目录下,而需要在include
目录下新建一个与程序包(creating_a_ros_library
)同名的子目录,类原型的头文件放在该目录下。
编辑CMakeLists.txt
文件,增加以下行:
cs_add_library(example_ros_library src/example_ros_class.cpp)
在工作空间进行编译:
roscd
catkin_make
编译完成后,目录~ros_ws/devel/lib
下出现文件libexample_ros_library.so
。
在目录~ros_ws/src/creating_a_ros_library/src
下创建一个测试程序,其内容如下:
#include <creating_a_ros_library/example_ros_class.h>
int main(int argc, char** argv)
{
ros::init(argc, argv, "example_lib_test_main"); // 节点名
ros::NodeHandle nh;
ROS_INFO("main: instantiating an object of type ExampleRosClass");
ExampleRosClass exampleRosClass(&nh);
ROS_INFO("main: going into spin; let the callbacks do all the work");
ros::spin();
return 0;
}
编辑CMakeLists.txt
文件,插入以下行:
cs_add_executable(ros_library_test_main src/example_ros_class_test_main.cpp)
target_link_library(ros_library_test_main example_ros_library)
测试节点:
rosrun creating_a_ros_library ros_library_test_main
显示结果如下:
[ INFO] [1658485905.853814086]: main: instantiating an object of type ExampleRosClass
[ INFO] [1658485905.853958655]: in class constructor of ExampleRosClass
[ INFO] [1658485905.853995754]: Initializing Subscribers
[ INFO] [1658485905.856053623]: Initializing Publishers
[ INFO] [1658485905.856536291]: Initializing Services
[ INFO] [1658485905.857084721]: main: going into spin; let the callbacks do all the work