ROS 프로그래밍 기초(Python)

두부김치·2024년 2월 11일
1

ROS2

목록 보기
14/29

1. ROS의 Hello world, rclpy 버전

프로그래밍 언어의 시작은 화면에 "Hello world"문구를 출력하는 것이다. ROS의 "Hello world" 프로그램 또한 다르지 않지만 출력보다는 메시지 전송에 더 초점을 둔다. 이번 장에서는 파이썬 언어로 ROS2의 가장 간단한 구조의 토픽(Topic), 퍼블리셔(Publisher)와 서브스크라이버(Subscriber)를 작성하고 동작시켜 보겠다.

2. 패키지 생성

ROS2의 패키지 생성 명령어는 다음과 같다.

$ ros2 pkg create <패키지이름> --build-type <빌드 타입> --dependencies <의존하는패키지1> <의존하는패키지n>

ros2 pkg create 명령어를 사용하고 그 뒤에 옵션을 붙여준다. 참고로 명령어 실행 폴더는 앞에서 작성했던 사용자 작업폴더(예: ~/robot_ws/src)임을 잊지말자

$ cd ~/robot_ws/src/
$ ros2 pkg create my_first_ros_rclpy_pkg --build-type ament_python --dependencies rclpy std_msgs

앞의 명령어를 보면 의존하는 패키지로 rclpy와 std_msgs를 옵션으로 사용했다. 이는 ROS 파이썬 클라이언트 라이브러리(rclpy)와 ROS의 표준 메시지 패키지(std_msgs)를 사용하겠다는 의미로 패키지 생성에 앞서 미리 설치되어 있어야 한다. 이러한 의존성 패키지 설정은 이 예제처럼 패키지를 생성할 때 지정할수 있지만, 생성한 다음 package.xml 에서 직접 입력해도 된다.
패키지를 생성했다면 ~/robot_ws/src 경로에 my_first_ros_rclpy_pkg 패키지 폴더와 ROS패키지가 갖추어야 할 기본 폴더 그리고 package.xml 파일 등이 생성된다. ament_cmakeament_python 에 따라 기본 구성 파일 시스템이 좀 상이한데 기본적으로는 다음과 같이 구성된다.

.
├── my_first_ros_rclpy_pkg
│   └── __init__.py
├── resource
│   └── my_first_ros_rclpy_pkg
├── test
│   ├── test_copyright.py
│   ├── test_flake8.py
│   └── test_pep257.py
├── package.xml
├── setup.cfg
└── setup.py

3 directories, 8 files

3. 패키지 설정

지금부터 "Hello world" 예제를 위해 작성할 파일은 my_first_ros_rclpy_pkg의 패키지 설정 파일(package.xml), 파이썬 패키지 설정 파일(setup.py), 파이썬 패키지 환경설정 파일(setup.cfg)이다.

3.1 패키지 설정 파일(package.xml)

패키지 설정 파일은 사용할 RCL(ROS2 client libraries)에 따라 달라지는데 C++는 build_type을 ament_cmake로 설정하고, 파이썬은 ament_python으로 설정하면 된다. 그 이외에는 각기 다른 개발환경에 맞춘 의존성 패키지 설정을 해주면 여느 패키지나 대동소이하다.

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>my_first_ros_rclpy_pkg</name>
  <version>0.0.2</version>
  <description>ROS 2 rclpy basic package for the ROS 2 seminar</description>
  <maintainer email="choi@naver.com">Pyo</maintainer>
  <license>Apache License 2.0</license>
  <author email="mikael@osrfoundation.org">Mikael Arguedas</author>
  <author email="choi@naver.com">Pyo</author>

  <depend>rclpy</depend>
  <depend>std_msgs</depend>

  <test_depend>ament_copyright</test_depend>
  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <test_depend>python3-pytest</test_depend>

  <export>
    <build_type>ament_python</build_type>
  </export>
</package>

3.2 파이썬 패키지 설정 파일(setup.py)

패키지 설정 파일 에서 주의할 점은 entry_point 옵션의 console_scripts 키를 사용한 실행 파일 설정이다.
예를 들어 helloworld_publisherhelloworld_subscriber 콘솔 스크립트는 각각 my_first_ros_rclpy_pkg.helloworld_publisher 모듈과 my_first_ros_rclpy_pkg.helloworld_subscriber 모듈의 main 함수가 호출되게 된다. 이를 통해 ros2 run 또는 ros2 launch를 이용하여 해당 스크립트를 실행시킬 수 있다.

from setuptools import find_packages
from setuptools import setup

package_name = 'my_first_ros_rclpy_pkg'

