티스토리 친구하기

본문 바로가기

Robotics/ROS2

Writing a simple service and client (Python)

728x90

Background

노드들이 Service를 이용해 통신할 때, 데이터를 요청하는 노드를 클라이언트 노드(Client node)라고 하고, 요청에 응답하는 노드를 서비스 노드(Service node)라고 합니다. 요청과 응답의 구조는 .srv 파일에 의해 결정됩니다.

여기 사용된 예는 간단한 정수 덧셈 시스템입니다. 한 노드가 두 정수의 합을 요청하면 다른 노드는 결과를 응답합니다.

Prerequisites

이전 튜토리얼에서는 workspace를 생성하고 pakage를 생성하는 방법을 학습했습니다.

Task

1 Create a pakage

새 터미널을 열고 ROS 2 installation을  source로 설정하여 ros2 명령이 작동하도록 만듭니다.

이전 튜토리얼에서 생성한 ros2_ws 디렉터리로 이동합니다.

패키지는 workspace 루트가 아닌 src 디렉터리에 생성해야 합니다. ros2_ws/src로 이동하고 다음 명령을 사용하여 새 패키지를 생성합니다:

 

ros2 pkg create --build-type ament_python --license Apache-2.0 py_srvcli --dependencies rclpy example_interfaces

 

터미널은 py_srvcli 패키지와 필요한 모든 파일과 폴더가 생성되었음을 확인하는 메시지를 return 합니다.

--dependencies argument는 package.xml 에 필요한 dependency line을 자동으로 추가합니다. example_interfaces는 request(요청)와 response(응답)를 구성하는 데 필요한 '. srv' 파일을 포함한 패키지입니다:

 

int64 a
int64 b
---
int64 sum

 

첫 두 줄은 request의 매개변수이고, 하이픈 아래에는 response입니다.

 

1.1 Update package.xml

package를 만드는 동안 `--dependencies` 옵션을 사용했기 때문에, 수동으로 'package.xml' 를 add 할 필요가 없습니다.

그러나 description, maintainer email과 name, 그리고 license 정보를 'package.xml'에 add 하는 것을 잊지 마시고, 아래와 같이 추가하세요.

 

<description>Python client server tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>

1.2 Update setup.py

setup.py 파일에 maintainer, maintainer_email, description, 그리고 license 필드를 아래와 같이 Add 하세요.

 

maintainer='Your Name',
maintainer_email='you@email.com',
description='Python client server tutorial',
license='Apache License 2.0',

2 Write the service node

'ros2_ws/src/py_srvcli/py_srvcli' 디렉토리에 'service_member_function.py' 라는 새로운 파일을 만들고, 아래의 코드를 붙여넣으세요.

 

from example_interfaces.srv import AddTwoInts

import rclpy
from rclpy.node import Node


class MinimalService(Node):

    def __init__(self):
        super().__init__('minimal_service')
        self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)

    def add_two_ints_callback(self, request, response):
        response.sum = request.a + request.b
        self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b))

        return response


def main():
    rclpy.init()

    minimal_service = MinimalService()

    rclpy.spin(minimal_service)

    rclpy.shutdown()


if __name__ == '__main__':
    main()

2.1 Examine the code

먼저 import 명령문은 AddTwoInts 서비스 타입을 example_interface 패키지에서 가져옵니다. 다음 import 명령문은 ROS2 Python clinet library를 import 하고, rclpy.node 에서 Node 클래스를 import 합니다.

 

from example_interfaces.srv import AddTwoInts

import rclpy
from rclpy.node import Node

 

MinimalService 클래스 생성자는 minimal_service 라는 이름을 가진 노드를 초기화 합니다. 그리고 service를 만들고, type, name, 그리고 callback 함수를 정의합니다.

 

def __init__(self):
    super().__init__('minimal_service')
    self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)

 

service callback 함수의 definition은 request data를 받아서 더하고, 더한 값은 response로 return 합니다.

 

def add_two_ints_callback(self, request, response):
    response.sum = request.a + request.b
    self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b))

    return response

 

 

마지막으로, main 클래스는 ROS2 Python client library를 초기화하고, MinimalService 클래스를 객체화(instantiate)해서 service node를 생성하고, callback을 다루기 위해 노드를 spin 합니다.

2.2 Add an entry point

ros2 run 명령어가 당신이 만든 노드를 실행하도록 허락하기 위해서는, 반드시 setup.py (위치: ros2_ws/src/py_srvcli 디렉토리)파일에 entry point를 더해야 합니다.

아래의 line을 'consol_scripts' 괄호 사이에 더하세요.

 

'service = py_srvcli.service_member_function:main',

3 Write the client node

ros2_ws/src/py_srvcli/py_srvcli 디렉토리 안에, client_member_function.py 라는 새로운 파일을 만들고 아래의 코드를 파일안에 복사하세요.

 

import sys

from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.node import Node


class MinimalClientAsync(Node):

    def __init__(self):
        super().__init__('minimal_client_async')
        self.cli = self.create_client(AddTwoInts, 'add_two_ints')
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        self.req = AddTwoInts.Request()

    def send_request(self, a, b):
        self.req.a = a
        self.req.b = b
        self.future = self.cli.call_async(self.req)
        rclpy.spin_until_future_complete(self, self.future)
        return self.future.result()


