文章目录
- 前言
- 一、话题通信
- 1.话题通信理论
- 2.话题通信常用API
- 🍗发布者
- advertise
- publish
- 🍖订阅者
- subscribe
- 3.自定义msg
- 二、服务通信
- 1.服务通信理论
- 2.服务通信常用API
- 🎆服务端
- advertiseService
- 🎇客户端
- serviceClient
- ros::service::waitForService
- waitForExistence
- call
- 3.自定义srv
- 三、参数服务器
- 1.参数服务器通信理论
- 2.参数服务器常用API
- 🎉ros::NodeHandle
- 🎊ros::param
- 四、其他常用API
- ros::init
- ros::NodeHandle
- ros::spinOnce
- ros::spin
- ros::ok
- shutdown
- 日志相关
- ROS_DEBUG
- ROS_INFO
- ROS_WARN
- ROS_ERROR
- ROS_FATAL
- 时间相关
- ros::Time::now
- ros::Duration
- ros::Rate
- createTimer
- 结束语
- 💂 个人主页:风间琉璃
- 🤟 版权: 本文由【风间琉璃】原创、在CSDN首发、需要转载请联系博主
- 💬 如果文章对你有
帮助
、欢迎关注
、点赞
、收藏(一键三连)
和订阅专栏
哦
前言
提示:这里可以添加本文要记录的大概内容:
ROS的通信架构是ROS的灵魂,也是整个ROS正常运行的关键所在。ROS通信架构包括各种数据的处理,进程的运行,消息的传递等等。ROS中的通信方式有:基于发布/订阅的话题通信
、基于客户端/服务器的服务通信
、基于RPC的参数服务器
,每个通信方式都有自己的特点。这里主要记录了通信架构的基础通信方式和相关概念、各种通信常用API以及其他常用API。
一、话题通信
话题通信是ROS中使用频率最高的一种通信模式,话题通信是基于发布/订阅模式,一个节点发布信息,另一个节点订阅信息。这种通信方式可实现不同节点之间数据交互的通信模型,用于实时更新、少逻辑处理的数据传送
场景。
1.话题通信理论
话题通信模型主要包含三个角色分别是:
- ROS Master(管理者):负责保管Talker和Listener注册的信息,并匹配话题相同的Talker与Listener,并建立两者之间的连接
- Talker(发布者):发布信息
- Listener(订阅者):接收Talker的消息
具体两者之间建立连接和通信的流程可参考这个笔记:话题通信,了解即可,在ROS中我们只需要编程实现发布者和订阅者。
2.话题通信常用API
🍗发布者
advertise
advertise
创建一个发布者对象
,用于向 ROS 系统中的指定主题发布消息。其函数原型如下所示,这是一个函数模板
。
template <class M>
Publisher advertise(const std::string& topic, uint32_t queue_size, bool latch = false)
{
AdvertiseOptions ops;
ops.template init<M>(topic, queue_size);
ops.latch = latch;
return advertise(ops);
}
- 模板参数 : 表示这个函数可以接受
任意类型 M 的消息
。在 ROS 中,消息类型通常是由 std_msgs 命名空间中的消息类型之一表示,比如 std_msgs::String、geometry_msgs或者自定义消息类型。 - topic:指定发布的
主题名称
。 - queue_size:指定发布队列的大小。
- latch:(可选参数)如果设置为 true,则发布器会将最新的消息保存并在新的订阅者连接时立即发送给它,否则只在有新消息发布时才发送。
publish
publish函数是ros::Publisher类中的一个成员函数,用于将消息发布到相应的主题。函数原型如下所示
template <typename M>
void publish(const M& message) const
-
模板参数: 这是消息的类型参数,表示要发布的
具体消息类型
,例如std_msgs::String。 -
message: 这是要发布的
具体消息对象
。你需要创建一个相应类型的消息对象并填充它的数据,然后将其作为参数传递给publish函数。
下面是一个例子,演示如何使用advertise和publish函数发布一个std_msgs::String类型的消息:
#include <ros/ros.h>
#include <std_msgs/String.h>
int main(int argc, char **argv)
{
// 初始化 ROS 节点
ros::init(argc, argv, "example_node");
ros::NodeHandle nh;
// 创建一个发布者,发布 std_msgs::String 类型的消息到主题 "example_topic",队列大小为 10
ros::Publisher pub = nh.advertise<std_msgs::String>("example_topic", 10);
// 创建并填充一个 std_msgs::String 类型的消息对象
std_msgs::String msg;
msg.data = "Hello, ROS!";
// 设置发送频率为10Hz
ros::Rate r(10);
// 循环发送消息
while (ros::ok())
{
// 使用 publish 函数发布消息
pub.publish(msg);
// 处理ROS回调函数
ros::spinOnce();
// 以指定频率休眠,以满足发送频率
r.sleep();
}
return 0;
}
使用rostopic echo打印输出如下
🍖订阅者
subscribe
subscribe函数用于创建消息订阅者,有两个作用,其一是创建一个消息订阅者
,用于订阅特定主题的消息;其二是当订阅的主题上有新消息时,调用指定的回调函数来处理消息
。函数原型如下所示:
template<class M>
Subscriber subscribe(const std::string& topic, uint32_t queue_size, void(*fp)(const boost::shared_ptr<M const>&), const TransportHints& transport_hints = TransportHints())
- M(模板参数):
指定消息的类型
。例如,如果你想要订阅std_msgs::String类型的消息,那么M将被实例化为std_msgs::String。 - topic:指定要订阅的
主题名称
。 - queue_size:指定订阅者的消息队列大小。队列用于存储尚未被处理的消息,以便稍后进行处理。
- fp:是一个函数指针,
指定一个回调函数
,该回调函数将在接收到新消息时被调用。回调函数的参数是一个boost::shared_ptr
,指向常量M类型的消息
。 - transport_hints(可选参数):提供关于传输的提示,一般默认即可,例如指定使用的传输协议等。
下面是一个使用subscribe函数的简单示例,配合上面的发布者程序使用:
#include <ros/ros.h>
#include <std_msgs/String.h>
// 回调函数的定义
void messageCallback(const boost::shared_ptr<std_msgs::String const>& msg)
{
ROS_INFO("Received message: %s", msg->data.c_str());
}
int main(int argc, char** argv)
{
// 初始化 ROS 节点
ros::init(argc, argv, "example_node2");
ros::NodeHandle nh;
// 创建一个订阅者,订阅 std_msgs::String 类型的消息,回调函数为 messageCallback
ros::Subscriber sub = nh.subscribe<std_msgs::String>("example_topic", 10, messageCallback);
// 进入 ROS 主循环
ros::spin();
return 0;
}
运行结果如下:
3.自定义msg
在ROS通信协议中,数据载体是通信中的核心组成部分。ROS通过std_msgs
库封装了一些原生的数据类型,如String、Int32、Int64、Char、Bool、Empty等。然而,这些数据类型一般只包含一个data字段,结构相对简单
,对于一些复杂的数据传输,比如激光雷达的信息等,其功能上显得有些局限。
在ROS中,可以通过创建自定义消息来定义你自己的消息类型。自定义消息允许你在ROS系统中使用特定的数据结构来进行通信。以下是如何创建和使用自定义消息的一般步骤:
①创建自定义消息msg文件
功能包下新建 msg 目录,添加文件 一个.msg,该文件包含自定义消息的定义
。例如,假设你想创建一个名为MyCustomMsg的自定义消息
,文件名可以是MyCustomMsg.msg。示例内容如下:
# MyCustomMsg.msg
string name
int64 age
int64 height
上面的例子定义了一个包含姓名、年龄和身高的自定义消息。
②修改package.xml文件
package.xml中添加编译依赖与执行依赖
。
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
③编辑CMakeLists.txt文件
在ROS工作空间的CMakeLists.txt文件中,确保包含以下内容,以编译并生成自定义消息:
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
# 需要加入 message_generation,必须有 std_msgs
## 配置 msg 源文件
add_message_files(
FILES
<自定义名称>.msg
)
# 生成消息时依赖于 std_msgs
generate_messages(
DEPENDENCIES
std_msgs
)
#执行时依赖,添加message_runtime
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES demo02_talker_listener
CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
# DEPENDS system_lib
)
注意: 官网没有在catkin_package
中配置 message_runtime
,经测试配置也可以。
最后,需要添加 add_dependencies
用以设置所依赖的消息相关的中间文件。
add_dependencies(可执行程序名称 ${PROJECT_NAME}_generate_messages_cpp)
编译后的中间文件查看:C++ 需要调用的中间文件(…/工作空间/devel/include/包名/xxx.h)。后续调用相关 msg 时,是从这些中间文件调用的。
生成的中间文件是在devel目录
下,为了方便vscode编写程序,还需要设置其中间文件的路
径,如下图所示,点击头文件报错的那行,然后点击小黄灯泡
即可快速完成。
④调用中间文件
发布者代码:
#include <ros/ros.h>
#include <turtle_pos/MyCustomMsg.h>
int main(int argc, char **argv)
{
// 初始化 ROS 节点
ros::init(argc, argv, "example_node");
ros::NodeHandle nh;
// 创建一个发布者,发布turtle_pos::MyCustomMsg类型的消息到主题 "example_topic",队列大小为 10
ros::Publisher pub = nh.advertise<turtle_pos::MyCustomMsg>("example_topic", 10);
// 创建并填充一个turtle_pos::MyCustomMsg类型的消息对象
turtle_pos::MyCustomMsg msg;
msg.age = 18;
msg.height = 170;
msg.name = "sue";
// 设置发送频率为1000Hz
ros::Rate r(1000);
// 循环发送消息
while (ros::ok())
{
// 使用 publish 函数发布消息
pub.publish(msg);
// 处理ROS回调函数
ros::spinOnce();
// 以指定频率休眠,以满足发送频率
r.sleep();
}
return 0;
}
订阅者代码:
#include <ros/ros.h>
#include <turtle_pos/MyCustomMsg.h>
// 回调函数的定义
void messageCallback(const boost::shared_ptr<turtle_pos::MyCustomMsg const>& msg)
{
// 打印接收到的消息内容
ROS_INFO("Received message: name:%s, age:%d, height:%d", msg->name.c_str(), msg->age, msg->height);
}
int main(int argc, char** argv)
{
// 初始化 ROS 节点
ros::init(argc, argv, "example_node2");
ros::NodeHandle nh;
// 创建一个订阅者,订阅turtle_pos::MyCustomMsg类型的消息,回调函数为 messageCallback
ros::Subscriber sub = nh.subscribe<turtle_pos::MyCustomMsg>("example_topic", 10, messageCallback);
// 进入 ROS 主循环,等待接收消息并调用回调函数
ros::spin();
return 0;
}
结果如下:
二、服务通信
服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式
的,是一种应答机制
。即: 一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。
1.服务通信理论
服务通信较于话题通信比较简单,理论模型如下图所示,该模型中涉及到三个角色:
- ROS Master:负责记录 Server 和 Client 注册的信息,并匹配
话题相同
的 Server 与 Client ,帮助 Server 与 Client 建立连接 - Talker(Server,服务端):返回响应信息
- Listener(Client,客户端):发送请求信息
其整个流程可以参考如下文章:服务通信理论。
注意:
1.客户端请求被处理时,需要保证服务器已经启动;(在客户端中可以使用代码解决,等待服务器启动后在进行连接)
2.服务端和客户端都可以存在多个。
2.服务通信常用API
🎆服务端
advertiseService
advertiseService
函数是创建一个ROS服务服务器对象
,它允许其他节点请求服务并接收相应的结果。下面是对该函数的作用和参数介绍:
template<class MReq, class MRes>
ServiceServer advertiseService(const std::string& service, bool(*srv_func)(MReq&, MRes&))
service:服务的名称
,是一个字符串,用于唯一标识服务
。
srv_func:是一个函数指针
,指向处理服务请求的回调函数
,当有节点请求该服务时, 函数将被调用来处理请求并生成响应。该回调函数应该有两个参数,一个是请求消息的引用(MReq&
),另一个是响应消息的引用(MRes&
)。这个函数应该返回一个布尔值,表示服务处理是否成功。
-MReq:请求消息类型(Request Message Type),表示服务的
请求消息类型
。
-MRes:响应消息类型(Response Message Type),表示服务的响应消息类型
。
注意,虽然这是一个模板函数,但是其模板参数是可选的,如果服务的请求消息和响应消息类型可以从回调函数的参数中推断出来,就可以省略模板参数
。这样的情况下,ROS会通过回调函数的参数类型来自动推断消息类型。
🎇客户端
serviceClient
serviceClient
函数是 ros::NodeHandle 类中的一个成员函数,用于创建 ROS 服务客户端,用于向指定服务发送请求。下面是对这个函数的作用和参数的解释:
template<class Service>
ServiceClient serviceClient(const std::string& service_name, bool persistent = false,
const M_string& header_values = M_string())
- service_name:是
服务的名称
,用于唯一标识服务
。 - persistent:是一个布尔值,表示是否创建一个持久化的服务客户端。如果为 true,则客户端将在网络断开时自动重新连接。
- header_values:是一个 std::map<std::string, std::string> 类型的参数,用于指定服务调用的头部信息。这可以包括认证信息、附加的元数据等。
- 模板参数:Service是服务的类型,表示服务的请求和响应
消息类型
。
ros::service::waitForService
waitForService
函数是 ros::NodeHandle 类中的成员函数,用于等待指定服务启动成功,通常用于服务客户端在调用服务之前等待服务启动
。以下是对这个函数的作用和参数的介绍:
ROSCPP_DECL bool waitForService(const std::string& service_name, ros::Duration timeout = ros::Duration(-1));
- service_name:是服务的名称,用于唯一标识服务。
- timeout:是一个可选参数,表示等待服务的超时时间。默认值为 ros::Duration(-1),表示无限等待,直到服务启动成功或节点关闭。如果指定了超时时间,函数将在超时时返回 false。
- 返回一个布尔值,表示等待服务是否成功启动。如果返回 true,表示服务启动成功;如果返回 false,表示等待超时或节点关闭。
waitForExistence
waitForExistence
函数是 ros::ServiceClient 类中的成员函数,用于等待服务存在,通常用于服务客户端在调用服务之前等待服务的注册。以下是对这个函数的作用和参数的介绍:
bool waitForExistence(ros::Duration timeout = ros::Duration(-1));
- timeout:是一个可选参数,表示等待服务存在的超时时间。默认值为 ros::Duration(-1),表示无限等待,直到服务存在或节点关闭。如果指定了超时时间,函数将在超时时返回 false。
- 返回一个布尔值,表示等待服务是否成功存在。如果返回 true,表示服务存在;如果返回 false,表示等待超时或节点关闭。
call
call
函数是 ros::ServiceClient 类中的成员函数,用于向服务发送请求并等待响应。下面是对这个函数的作用和参数介绍:
template<class Service>
bool call(Service& service)
- Service:表示服务的请求和响应
消息类型
- service:是服务请求的对象,它包含了
请求消息的数据
。在调用该函数时,这个对象将被填充为服务的请求消息,同时也可以包含用于接收服务响应的字段
。 - 返回一个布尔值,表示服务调用是否成功。如果返回 true,表示服务调用成功;如果返回 false,表示服务调用失败。
调用流程:
-
调用 client.call(service) 时,客户端将发送请求消息(service 对象中的数据)给服务端,并阻塞等待服务端的响应。
-
如果服务调用成功(服务端成功处理了请求),call 函数返回 true,同时服务端的响应消息被填充到 service 对象中,你可以通过 service.response 获取响应的数据。
-
如果服务调用失败,call 函数返回 false,通常表示服务端未能成功处理请求或服务不存在等原因。
3.自定义srv
srv
文件内的可用数据类型与 msg
文件一致,且定义 srv 实现流程与自定义 msg 实现流程类似:
①定义srv文件
服务通信中,数据分成两部分,请求
与响应
,在 srv 文件中请求和响应使用---
分割,具体实现如下:
功能包下新建 srv 目录,添加 xxx.srv 文件,内容:
# 客户端请求时发送的两个数字
int32 num1
int32 num2
---
# 服务器响应发送的数据
int32 sum
②修改package.xml文件
package.xml中添加编译依赖
与执行依赖
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
③编辑CMakeLists.txt文件
在CMakeLists.txt编辑 srv 相关配置。
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
# 需要加入 message_generation,必须有 std_msgs
## Generate services in the 'srv' folder
add_service_files(
FILES
addint.srv
)
## Generate added messages and services with any dependencies listed here
generate_messages(
DEPENDENCIES
std_msgs
)
注意: 官网没有在catkin_package
中配置 message_runtime
,经测试配置也可以,具体配置可见自定义msg,这里就懒得配置啦。
最后,需要添加 add_dependencies
用以设置所依赖的消息相关的中间文件,注意后缀名与msg的区别
。
add_dependencies(可执行程序名称${PROJECT_NAME}_gencpp)
编译后的中间文件查看:C++ 需要调用的中间文件(…/工作空间/devel/include/包名/xxx.h)。后续调用相关 srv 时,是从这些中间文件调用的。
生成的中间文件是在devel目录
下,为了方便vscode编写程序,还需要设置其中间文件的路
径,如下图所示,点击头文件报错的那行,然后点击小黄灯泡
即可快速完成。具体见自定义msg。
④调用中间文件
服务端代码:
#include <ros/ros.h>
#include <serve_client/addint.h>
// 回调函数的定义
bool addIntsCallback(serve_client::addint::Request& req, serve_client::addint::Response& res)
{
// 打印服务器接收到的请求数据
ROS_INFO("服务器接收到的请求数据为: num1 = %d, num2 = %d", req.num1, req.num2);
// 执行服务请求的操作(在这个例子中,将两个整数相加)
res.sum = req.num1 + req.num2;
// 返回 true 表示服务处理成功
return true;
}
int main(int argc, char** argv)
{
setlocale(LC_ALL, "");
// 初始化 ROS 节点
ros::init(argc, argv, "example_node");
ros::NodeHandle nh;
// 创建一个服务服务器,服务名称为 "AddInts",回调函数为 addIntsCallback,二选一即可,具体原因见advertiseService那里
//ros::ServiceServer server = nh.advertiseService("AddInts", addIntsCallback);
ros::ServiceServer server = nh.advertiseService<serve_client::addint::Request, serve_client::addint::Response>("AddInts", addIntsCallback);
// 输出服务器启动成功的消息
ROS_INFO("服务器启动成功");
// 进入 ROS 主循环,等待服务请求
ros::spin();
return 0;
}
客户端代码:
#include <ros/ros.h>
#include <serve_client/addint.h>
int main(int argc, char** argv)
{
setlocale(LC_ALL, "");
// 调用时动态传值,如果通过 launch 的 args 传参,需要传递的参数个数 +3
if (argc != 3)
// if (argc != 5) //launch 传参(0-文件路径 1传入的参数 2传入的参数 3节点名称 4日志路径)
{
ROS_ERROR("请提交两个整数");
return 1;
}
// 初始化 ROS 节点
ros::init(argc, argv, "example_client");
// 创建 ROS 句柄
ros::NodeHandle nh;
// 创建服务客户端,服务名称为 "AddInts"
ros::ServiceClient client = nh.serviceClient<serve_client::addint>("AddInts");
// 等待服务启动成功
//client.waitForExistence();
ros::service::waitForService("AddInts");
// 组织请求数据
serve_client::addint srv;
srv.request.num1 = atoi(argv[1]);
srv.request.num2 = atoi(argv[2]);
// 调用服务并等待响应
if (client.call(srv))
{
// 请求正常处理,输出响应结果
ROS_INFO("请求正常处理,响应结果: %d", srv.response.sum);
}
else
{
// 请求处理失败
ROS_ERROR("请求处理失败");
return 1;
}
return 0;
}
运行结果如下:
如果先启动客户端,那么会导致运行失败。在客户端发送请求前添加:client.waitForExistence()或:ros::service::waitForService(“AddInts”);这是一个阻塞式函数
,只有服务启动成功后才会继续执行。此处可以使用 launch 文件优化,但是需要注意 args 传参特点。
三、参数服务器
参数服务器在ROS中主要用于实现不同节点之间的数据共享
。参数服务器相当于是独立于所有节点的一个公共容器,可以将数据存储在该容器中,被不同的节点调用,当然不同的节点也可以往其中存储数据。
参数服务器是ROS参数系统的一部分,它允许在运行时存储和检索参数值。参数服务器是一个分布式的键-值存储系统
,用于在ROS系统中共享和存储配置参数、配置文件、常量值等信息。它是一种以共享的方式实现不同节点之间数据交互
的通信模式,其主要作用是存储多节点共享的数据
,类似于全局变量
。
1.参数服务器通信理论
参数服务器实现是最为简单的,该模型如下图所示,该模型中涉及到三个角色:
- ROS Master (管理者),作为一个公共容器保存参数
- Talker (参数设置者),可以向容器中设置参数
- Listener (参数调用者),可以获取参数
整个流程可以参考如下文章:参数服务器理论。
参数可使用数据类型:32-bit integers,booleans,strings,doubles,iso8601 dates,lists,base64-encoded binary data,字典。
注意:参数服务器不是为高性能而设计的,因此最好用于存储静态的非二进制
的简单数据。
2.参数服务器常用API
在 C++ 中实现参数服务器数据的增删改查,可以通过两套 API 实现。
🎉ros::NodeHandle
🎊ros::param
四、其他常用API
ros::init
ros::NodeHandle
ros::spinOnce
ros::spin
ros::ok
shutdown
日志相关
ROS_DEBUG
ROS_INFO
ROS_WARN
ROS_ERROR
ROS_FATAL
时间相关
ros::Time::now
ros::Duration
ros::Rate
createTimer
小结:
ROS 架构中的主要组件:
-
节点(Nodes)
: 节点是 ROS 架构的基本单位,通常是一个执行特定运算任务的独立进程。每个节点可以执行感知、控制、规划等不同功能。节点之间通过消息进行通信。 -
主题(Topics)
: 主题是节点之间进行消息传递的通道。一个节点可以发布(publish)消息到一个主题,而其他节点可以订阅(subscribe)这个主题来接收该消息。主题是一种异步通信机制,多个节点可以同时发布和订阅同一个主题。 -
消息(Messages)
: 消息是在节点之间传递的数据单元。节点可以根据定义的消息类型创建消息实例,并通过主题发布或接收这些消息。消息可以是包含任意数据的结构化数据类型,如图像、点云、传感器数据等,每一个消息都是一种严格的数据结构,支持标准数据类型(整型、浮点型、布尔型等),也支持嵌套结构和数组(类似于C语言的结构体struct),还可以根据需求由开发者自主定义。 -
服务(Services)
: 服务提供了节点之间的一对一通信机制,类似于传统的客户端-服务器模型。一个节点可以提供一个服务,而其他节点可以请求该服务并等待响应。与主题不同,服务是一种同步通信机制,请求节点会等待直到收到响应。 -
参数服务器(Parameter Server)
: 参数服务器也称节点管理器是一个全局的、分布式的键值存储系统,用于存储节点的配置参数。节点可以读取和写入参数服务器中的值,以便在运行时调整系统的行为。
ROS 架构实现了高度的灵活性和可扩展性。开发人员可以将不同的节点组合在一起,构建复杂的机器人系统,并实现各种功能和算法的集成和协同工作。
重点标记性文字设置
具体的标记性正文
具体的标记性正文
具体的标记性正文
具体的标记性正文
具体的标记性正文
具体的标记性正文
结束语
感谢阅读吾之文章,今已至此次旅程之终站 🛬。
吾望斯文献能供尔以宝贵之信息与知识也 🎉。
学习者之途,若藏于天际之星辰🍥,吾等皆当努力熠熠生辉,持续前行。
然而,如若斯文献有益于尔,何不以三连为礼?点赞、留言、收藏 - 此等皆以证尔对作者之支持与鼓励也 💞。