setup(
    name=package_name,
    version='0.0.2',
    packages=find_packages(exclude=['test']),
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    author='Mikael Arguedas, Pyo',
    author_email='mikael@osrfoundation.org, pyo@robotis.com',
    maintainer='Pyo',
    maintainer_email='pyo@robotis.com',
    keywords=['ROS'],
    classifiers=[
        'Intended Audience :: Developers',
        'License :: OSI Approved :: Apache Software License',
        'Programming Language :: Python',
        'Topic :: Software Development',
    ],
    description='ROS 2 rclpy basic package for the ROS 2 seminar',
    license='Apache License, Version 2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'helloworld_publisher = my_first_ros_rclpy_pkg.helloworld_publisher:main',
            'helloworld_subscriber = my_first_ros_rclpy_pkg.helloworld_subscriber:main',
        ],
    },
)

3.3 파이썬 패키지 환경설정 파일(setup.cfg)

패키지 환경설정 파일에서 주의할 점은 my_first_ros_rclpy_pkg와 같이 패키지 이름을 기재해야 하는 것과 나중에 colcon을 이용하여 빌드하게 되면 /home/[유저이름]/robot_ws/install/my_first_ros_rclpy_pkg/lib/my_first_ros_rclpy_pkg 와 같이 지정 폴더에 실행 파일이 생성된다는 점이다.

[develop]
script-dir=$base/lib/my_first_ros_rclpy_pkg
[install]
install-scripts=$base/lib/my_first_ros_rclpy_pkg

4. 퍼블리셔 노드 작성

퍼블리셔 노드의 파이썬 코드는 ~/robot_ws/src/my_first_ros_rclpy_pkg/my_first_ros_rclpy_pkg/ 폴더에 helloworld_publisher.py라는 이름으로 소스 코드 파일을 직접 생성하여 넣어주고 퍼블리셔 노드의 전체 코드는 다음과 같이 작성하면 된다. 코드 내용은 이어지는 설명에서 하나씩 알아보도록 하자.

import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile
from std_msgs.msg import String


class HelloworldPublisher(Node):

    def __init__(self):
        super().__init__('helloworld_publisher')
        qos_profile = QoSProfile(depth=10)
        self.helloworld_publisher = self.create_publisher(String, 'helloworld', qos_profile)
        self.timer = self.create_timer(1, self.publish_helloworld_msg)
        self.count = 0

    def publish_helloworld_msg(self):
        msg = String()
        msg.data = 'Hello World: {0}'.format(self.count)
        self.helloworld_publisher.publish(msg)
        self.get_logger().info('Published message: {0}'.format(msg.data))
        self.count += 1


def main(args=None):
    rclpy.init(args=args)
    node = HelloworldPublisher()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        node.get_logger().info('Keyboard Interrupt (SIGINT)')
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

첫 구절은 import 구문이다. rclpyNode 클래스를 사용하며, 퍼블리셔의 Qos 설정을 위하여 QosProfile 클래스를 사용한다. 퍼블리시하는 메시지 타입은 std_msgs.msg 모듈의 String 메시지 인터페이스를 사용하기 위해 import 하였다.

import rclpy
from rclpy.node import Node   # rclpy.node 에서 Node 클래스 사용
from rclpy.qos import QoSProfile
from std_msgs.msg import String

이 노드의 메인 클래스는 HelloworldPublisher 이고 Node 클래스를 상속해 사용할 예정이다.

class HelloworldPublisher(Node):

다음은 클래스 생성자 정의로 super().__init__("helloworld_publisher")를 이용해 부모 클래스(Node)의 생성자를 호출하고 노드 이름을 "helloworld_publisher" 로 지정하였다.
그 다음 퍼블리셔의 QoS 설정을 위하여 QoSProfile을 호출하고 기본 depth를 10으로 설정하였다. 이는 통신 상태가 원할하지 못하거나 예기치 못한 문제가 발생할 경우를 대비해 퍼블리시할 데이터를 버퍼에 10개까지 저장하는 설정이다.
그 다음으로는 Node 클래스의 create_publisher 함수를 이용하여 helloworld_publisher 라는 퍼블리셔를 설정하고 있다. 매개변수로는 토픽에 사용할 토픽 메시지 타입과 토픽의 이름, QoS 설정을 기입하도록 되어 있으며 여기서는 토픽 메시지 타입으로 String, 토픽 이름으로 helloworld, QoS 설정으로 좀전에 설정한 qos_profile으로 설정하였다.
마지막으로 Node 클래스의 create_timer 함수를 이용하여 콜백함수를 수행하는 구문인데 첫번째 매개변수는 timer_period_sec 으로 1을 설정하였다. 이는 1초마다 지정한 콜백함수를 실행하라는 것으로 아래 코드와 같이 설정하면 1초마다 publish_helloworld_msg 함수를 실행하게 된다. count는 콜백함수에 사용되는 카운터 값이다.