def main():
    rclpy.init()

    minimal_client = MinimalClientAsync()
    response = minimal_client.send_request(int(sys.argv[1]), int(sys.argv[2]))
    minimal_client.get_logger().info(
        'Result of add_two_ints: for %d + %d = %d' %
        (int(sys.argv[1]), int(sys.argv[2]), response.sum))

    minimal_client.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

3.1 Examine the code

service 코드에서 처럼, 먼저 필수적인 라이브러리를 import 합니다.

 

import sys

from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.node import Node

 

MinimalClientAsync 클래스의 생성자는 minimal_client_async 라는 이름을 가진 노드를 초기화합니다. 생성자 정의는 service node와 같은 타입과 이름으로 client를 생성합니다. Service와 Client의 타입과 이름은 통신(Communication)을 하기 위해서 반드시 일치해야 합니다. 생성자 내의 while 루프는 클라이언트의 타입과 이름에 맞는 서비스가 사용 가능한지 1초에 한 번씩 확인합니다. 마지막으로 새로운 AddTwoInts 요청 객체를 생성합니다.

 

def __init__(self):
    super().__init__('minimal_client_async')
    self.cli = self.create_client(AddTwoInts, 'add_two_ints')
    while not self.cli.wait_for_service(timeout_sec=1.0):
        self.get_logger().info('service not available, waiting again...')
    self.req = AddTwoInts.Request()

 

아래 생성자는 send_request 메서드입니다. 이 메서드는 request를 보내고 response 또는 fails를 받을 때까지 spin 합니다.

 

def send_request(self, a, b):
    self.req.a = a
    self.req.b = b
    self.future = self.cli.call_async(self.req)
    rclpy.spin_until_future_complete(self, self.future)
    return self.future.result()

 

마지막으로, 메인 메서드는 MinimalClientAsync 객체를 생성하고, 전달된 명령줄 인수를 사용하여 요청을 보내고, 결과를 기록합니다.

 

def main():
    rclpy.init()

    minimal_client = MinimalClientAsync()
    response = minimal_client.send_request(int(sys.argv[1]), int(sys.argv[2]))
    minimal_client.get_logger().info(
        'Result of add_two_ints: for %d + %d = %d' %
        (int(sys.argv[1]), int(sys.argv[2]), response.sum))

    minimal_client.destroy_node()
    rclpy.shutdown()

3.2 Add an entry point

서비스 노드와 마찬가지로 클라이언트 노드를 실행할 수 있도록 진입점을 추가해야 합니다. setup.py 파일의 entry_points 필드는 다음과 같이 생겨야 합니다:

 

entry_points={
    'console_scripts': [
        'service = py_srvcli.service_member_function:main',
        'client = py_srvcli.client_member_function:main',
    ],
},

4 Build and run

빌드하기 전에 누락된 의존성을 확인하기 위해 작업공간의 루트(ros2_ws)에서 rosdep을 실행하는 습관을 가지는 것이 좋습니다.

 

rosdep install -i --from-path src --rosdistro humble -y

 

작업공간의 루트인 ros2_ws로 다시 이동하여 새 패키지를 빌드합니다:

 

colcon build --packages-select py_srvcli

 

 

새 터미널을 열고 ros2_ws로 이동하여 setup 파일을 소스합니다:

 

source install/setup.bash

 

이제 서비스 노드를 실행합니다

 

ros2 run py_srvcli service


노드는 클라이언트의 요청을 기다릴 것입니다. 다른 터미널을 열고 다시 ros2_ws 안에서 setup 파일을 소스합니다. 클라이언트 노드를 시작하고, 공백으로 구분된 두 정수를 입력합니다:

ros2 run py_srvcli client 2 3

 

예를 들어 2와 3을 선택하면 클라이언트는 다음과 같은 응답을 받게 됩니다:

[INFO] [minimal_client_async]: Result of add_two_ints: for 2 + 3 = 5

 

서비스 노드가 실행 중인 터미널로 돌아가 요청을 받았을 때 로그 메시지를 게시한 것을 확인합니다:

 

[INFO] [minimal_service]: Incoming request
a: 2 b: 3

 

서버 터미널에서 Ctrl+C를 눌러 노드의 실행을 중지합니다.

Summary

이번 시간에는 서비스를 통해 데이터를 요청하고 응답하는 두 개의 노드를 생성했습니다. 이러한 노드의 의존성과 실행 파일을 패키지 구성 파일에 추가하여 빌드하고 실행할 수 있게 하였고, 이를 통해 서비스/클라이언트 시스템이 작동하는 모습을 확인할 수 있었습니다.

Next steps

지난 몇 가지 튜토리얼에서는 인터페이스를 사용하여 토픽과 서비스를 통해 데이터를 전달했습니다. 다음으로는 사용자 정의 인터페이스를 만드는 방법을 배울 것입니다.

Related content

서비스와 클라이언트를 Python으로 작성하는 방법은 여러 가지가 있습니다. ros2/examples 리포지토의 minimal_client 및 minimal_service 패키지를 확인해 보세요.
이 튜토리얼에서는 클라이언트 노드에서 call_async() API를 사용하여 서비스를 호출했습니다. Python에서는 동기 호출이라는 또 다른 서비스 호출 API가 있습니다. 동기 호출 사용은 권장하지 않지만, 더 알고 싶다면 동기 클라이언트와 비동기 클라이언트에 대한 가이드를 읽어보세요.

반응형