초보자를 위한 ROS : Publisher와 Subscriber

SoYu·2022년 5월 7일
2

ROS

목록 보기
2/3
post-thumbnail

1. 서론 : Publisher와 Subscriber는 무엇인가?

본 글에서 다룰 모든 예시는 python으로 작성되었습니다. C++를 이용할 경우 조금 다르지만, 큰 틀에서는 같은 내용을 공유합니다

ROS_node

ROS는 각 노드들끼리 메세지를 서로 주고 받음으로 전체적인 프로그램이 돌아가도록 설계되었다는 것을 알고 있다면, 다음에 나올 질문은 매우 간단하다.

어떻게 노드끼리 데이터를 주고 받을까?

이 의문에 대한 답이 바로 Publisher와 Subscriber이다.

1.1 Publisher

Publisher는, 말 그대로 메세지를 발행하는 역할을 한다. 이 때, 중요한 점은 Publisher가 발행하는 메세지는 수신인이 없다는 것이다.

관심

Publisher만 존재한다면, 그것은 청중 없이 혼자 열심히 떠들고 있는 것에 불과하다

Publisher는 Master Node를 중심으로 한 ROS 네트워크 상에서 그저 묵묵하게 메세지를 열심히 외치고 있는 샘이다.

1.2 Subscriber

그렇다면, 열심히 외치고 있는 Publisher의 메세지를 경청해줄 사람이 필요한데, 이것이 바로 Subscriber이다.

Subscriber는, 같은 ROS 네트워크 상에 존재하는 특정 토픽명을 가진 메세지가 발행되는 것을 주시하고, 그 메세지가 발행될 경우 이벤트 리스터를 실행한다.

2. 어떻게 만들까?

그렇다면, 예제 코드와 함께 Publisher와 Subscriber를 만드는 법을 알아보자. 예제로 사용할 환경은 아래와 같다.

OS: Ubuntu 18.04 on WSL2, ROS: Melodic, Python: 2.7.17, Editor: VSCode

catkin_ws
├── build
├── devel
└── src
    └── CMakeLists.txt

Workspace의 트리는 위와 같이 잡혀있다.

$ cd ~/catkin_ws/src/
$ roscreate-pkg pns_test
$ cd ../
$ catkin_make

Workspace안에, src 폴더 안에, pns_test라는 새로운 패키지를 생성해준다. 패키지 생성에 관련해서는 ROS Wiki를 참고하자. 이후, 생성된 패키지 내부에, src폴더와 그 안에 app.py 파일을 생성한다.

2.1 Publisher

우선, 간단한 Publisher부터 만들어보자. Publisher 클래스는 rospy에 정의되어 있다.

from rospy import Publisher, Rate, init_node

Rate 클래스는 노드의 실행 주기를 조절하기 위한 클래스이고, init_node 함수는 rospy에서 노드를 정의하는 방법입니다.

if __name__ == '__main__':
    init_node("publisher_test")

    publisher = Publisher()

Publisher 객체는 생성자로 아래와 같은 파라미터들을 받는다.

name, data_class, subscriber_listener, tcp_nodelay, latch, headers, queue_size

이 중에서 필수적으로 들어가야 하는 것은 name과 data_class, queue_size로, name은 Publisher가 발행하는 topic의 이름으로, ROS 네트워크 상에서 존재하는 유니크한 값입니다. data_class는 Publisher가 발행할 메시지의 타입(클래스)을 의미합니다. 마지막으로, queue_size는 데이터의 큐(자료구조) 사이즈를 의미합니다.

여기서, data_class에서 요구하는 데이터 클래스는, ROS에서 제공해주는 메세지 클래스입니다. 자세한 내용은 ROS Wiki를 참고하고, 본 글에서는 기본적으로 제공해주는 메세지 클래스인 std_msg를 이용합니다.

std_msg