def __init__(self):
        super().__init__('helloworld_publisher') #<노드 생성>
        qos_profile = QoSProfile(depth=10)
        self.helloworld_publisher = self.create_publisher(String, 'helloworld', qos_profile) #<토픽 생성>
        self.timer = self.create_timer(1, self.publish_helloworld_msg) #<콜백 함수 호출>
        self.count = 0

다음은 위에서 지정한 콜백함수인 publish_helloworld_msg 함수이다. 퍼블리시할 메시지는 String타입으로 msg이라는 이름으로 선언하였으며 보낼 메시지는 msg.data에 저장하게 되는데 여기서는 Hello World: 1과 같이 매번 콜백함수가 실행될때마다 1씩 증가하는 count 값을 문자열에 포함시켜 publish 함수를 통해 퍼블리시하게 된다.get_logger 함수는 콘솔창에 출력하는 함수로 로거의 종류에 따라 debug, info, warning, error, fatal와 같이 5가지 종류가 있다. 일반적인 정보 전달에는 info를 사용하고 있기에 info 함수를 통해 현재 퍼블리시되는 메시지를 콘솔창에 출력시키는 구문을 마지막에 넣어주었다. get_logger는 프로그래밍에서 흔히 사용되는 print 함수라고 생각하면 이해하기 편할 것이다.

참고로 콜백함수의 구현에는 member function, lambda, local function 방법이 있는데 이 예제 코드에서는 member function 방식을 택하였다. 시퀀스에 의해 처리되는 local function 방법은 잘 사용되지 않고 member function 또는 lambda 방식이 많이 사용된다. 자신이 좋아하는 구현 방법이 있다면 그 방법으로 작성하도록 하자.

def publish_helloworld_msg(self):
        msg = String()
        msg.data = 'Hello World: {0}'.format(self.count)
        self.helloworld_publisher.publish(msg)
        self.get_logger().info('Published message: {0}'.format(msg.data))
        self.count += 1

마지막 main 함수는 rclpy.init 를 이용해 초기화하고 앞에서 작성한 HelloworldPublisher 클래스를 node 변수로 생성한 다음 rclpy.spin 함수를 이용하여 생성한 노드를 spin 시켜 지정된 콜백 함수가 실행될 수 있도록 하고 있다. 종료(Ctrl+c) 와 같은 인터럽트 시그널 예외 상황에서는 node 를 소멸시키고 rclpy.shutdown 함수로 노드를 종료하게 된다.

def main(args=None):
    rclpy.init(args=args)
    node = HelloworldPublisher()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        node.get_logger().info('Keyboard Interrupt (SIGINT)')
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

5. 서브스크라이버 노드 작성

서브스크라이버 노드의 Python 코드도 퍼블리셔 노드와 마찬가지로 ~/robot_ws/src/my_first_ros_rclpy_pkg/my_first_ros_rclpy_pkg/ 폴더에 helloworld_subscriber.py라는 이름으로 소스 코드 파일을 직접 생성하여 넣어주고 서브스크라이버 노드의 전체 코드는 다음과 같이 작성하면 된다. 코드 내용은 이어지는 설명에서 하나씩 알아보도록 하자.

import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile
from std_msgs.msg import String


class HelloworldSubscriber(Node):

    def __init__(self):
        super().__init__('Helloworld_subscriber')
        qos_profile = QoSProfile(depth=10)
        self.helloworld_subscriber = self.create_subscription(
            String,
            'helloworld',
            self.subscribe_topic_message,
            qos_profile)

    def subscribe_topic_message(self, msg):
        self.get_logger().info('Received message: {0}'.format(msg.data))


def main(args=None):
    rclpy.init(args=args)
    node = HelloworldSubscriber()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        node.get_logger().info('Keyboard Interrupt (SIGINT)')
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

첫 구절은 import 구문으로 퍼블리셔 노드와 동일하다.

import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile
from std_msgs.msg import String

이 노드의 메인 클래스는 HelloworldSubscriberNode 클래스를 상속해 사용한다.

class HelloworldSubscriber(Node):

다음은 클래스 생성자의 정의로 super().__init__('Helloworld_subscriber')를 이용하여 Node 클래스의 생성자를 호출하고 노드 이름을 Helloworld_subscriber으로 지정하였다.

