目标:定义自定义接口文件( .msg
和 .srv
)并将它们与 Python 和 C++节点一起使用。
教程级别:初学者
时间:20 分钟
目录
背景
先决条件
任务
1. 创建一个新包
2. 创建自定义定义
3
CMakeLists.txt
4
package.xml
5. 构建
tutorial_interfaces
包6 确认消息和服务创建
7. 测试新界面
摘要
下一步
背景
在之前的教程中,您利用消息和服务接口来学习关于主题、服务以及简单的发布者/订阅者(C++/Python)和服务/客户端(C++/Python)节点。在那些情况下,您使用的接口是预先定义的。
虽然使用预定义的接口定义是一种好习惯,但有时您可能也需要定义自己的消息和服务。本教程将向您介绍创建自定义接口定义的最简单方法。
先决条件
您应该有一个 ROS 2 工作空间。
本教程还使用了在发布者/订阅者(C++ 和 Python)和服务/客户端(C++ 和 Python)教程中创建的包来尝试新的自定义消息。
任务
1. 创建一个新包
在本教程中,您将在各自的包中创建自定义的 .msg
和 .srv
文件,然后在另一个包中使用它们。两个包应该在同一个工作空间中。
由于我们将使用在早期教程中创建的 pub/sub 和 service/client 包,请确保您位于这些包的同一工作空间( ros2_ws/src
),然后运行以下命令来创建一个新包:
ros2 pkg create --build-type ament_cmake --license Apache-2.0 tutorial_interfaces
cxy@ubuntu2404-cxy:~/ros2_ws/src$ ros2 pkg create --build-type ament_cmake --license Apache-2.0 tutorial_interfaces
going to create a new package
package name: tutorial_interfaces
destination directory: /home/cxy/ros2_ws/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['cxy <cxy@todo.todo>']
licenses: ['Apache-2.0']
build type: ament_cmake
dependencies: []
creating folder ./tutorial_interfaces
creating ./tutorial_interfaces/package.xml
creating source and include folder
creating folder ./tutorial_interfaces/src
creating folder ./tutorial_interfaces/include/tutorial_interfaces
creating ./tutorial_interfaces/CMakeLists.txt
tutorial_interfaces
是新包的名称。请注意,它是且只能是一个 CMake 包,但这并不限制您可以在哪种类型的包中使用您的消息和服务。您可以在 CMake 包中创建自己的自定义接口,然后在 C++或 Python 节点中使用它,这将在最后一节中介绍。
.msg
和 .srv
文件需要分别放置在名为 msg
和 srv
的目录中。在 ros2_ws/src/tutorial_interfaces
中创建目录:
cxy@ubuntu2404-cxy:~/ros2_ws/src/tutorial_interfaces$
mkdir msg srv
2. 创建自定义定义
2.1 消息定义
在您刚刚创建的 tutorial_interfaces/msg
目录中,新建一个名为 Num.msg
的文件,其中包含一行代码声明其数据结构:
int64 num
这是一个自定义消息,用于传输一个名为 num
的 64 位整数。
在您刚刚创建的 tutorial_interfaces/msg
目录中,还要新建一个名为 Sphere.msg
的文件,内容如下:
geometry_msgs/Point center
float64 radius
此自定义消息使用了另一个消息包中的消息(在这种情况下为 geometry_msgs/Point
)。
2.2 srv 定义
在您刚刚创建的 tutorial_interfaces/srv
目录中,创建一个名为 AddThreeInts.srv
的新文件,包含以下请求和响应结构:
int64 a
int64 b
int64 c
---
int64 sum
这是您的自定义服务,它请求三个整数,分别命名为 a
、 b
和 c
,并返回一个名为 sum
的整数。
3 CMakeLists.txt
要将您定义的接口转换为特定语言的代码(如 C++和 Python),以便它们可以在这些语言中使用,请将以下行添加到 CMakeLists.txt
:
find_package(geometry_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/Num.msg"
"msg/Sphere.msg"
"srv/AddThreeInts.srv"
DEPENDENCIES geometry_msgs # Add packages that above messages depend on, in this case geometry_msgs for Sphere.msg
)
便条
在 rosidl_generate_interfaces 中的第一个参数(库名称)必须与${PROJECT_NAME}相匹配(见 https://github.com/ros2/rosidl/issues/441#issuecomment-591025515)。
cmake_minimum_required(VERSION 3.8)
# 设置CMake的最低版本要求为3.8
project(tutorial_interfaces)
# 定义项目名称为'tutorial_interfaces'
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# 如果使用GNU C++编译器或Clang编译器,则添加编译选项-Wall -Wextra -Wpedantic
# find dependencies
# 查找依赖项
find_package(ament_cmake REQUIRED)
# 查找ament_cmake包,标记为必需
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)
# 取消注释以下部分以手动添加更多依赖项
# find_package(<dependency> REQUIRED)
find_package(geometry_msgs REQUIRED)
# 查找geometry_msgs包,标记为必需
find_package(rosidl_default_generators REQUIRED)
# 查找rosidl_default_generators包,标记为必需
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/Num.msg"
"msg/Sphere.msg"
"srv/AddThreeInts.srv"
DEPENDENCIES geometry_msgs # Add packages that above messages depend on, in this case geometry_msgs for Sphere.msg
)
# 生成接口文件,包括Num.msg、Sphere.msg和AddThreeInts.srv
# 依赖项为geometry_msgs,因为Sphere.msg依赖于geometry_msgs
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# 如果启用了测试,查找ament_lint_auto包,标记为必需
# the following line skips the linter which checks for copyrights
# comment the line when a copyright and license is added to all source files
set(ament_cmake_copyright_FOUND TRUE)
# 以下行跳过检查版权的linter
# 当所有源文件添加了版权和许可证时,注释掉该行
# the following line skips cpplint (only works in a git repo)
# comment the line when this package is in a git repo and when
# a copyright and license is added to all source files
set(ament_cmake_cpplint_FOUND TRUE)
# 以下行跳过cpplint检查(仅在git仓库中有效)
# 当该包在git仓库中且所有源文件添加了版权和许可证时,注释掉该行
ament_lint_auto_find_test_dependencies()
# 自动查找测试依赖项
endif()
ament_package()
# 声明这是一个ament包
4 package.xml
因为接口依赖于 rosidl_default_generators
来生成特定语言的代码,您需要声明一个对它的构建工具依赖。 rosidl_default_runtime
是一个运行时或执行阶段的依赖,稍后使用接口时需要它。 rosidl_interface_packages
是您的包 tutorial_interfaces
应该关联的依赖组的名称,使用 <member_of_group>
标签声明。
在 package.xml
的 <package>
元素内添加以下行:
<depend>geometry_msgs</depend>
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
<?xml version="1.0"?>
<!-- 定义XML版本为1.0 -->
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<!-- 指定XML模式文件的位置和类型 -->
<package format="3">
<!-- 定义包的格式版本为3 -->
<name>tutorial_interfaces</name>
<!-- 包的名称为'tutorial_interfaces' -->
<version>0.0.0</version>
<!-- 包的版本号为0.0.0 -->
<description>Creating custom msg and srv files</description>
<!-- 包的描述,待填写 -->
<maintainer email="cxy@126.com">cxy</maintainer>
<!-- 维护者信息,包含电子邮件地址 -->
<license>Apache-2.0</license>
<!-- 包的许可证类型为Apache-2.0 -->
<buildtool_depend>ament_cmake</buildtool_depend>
<!-- 构建工具依赖项为ament_cmake -->
<depend>geometry_msgs</depend>
<!-- 依赖项为geometry_msgs -->
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<!-- 构建工具依赖项为rosidl_default_generators -->
<exec_depend>rosidl_default_runtime</exec_depend>
<!-- 运行时依赖项为rosidl_default_runtime -->
<member_of_group>rosidl_interface_packages</member_of_group>
<!-- 包属于rosidl_interface_packages组 -->
<test_depend>ament_lint_auto</test_depend>
<!-- 测试依赖项为ament_lint_auto -->
<test_depend>ament_lint_common</test_depend>
<!-- 测试依赖项为ament_lint_common -->
<export>
<!-- 导出部分 -->
<build_type>ament_cmake</build_type>
<!-- 构建类型为ament_cmake -->
</export>
</package>
5. 构建 tutorial_interfaces
包
现在您的自定义接口包的所有部分都已就位,您可以构建该包。在您工作区的根目录 ( ~/ros2_ws
) 中,运行以下命令:
cxy@ubuntu2404-cxy:~/ros2_ws$ colcon build --packages-select tutorial_interfaces
Starting >>> tutorial_interfaces
Finished <<< tutorial_interfaces [26.0s]
Summary: 1 package finished [26.7s]
现在这些接口将能被其他 ROS 2 包发现。
6 确认消息和服务创建
在新终端中,运行以下命令以在您的工作区中( ros2_ws
)对其进行源设置:
source install/setup.bash
现在您可以通过使用 ros2 interface show
命令来确认您的 接口 创建是否成功:
cxy@ubuntu2404-cxy:~/ros2_ws$ ros2 interface show tutorial_interfaces/msg/Num
int64 num
和
cxy@ubuntu2404-cxy:~/ros2_ws$ ros2 interface show tutorial_interfaces/msg/Sphere
geometry_msgs/Point center
float64 x
float64 y
float64 z
float64 radius
和
cxy@ubuntu2404-cxy:~/ros2_ws$ ros2 interface show tutorial_interfaces/srv/AddThreeInts
int64 a
int64 b
int64 c
---
int64 sum
7 测试新接口
在这一步,您可以使用在之前教程中创建的包。对节点、 CMakeLists.txt
和 package.xml
文件进行一些简单的修改,将允许您使用新的接口。
7.1 测试 Num.msg
与发布/订阅
在对之前教程中创建的发布者/订阅者包进行一些修改(C++或 Python)后,您可以看到 Num.msg
的实际运行。由于您将标准字符串消息更改为数字消息,输出将会有些许不同。
发布者
C++
#include <chrono> // 包含chrono库,用于处理时间。
#include <memory> // 包含memory库,用于处理智能指针。
#include "rclcpp/rclcpp.hpp" // 包含ROS2 C++客户端库的头文件。
#include "tutorial_interfaces/msg/num.hpp" // 导入自定义消息Num的头文件。
using namespace std::chrono_literals; // 使用std::chrono_literals命名空间,允许使用后缀如500ms表示时间。
// 定义一个MinimalPublisher类,继承自rclcpp::Node。
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0) // 初始化Node,并设置节点名称为'minimal_publisher',计数器count_初始化为0。
{
publisher_ = this->create_publisher<tutorial_interfaces::msg::Num>("topic", 10); // 创建发布者对象,发布Num类型的消息到'topic'话题,队列大小为10。
auto timer_callback = [this](){ // 定义定时器回调函数。
auto message = tutorial_interfaces::msg::Num(); // 创建Num类型的消息实例。
message.num = this->count_++; // 将计数器count_的值赋给消息的num字段,并将count_加1。
RCLCPP_INFO_STREAM(this->get_logger(), "Publishing: '" << message.num << "'"); // 使用流式日志记录发布的消息。
publisher_->publish(message); // 发布消息。
};
timer_ = this->create_wall_timer(500ms, timer_callback); // 创建定时器,每500毫秒调用一次timer_callback函数。
}
private:
rclcpp::TimerBase::SharedPtr timer_; // 定义定时器智能指针。
rclcpp::Publisher<tutorial_interfaces::msg::Num>::SharedPtr publisher_; // 定义发布者对象的智能指针。
size_t count_; // 定义计数器变量。
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv); // 初始化ROS2。
rclcpp::spin(std::make_shared<MinimalPublisher>()); // 创建MinimalPublisher对象并进入循环等待回调。
rclcpp::shutdown(); // 关闭ROS2。
return 0; // 返回0表示程序正常退出。
}
Python
import rclpy # 导入ROS2 Python客户端库。
from rclpy.node import Node # 从rclpy.node模块导入Node类。
from tutorial_interfaces.msg import Num # 导入自定义消息Num。
# 定义一个MinimalPublisher类,它继承自Node类。
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher') # 初始化Node,并设置节点名称为'minimal_publisher'。
self.publisher_ = self.create_publisher(Num, 'topic', 10) # 创建发布者对象,发布Num类型的消息到'topic'话题,队列大小为10。
timer_period = 0.5 # 设置定时器周期为0.5秒。
self.timer = self.create_timer(timer_period, self.timer_callback) # 创建定时器,调用timer_callback函数。
self.i = 0 # 初始化计数器i。
def timer_callback(self):
msg = Num() # 创建Num类型的消息实例。
msg.num = self.i # 将计数器i的值赋给消息的num字段。
self.publisher_.publish(msg) # 发布消息。
self.get_logger().info('Publishing: "%d"' % msg.num) # 记录日志信息,显示发布的数字。
self.i += 1 # 计数器i加1。
# main函数作为程序入口点。
def main(args=None):
rclpy.init(args=args) # 初始化ROS2。
minimal_publisher = MinimalPublisher() # 创建MinimalPublisher对象。
rclpy.spin(minimal_publisher) # 进入循环等待回调。
minimal_publisher.destroy_node() # 销毁节点。
rclpy.shutdown() # 关闭ROS2。
# 当该脚本被直接运行时,调用main函数。
if __name__ == '__main__':
main()
订阅者
C++
#include <functional> // 包含functional头文件,用于函数对象和预定义的函数。
#include <memory> // 包含memory头文件,用于管理动态内存。
#include "rclcpp/rclcpp.hpp" // 包含ROS2 C++客户端库的头文件。
#include "tutorial_interfaces/msg/num.hpp" // 导入自定义消息Num。
using std::placeholders::_1; // 使用占位符_1,它是std::placeholders命名空间中的一个对象。
// 定义一个MinimalSubscriber类,它继承自rclcpp::Node类。
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber") // 初始化Node,并设置节点名称为'minimal_subscriber'。
{
auto topic_callback = [this](const tutorial_interfaces::msg::Num & msg){ // 定义一个lambda函数作为话题回调。
RCLCPP_INFO_STREAM(this->get_logger(), "I heard: '" << msg.num << "'"); // 使用流式日志记录接收到的消息。
};
subscription_ = this->create_subscription<tutorial_interfaces::msg::Num>( // 创建订阅对象。
"topic", 10, topic_callback); // 订阅名为'topic'的话题,队列大小为10,回调函数为topic_callback。
}
private:
rclcpp::Subscription<tutorial_interfaces::msg::Num>::SharedPtr subscription_; // 定义订阅对象的智能指针。
};
// main函数作为程序入口点。
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv); // 初始化ROS2。
rclcpp::spin(std::make_shared<MinimalSubscriber>()); // 创建MinimalSubscriber对象并进入循环等待回调。
rclcpp::shutdown(); // 关闭ROS2。
return 0; // 返回0表示程序正常退出。
}
Python
import rclpy # 导入ROS2 Python客户端库。
from rclpy.node import Node # 从rclpy模块导入Node类。
from tutorial_interfaces.msg import Num # 导入自定义消息Num。
# 定义一个MinimalSubscriber类,它继承自Node类。
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber') # 初始化Node,并设置节点名称为'minimal_subscriber'。
self.subscription = self.create_subscription(
Num, # 设置订阅的消息类型为Num。
'topic', # 设置订阅的话题名称为'topic'。
self.listener_callback, # 设置回调函数为listener_callback。
10) # 设置队列大小为10。
self.subscription # 这行代码实际上没有执行任何操作,只是重复了上一行创建的订阅对象。
def listener_callback(self, msg):
self.get_logger().info('I heard: "%d"' % msg.num) # 定义回调函数,当接收到消息时打印消息内容。
# 定义main函数作为程序入口点。
def main(args=None):
rclpy.init(args=args) # 初始化ROS2。
minimal_subscriber = MinimalSubscriber() # 创建MinimalSubscriber对象。
rclpy.spin(minimal_subscriber) # 进入循环,等待并处理回调函数。
minimal_subscriber.destroy_node() # 销毁节点。
rclpy.shutdown() # 关闭ROS2。
# 当该脚本被直接运行时,调用main函数。
if __name__ == '__main__':
main()
CMakeLists.txt 添加以下行(仅限 C++):
#...
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tutorial_interfaces REQUIRED) # CHANGE
add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp tutorial_interfaces) # CHANGE
add_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp tutorial_interfaces) # CHANGE
install(TARGETS
talker
listener
DESTINATION lib/${PROJECT_NAME})
ament_package()
package.xml 添加以下行:
C++
<depend>tutorial_interfaces</depend>
Python
<exec_depend>tutorial_interfaces</exec_depend>
在进行上述编辑并保存所有更改后,构建包:
C++
colcon build --packages-select cpp_pubsub
Python
colcon build --packages-select py_pubsub
然后打开两个新的终端,在每个终端中运行 source ros2_ws
,并执行:
C++
ros2 run cpp_pubsub talker
ros2 run cpp_pubsub listener
Python
ros2 run py_pubsub talker
ros2 run py_pubsub listener
由于 Num.msg
仅传递一个整数,因此发布者应该只发布整数值,而不是它之前发布的字符串:
[INFO] [minimal_publisher]: Publishing: '0'
[INFO] [minimal_publisher]: Publishing: '1'
[INFO] [minimal_publisher]: Publishing: '2'
7.2 测试 AddThreeInts.srv
与服务/客户端
通过对之前教程中创建的服务/客户端包(C++或 Python)进行一些修改,您可以看到 AddThreeInts.srv
的实际运行效果。由于您将原始的两个整数请求 srv 更改为三个整数请求 srv,所以输出会有些许不同。
服务端
C++
#include "rclcpp/rclcpp.hpp" // 包含ROS2 C++客户端库的头文件。
#include "tutorial_interfaces/srv/add_three_ints.hpp" // 包含自定义服务AddThreeInts的头文件。
#include <memory> // 包含内存管理的头文件。
// 定义一个处理服务请求的函数。
void add(const std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Request> request,
std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Response> response)
{
response->sum = request->a + request->b + request->c; // 计算请求中三个整数的和,并将结果赋值给响应对象的sum属性。
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld b: %ld c: %ld",
request->a, request->b, request->c); // 使用ROS2日志系统打印请求信息。
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum); // 打印响应信息。
}
int main(int argc, char **argv)
{
rclcpp::init(argc, argv); // 初始化ROS2。
// 创建一个Node对象。
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_server");
// 创建一个服务,并指定服务类型、服务名称和处理函数。
rclcpp::Service<tutorial_interfaces::srv::AddThreeInts>::SharedPtr service =
node->create_service<tutorial_interfaces::srv::AddThreeInts>("add_three_ints", &add);
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add three ints."); // 打印准备就绪的信息。
rclcpp::spin(node); // 进入循环,等待并处理回调函数。
rclcpp::shutdown(); // 关闭ROS2。
}
Python:
from tutorial_interfaces.srv import AddThreeInts # 从tutorial_interfaces服务中导入AddThreeInts服务。
import rclpy # 导入rclpy模块,它是ROS2客户端库的Python接口。
from rclpy.node import Node # 从rclpy.node模块导入Node类。
class MinimalService(Node): # 定义一个名为MinimalService的类,它继承自Node类。
def __init__(self): # 类的初始化函数。
super().__init__('minimal_service') # 调用父类的初始化函数,并设置节点名称为'minimal_service'。
self.srv = self.create_service(AddThreeInts, 'add_three_ints', self.add_three_ints_callback) # 创建一个服务,服务类型为AddThreeInts,服务名称为'add_three_ints',并指定回调函数。
def add_three_ints_callback(self, request, response): # 定义服务的回调函数。
response.sum = request.a + request.b + request.c # 计算请求中三个整数的和,并将结果赋值给响应的sum字段。
self.get_logger().info('Incoming request\na: %d b: %d c: %d' % (request.a, request.b, request.c)) # 打印请求信息。
return response # 返回响应。
def main(args=None): # 定义main函数。
rclpy.init(args=args) # 初始化ROS2客户端库。
minimal_service = MinimalService() # 创建MinimalService类的实例。
rclpy.spin(minimal_service) # 将控制权交给ROS2,开始处理事件(例如服务请求)。
rclpy.shutdown() # 关闭ROS2客户端库。
if __name__ == '__main__': # 如果该文件作为主程序运行。
main() # 调用main函数。
客户端
C++
#include "rclcpp/rclcpp.hpp" // 包含RCLCPP库的头文件,它是ROS2客户端库的一部分。
#include "tutorial_interfaces/srv/add_three_ints.hpp" // 包含自定义服务AddThreeInts的头文件。
#include <chrono> // 包含C++标准库中的chrono库,用于处理时间。
#include <cstdlib> // 包含C++标准库中的cstdlib库,提供通用工具函数。
#include <memory> // 包含C++标准库中的memory库,提供智能指针等内存管理功能。
using namespace std::chrono_literals; // 使用std::chrono_literals命名空间,允许我们使用时间字面量(例如1s)。
int main(int argc, char **argv) // 主函数,程序入口点。
{
rclcpp::init(argc, argv); // 初始化ROS2客户端库。
if (argc != 4) { // 如果命令行参数不等于4(程序名+三个整数)。
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_three_ints_client X Y Z"); // 打印使用方法。
return 1; // 返回1,表示错误。
}
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_client"); // 创建一个名为"add_three_ints_client"的ROS2节点。
rclcpp::Client<tutorial_interfaces::srv::AddThreeInts>::SharedPtr client = // 创建一个服务客户端,用于调用AddThreeInts服务。
node->create_client<tutorial_interfaces::srv::AddThreeInts>("add_three_ints");
auto request = std::make_shared<tutorial_interfaces::srv::AddThreeInts::Request>(); // 创建一个AddThreeInts服务请求。
request->a = atoll(argv[1]); // 将第一个命令行参数转换为长整型并赋值给请求的a成员。
request->b = atoll(argv[2]); // 将第二个命令行参数转换为长整型并赋值给请求的b成员。
request->c = atoll(argv[3]); // 将第三个命令行参数转换为长整型并赋值给请求的c成员。
while (!client->wait_for_service(1s)) { // 循环等待服务变得可用。
if (!rclcpp::ok()) { // 如果ROS2客户端库不再运行(例如,如果有人按下Ctrl+C)。
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting."); // 打印错误信息并退出。
return 0; // 返回0,表示正常退出。
}
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again..."); // 打印信息,服务不可用,再次等待。
}
auto result = client->async_send_request(request); // 异步发送服务请求,并返回一个包含结果的future对象。
// 等待结果。
if (rclcpp::spin_until_future_complete(node, result) ==
rclcpp::FutureReturnCode::SUCCESS)
{
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum); // 如果调用成功,打印出结果之和。
} else {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_three_ints"); // 如果调用失败,打印错误信息。
}
rclcpp::shutdown(); // 关闭ROS2客户端库。
return 0; // 返回0,表示程序正常退出。
}
Python
from tutorial_interfaces.srv import AddThreeInts # 导入服务AddThreeInts
import sys
import rclpy
from rclpy.node import Node
class MinimalClientAsync(Node):
def __init__(self):
super().__init__('minimal_client_async') # 初始化节点名为'minimal_client_async'
self.cli = self.create_client(AddThreeInts, 'add_three_ints') # 创建客户端,服务类型为AddThreeInts,服务名称为'add_three_ints'
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('service not available, waiting again...') # 如果服务不可用,则等待
self.req = AddThreeInts.Request() # 创建一个请求
def send_request(self):
self.req.a = int(sys.argv[1]) # 设置请求的第一个整数参数
self.req.b = int(sys.argv[2]) # 设置请求的第二个整数参数
self.req.c = int(sys.argv[3]) # 设置请求的第三个整数参数
self.future = self.cli.call_async(self.req) # 异步调用服务
def main(args=None):
rclpy.init(args=args)
minimal_client = MinimalClientAsync() # 创建MinimalClientAsync类的实例
minimal_client.send_request() # 发送请求
while rclpy.ok():
rclpy.spin_once(minimal_client) # 单次旋转节点,等待响应
if minimal_client.future.done():
try:
response = minimal_client.future.result() # 获取响应结果
except Exception as e:
minimal_client.get_logger().info(
'Service call failed %r' % (e,)) # 如果调用服务失败,则打印错误信息
else:
minimal_client.get_logger().info(
'Result of add_three_ints: for %d + %d + %d = %d' %
(minimal_client.req.a, minimal_client.req.b, minimal_client.req.c, response.sum)) # 打印服务调用结果
break
minimal_client.destroy_node() # 销毁节点
rclpy.shutdown() # 关闭rclpy
if __name__ == '__main__':
main()
CMakeLists.txt
添加以下行(仅限 C++):
#...
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tutorial_interfaces REQUIRED) # CHANGE
add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server
rclcpp tutorial_interfaces) # CHANGE
add_executable(client src/add_two_ints_client.cpp)
ament_target_dependencies(client
rclcpp tutorial_interfaces) # CHANGE
install(TARGETS
server
client
DESTINATION lib/${PROJECT_NAME})
ament_package()
package.xml 添加以下行:
C++
<depend>tutorial_interfaces</depend>
Python
<exec_depend>tutorial_interfaces</exec_depend>
在进行上述编辑并保存所有更改后,构建包:
C++
colcon build --packages-select cpp_srvcli
Python
colcon build --packages-select py_srvcli
然后打开两个新的终端,在每个终端中运行 source ros2_ws
,并执行:
C++
ros2 run cpp_srvcli server
ros2 run cpp_srvcli client 2 3 1
Python
ros2 run py_srvcli service
ros2 run py_srvcli client 2 3 1
摘要
在本教程中,您学习了如何在自己的包中创建自定义接口,以及如何在其他包中使用这些接口。
这个教程只是简单介绍了如何定义自定义接口。您可以在关于 ROS 2 接口中了解更多信息。
下一步
下一教程将介绍更多在 ROS 2 中使用接口的方法。https://docs.ros.org/en/jazzy/Concepts/Basic/About-Interfaces.html