Study/[ROS2] ROS2 Basic

ROS2 with python - Basic Concept

soohwan_justin 2023. 7. 26. 17:55

본 포스트는 ROS1에 대한 전반적인 지식이 있다는 가정 하에 작성되었으며, The Construct Youtube 영상을 참고하였습니다.

 

1 패키지란?

 

ROS2도 프로그램을 다루기 위해 ROS1처럼 패키지를 사용합니다. 패키지는 특정한 ROS2 프로그램을 포함하는 모든 파일들(cpp, python, configuration, compliation, launch, parameters 파일들)이라고 생각하면 되겠습니다.

 

ROS2에서는 2가지 종류의 패키지를 만들 수 있는데... 하나는 파이썬 패키지, 다른 하나는 CMake(C++) 패키지 입니다.

 

모든 파이썬 패키지는 다음과 같은 파일 및 디렉터리 구조를 갖습니다.

- package.xml - 패키지에 대한 meta-information(패키지 관리자, 의존성 등등...)에 대한 파일

- setup.py - 패키지를 컴파일하는 방법에 대한 설명 파일

- setup.cfg - 해당 스크립트가 설치될 위치를 정의하는 파일

- /<package_name> - ROS2에서는 패키지를 만들면 해당 패키지와 똑같은 이름의 디렉토리가 패키지 내부에 생성됩니다. 우리는 파이썬 스크립트를 이 폴더 안에다가 넣을 것이고, 처음 생성하면 기본적으로 __init__.py 파일이 생성됩니다. 

 

일부 패키지들은 예를 들어 launch 같은 추가 디렉토리를 갖고 있기도 합니다.

 

 

 

2 패키지 만들기

 

이제 패키지를 직접 만들어보겠습니다. 패키지를 만들 때는 ROS1과 비슷하게, ROS2 workspace 에서 작업을 할 것입니다. ROS1에서는 일반적으로 catkin_ws 라는 workspace를 만들었지만, ros2에서는 일반적으로 ros2_ws를 사용합니다.

 

 


예제 2.1.

(이전 포스트의 내용을 진행했다는 가정 하에 설명합니다)

 

먼저, 터미널 창에서 ROS2를 source 합니다.

$ source /opt/ros/humble/setup.bash

 

ros2_ws 디렉토리에서 패키지를 생성합니다.

$ cd ~/ros2_ws/
$ cd src
$ ros2 pkg create --build-type ament_python my_package --dependencies rclpy

 

실행 결과

 

즉, ros2에서 python을 사용하는 사용자 정의 패키지를 만드는 명령어는 다음과 같습니다(물론 src 디렉토리에서 실행해야 합니다)

$ ros2 pkg create --build-type ament_python <package_name> --dependencies <package_dependencies>

 

다음으로 패키지를 빌드하고, source하면 패키지를 사용할 수 있습니다(ros1의 catkin_make에 대응됩니다)

$ cd ~/ros2_ws && colcon build --symlink-install
$ source install/setup.bash

 

 

만약 이전 포스트에서 설명한 대로 bashrc 파일을 수정하셨으면, 아래와 같이 명령어를 입력합니다

$ cb
$ sb

 

 

패키지가 잘 만들어졌는지 확인해봅니다

$ ros2 pkg list | grep my_package

 

 

만약 특정한 패키지만 빌드하고 싶다면, 다음 명령을 실행합니다

colcon build --packages-select <package_name> --symlink-install

 


 

3. ROS2 프로그래밍 해보기 

 

1. my_package 패키지 내부의 my_directory에 simple.py 를 하나 만들고, (편집기는 vs code를 사용했습니다)

$ cd ~/ros2_ws/src/my_package/my_package
$ code simple.py

 

다음과 같이 코드를 작성합니다.

simple.py

import rclpy # ROS1의 rospy에 대응
from rclpy.node import Node

def main(args=None):
    rclpy.init(args=args)
    print("ROS2 is working! ")
    rclpy.shutdown()

if __name__ == '__main__':
    main()

 

 

2. my_package 패키지 내부에 launch 디렉토리를 만듭니다.

$ cd ~/ros2_ws/src/my_package
$ mkdir launch

 

3. launch 파일을 하나 만들고,

$ cd ~/ros2_ws/src/my_package/launch
$ code my_package_launch_file.launch.py

 

4. 다음과 같이 작성하고,

my_pacakge_launch_file.launch.py

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='my_package',
            executable='simple_node',
            output='screen'),
    ])

 

실행 권한을 부여해줍니다

$ chmod +x my_package_launch_file.launch.py

 

5. my_package 패키지 디렉토리에 있는 setup.py 파일을 다음과 같이 수정합니다.

 

원본 파일

 

위 파일을 아래와 같이 수정합니다

setup.py

from setuptools import setup
import os
from glob import glob

