目标:定义自定义接口文件( .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.8project(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_msgsif(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 filesset(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 filesset(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 centerfloat64 xfloat64 yfloat64 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) # CHANGEadd_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp tutorial_interfaces) # CHANGEadd_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp tutorial_interfaces) # CHANGEinstall(TARGETStalkerlistenerDESTINATION 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 Nodeclass 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)) # 打印服务调用结果breakminimal_client.destroy_node() # 销毁节点rclpy.shutdown() # 关闭rclpyif __name__ == '__main__':main()
CMakeLists.txt
添加以下行(仅限 C++):
#...find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tutorial_interfaces REQUIRED) # CHANGEadd_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(serverrclcpp tutorial_interfaces) # CHANGEadd_executable(client src/add_two_ints_client.cpp)
ament_target_dependencies(clientrclcpp tutorial_interfaces) # CHANGEinstall(TARGETSserverclientDESTINATION 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