std_msg에 포함된 메세지 종류는 위와 같습니다. 본 예제에서는, int64 타입의 메세지를 발행해볼 예정임으로, 따라서 Publisher 객체를 아래와 같이 수정해줍니다.

from std_msgs.msg import Int64

publisher = Publisher(name="simple_pub", data_class=Int64, queue_size=1)

이것으로, publisher_test라는 이름을 가지는 노드와, 그 노드에서 작동하는 simple_pub이라는 이름의 topic을 가지는 Int64 타입의 Publisher를 작성했습니다.

해당 노드가 멈추지 않도록 아래와 같은 코드를 추가하고, 실행해봅니다.

import rospy

rospy.spin()
$ roscore && rosrun pns_test app.py

roscore는 ROS Master None, ROS Param Server 등을 한번에 작동시키는 ROS 명령어입니다. rosrun은 rosrun <pkg_name> <node_name> 의 구조를 가지는 노드를 실행시키는 명령어입니다.

위와 같은 명령어를 입력하면, 아무 일도 일어나지 않습니다. 하지만 ROS 상에서는 큰 변화가 나타납니다. 이를 확인하기 위하여 아래와 같은 명령어를 입력합니다.

$ rqt_graph

rqt_graph는 ROS에서 제공하는 시각화 도구의 일종으로, 노드와 토픽간의 연결관계를 시각화해서 보여줍니다.

rqt_graph

위의 사진은 rqt_graph의 실행 결과입니다. publisher_test라는 이름의 노드가 생성된 것을 확인할 수 있습니다.

또한, 해당 내용을 ROS에서 제공해주는 도구중 하나인 rostopic을 이용하여 ROS 네트워크상 존재하는 모든 메세지 토픽을 확인해보면, 아래와 같습니다.

$ rostopic list -v

Published topics:
 * /rosout [rosgraph_msgs/Log] 1 publisher
 * /simple_pub [std_msgs/Int64] 1 publisher
 * /rosout_agg [rosgraph_msgs/Log] 1 publisher

Subscribed topics:
 * /rosout [rosgraph_msgs/Log] 1 subscriber

성공적으로, std_msgs.msg.Int64 타입으로, simple_pub이라는 토픽명의 Publisher가 등록된 것을 확인할 수 있습니다.

다음으로, 정의한 Publisher를 이용하여, 실제로 메세지를 발행해봅시다. 메세지는 Publisher 객체의 publish 함수를 이용하여 발행할 수 있습니다.

random 함수를 이용하여, 0부터 255까지의 랜덤한 값을 1초 주기로 발행하는 코드를 작성하면, 아래와 같습니다.

#!/usr/bin/python

import rospy
from random import randint
from std_msgs.msg import Int64


if __name__ == '__main__':
    rospy.init_node("publisher_test")

    publisher = rospy.Publisher(
        name="simple_pub", data_class=Int64, queue_size=1)

    r = rospy.Rate(1)
    while not rospy.is_shutdown():

        msg = Int64()
        msg.data = randint(0, 255)

        rospy.loginfo(msg)

        publisher.publish(msg)

        r.sleep()

위와 같은 노드의 실행 결과는 아래와 같습니다.

$ rosrun pns_test app.py 
[INFO] [1651948710.958126]: data: 186
[INFO] [1651948711.959265]: data: 174
[INFO] [1651948712.959405]: data: 8
[INFO] [1651948713.959293]: data: 106
[INFO] [1651948714.959313]: data: 56
[INFO] [1651948715.959301]: data: 188
[INFO] [1651948716.959352]: data: 167
[INFO] [1651948717.959304]: data: 27
[INFO] [1651948718.959255]: data: 139
[INFO] [1651948719.959348]: data: 11
[INFO] [1651948720.959383]: data: 46
[INFO] [1651948721.959318]: data: 71
[INFO] [1651948722.959257]: data: 87
...

그리고 해당 결과를 rostopic을 이용하여 확인해본 결과는 아래와 같습니다.