package_name = 'my_package'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share', package_name), glob('launch/*.launch.py')),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='justin',
    maintainer_email='justin@todo.todo',
    description='TODO: Package description',
    license='TODO: License declaration',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'simple_node = my_package.simple:main'
        ],
    },
)

 

6. 빌드합니다.

$ cb
$ sb

 

7. 실행해봅니다.

$ ros2 launch my_package my_package_launch_file.launch.py

 

 

4. setup.py 파일 설명

setup.py 파일은 패키지를 적절하게 컴파일하기 위한 내용을 담고있습니다.

이전에 사용했던 코드는 다음과 같았습니다.

from setuptools import setup
import os
from glob import glob

package_name = 'my_package'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share', package_name), glob('launch/*.launch.py'))
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='somebody very awesome',
    maintainer_email='user@user.com',
    description='TODO: Package description',
    license='TODO: License declaration',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'simple_node = my_package.simple:main'
        ],
    },
)

 

 

이 setup.py의 주요 목표는 위 코드로부터 실행 파일을 만드는 것입니다. 이를 위해서는 entry_points 라는 이름의 딕셔너리 타입 변수에다 console_scripts에다 다음과 같은 형식으로 입력해줍니다.

'<executable_name> = <package_name>.<script_name>:main'

 

우리가 이전에 사용했던 코드와 비교해보면...

import os
from glob import glob
from setuptools import setup

package_name = 'my_package'


setup(
    
    #code
    ...
    #code
    
    entry_points={
            'console_scripts': [
                'simple_node = my_package.simple:main'
            ],
        },
    
    #code
    ...
    
)

 

즉, 위 코드에서는 my_package라는 패키지의 simple.py라는 파일로 simple_node라는 이름의 실행 노드를 만들어줍니다.

 

또한, 컴파일 과정 중에 setup.py의 파라미터 중 data_files라는 파라미터를 사용하는 launch 파일들의 파이썬 setup tools에 다음과 같이 정보를 건네줄 수 있습니다.

import os
from glob import glob
from setuptools import setup

package_name = 'my_package'


setup(
    
    #code
    ...
    #code
    
    data_files=[
            ('share/ament_index/resource_index/packages',
                ['resource/' + package_name]),
            ('share/' + package_name, ['package.xml']),
            (os.path.join('share', package_name), glob('launch/*.launch.py'))
        ],
    
    #code
    ...
    #code
    
)

 

위 코드의 목적은 launch files를 설치하기 위함입니다. 위의 예시에서는 my_package라는 패키지 내부의 launch 디렉토리에 있는 모든 launch 파일들(launch/*.launch.py)을  ~/ros2_ws/install/my_package/share/my_package/ 디렉토리에다 설치한다는 것입니다.

 

 

5. ROS2 Nodes

ROS2에서 각의 노드는 하나의 모듈만 다뤄야 합니다(예를 들어, 모터만 제어하는 노드, 라이다만 제어하는 노드 등...). 각 노드는 다른 방식을 사용하여 다른 노드와 통신할 수 있습니다.

 

https://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Understanding-ROS2-Nodes/Understanding-ROS2-Nodes.html

 

 


예제 2.2

 

아까 위에서 사용했던 my_package의 simple.py를 수정합니다.

import rclpy
from rclpy.node import Node

class MyNode(Node):
    def __init__(self):
        # super()를 호출하여 Node object를 초기화
        # 매개변수로 노드의 이름 전달
        super().__init__('Test_node')
        # 2개의 파라미터를 받는 타이머 생성
        # - 콜백 사이의 시간 (0.2초)
        # - 콜백때마다 호출할 함수 (timer_callback)
        self.create_timer(0.2, self.timer_callback)
        
    def timer_callback(self):
        # 터미널에 메시지 띄우기
        self.get_logger().info("This message is from Test_node")

def main(args=None):
    # ROS2 communication 초기화
    rclpy.init(args=args)
    # MyNode object 생성
    node = MyNode()
    # ctrl+c 누를 때까지 노드 실행
    rclpy.spin(node)
    # ROS2 communication 종료
    rclpy.shutdown()

if __name__ == '__main__':
    main()

 

 

빌드 및 source 후, 해당 노드를 실행해봅니다.

$ cb
$ sb
$ ros2 launch my_package my_package_launch_file.launch.py

 

 

다음 명령으로 node 목록을 확인할 수 있습니다.

$ ros2 node list

 

 

또한, 다음 명령으로 node의 정보를 확인할 수 있습니다.

$ ros2 node info /Byakugan

 

 

 

6. Client Libraries

지금까지 위의 예제들에서 Client Libraries를 사용했었습니다. ROS1의 rospy, roscpp에 대응되는 것이라고 보시면 됩니다. ROS2에서는 ROS Client Library(RCL)이라고 하며, 다양한 ROS API가 필요로 하는 기본적인 기능들이 구현되어있습니다.

- rclcpp = C++ client library

- rclpy = Python client library