티스토리 친구하기

본문 바로가기

Robotics/ROS2

ROS2 humble Writing a simple publisher and subscriber (Python)

728x90

Background

이 튜토리얼에서는 문자열 메시지에 있는 정보를 topic을 통해 서로 주고받는 node들을 생성하는 방법에 대해 설명합니다. 여기서 사용하는 example은 "talker"와 "listener" 시스템입니다. "talker" 노드는 데이터를 publish하고 "listener" 노드는 topic을 subscribe 해서 데이터를 수신합니다. 

본 튜토리얼은 Python으로 진행됩니다.

prerequisites

이전 튜토리얼에서는, 어떻게 workspace와 pakage를 만드는지 배웠습니다.

Python의 전체적인 이해가 필수적인건 아니지만, 기본적인 이해가 있으면 좋습니다.

Task

1 Create a package

ros2 command가 작동하도록, 터미널을 열고 ROS2 intallation을 source 하세요.

이전 튜토리얼에서 만든 ros2_ws 디렉토리로 이동하세요.

Pakage들은 workspace의 root가 아니라 src 디렉토리 안에 만들어야 한다는 것을 꼭 기억하세요. ros2_ws/src 로 이동하고, pakage를 만들기 위해 아래의 command를 터미널에 입력하세요.

 

ros2 pkg create --build-type ament_python py_pubsub

 

위의 명령어를 입력하면 py_pubsub 패키지 생성, 필요한 파일들과 폴더들을 확인하는 메시지가 return 된다.

2 Write the publisher node

ros2_ws/src/py_pubsub/py_pubsub 디렉토리로 이동하세요. 이 디렉토리는 ROS 2 패키지와 동일한 이름을 가진 Python 패키지입니다.

다음 명령을 입력하여 예제 "talker" 코드를 다운로드하세요:

 

wget https://raw.githubusercontent.com/ros2/examples/humble/rclpy/topics/minimal_publisher/examples_rclpy_minimal_publisher/publisher_member_function.py

 

이제 __init__.py 파일과 인접한 곳에 publisher_member_function.py라는 새 파일이 생성되었을 것입니다.

원하는 텍스트 편집기를 사용하여 해당 파일을 열어주세요.

 

import rclpy
from rclpy.node import Node

from std_msgs.msg import String


class MinimalPublisher(Node):

    def __init__(self):
        super().__init__('minimal_publisher')
        self.publisher_ = self.create_publisher(String, 'topic', 10)
        timer_period = 0.5  # seconds
        self.timer = self.create_timer(timer_period, self.timer_callback)
        self.i = 0

    def timer_callback(self):
        msg = String()
        msg.data = 'Hello World: %d' % self.i
        self.publisher_.publish(msg)
        self.get_logger().info('Publishing: "%s"' % msg.data)
        self.i += 1


def main(args=None):
    rclpy.init(args=args)

    minimal_publisher = MinimalPublisher()

    rclpy.spin(minimal_publisher)

    # Destroy the node explicitly
    # (optional - otherwise it will be done automatically
    # when the garbage collector destroys the node object)
    minimal_publisher.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

2.1 Examine the code

주석 다음의 코드 첫 줄은 rclpy를 가져와서 해당 Node 클래스를 사용할 수 있게 합니다.

import rclpy
from rclpy.node import Node


다음 문장은 노드에서 데이터를 topic을 통해 전달하는 데 사용되는 build-in string message 를 가져옵니다 (import).

from std_msgs.msg import String


이러한 줄들은 노드의 의존성을 나타냅니다. 의존성은 package.xml에 추가되어야 하며, 다음 섹션에서 그 작업을 수행하게 될 것입니다.

 

그 다음으로 MinimalPublisher 클래스가 생성됩니다. 이 클래스는 Node에서 상속받습니다.

class MinimalPublisher(Node):


다음은 클래스 생성자의 정의입니다. super().__init__는 Node 클래스의 생성자를 호출하고 노드 이름인 "minimal_publisher"를 전달합니다.

def __init__(self):
    super().__init__('minimal_publisher')

 

create_publisher는 std_msgs.msg 로부터 import한 String 타입의 메시지를 "topic" 이라는 이름의 topic으로 publish 하는 node 를 선언한다. 그리고 queue size(큐 사이즈) 는 10으로 설정됩니다. Queue size는 subscriber가 메시지를 충분히 빨리 수신하지 못하는 경우 대기 중인 메시지의 양을 제한하는 필수 QoS(quality of service, 품질 서비스) 설정입니다. 

 

self.publisher_ = self.create_publisher(String, 'topic', 10)