$ rostopic echo /simple_pub 
data: 174
---
data: 8
---
data: 106
---
data: 56
---
data: 188
---
data: 167
---
data: 27
---
data: 139
---
data: 11
---
data: 46
---
data: 71
---
data: 87
---
...

정상적으로 정수형 데이터를 발행하는 Publisher가 작성되었음을 확인할 수 있습니다. 여기까지의 결과를 rqt로 확인해보면, 아래와 같습니다.

rqt_graph_2

publisher_test라는 노드에서, simple_pub이라는 토픽이 발행되어, rostopic_... 라는 노드에서 이를 구독하고 있음을 확인할 수 있습니다. 여기서 rostopic_... 노드는 rostopic echo입니다.

2.2 Subscriber

위와 같은 방법으로 메세지를 발행하는 것에 성공하였고, 다음으로 어떻게 하면 발행된 메세지를 구독할 수 있는지 알아봅니다.

우선, 기존에 작성한 파일의 이름을 pub.py로 변경하고, 새로 sub.py 파일을 작성해줍니다.

/home/***/catkin_ws/src/pns_test/
├── CMakeLists.txt
├── Makefile
├── mainpage.dox
├── manifest.xml
└── src
    ├── pub.py
    └── sub.py

sub.py 파일에는 pub.py과 같은 뼈대를 공유하도록 코드를 작성해줍니다.

#!/usr/bin/python

import rospy
from std_msgs.msg import Int64


if __name__ == '__main__':
    rospy.init_node("sublisher_test")

    r = rospy.Rate(1)
    while not rospy.is_shutdown():
        r.sleep()

이후, 메인 함수에 Subscriber 객체를 선언해줍니다.

subscriber = rospy.Subscriber()

Subscriber 클래스는 생성자로 아래와 같은 파라미터를 받습니다.

name, data_class, callback, callback_args, queue_size, buff_size, tcp_nodelay

Publisher와 유사하게, name은 구독할 토픽명, data_class는 해당 토픽의 데이터 타입입니다.

그리고, Publisher와 가장 큰 차이점을 보이는 부분은 바로 callback입니다. callback 변수는, 토픽이 발행되는 이벤트가 발생하였을 때, 작동할 이벤트 리스너 함수를 콜백 함수의 형태로 요구합니다. 콜백 함수의 기본 argument는 구독한 메세지 객체입니다.

data = None


def callbackFunction(msg):
    global data
    data = msg

    rospy.loginfo(data)

Subscriber 객체를 아래와 같이 수정하고 실행시킨 결과는 다음과 같습니다.

subscriber = rospy.Subscriber(
        name="simple_pub", data_class=Int64, callback=callbackFunction)
$ roscore && rosrun pns_test pub.py && rosrun pns_test sub.py

[INFO] [1651949917.660658]: data: 7
[INFO] [1651949918.663203]: data: 27
[INFO] [1651949919.663436]: data: 68
[INFO] [1651949920.658246]: data: 30
[INFO] [1651949921.658043]: data: 172
[INFO] [1651949922.658009]: data: 193
[INFO] [1651949923.658300]: data: 166
...

성공적으로 Subscriber가 동작함을 확인할 수 있습니다. 위와 같은 방법으로, 원하는 callback 함수를 정의하여 데이터 후처리 등을 진행할 수 있습니다.

3. 결론

rqt_graph_3

위와 같은 과정을 통하여, python을 이용하여, 기본적인 구조의 Publisher와 Subscriber을 정의, 메세지를 발행하고, 구독하는 방법에 대하여 알아보았습니다.

ROS를 이용한 로봇 프로그래밍이라 하는 것은, 결국 이런 메세지를 발행하고 구독하는 노드를 최소 단위별로 나눠서 하나의 거대한 프로그램을 이루도록 작성하는 것입니다.

0개의 댓글