7.1.4-ROS 通信机制:
话题通信教程
话题通信简介
在 ROS 中,话题通信是节点之间交换信息的基本方式之一。本教程将引导您使用 C++ 和 Python 完成基本话题通信的搭建。我们将实现一个简单的发布者-订阅者模型,其中发布者以 10Hz 的频率发送文本消息,订阅者接收并打印这些消息。
1. 理论模型
话题通信涉及三个主要组件:
- ROS Master: 管理节点的注册与连接。
- Talker(发布者): 发送消息。
- Listener(订阅者): 接收消息。
ROS Master 帮助 Talker 和 Listener 之间建立连接。下面分步骤说明通信过程:
- Talker 注册: Talker 向 ROS Master 注册自身,包括其消息的话题名称。
- Listener 注册: Listener 向 ROS Master 注册自身,指定它要订阅的话题。
- 匹配: ROS Master 根据话题名匹配 Talker 与 Listener,并发送必要的连接信息。
- 建立连接: Listener 请求与 Talker 建立连接,Talker 进行确认。
- 消息交换: 一旦建立连接,Talker 开始向 Listener 发送消息。
要点:
- 仅在建立连接时才需要 ROS Master。
- 在连接建立后,即使关闭 ROS Master,通信仍可继续。
- 可以存在多个 Talker 与 Listener,且它们的启动顺序可以任意。
2. 基本话题通信操作 (C++)
目标: 创建一个以 10Hz 发送文本消息的发布者节点和一个打印接收到消息的订阅者节点。
步骤:
-
bash
cd ~/seeed_ws/src/ catkin_create_pkg listener_and_talker roscpp rospy std_msgs cd ~/seeed_ws/src/listener_and_talker/src touch listener.cpp talker.cpp -
发布者实现:
talker.cppcpp#include "ros/ros.h" #include "std_msgs/String.h" #include <sstream> int main(int argc, char *argv[]) { // Set locale for printing messages in the local language setlocale(LC_ALL, ""); // Initialize the ROS node with a unique name ros::init(argc, argv, "talker"); // Create a ROS node handle ros::NodeHandle nh; // Create a publisher object ros::Publisher pub = nh.advertise<std_msgs::String>("chatter", 10); std_msgs::String msg; std::string msg_front = "Hello Seeed"; int count = 0; ros::Rate r(10); // 10Hz while (ros::ok()) { std::stringstream ss; ss << msg_front << count; msg.data = ss.str(); pub.publish(msg); ROS_INFO("Sent message: %s", msg.data.c_str()); r.sleep(); count++; } return 0; } -
订阅者实现:
listener.cppcpp#include "ros/ros.h" #include "std_msgs/String.h" void doMsg(const std_msgs::String::ConstPtr& msg_p) { ROS_INFO("Heard: %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 配置:
在功能包的
CMakeLists.txt末尾添加以下代码:cmakeadd_executable(listener src/listener.cpp) add_executable(talker src/talker.cpp) target_link_libraries(listener ${catkin_LIBRARIES}) target_link_libraries(talker ${catkin_LIBRARIES}) -
运行代码:
-
打开终端并启动
roscore:bashroscore -
在新终端中,导航到工作空间并运行发布者节点:
bashrosrun listener_and_talker listener -
在另一个终端中运行订阅者节点:
bashrosrun listener_and_talker talker
您应该能在终端中看到正在发布与接收的消息。
3. 基本话题通信操作 (Python)
目标: 创建一个以 10Hz 发送文本消息的发布者节点和一个打印接收到消息的订阅者节点。
步骤:
-
bash
cd ~/seeed_ws/src/ catkin_create_pkg listener_and_talker roscpp rospy std_msgs mkdir ~/seeed_ws/src/listener_and_talker/script cd ~/seeed_ws/src/listener_and_talker/script touch listener.py talker.py -
发布者实现:
talker.pypython#!/usr/bin/env python import rospy from std_msgs.msg import String if __name__ == "__main__": rospy.init_node("talker_p") pub = rospy.Publisher("chatter", String, queue_size=10) msg = String() msg_front = "hello 你好" count = 0 rate = rospy.Rate(10) # 10Hz while not rospy.is_shutdown(): msg.data = msg_front + str(count) pub.publish(msg) rospy.loginfo("Sent message: %s", msg.data) rate.sleep() count += 1 -
订阅者实现:
listener.pypython#!/usr/bin/env python import rospy from std_msgs.msg import String def doMsg(msg): rospy.loginfo("Heard: %s", msg.data) if __name__ == "__main__": rospy.init_node("listener_p") sub = rospy.Subscriber("chatter", String, doMsg, queue_size=10) rospy.spin() -
添加可执行权限:
bashsudo chmod +x *.py -
CMakeLists.txt 配置:
在功能包的
CMakeLists.txt末尾添加以下代码:cmakecatkin_install_python(PROGRAMS scripts/talker.py scripts/listener.py DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} ) -
运行代码:
- 打开终端并启动
roscore:bashroscore - 在新终端中,导航到工作空间并运行发布者节点:
bash
rosrun listener_and_talker talker.py - 在另一个终端中运行订阅者节点:
bash
rosrun listener_and_talker listener.py
- 打开终端并启动
您应该能在终端中看到正在发布与接收的消息。
ROS 话题常用命令
rostopic bw: 显示话题的带宽使用情况rostopic delay: 显示带 header 的话题延迟rostopic echo: 在屏幕上打印消息rostopic find: 按类型查找话题rostopic hz: 显示话题的发布频率rostopic info: 显示话题的信息rostopic list: 列出所有活跃的话题rostopic pub: 向话题发布数据rostopic type: 打印话题的类型
服务通信简介
ROS 中的服务通信不同于话题通信,它是双向的。它不仅允许发送消息,还可以接收反馈。该模型由两个主要部分组成:
- Client: 发送请求的实体。
- Server: 处理请求并发送响应的实体。
当客户端向服务器发送请求时,会等待服务器处理请求并返回响应。这种机制遵循"请求-应答"结构,完成通信。
工作原理
- Node B(服务器)提供一个服务接口,通常命名为
/service_name之类。 - Node A(客户端)向 Node B 发送请求。
- Node B 处理请求并发送回响应。
通信过程可如下图所示:
-
Talker 节点通过 ROS Master 通告服务:
- Talker 节点通过 ROS Master 通告一个服务(例如
advertiseService("bar", foo:1234)),表明其可用性。
- Talker 节点通过 ROS Master 通告一个服务(例如
-
Listener 节点通过 ROS Master 查找服务:
- Listener 节点向 ROS Master 发送请求以查找服务(例如
lookupService("bar"))。
- Listener 节点向 ROS Master 发送请求以查找服务(例如
-
ROS Master 返回服务地址:
- ROS Master 以服务地址(例如
foo:3456)响应,供 Listener 节点连接。
- ROS Master 以服务地址(例如
-
Listener 节点向 Talker 节点请求数据:
- Listener 节点使用 XML/RPC 向 Talker 节点发送服务请求。
-
Talker 节点返回所请求的数据:
- Talker 节点处理请求,通过 TCP 发送回应答数据。
要点:
- 客户端在收到服务器响应之前会被阻塞。
- 服务通信高效,只在需要时(即发起请求时)消耗资源。
理论模型
服务通信模型涉及三个关键组件:
- ROS Master: 管理服务端和客户端的注册,根据服务名称的匹配协助建立连接。
- Server: 提供服务。
- Client: 请求服务。
流程概述:
-
服务端注册:
- 服务端向 ROS Master 注册自身,包括其提供的服务名称。
-
客户端注册:
- 客户端向 ROS Master 注册自身,指定其要使用的服务。
-
匹配与连接:
- ROS Master 根据服务名匹配客户端与服务端,促成连接。
-
请求-响应循环:
- 客户端向服务端发送请求,服务端处理请求并返回响应。
话题通信与服务通信对比
让我们对这两种最常见的 ROS 通信方式进行比较,以加深理解:
| 方面 | 话题通信 | 服务通信 |
|---|---|---|
| 通信类型 | 异步 | 同步 |
| 协议 | TCP/IP | TCP/IP |
| 通信模型 | 发布-订阅 | 请求-应答 |
| 关系 | 多对多 | 一对多 |
| 特性 | 基于回调 | 远程过程调用 (RPC) |
| 使用场景 | 持续、高频数据 | 低频、特定任务 |
| 示例 | 发布激光雷达数据 | 触发传感器或拍照 |
注意: 远程过程调用(RPC)是指在不同进程上执行函数,就好像它是本地的一样。
5. 在 ROS 中创建自定义服务 (srv)
让我们通过一个动手示例,创建一个自定义服务,将客户端发送的两个整数相加。服务端处理该请求并将和返回给客户端。
实现步骤:
-
创建一个新的功能包
bashcd ~/seeed_ws/src catkin_create_pkg service_communication roscpp rospy std_msgs cd ~/seeed_ws catkin_make -
定义 srv 文件:
srv文件定义了请求与响应的结构。在本例中,请求包含两个整数,响应包含它们的和。 在功能包中创建一个名为srv的新目录,并添加一个名为AddInts.srv的文件:bashmkdir ~/seeed_ws/src/service_communication/srv cd ~/seeed_ws/src/service_communication/srv touch AddInts.srv将以下内容复制到
AddInts.srv:srvint32 num1 int32 num2 --- int32 sum -
更新 package.xml: 在功能包的
package.xml中添加生成消息文件所需的依赖:xml<build_depend>message_generation</build_depend> <exec_depend>message_runtime</exec_depend> -
更新 CMakeLists.txt:
-
编译您的功能包:
- 编译您的功能包以生成服务消息头文件:
bash
cd ~/seeed_ws catkin_make source devel/setup.bash
- 编译您的功能包以生成服务消息头文件:
实现服务通信 (C++)
本示例演示如何使用 C++ 在 ROS 中实现服务通信。我们将创建一个简单的服务,服务端将客户端提供的两个整数相加并返回它们的和。
1. 服务端实现:
add_two_ints_server.cpp
#include "ros/ros.h"
#include "service_communication/AddInts.h"
// Callback function to handle the client's request
bool add(service_communication::AddInts::Request &req,
service_communication::AddInts::Response &res) {
res.sum = req.num1 + req.num2; // Compute the sum
ROS_INFO("Request: a=%ld, b=%ld", (long int)req.num1, (long int)req.num2);
ROS_INFO("Sending back response: [%ld]", (long int)res.sum);
return true;
}
int main(int argc, char **argv) {
ros::init(argc, argv, "add_two_ints_server");
ros::NodeHandle nh;
// Advertise the service to the ROS master
ros::ServiceServer service = nh.advertiseService("add_two_ints", add);
ROS_INFO("Ready to add two integers.");
ros::spin();
return 0;
}2. 客户端实现:
add_two_ints_client.cpp
#include "ros/ros.h"
#include "service_communication/AddInts.h"
#include <cstdlib>
int main(int argc, char **argv) {
ros::init(argc, argv, "add_two_ints_client");
if (argc != 3) {
ROS_INFO("Usage: add_two_ints_client X Y");
return 1;
}
ros::NodeHandle nh;
ros::ServiceClient client = nh.serviceClient<service_communication::AddInts>("add_two_ints");
// Prepare the service request
service_communication::AddInts srv;
srv.request.num1 = atoll(argv[1]);
srv.request.num2 = atoll(argv[2]);
// Call the service and check if it was successful
if (client.call(srv)) {
ROS_INFO("Sum: %ld", (long int)srv.response.sum);
} else {
ROS_ERROR("Failed to call service add_two_ints");
return 1;
}
return 0;
}CMakeLists.txt 配置:
请确保在 CMakeLists.txt 末尾添加以下内容,以编译服务端与客户端:
add_executable(add_two_ints_server src/add_two_ints_server.cpp)
add_executable(add_two_ints_client src/add_two_ints_client.cpp)
add_dependencies(add_two_ints_server ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
add_dependencies(add_two_ints_client ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(add_two_ints_server ${catkin_LIBRARIES})
target_link_libraries(add_two_ints_client ${catkin_LIBRARIES})编译并运行演示
打开一个终端:
cd ~/seeed_ws
catkin_make
roscore打开另一个终端:
cd ~/seeed_ws
source devel/setup.bash
rosrun service_communication server打开另一个终端:
cd ~/seeed_ws
source devel/setup.bash
rosrun service_communication client 1 5实现服务通信 (Python)
该 Python 示例实现了与 C++ 示例相同的功能,使用一个服务来将两个整数相加。
在功能包下创建 scripts 文件夹,并在其中创建 add_two_ints_server.py 和 add_two_ints_client.py 文件。
cd ~/seeed/src/service_communication/
mkdir scripts
cd scripts
touch add_two_ints_server.py add_two_ints_client.py1. 服务端实现:
add_two_ints_server.py
#!/usr/bin/env python
import rospy
from service_communication.srv import AddInts,AddIntsRequest, AddIntsResponse
def doReq(req):
sum = req.num1 + req.num2
rospy.loginfo("data:num1 = %d, num2 = %d, sum = %d",req.num1, req.num2, sum)
resp = AddIntsResponse(sum)
return resp
if __name__ == "__main__":
rospy.init_node("addints_server_p")
server = rospy.Service("AddInts",AddInts,doReq)
rospy.spin()2. 客户端实现:
add_two_ints_client.py
#!/usr/bin/env python
import sys
import rospy
from service_communication.srv import *
if __name__ == "__main__":
if len(sys.argv) != 3:
rospy.logerr("error")
sys.exit(1)
rospy.init_node("AddInts_Client_p")
client = rospy.ServiceProxy("AddInts",AddInts)
client.wait_for_service()
req = AddIntsRequest()
req.num1 = int(sys.argv[1])
req.num2 = int(sys.argv[2])
resp = client.call(req)
rospy.loginfo("result:%d",resp.sum)CMakeLists.txt 配置:
在 CMakeLists.txt 中为 Python 脚本添加以下内容:
catkin_install_python(PROGRAMS
scripts/add_two_ints_server.py
scripts/add_two_ints_client.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)编译并运行演示
打开一个终端:
cd ~/seeed_ws
catkin_make
roscore打开另一个终端:
cd ~/seeed_ws
source devel/setup.bash
rosrun service_communication add_two_ints_server.py打开另一个终端:
cd ~/seeed_ws
source devel/setup.bash
rosrun service_communication add_two_ints_client.py 1 5服务通信命令
要在 ROS 中使用服务,可使用 rosservice 命令。下面列出常用的 rosservice 命令及其功能:
rosservice args: 打印服务所需的参数rosservice call: 使用提供的参数调用服务rosservice find: 按类型查找服务rosservice info: 打印服务的信息rosservice list: 列出所有活跃的服务rosservice type: 打印服务的类型rosservice uri: 打印服务的 ROSRPC URI
ROS 参数服务器简介
ROS 参数服务器是一个共享的、多用户的、可通过网络访问的参数存储空间。它提供了一种在运行时存储和检索参数的方法,可用于配置节点或在节点之间共享数据。服务器上的参数可以是各种数据类型,包括整数、布尔值、字符串、双精度浮点数、列表和字典。参数服务器由 ROS Master 管理,节点通过设置、检索或删除参数与其交互。
参数服务器的理论模型
参数服务器涉及三个主要角色:
- ROS Master: 管理参数服务器,作为参数的中央存储。
- Talker: 在服务器上设置参数的节点。
- Listener: 从服务器获取参数的节点。
与参数服务器交互的过程通常包含以下步骤:
-
设置参数(Talker):
- Talker 节点通过 RPC(远程过程调用)向参数服务器发送参数,包括参数的名称和值。ROS Master 将该参数存储在其列表中。
-
检索参数(Listener):
- Listener 节点通过发送一个带有参数名称的查询从参数服务器请求参数。
-
返回参数(ROS Master):
- ROS Master 在其存储中查找请求的参数,并将相应的值返回给 Listener。
支持的数据类型:
- 32 位整数
- 布尔值
- 字符串
- 双精度浮点数
- ISO8601 日期
- 列表
- Base64 编码的二进制数据
- 字典
C++ 实现
设置参数 (C++):
我们先使用两种不同的 API:ros::NodeHandle 和 ros::param,在参数服务器上设置各种类型的参数。
set_parameters.cpp
#include "ros/ros.h"
int main(int argc, char *argv[]) {
ros::init(argc, argv, "set_parameters");
std::vector<std::string> students = {"Alice", "Bob", "Charlie", "David"};
std::map<std::string, std::string> friends = {{"John", "Doe"}, {"Jane", "Smith"}};
// Using ros::NodeHandle to set parameters
ros::NodeHandle nh;
nh.setParam("int_param", 42);
nh.setParam("double_param", 3.14159);
nh.setParam("bool_param", true);
nh.setParam("string_param", "Hello ROS");
nh.setParam("vector_param", students);
nh.setParam("map_param", friends);
// Using ros::param to set parameters
ros::param::set("int_param_param", 84);
ros::param::set("double_param_param", 6.28318);
ros::param::set("bool_param_param", false);
ros::param::set("string_param_param", "Goodbye ROS");
ros::param::set("vector_param_param", students);
ros::param::set("map_param_param", friends);
return 0;
}在功能包的 CMakeLists.txt 末尾添加以下代码:
add_executable(set_parameters src/set_parameters.cpp)
target_link_libraries(set_parameters ${catkin_LIBRARIES})在该示例中:
- 我们在参数服务器上设置了多种类型的参数,包括整数、双精度浮点数、布尔值、字符串、向量和映射。
- 我们同时使用了
ros::NodeHandle和ros::param两种 API 来设置参数。
检索参数 (C++):
接下来,我们将检索之前设置在参数服务器上的参数。
get_parameters.cpp
#include "ros/ros.h"
int main(int argc, char *argv[]) {
ros::init(argc, argv, "get_parameters");
// Using ros::NodeHandle to retrieve parameters
ros::NodeHandle nh;
int int_value;
double double_value;
bool bool_value;
std::string string_value;
std::vector<std::string> students;
std::map<std::string, std::string> friends;
nh.getParam("int_param", int_value);
nh.getParam("double_param", double_value);
nh.getParam("bool_param", bool_value);
nh.getParam("string_param", string_value);
nh.getParam("vector_param", students);
nh.getParam("map_param", friends);
ROS_INFO("Retrieved values:");
ROS_INFO("int_param: %d", int_value);
ROS_INFO("double_param: %.5f", double_value);
ROS_INFO("bool_param: %d", bool_value);
ROS_INFO("string_param: %s", string_value.c_str());
for (const auto &student : students) {
ROS_INFO("Student: %s", student.c_str());
}
for (const auto &friend_pair : friends) {
ROS_INFO("Friend: %s = %s", friend_pair.first.c_str(), friend_pair.second.c_str());
}
return 0;
}在功能包的 CMakeLists.txt 末尾添加以下代码:
add_executable(get_parameters src/get_parameters.cpp)
target_link_libraries(get_parameters ${catkin_LIBRARIES})在该示例中:
- 我们使用
ros::NodeHandleAPI 检索服务器上的参数。 - 检索到的参数随后被打印到 ROS 日志中以便核对。
删除参数 (C++):
最后,让我们看看如何从参数服务器删除参数。
delete_parameters.cpp
#include "ros/ros.h"
int main(int argc, char *argv[]) {
ros::init(argc, argv, "delete_parameters");
ros::NodeHandle nh;
bool success;
// Using ros::NodeHandle to delete parameters
success = nh.deleteParam("int_param");
ROS_INFO("Delete int_param: %s", success ? "Success" : "Failure");
// Using ros::param to delete parameters
success = ros::param::del("int_param_param");
ROS_INFO("Delete int_param_param: %s", success ? "Success" : "Failure");
return 0;
}在功能包的 CMakeLists.txt 末尾添加以下代码:
add_executable(delete_parameters src/delete_parameters.cpp)
target_link_libraries(delete_parameters ${catkin_LIBRARIES})在该示例中:
- 我们同时使用
ros::NodeHandle与ros::paramAPI 从服务器删除参数。 - 删除是否成功会被记录到日志。
2.2 Python 实现
设置参数 (Python):
现在让我们使用 Python 设置参数。该过程与 C++ 版本非常相似。
#!/usr/bin/env python
import rospy
if __name__ == "__main__":
rospy.init_node("set_parameters_py")
# Setting various types of parameters
rospy.set_param("int_param", 42)
rospy.set_param("double_param", 3.14159)
rospy.set_param("bool_param", True)
rospy.set_param("string_param", "Hello ROS")
rospy.set_param("list_param", ["apple", "banana", "cherry"])
rospy.set_param("dict_param", {"first_name": "John", "last_name": "Doe"})
# Modifying a parameter
rospy.set_param("int_param", 84)在功能包的 CMakeLists.txt 末尾添加以下代码:
catkin_install_python(PROGRAMS
scripts/set_parameters_py.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)在该示例中:
- 我们设置了各种类型的参数,包括整数、双精度浮点数、布尔值、字符串、列表和字典。
- 我们还演示了如何修改一个已存在的参数。
检索参数 (Python):
接下来,我们将检索之前设置的参数。
#!/usr/bin/env python
import rospy
if __name__ == "__main__":
rospy.init_node("get_parameters_py")
# Retrieving parameters
int_value = rospy.get_param("int_param", 0)
double_value = rospy.get_param("double_param", 0.0)
bool_value = rospy.get_param("bool_param", False)
string_value = rospy.get_param("string_param", "")
list_value = rospy.get_param("list_param", [])
dict_value = rospy.get_param("dict_param", {})
rospy.loginfo("Retrieved values:")
rospy.loginfo("int_param: %d", int_value)
rospy.loginfo("double_param: %.5f", double_value)
rospy.loginfo("bool_param: %s", bool_value)
rospy.loginfo("string_param: %s", string_value)
rospy.loginfo("list_param: %s", list_value)
rospy.loginfo("dict_param: %s", dict_value)在功能包的 CMakeLists.txt 末尾添加以下代码:
catkin_install_python(PROGRAMS
scripts/set_parameters_py.py
scripts/get_parameters_py.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)在该示例中:
- 我们使用
rospy.get_param检索设置在服务器上的参数。 - 检索到的值通过
rospy.loginfo输出到日志。
删除参数 (Python):
最后,让我们使用 Python 从参数服务器删除参数。
#!/usr/bin/env python
import rospy
if __name__ == "__main__":
rospy.init_node("delete_parameters_py")
try:
rospy.delete_param("int_param")
rospy.loginfo("int_param deleted successfully.")
except KeyError:
rospy.logwarn("int_param does not exist.")
try:
rospy.delete_param("non_existent_param")
rospy.loginfo("non_existent_param deleted successfully.")
except KeyError:
rospy.logwarn("non_existent_param does not exist.")在功能包的 CMakeLists.txt 末尾添加以下代码:
catkin_install_python(PROGRAMS
scripts/set_parameters_py.py
scripts/get_parameters_py.py
scripts/delete_parameters_py.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)在该示例中:
- 我们尝试删除一个参数,并使用异常处理(
KeyError)处理参数不存在的情况。
ROS 参数服务器常用命令
rosparam 包含命令行工具,用于通过 YAML 编码文件在参数服务器上获取与设置 ROS 参数。
rosparam set: 设置参数rosparam get: 获取参数rosparam load: 从外部文件加载参数rosparam dump: 将参数导出到外部文件rosparam delete: 删除参数rosparam list: 列出所有参数
示例:
rosparam list: 列出参数服务器上的所有参数。rosparam set <param_name> <value>: 设置具有特定值的参数。rosparam get <param_name>: 获取特定参数的值。rosparam delete <param_name>: 删除特定参数。rosparam load <file_name.yaml>: 从 YAML 文件加载参数。rosparam dump <file_name.yaml>: 将当前参数导出到 YAML 文件。