그 다음으로는 0.5초마다 실행되는 콜백을 가진 타이머가 생성됩니다. self.i는 콜백에서 사용되는 카운터입니다.

timer_period = 0.5  # seconds
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0

 

timer_callback 함수는 카운터 값을 덧붙인 메시지를 생성하고, get_logger().info를 사용하여 해당 메시지를 콘솔에 publish합니다.

 

def timer_callback(self):
    msg = String()
    msg.data = 'Hello World: %d' % self.i
    self.publisher_.publish(msg)
    self.get_logger().info('Publishing: "%s"' % msg.data)
    self.i += 1

 

마지막으로, main 함수가 정의됩니다.

 

def main(args=None):
    rclpy.init(args=args)

    minimal_publisher = MinimalPublisher()

    rclpy.spin(minimal_publisher)

    # Destroy the node explicitly
    # (optional - otherwise it will be done automatically
    # when the garbage collector destroys the node object)
    minimal_publisher.destroy_node()
    rclpy.shutdown()

 

먼저 rclpy 라이브러리가 초기화되고, 그 다음에 노드가 생성됩니다. 그리고 노드가 "spin"되어 콜백 함수가 호출되는 것입니다.

2.2 Add dependencies

ros2_ws/src/py_pubsub 디렉토리로 이동합니다. 이 디렉토리에는 `setup.py`, `setup.cfg`, 그리고 `package.xml` 파일이 이미 생성되어 있습니다.
텍스트 편집기를 사용하여 `package.xml` 파일을 엽니다.
이전 튜토리얼에서 언급한 대로 `<description>`, `<maintainer>`, 그리고 `<license>` 태그를 채워넣습니다:

<description>Examples of minimal publisher/subscriber using rclpy</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>


위의 내용 다음에, 노드의 import 문에 해당하는 다음 의존성을 추가합니다:

 

<exec_depend>rclpy</exec_depend>
<exec_depend>std_msgs</exec_depend>


이렇게 하면 패키지가 코드를 실행할 때 rclpy와 std_msgs가 필요하다고 선언됩니다.

 

수정한 파일을 저장하는 것을 잊지마세요~!!

2.3 Add an entry point

setup.py 파일을 엽니다. 다시 한 번 `maintainer`, `maintainer_email`, `description`, `license` 필드를 `package.xml` 파일과 일치하도록 설정합니다:

maintainer='YourName',
maintainer_email='you@email.com',
description='Examples of minimal publisher/subscriber using rclpy',
license='Apache License 2.0',


`entry_points` 필드의 `console_scripts` 괄호 내에 다음 줄을 추가하세요:

entry_points={
        'console_scripts': [
                'talker = py_pubsub.publisher_member_function:main',
        ],
},


변경 사항을 저장하는 것을 잊지 마세요.

2.4 Check setup.cfg

`setup.cfg` 파일의 내용은 다음과 같이 자동으로 올바르게 채워집니다:

[develop]
script_dir=$base/lib/py_pubsub
[install]
install_scripts=$base/lib/py_pubsub


이것은 단순히 setuptools에게 실행 가능한 파일을 lib 디렉토리에 배치하라는 것입니다. 왜냐하면 `ros2 run`이 해당 위치에서 파일을 찾기 때문입니다.

이제 패키지를 빌드하고 로컬 설정 파일을 source하고 실행(run)할 수 있지만, 먼저 subscriber 노드를 생성하여 전체 시스템이 어떻게 작동하는지 확인해 보겠습니다.

3 Write the subscriber node

다음 노드를 생성하려면 `ros2_ws/src/py_pubsub/py_pubsub` 디렉토리로 돌아가세요. 다음 명령을 터미널에 입력하여 코드를 다운로드하세요:

 

wget https://raw.githubusercontent.com/ros2/examples/humble/rclpy/topics/minimal_subscriber/examples_rclpy_minimal_subscriber/subscriber_member_function.py


이제 해당 디렉토리에는 다음과 같은 파일이 있어야 합니다:

__init__.py  publisher_member_function.py  subscriber_member_function.py

3.1 Examine the code

subscriber_member_function.py 파일을 텍스트 에디터로 여세요.

import rclpy
from rclpy.node import Node

from std_msgs.msg import String


class MinimalSubscriber(Node):

    def __init__(self):
        super().__init__('minimal_subscriber')
        self.subscription = self.create_subscription(
            String,
            'topic',
            self.listener_callback,
            10)
        self.subscription  # prevent unused variable warning

    def listener_callback(self, msg):
        self.get_logger().info('I heard: "%s"' % msg.data)


