话题通讯
简介
话题通信基于发布订阅模式,适用于不断更新,少逻辑处理的数据传输相关场景。例如:ROS导航中,需要设置节点发布传感器获取的数据,导航模块中设置节点订阅并解析传感器获取的数据。
话题通讯ROS实现
mkdir -p demo_ws/src
cd demo_ws/src
catkin_create_pkg plumbing roscpp std_msgs
cd ..
code .
依次在终端中键入上述的一行,然后在vscode中进行后续操作。在功能包下的src目录下创建发布和订阅源文件pub.cpp和sub.cpp,再将下述代码分别复制到两个源文件中,代码有详细注释。
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"talker");
ros::NodeHandle nh;
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",10);
std_msgs::String msg;
std::string msg_front = "Hello 你好!";
int count = 0;
ros::Rate r(1);
while (ros::ok())
{
std::stringstream ss;
ss << msg_front << count;
msg.data = ss.str();
pub.publish(msg);
ROS_INFO("发送的消息:%s",msg.data.c_str());
r.sleep();
count++;
ros::spinOnce();
}
return 0;
}
#include "ros/ros.h"
#include "std_msgs/String.h"
void doMsg(const std_msgs::String::ConstPtr& msg_p){
ROS_INFO("我听见:%s",msg_p->data.c_str());
}
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"listener");
ros::NodeHandle nh;
ros::Subscriber sub = nh.subscribe<std_msgs::String>("chatter",10,doMsg);
ros::spin();
return 0;
}
写好源文件后配置CMakeLists.txt文件(注意是配置package.xml文件上面的CMakeLists.txt文件),在文件的最下放复制下面代码就可以。
add_executable(pub
src/pub.cpp
)
add_executable(sub
src/sub.cpp
)
target_link_libraries(pub
${catkin_LIBRARIES}
)
target_link_libraries(sub
${catkin_LIBRARIES}
)
完成这些后就可以编译实现了:vscode中Ctrl+shift+~打开终端,默认是在工作空间,直接键入catkin_make进行编译。编译完成后键入roscore启动ros核心,这时需要另起一个终端界面,即点击终端窗口右上的+添加一个新的窗口,在新的窗口中添加环境变量,即键入source ./devel/setup.bash,而后启动发布节点,即在此窗口键入rosrun plumbing pub,其中plumbing是自定义软件包的名字,pub是写的发布节点源文件名。最后再起一个新的终端窗口,添加环境变量后运行订阅节点,即键入rosrun plumbing sub。至此,一个简单的文本发布订阅demo完成。
自定义消息类型ROS实现
上述话题通信发布的数据是系统定义好的,如常见的整型、浮点型、布尔型、字符型数据,那如何向C++一样发布自定义的数据类型呢(C++自定义数据类型是通过结构体),下面介绍实现流程:
首先要自定义数据类型,在功能包下的src目录创建msg文件夹,在此文件夹里创建.msg文件,本文创建person.msg文件,在文件中定义如下内容。
string name
uint16 age
float64 height
定义好后需要编辑配置文件,package.xml中添加编译依赖与执行依赖,CMakeLists.txt编辑 msg 相关配置,需要添加的内容分别如下:
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
add_message_files(
FILES
person.msg
)
generate_messages(
DEPENDENCIES
std_msgs
)
catkin_package(
CATKIN_DEPENDS roscpp std_msgs message_runtime
)
这里需要注意,package.xml中是添加上述两行代码到和代码有类似样式的下面即可,CMakeLists.txt是修改package.xml上面的,且不宜直接复制添加到最后(会报错),应该在文件中使用Ctrl+f查找添加代码括号前的内容如find_package,适用Ctrl+k+u解开这一段的注释,并加上括号内缺少的内容,保持和上面代码一致即可。完成这些后,Ctrl+shift+~打开终端,键入catkin_make编译,编译完成后会生成中间文件,在工作空间devel文件夹的include文件夹中生成person.h文件。为避免cpp源文件出现红色波浪线报错,需配置一下c_cpp_properties.json 的 includepath属性,添加上person.h的路径“~/demo_ws/devel/include/**”,**代表把include文件夹中所有文件都包含。完成这些工作后,实现自定义消息话题通讯与上述普通话题通讯就基本一致了,在plumbing软件包的src目录下创建发布及订阅源文件pub_iwant.cpp,sub_iwant.cpp,并分别添加如下内容:
#include "ros/ros.h"
#include "plumbing/person.h"
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"talker_person");
ros::NodeHandle nh;
ros::Publisher pub = nh.advertise<plumbing::person>("chatter_person",1000);
plumbing::person p;
p.name = "sunwukong";
p.age = 2000;
p.height = 1.45;
ros::Rate r(1);
while (ros::ok())
{
pub.publish(p);
p.age += 1;
ROS_INFO("我叫:%s,今年%d岁,高%.2f米", p.name.c_str(), p.age, p.height);
r.sleep();
ros::spinOnce();
}
return 0;
}
#include "ros/ros.h"
#include "plumbing/person.h"
void doPerson(const plumbing::person::ConstPtr& person_p){
ROS_INFO("订阅的人信息:%s, %d, %.2f", person_p->name.c_str(), person_p->age, person_p->height);
}
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"listener_person");
ros::NodeHandle nh;
ros::Subscriber sub = nh.subscribe<plumbing::person>("chatter_person",10,doPerson);
ros::spin();
return 0;
}
在package.xml上面的CMakeLists.txt文件中配置如下内容,在文件最下面粘贴下面代码即可。
add_executable(pub_iwant src/pub_iwant.cpp)
add_executable(sub_iwant src/sub_iwant.cpp)
add_dependencies(pub_iwant ${PROJECT_NAME}_generate_messages_cpp)
add_dependencies(sub_iwant ${PROJECT_NAME}_generate_messages_cpp)
target_link_libraries(pub_iwant
${catkin_LIBRARIES}
)
target_link_libraries(sub_iwant
${catkin_LIBRARIES}
)
完成这些后需要再次编译:vscode中Ctrl+shift+~打开终端,默认是在工作空间,直接键入catkin_make进行编译。编译完成后键入roscore启动ros核心,这时需要另起一个终端界面,即点击终端窗口右上的+添加一个新的窗口,在新的窗口中添加环境变量,即键入source ./devel/setup.bash,而后启动发布节点,即在此窗口键入rosrun plumbing pub_iwant,其中plumbing是自定义软件包的名字,pub_iwant是写的自定义发布节点源文件名。最后再起一个新的终端窗口,添加环境变量后运行订阅节点,即键入rosrun plumbing sub_iwant。至此,自定义数据类型发布订阅demo完成。
服务通信
简介
服务通信是基于请求响应模式,用于偶然的、对时时性有要求、有一定逻辑处理需求的数据传输场景,例如:一个节点需要向相机节点发送拍照请求,相机节点处理请求,并返回处理结果。
ROS实现
在plumbing软件包下新建srv文件夹,在此文件夹下创建.src文件,本文创建sum.srv文件,客户端发送两数求和请求,服务端实现两数求和,sum.srv文件内容如下:
int32 num1
int32 num2
---
int32 sum
CMakeLists.txt编辑 srv 相关配置,因为我们三种通信方式都在plumbing功能包下实现,所以修改的都是同一个CMakeLists.txt文件,且已经配置的不需要重复配置,此处搜索add_service_files解开这段内容内容的注释,并在括号内添加sum.srv,和如下一致即可:
add_service_files(
FILES
sum.srv
)
完成这些后编译,和自定义消息话题通讯实现一样会生成中间文件。完成后在plumbing功能包src目录下创建客户端、服务端源文件client.cpp、server.cpp,分别添加如下内容:
#include "ros/ros.h"
#include "plumbing/sum.h"
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
if (argc != 3)
{
ROS_ERROR("请提交两个整数");
return 1;
}
ros::init(argc,argv,"AddInts_Client");
ros::NodeHandle nh;
ros::ServiceClient client = nh.serviceClient<plumbing::sum>("AddInts");
//方式1
ros::service::waitForService("AddInts");
//方式2
// client.waitForExistence();
plumbing::sum ai;
ai.request.num1 = atoi(argv[1]);
ai.request.num2 = atoi(argv[2]);
bool flag = client.call(ai);
if (flag)
{
ROS_INFO("请求正常处理,响应结果:%d",ai.response.sum);
}
else
{
ROS_ERROR("请求处理失败....");
return 1;
}
return 0;
}
#include "ros/ros.h"
#include "plumbing/sum.h"
bool doReq(plumbing::sum::Request& req,
plumbing::sum::Response& resp){
int num1 = req.num1;
int num2 = req.num2;
ROS_INFO("服务器接收到的请求数据为:num1 = %d, num2 = %d",num1, num2);
if (num1 < 0 || num2 < 0)
{
ROS_ERROR("提交的数据异常:数据不可以为负数");
return false;
}
resp.sum = num1 + num2;
return true;
}
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"AddInts_Server");
ros::NodeHandle nh;
ros::ServiceServer server = nh.advertiseService("AddInts",doReq);
ROS_INFO("服务已经启动....");
ros::spin();
return 0;
}
在package.xml上面的CMakeLists.txt文件中配置如下内容,在文件最下面粘贴下面代码即可。
add_executable(server src/server.cpp)
add_executable(client src/client.cpp)
add_dependencies(server ${PROJECT_NAME}_gencpp)
add_dependencies(client ${PROJECT_NAME}_gencpp)
target_link_libraries(server
${catkin_LIBRARIES}
)
target_link_libraries(client
${catkin_LIBRARIES}
)
完成这些后需要再次编译:vscode中Ctrl+shift+~打开终端,默认是在工作空间,直接键入catkin_make进行编译。编译完成后键入roscore启动ros核心,这时需要另起一个终端界面先启动服务端,即点击终端窗口右上的+添加一个新的窗口,在新的窗口中添加环境变量,即键入source ./devel/setup.bash,而后启动服务端,即在此窗口键入rosrun plumbing server,其中plumbing是自定义软件包的名字,server是写的服务端源文件名。最后再起一个新的终端窗口,添加环境变量后运行客户端节点,即键入rosrun plumbing client 10 20,,终端会输出求和结果30,10 20即代码需要读入的两个整数。至此,服务端客户端通信demo完成。
参数服务器
简介
参数服务器相当于是独立于所有节点的一个公共容器,可以将数据存储在该容器中,被不同的节点调用,当然不同的节点也可以往其中存储数据,主要实现ROS不同节点数据共享。例如:路径规划时,需要参考小车的尺寸,我们可以将这些尺寸信息存储到参数服务器,全局路径规划节点与本地路径规划节点都可以从参数服务器中调用这些参数。
ROS实现
在plumbing功能包src目录下新建设置修改参数、查找参数、删除参数的源文件param_set_change.cpp、param_find.cpp、param_delete.cpp。分别添加如下内容:
#include "ros/ros.h"
int main(int argc, char *argv[])
{
ros::init(argc,argv,"set_update_param");
std::vector<std::string> stus;
stus.push_back("zhangsan");
stus.push_back("李四");
stus.push_back("王五");
stus.push_back("孙大脑袋");
std::map<std::string,std::string> friends;
friends["guo"] = "huang";
friends["yuang"] = "xiao";
//NodeHandle--------------------------------------------------------
ros::NodeHandle nh;
nh.setParam("nh_int",10); //整型
nh.setParam("nh_double",3.14); //浮点型
nh.setParam("nh_bool",true); //bool
nh.setParam("nh_string","hello NodeHandle"); //字符串
nh.setParam("nh_vector",stus); // vector
nh.setParam("nh_map",friends); // map
//修改演示(相同的键,不同的值)
nh.setParam("nh_int",10000);
//param--------------------------------------------------------
ros::param::set("param_int",20);
ros::param::set("param_double",3.14);
ros::param::set("param_string","Hello Param");
ros::param::set("param_bool",false);
ros::param::set("param_vector",stus);
ros::param::set("param_map",friends);
//修改演示(相同的键,不同的值)
ros::param::set("param_int",20000);
return 0;
}
#include "ros/ros.h"
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"get_param");
//NodeHandle--------------------------------------------------------
/*
ros::NodeHandle nh;
// param 函数
int res1 = nh.param("nh_int",100); // 键存在
int res2 = nh.param("nh_int2",100); // 键不存在
ROS_INFO("param获取结果:%d,%d",res1,res2);
// getParam 函数
int nh_int_value;
double nh_double_value;
bool nh_bool_value;
std::string nh_string_value;
std::vector<std::string> stus;
std::map<std::string, std::string> friends;
nh.getParam("nh_int",nh_int_value);
nh.getParam("nh_double",nh_double_value);
nh.getParam("nh_bool",nh_bool_value);
nh.getParam("nh_string",nh_string_value);
nh.getParam("nh_vector",stus);
nh.getParam("nh_map",friends);
ROS_INFO("getParam获取的结果:%d,%.2f,%s,%d",
nh_int_value,
nh_double_value,
nh_string_value.c_str(),
nh_bool_value
);
for (auto &&stu : stus)
{
ROS_INFO("stus 元素:%s",stu.c_str());
}
for (auto &&f : friends)
{
ROS_INFO("map 元素:%s = %s",f.first.c_str(), f.second.c_str());
}
// getParamCached()
nh.getParamCached("nh_int",nh_int_value);
ROS_INFO("通过缓存获取数据:%d",nh_int_value);
//getParamNames()
std::vector<std::string> param_names1;
nh.getParamNames(param_names1);
for (auto &&name : param_names1)
{
ROS_INFO("名称解析name = %s",name.c_str());
}
ROS_INFO("----------------------------");
ROS_INFO("存在 nh_int 吗? %d",nh.hasParam("nh_int"));
ROS_INFO("存在 nh_intttt 吗? %d",nh.hasParam("nh_intttt"));
std::string key;
nh.searchParam("nh_int",key);
ROS_INFO("搜索键:%s",key.c_str());
*/
//param--------------------------------------------------------
ROS_INFO("++++++++++++++++++++++++++++++++++++++++");
int res3 = ros::param::param("param_int",20); //存在
int res4 = ros::param::param("param_int2",20); // 不存在返回默认
ROS_INFO("param获取结果:%d,%d",res3,res4);
// getParam 函数
int param_int_value;
double param_double_value;
bool param_bool_value;
std::string param_string_value;
std::vector<std::string> param_stus;
std::map<std::string, std::string> param_friends;
ros::param::get("param_int",param_int_value);
ros::param::get("param_double",param_double_value);
ros::param::get("param_bool",param_bool_value);
ros::param::get("param_string",param_string_value);
ros::param::get("param_vector",param_stus);
ros::param::get("param_map",param_friends);
ROS_INFO("getParam获取的结果:%d,%.2f,%s,%d",
param_int_value,
param_double_value,
param_string_value.c_str(),
param_bool_value
);
for (auto &&stu : param_stus)
{
ROS_INFO("stus 元素:%s",stu.c_str());
}
for (auto &&f : param_friends)
{
ROS_INFO("map 元素:%s = %s",f.first.c_str(), f.second.c_str());
}
// getParamCached()
ros::param::getCached("param_int",param_int_value);
ROS_INFO("通过缓存获取数据:%d",param_int_value);
//getParamNames()
std::vector<std::string> param_names2;
ros::param::getParamNames(param_names2);
for (auto &&name : param_names2)
{
ROS_INFO("名称解析name = %s",name.c_str());
}
ROS_INFO("----------------------------");
ROS_INFO("存在 param_int 吗? %d",ros::param::has("param_int"));
ROS_INFO("存在 param_intttt 吗? %d",ros::param::has("param_intttt"));
std::string key;
ros::param::search("param_int",key);
ROS_INFO("搜索键:%s",key.c_str());
return 0;
}
#include "ros/ros.h"
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"delete_param");
ros::NodeHandle nh;
bool r1 = nh.deleteParam("nh_int");
ROS_INFO("nh 删除结果:%d",r1);
bool r2 = ros::param::del("param_int");
ROS_INFO("param 删除结果:%d",r2);
return 0;
}
在package.xml上面的CMakeLists.txt文件中配置如下内容,在文件最下面粘贴下面代码即可。
add_executable(param_set_change src/param_set_change.cpp)
add_executable(param_find src/param_find.cpp)
add_executable(param_delete src/param_delete.cpp)
target_link_libraries(param_set_change
${catkin_LIBRARIES}
)
target_link_libraries(param_find
${catkin_LIBRARIES}
)
target_link_libraries(param_delete
${catkin_LIBRARIES}
)
完成后即可进行编译,Ctrl+shift+~调出vscode终端,键入catkin_make编译,编译通过后键入source ./devel/setup.bash添加环境变量。此时我们可以键入命令rosparam list看看ros现在参数服务器中的参数有哪些,而后运行参数设置修改节点,即键入rosrun plumbing param_set_change,再键入rosparam list查看,会发现多了很多我们代码中设置的参数,并可以通过j键入rosparam get /参数名,例如rosparam get /param_map,查看参数的具体值,而后可运行rosrun plumbing param_find查看代码中写的参数和rosrun plumbing param_delete删除代码中写的参数。至此,参数服务器demo完成。
结语
此学习内容为赵虚左的文档:http://www.autolabor.com.cn/book/ROSTutorials/di-2-zhang-ros-jia-gou-she-ji.html
其中代码没有改动,只改动了工作空间名,功能包名,源文件名以及msg、srv文件的名称,和相应配置文件中此部分的名称。且在vscode中进行编写代码、编译和运行操作,也可在终端中实现,将三种话题通信的源文件放在了一个功能包下,方便运行对比,不易混淆。
代码文件github地址:https://github.com/Xieccz/ROS-noetic-demo/tree/main