Skip to content

Latest commit

 

History

History
829 lines (515 loc) · 23.4 KB

Custom-ROS2-Interfaces.rst

File metadata and controls

829 lines (515 loc) · 23.4 KB

Tutorials/Custom-ROS2-Interfaces.rst

Creating custom msg and srv files

Goal: Define custom interface files (.msg and .srv) and use them with Python and C++ nodes.

Tutorial level: Beginner

Time: 20 minutes

Contents

Background

In previous tutorials you utilized message and service interfaces to learn about topics <../Beginner-CLI-Tools/Understanding-ROS2-Topics/Understanding-ROS2-Topics>, services <../Beginner-CLI-Tools/Understanding-ROS2-Services/Understanding-ROS2-Services>, and simple publisher/subscriber (C++ <./Writing-A-Simple-Cpp-Publisher-And-Subscriber>/Python<./Writing-A-Simple-Py-Publisher-And-Subscriber>) and service/client (C++ <./Writing-A-Simple-Cpp-Service-And-Client>/Python<./Writing-A-Simple-Py-Service-And-Client>) nodes. The interfaces you used were predefined in those cases.

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 <./Creating-A-Workspace/Creating-A-Workspace>.

This tutorial also uses the packages created in the publisher/subscriber (C++ <./Writing-A-Simple-Cpp-Publisher-And-Subscriber> and Python<./Writing-A-Simple-Py-Publisher-And-Subscriber>) and service/client (C++ <./Writing-A-Simple-Cpp-Service-And-Client> and Python<./Writing-A-Simple-Py-Service-And-Client>) tutorials to try out the new custom messages.

Tasks

1 Create a new package

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.

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:

ros2 pkg create --build-type ament_cmake tutorial_interfaces

tutorial_interfaces is the name of the new package. Note that it is a CMake package; there currently isn’t a way to generate a .msg or .srv file in a pure Python package. You can create a custom interface in a CMake package, and then use it in a Python node, which will be covered in the last section.

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:

mkdir msg

mkdir srv

2 Create custom definitions

2.1 msg definition

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:

int64 num

This is a custom message that transfers a single 64-bit integer called num.

Also in the tutorial_interfaces/msg directory you just created, make a new file called Sphere.msg with the following content:

geometry_msgs/Point center
float64 radius

This custom message uses a message from another message package (geometry_msgs/Point in this case).

2.2 srv definition

Back in the tutorial_interfaces/srv directory you just created, make a new file called AddThreeInts.srv with the following request and response structure:

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.

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:

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 library name must match ${PROJECT_NAME} (see ros2/rosidl#441 (comment)).

4 package.xml

Because the interfaces rely on rosidl_default_generators for generating language-specific code, you need to declare a dependency on it. The <exec_depend> tag is used to specify runtime or execution-stage dependencies and the rosidl_interface_packages is the name of the dependency group to which the package belongs, declared using the <member_of_group> tag.

Add the following lines to package.xml

<depend>geometry_msgs</depend>

<build_depend>rosidl_default_generators</build_depend>

<exec_depend>rosidl_default_runtime</exec_depend>

<member_of_group>rosidl_interface_packages</member_of_group>

5 Build the tutorial_interfaces package

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:

Linux

colcon build --packages-select tutorial_interfaces

macOS

colcon build --packages-select tutorial_interfaces

Windows

colcon build --merge-install --packages-select tutorial_interfaces

Now the interfaces will be discoverable by other ROS 2 packages.

6 Confirm msg and srv creation

In a new terminal, run the following command from within your workspace (ros2_ws) to source it:

Linux

. install/setup.bash

macOS

. install/setup.bash

Windows

call install/setup.bat

Now you can confirm that your interface creation worked by using the ros2 interface show command:

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

For this step you can use the packages you created in previous tutorials. A few simple modifications to the nodes, CMakeLists and package files will allow you to use your new interfaces.

7.1 Testing Num.msg with pub/sub

With some slight modifications to the publisher/subscriber package created in a previous tutorial (C++ <./Writing-A-Simple-Cpp-Publisher-And-Subscriber> or Python <./Writing-A-Simple-Py-Publisher-And-Subscriber>), 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.

Publisher:

C++

Python

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:

C++

Python

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):

#...

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:

Python

<exec_depend>tutorial_interfaces</exec_depend>

After making the above edits and saving all the changes, build the package:

C++

colcon build --packages-select cpp_pubsub

On Windows:

colcon build --merge-install --packages-select cpp_pubsub

Python

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:

C++

ros2 run cpp_pubsub talker
ros2 run cpp_pubsub listener

Python

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:

[INFO] [minimal_publisher]: Publishing: '0'
[INFO] [minimal_publisher]: Publishing: '1'
[INFO] [minimal_publisher]: Publishing: '2'

7.2 Testing AddThreeInts.srv with service/client

With some slight modifications to the service/client package created in a previous tutorial (C++ <./Writing-A-Simple-Cpp-Service-And-Client> or Python <./Writing-A-Simple-Py-Service-And-Client>), 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.

Service:

C++

Python

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:

C++

Python

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):

#...

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:

Python

<exec_depend>tutorial_interfaces</exec_depend>

After making the above edits and saving all the changes, build the package:

C++

colcon build --packages-select cpp_srvcli

On Windows:

colcon build --merge-install --packages-select cpp_srvcli

Python

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:

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

Summary

In this tutorial, you learned how to create custom interfaces in their own package and how to utilize those interfaces from within other packages.

This is a simple method of interface creation and utilization. You can learn more about interfaces here <../../Concepts/About-ROS-Interfaces>.

Next steps

The next tutorial <./Single-Package-Define-And-Use-Interface> covers more ways to use interfaces in ROS 2.