def main(args=None):
    rclpy.init(args=args)

    minimal_subscriber = MinimalSubscriber()

    rclpy.spin(minimal_subscriber)

    # Destroy the node explicitly
    # (optional - otherwise it will be done automatically
    # when the garbage collector destroys the node object)
    minimal_subscriber.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

 

subscriber 노드의 코드는 publisher 노드와 거의 동일합니다. 생성자는 publisher와 동일한 인수로 subscriber를 생성합니다. publisher와 subscriber가 통신하려면 topic name과 메시지 타입이 일치해야 한다는 것을 기억해주세요.

self.subscription = self.create_subscription(
    String,
    'topic',
    self.listener_callback,
    10)

 

subscriber의 생성자와 콜백은 타이머 정의가 없습니다. 왜냐하면 subscriber는 메시지를 받으면 즉시 콜백이 호출되기 때문입니다.

콜백 정의는 수신한 데이터와 함께 콘솔에 info message를 출력합니다. publisher가 `msg.data = 'Hello World: %d' % self.i`를 정의했던 것을 기억하세요.

def listener_callback(self, msg):
    self.get_logger().info('I heard: "%s"' % msg.data)


main 정의는 publisher의 생성과 spin을 구독자로 subscriber하는 것을 제외하고 거의 동일합니다.

 

minimal_subscriber = MinimalSubscriber()

rclpy.spin(minimal_subscriber)


이 노드는 publisher와 동일한 의존성을 가지므로 package.xml에 새로운 내용을 추가할 필요가 없습니다. setup.cfg 파일도 수정할 필요가 없습니다.

3.2 Add an entry point

`setup.py` 파일을 다시 열고, publisher 노드의 entry point 아래에 subscriber 노드의 entry point를 추가하세요. `entry_points` 필드는 이제 다음과 같이 보일 것입니다:

entry_points={
        'console_scripts': [
                'talker = py_pubsub.publisher_member_function:main',
                'listener = py_pubsub.subscriber_member_function:main',
        ],
},


파일을 저장한 후, 이제 pub/sub 시스템이 준비되었습니다.

4 Build and run

아마도 이미 ROS 2 시스템의 일부로 rclpy와 std_msgs 패키지가 설치되어 있을 것입니다. build하기 전에 빈칸이 있는 의존성을 확인하기 위해 workspace의 root(ros2_ws 디렉토리)에서 rosdep을 실행하는 것은 좋습니다.

 

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

 

새로운 터미널을 열고 `ros2_ws`로 이동한 다음, 설정 파일을 소스하세요.

 

source install/setup.bash

 

talker 노드를 실행하세요.

 

ros2 run py_pubsub talker

 

터미널은 다음과 같이 매 0.5초마다 info message를 publish하기 시작해야합니다.

 

[INFO] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [minimal_publisher]: Publishing: "Hello World: 1"
[INFO] [minimal_publisher]: Publishing: "Hello World: 2"
[INFO] [minimal_publisher]: Publishing: "Hello World: 3"
[INFO] [minimal_publisher]: Publishing: "Hello World: 4"
...

 

다른 터미널을 열고, 다시 'ros2_ws' 내에서  setup 파일을 source한 후, listener 노드를 실행하세요.

 

source install/setup.bash
ros2 run py_pubsub listener


subscriber는 해당 시간에 publisher가 어떤 message count에 도달했는지에 따라 메시지를 콘솔에 출력하기 시작할 것입니다. 다음과 같이 출력될 것입니다:

[INFO] [minimal_subscriber]: I heard: "Hello World: 10"
[INFO] [minimal_subscriber]: I heard: "Hello World: 11"
[INFO] [minimal_subscriber]: I heard: "Hello World: 12"
[INFO] [minimal_subscriber]: I heard: "Hello World: 13"
[INFO] [minimal_subscriber]: I heard: "Hello World: 14"


각 터미널에서 Ctrl+C를 눌러 노드의 실행을 중지하세요.

Summary

여기서는 두 개의 노드를 생성하여 topic을 통해 데이터를 pub/sub했습니다. 실행하기 전에 의존성과 엔트리 포인트를 패키지 구성 파일에 추가했습니다.

Next steps

다음으로, 서비스/클라이언트 모델을 사용하여 또 다른 간단한 ROS 2 패키지를 생성할 수 있습니다. 이번에도 C++ 또는 Python 중 원하는 언어로 작성할 수 있습니다.

Related contents

Python에서 publisher와 subscriber를 작성하는 다양한 방법이 있습니다. ros2/examples 저장소의 `minimal_publisher`와 `minimal_subscriber` 패키지를 확인해보세요.

반응형