Creating custom msg and srv files
创建自定义 msg 和 srv 文件
Goal: Define custom interface files (.msg
and .srv
) and use them with Python and C++ nodes.
目标: 定义自定义接口文件 (.msg
和 .srv
) 并在 Python 和 C++ 节点中使用它们。
Tutorial level: Beginner
教程级别: 初学者
Time: 20 minutes
时间: 20 分钟
Background
背景
In previous tutorials you utilized message and service interfaces to learn about topics, services, and simple publisher/subscriber (C++/Python) and service/client (C++/Python) nodes.
The interfaces you used were predefined in those cases.
在之前的教程中,您利用消息和服务接口学习了主题、服务以及简单的发布/订阅(C++/Python)和服务/客户端(C++/Python)节点。您使用的接口在这些情况下是预定义的。
While it’s good practice to use predefined interface definitions, you will probably need to define your own messages and services sometimes as well.
This tutorial will introduce you to the simplest method of creating custom interface definitions.
虽然使用预定义的接口定义是一个好习惯,但有时您可能需要自己定义消息和服务。这个教程将向您介绍创建自定义接口定义的最简单方法。
Prerequisites
先决条件
You should have a ROS 2 workspace.
您应该有一个 ROS 2 工作区。
This tutorial also uses the packages created in the publisher/subscriber (C++ and Python) and service/client (C++ and Python) tutorials to try out the new custom messages.
本教程还使用了在发布者/订阅者(C++ 和 Python)以及服务/客户端(C++ 和 Python)教程中创建的包来尝试新的自定义消息。
Tasks
1 Create a new package
1 创建一个新包
For this tutorial you will be creating custom .msg
and .srv
files in their own package, and then utilizing them in a separate package.
Both packages should be in the same workspace.
在本教程中,您将创建自定义 .msg
和 .srv
文件,并将它们放在自己的包中,然后在一个单独的包中使用它们。两个包应位于同一个工作区。
Since we will use the pub/sub and service/client packages created in earlier tutorials, make sure you are in the same workspace as those packages (ros2_ws/src
), and then run the following command to create a new package:
由于我们将使用在之前教程中创建的 pub/sub 和 service/client 包,请确保您在与这些包相同的工作区中(ros2_ws/src
),然后运行以下命令以创建新包:
ros2 pkg create --build-type ament_cmake --license Apache-2.0 tutorial_interfaces
tutorial_interfaces
is the name of the new package.
Note that it is, and can only be, a CMake package, but this doesn’t restrict in which type of packages you can use your messages and services.
You can create your own custom interfaces in a CMake package, and then use it in a C++ or Python node, which will be covered in the last section.tutorial_interfaces
是新包的名称。请注意,它是且只能是一个 CMake 包,但这并不限制您可以在何种类型的包中使用您的消息和服务。您可以在 CMake 包中创建自己的自定义接口,然后在 C++ 或 Python 节点中使用它,这将在最后一节中介绍。
The .msg
and .srv
files are required to be placed in directories called msg
and srv
respectively.
Create the directories in ros2_ws/src/tutorial_interfaces
:
将 .msg
和 .srv
文件放置在名为 msg
和 srv
的目录中是必需的。请在 ros2_ws/src/tutorial_interfaces
中创建这些目录:
mkdir msg srv
2 Create custom definitions
2 创建自定义定义
2.1 msg definition
2.1 消息定义
In the tutorial_interfaces/msg
directory you just created, make a new file called Num.msg
with one line of code declaring its data structure:
在您刚刚创建的 tutorial_interfaces/msg
目录中,创建一个名为 Num.msg
的新文件,文件中包含一行代码声明其数据结构:
int64 num
This is a custom message that transfers a single 64-bit integer called num
.
这是一个自定义消息,传输一个名为num
的 64 位整数。
Also in the tutorial_interfaces/msg
directory you just created, make a new file called Sphere.msg
with the following content:
在您刚刚创建的 tutorial_interfaces/msg
目录中,创建一个名为 Sphere.msg
的新文件,内容如下:
geometry_msgs/Point center
float64 radius
This custom message uses a message from another message package (geometry_msgs/Point
in this case).
此自定义消息使用来自另一个消息包的消息(在这种情况下是geometry_msgs/Point
)。
2.2 srv definition
2.2 srv 定义
Back in the tutorial_interfaces/srv
directory you just created, make a new file called AddThreeInts.srv
with the following request and response structure:
在您刚刚创建的 tutorial_interfaces/srv
目录中,创建一个名为 AddThreeInts.srv
的新文件,文件内容包含以下请求和响应结构:
int64 a
int64 b
int64 c
---
int64 sum
This is your custom service that requests three integers named a
, b
, and c
, and responds with an integer called sum
.
这是您的自定义服务,它请求三个整数,分别名为 a
、b
和 c
,并返回一个名为 sum
的整数。
3 CMakeLists.txt
To convert the interfaces you defined into language-specific code (like C++ and Python) so that they can be used in those languages, add the following lines to 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
)
Note 注意
The first argument (library name) in the rosidl_generate_interfaces must match ${PROJECT_NAME} (see https://github.com/ros2/rosidl/issues/441#issuecomment-591025515).
rosidl_generate_interfaces 中的第一个参数(库名称)必须与${PROJECT_NAME}匹配(请参见https://github.com/ros2/rosidl/issues/441#issuecomment-591025515)。
4 package.xml
Because the interfaces rely on rosidl_default_generators
for generating language-specific code, you need to declare a build tool dependency on it.
rosidl_default_runtime
is a runtime or execution-stage dependency, needed to be able to use the interfaces later.
The rosidl_interface_packages
is the name of the dependency group that your package, tutorial_interfaces
, should be associated with, declared using the <member_of_group>
tag.
因为接口依赖于 rosidl_default_generators
来生成特定语言的代码,所以您需要声明对它的构建工具依赖。 rosidl_default_runtime
是一个运行时或执行阶段的依赖,后续使用接口时需要它。 rosidl_interface_packages
是您的包 tutorial_interfaces
应该关联的依赖组的名称,通过 <member_of_group>
标签声明。
Add the following lines within the <package>
element of package.xml
:
在<package>
元素中添加以下行,位于package.xml
:
<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>
5 Build the tutorial_interfaces
package
5 构建 tutorial_interfaces
包
Now that all the parts of your custom interfaces package are in place, you can build the package.
In the root of your workspace (~/ros2_ws
), run the following command:
现在您的自定义接口包的所有部分都已就位,您可以构建该包。在您的工作区根目录(~/ros2_ws
)中,运行以下命令:
colcon build --packages-select tutorial_interfaces
colcon build --packages-select tutorial_interfaces
colcon build --merge-install --packages-select tutorial_interfaces
Now the interfaces will be discoverable by other ROS 2 packages.
现在接口将可以被其他 ROS 2 包发现。
6 Confirm msg and srv creation
6 确认消息和服务创建
In a new terminal, run the following command from within your workspace (ros2_ws
) to source it:
在新的终端中,从您的工作区(ros2_ws
)运行以下命令以进行源设置:
source install/setup.bash
. install/setup.bash
call install/setup.bat
Now you can confirm that your interface creation worked by using the ros2 interface show
command:
现在您可以通过使用 ros2 interface show
命令来确认您的接口创建是否成功:
ros2 interface show tutorial_interfaces/msg/Num
should return: 应该返回:
int64 num
And 和
ros2 interface show tutorial_interfaces/msg/Sphere
should return: 应该返回:
geometry_msgs/Point center
float64 x
float64 y
float64 z
float64 radius
And 和
ros2 interface show tutorial_interfaces/srv/AddThreeInts
should return: 应该返回:
int64 a
int64 b
int64 c
---
int64 sum
7 Test the new interfaces
7 测试新接口
For this step you can use the packages you created in previous tutorials.
A few simple modifications to the nodes, CMakeLists.txt
and package.xml
files will allow you to use your new interfaces.
在此步骤中,您可以使用之前教程中创建的包。对节点 CMakeLists.txt
和 package.xml
文件进行一些简单的修改,将允许您使用新的接口。
7.1 Testing Num.msg
with pub/sub
7.1 测试 Num.msg
与 pub/sub
With a few modifications to the publisher/subscriber package created in a previous tutorial (C++ or Python), you can see Num.msg
in action.
Since you’ll be changing the standard string msg to a numerical one, the output will be slightly different.
通过对之前教程中创建的发布/订阅包进行一些修改(C++ 或 Python),您可以看到 Num.msg
的实际效果。由于您将把标准字符串 msg 更改为数字,因此输出会略有不同。
Publisher 出版商
#include <chrono>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp" // CHANGE
using namespace std::chrono_literals;
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<tutorial_interfaces::msg::Num>("topic", 10); // CHANGE
auto timer_callback = [this](){
auto message = tutorial_interfaces::msg::Num(); // CHANGE
message.num = this->count_++; // CHANGE
RCLCPP_INFO_STREAM(this->get_logger(), "Publishing: '" << message.num << "'"); // CHANGE
publisher_->publish(message);
};
timer_ = this->create_wall_timer(500ms, timer_callback);
}
private:
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<tutorial_interfaces::msg::Num>::SharedPtr publisher_; // CHANGE
size_t count_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
import rclpy
from rclpy.node import Node
from tutorial_interfaces.msg import Num # CHANGE
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher')
self.publisher_ = self.create_publisher(Num, 'topic', 10) # CHANGE
timer_period = 0.5
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
def timer_callback(self):
msg = Num() # CHANGE
msg.num = self.i # CHANGE
self.publisher_.publish(msg)
self.get_logger().info('Publishing: "%d"' % msg.num) # CHANGE
self.i += 1
def main(args=None):
rclpy.init(args=args)
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
minimal_publisher.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Subscriber 订阅者
#include <functional>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp" // CHANGE
using std::placeholders::_1;
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
auto topic_callback = [this](const tutorial_interfaces::msg::Num & msg){ // CHANGE
RCLCPP_INFO_STREAM(this->get_logger(), "I heard: '" << msg.num << "'"); // CHANGE
};
subscription_ = this->create_subscription<tutorial_interfaces::msg::Num>( // CHANGE
"topic", 10, topic_callback);
}
private:
rclcpp::Subscription<tutorial_interfaces::msg::Num>::SharedPtr subscription_; // CHANGE
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
import rclpy
from rclpy.node import Node
from tutorial_interfaces.msg import Num # CHANGE
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber')
self.subscription = self.create_subscription(
Num, # CHANGE
'topic',
self.listener_callback,
10)
self.subscription
def listener_callback(self, msg):
self.get_logger().info('I heard: "%d"' % msg.num) # CHANGE
def main(args=None):
rclpy.init(args=args)
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
minimal_subscriber.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
CMakeLists.txt
Add the following lines (C++ only):
添加以下行(仅限 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
Add the following line: 添加以下行:
<depend>tutorial_interfaces</depend>
<exec_depend>tutorial_interfaces</exec_depend>
After making the above edits and saving all the changes, build the package:
在进行上述编辑并保存所有更改后,构建该包:
On Linux/macOS: 在 Linux/macOS 上:
colcon build --packages-select cpp_pubsub
On Windows: 在 Windows 上:
colcon build --merge-install --packages-select cpp_pubsub
On Linux/macOS:
colcon build --packages-select py_pubsub
On Windows:
colcon build --merge-install --packages-select py_pubsub
Then open two new terminals, source ros2_ws
in each, and run:
然后打开两个新的终端,在每个终端中源 ros2_ws
,并运行:
ros2 run cpp_pubsub talker
ros2 run cpp_pubsub listener
ros2 run py_pubsub talker
ros2 run py_pubsub listener
Since Num.msg
relays only an integer, the talker should only be publishing integer values, as opposed to the string it published previously:
由于Num.msg
仅传递一个整数,讲话者应该只发布整数值,而不是之前发布的字符串:
[INFO] [minimal_publisher]: Publishing: '0'
[INFO] [minimal_publisher]: Publishing: '1'
[INFO] [minimal_publisher]: Publishing: '2'
7.2 Testing AddThreeInts.srv
with service/client
7.2 测试 AddThreeInts.srv
与服务/客户端
With a few modifications to the service/client package created in a previous tutorial (C++ or Python), you can see AddThreeInts.srv
in action.
Since you’ll be changing the original two integer request srv to a three integer request srv, the output will be slightly different.
通过对之前教程中创建的服务/客户端包进行一些修改(C++ 或 Python),您可以看到 AddThreeInts.srv
的实际应用。由于您将把原始的两个整数请求 srv 更改为三个整数请求 srv,因此输出会略有不同。
Service 服务
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_ints.hpp" // CHANGE
#include <memory>
void add(const std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Request> request, // CHANGE
std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Response> response) // CHANGE
{
response->sum = request->a + request->b + request->c; // CHANGE
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld" " c: %ld", // CHANGE
request->a, request->b, request->c); // CHANGE
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_server"); // CHANGE
rclcpp::Service<tutorial_interfaces::srv::AddThreeInts>::SharedPtr service = // CHANGE
node->create_service<tutorial_interfaces::srv::AddThreeInts>("add_three_ints", &add); // CHANGE
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add three ints."); // CHANGE
rclcpp::spin(node);
rclcpp::shutdown();
}
from tutorial_interfaces.srv import AddThreeInts # CHANGE
import rclpy
from rclpy.node import Node
class MinimalService(Node):
def __init__(self):
super().__init__('minimal_service')
self.srv = self.create_service(AddThreeInts, 'add_three_ints', self.add_three_ints_callback) # CHANGE
def add_three_ints_callback(self, request, response):
response.sum = request.a + request.b + request.c # CHANGE
self.get_logger().info('Incoming request\na: %d b: %d c: %d' % (request.a, request.b, request.c)) # CHANGE
return response
def main(args=None):
rclpy.init(args=args)
minimal_service = MinimalService()
rclpy.spin(minimal_service)
rclpy.shutdown()
if __name__ == '__main__':
main()
Client 客户
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_ints.hpp" // CHANGE
#include <chrono>
#include <cstdlib>
#include <memory>
using namespace std::chrono_literals;
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
if (argc != 4) { // CHANGE
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_three_ints_client X Y Z"); // CHANGE
return 1;
}
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_client"); // CHANGE
rclcpp::Client<tutorial_interfaces::srv::AddThreeInts>::SharedPtr client = // CHANGE
node->create_client<tutorial_interfaces::srv::AddThreeInts>("add_three_ints"); // CHANGE
auto request = std::make_shared<tutorial_interfaces::srv::AddThreeInts::Request>(); // CHANGE
request->a = atoll(argv[1]);
request->b = atoll(argv[2]);
request->c = atoll(argv[3]); // CHANGE
while (!client->wait_for_service(1s)) {
if (!rclcpp::ok()) {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
return 0;
}
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
}
auto result = client->async_send_request(request);
// Wait for the result.
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"); // CHANGE
}
rclcpp::shutdown();
return 0;
}
from tutorial_interfaces.srv import AddThreeInts # CHANGE
import sys
import rclpy
from rclpy.node import Node
class MinimalClientAsync(Node):
def __init__(self):
super().__init__('minimal_client_async')
self.cli = self.create_client(AddThreeInts, 'add_three_ints') # CHANGE
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('service not available, waiting again...')
self.req = AddThreeInts.Request() # CHANGE
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]) # CHANGE
self.future = self.cli.call_async(self.req)
def main(args=None):
rclpy.init(args=args)
minimal_client = 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' % # CHANGE
(minimal_client.req.a, minimal_client.req.b, minimal_client.req.c, response.sum)) # CHANGE
break
minimal_client.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
CMakeLists.txt
Add the following lines (C++ only):
添加以下行(仅限 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
Add the following line: 添加以下行:
<depend>tutorial_interfaces</depend>
<exec_depend>tutorial_interfaces</exec_depend>
After making the above edits and saving all the changes, build the package:
在进行上述编辑并保存所有更改后,构建该包:
On Linux/macOS: 在 Linux/macOS 上:
colcon build --packages-select cpp_srvcli
On Windows: 在 Windows 上:
colcon build --merge-install --packages-select cpp_srvcli
On Linux/macOS:
colcon build --packages-select py_srvcli
On Windows:
colcon build --merge-install --packages-select py_srvcli
Then open two new terminals, source ros2_ws
in each, and run:
然后打开两个新的终端,在每个终端中源 ros2_ws
,并运行:
ros2 run cpp_srvcli server
ros2 run cpp_srvcli client 2 3 1
ros2 run py_srvcli service
ros2 run py_srvcli client 2 3 1
Summary
摘要
In this tutorial, you learned how to create custom interfaces in their own package and how to utilize those interfaces in other packages.
在本教程中,您学习了如何在自己的包中创建自定义接口,以及如何在其他包中使用这些接口。
This tutorial only scratches the surface about defining custom interfaces.
You can learn more about it in About ROS 2 interfaces.
本教程仅仅触及了定义自定义接口的表面。您可以在关于 ROS 2 接口中了解更多信息。
Next steps
下一步
The next tutorial covers more ways to use interfaces in ROS 2.
下一个教程将介绍在 ROS 2 中使用接口的更多方法。