그 다음 서브스크라이버의 QoS 설정을 위하여 QoSProfile 호출하고 기본 depth10으로 설정하여 통신 상태가 원할하지 못한 상황 등 예기치 못한 경우 서브스크라이브 데이터를 버퍼에 10개까지 저장하라는 설정이다.

그 다음으로는 Node 클래스의 create_subscription 함수를 이용하여 helloworld_subscriber 라는 서브스크라이버를 설정하고 있다. 매개변수로는 토픽에 사용할 토픽 메시지 타입과 토픽의 이름, 수신받은 메시지를 처리할 콜백함수과 QoS 설정을 기입하도록 되어 있다. 여기서는 토픽 메시지 타입으로 String, 토픽 이름으로 helloworld, 콜백함수는 subscribe_topic_message, QoS 설정으로 좀전에 설정한 qos_profile으로 설정하였다.

def __init__(self):
        super().__init__('Helloworld_subscriber')
        qos_profile = QoSProfile(depth=10)
        self.helloworld_subscriber = self.create_subscription(
            String,
            'helloworld',
            self.subscribe_topic_message,
            qos_profile)

다음은 위에서 지정한 콜백함수인 subscribe_topic_message 함수이다. 서브스크라이브한 메시지는 String타입으로 msg이라는 이름을 사용하며 받은 메시지는 msg.data에 저장하게 되어 있다. 여기서는 Hello World: 1과 같은 메시지를 서브스크라이브하게 된다. 이 msg.data를 get_logger의 info 함수를 이용하여 서브스크라이브된 메시지를 콘솔창에 출력시키는 구문을 마지막에 넣어주었다.

    def subscribe_topic_message(self, msg):
        self.get_logger().info('Received message: {0}'.format(msg.data))

마지막은 main 함수로 HelloworldSubscriber을 node로 선언하여 사용한다는 것 이외에는 위에서 설명한 퍼블리셔 노드의 main 함수와 동일하다.

def main(args=None):
    rclpy.init(args=args)
    node = HelloworldSubscriber()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        node.get_logger().info('Keyboard Interrupt (SIGINT)')
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

7. 빌드

ROS 2 특정 패키지 또는 전체 패키지를 빌드할 때에는 colcon 빌드 툴을 사용한다. 사용 방법은 매우 간단한 편인데 우선 소스 코드가 있는 workspace로 이동하고 colcon build 명령어로 전체를 빌드하게 된다. 여기서 빌드 옵션을 추가하여 사용하는게 일반적인데 특정 패키지만 선택하여 빌드하고자 할 때에는 --packages-select 옵션을 이용하고 symlink를 이용하려면 --symlink-install 옵션을 붙여주면 된다.

(워크스페이스내의 모든 패키지 빌드하는 방법) 
$ cd ~/robot_ws && colcon build --symlink-install

(특정 패키지만 빌드하는 방법)
$ cd ~/robot_ws && colcon build --symlink-install --packages-select [패키지 이름1] [패키지 이름2] [패키지 이름N]

(특정 패키지 및 의존성 패키지를 함께 빌드하는 방법)
$ cd ~/robot_ws && colcon build --symlink-install --packages-up-to [패키지 이름]

위에서 작성해둔 my_first_ros_rclpy_pkg 패키지만 빌드하려면 하기와 같은 명령어를 통해 가능하다.

$ cd ~/robot_ws
$ colcon build --symlink-install --packages-select my_first_ros_rclpy_pkg
Starting >>> my_first_ros_rclpy_pkg
Finished <<< my_first_ros_rclpy_pkg [0.66s]

Summary: 1 package finished [0.87s]
$ source install/setyp.bash

8. 실행

각 노드의 실행은 ros2 run 명령어를 사용해 실행하면 된다.

$ ros2 run my_first_ros_rclpy_pkg helloworld_publisher
[INFO]: Published message: Hello World: 0
[INFO]: Published message: Hello World: 1
[INFO]: Published message: Hello World: 2
[INFO]: Published message: Hello World: 3
[INFO]: Published message: Hello World: 4
[INFO]: Published message: Hello World: 5
$ ros2 run my_first_ros_rclpy_pkg helloworld_subscriber
[INFO]: Received message: Hello World: 0
[INFO]: Received message: Hello World: 1
[INFO]: Received message: Hello World: 2
[INFO]: Received message: Hello World: 3
[INFO]: Received message: Hello World: 4
[INFO]: Received message: Hello World: 5

아주 기초적인 방법으로 rclpy 버전 패키지를 작성해보고 빌드해서 실행해보았다. 다음장에서는 rclcpp 버전으로 작성해보겠다.

profile
Robotics

0개의 댓글