ROS2 에서는 하나의 노드를 실행시키기 위해서는 ros2 run
명령어를 사용한다. 이 명령어만으로도 노드를 실행시키는 것에는 큰 문제가 없다. 하지만 ROS2 에서는 하나의 노드만을 실행시키는 일보다 복수의 노드를 함께 실행시켜 노드 간의 메시지를 주고받게 되는 경우가 더 많다. 그리고 직접 개발한 패키지의 노드만을 실행하기도 하지만 이미 개발되어 공개된 패키지의 노드들을 사용하는 경우도 많다. 또한 각 노드마다 여러 개의 파라미터를 변경해야 할 때도 있을 것이다.
ROS2 의 Launch는 하나 이상의 정해진 노드를 실행시킬 수 있다. 더불어, 노드를 실행할 때 패키지의 매개변수나 노드 이름 변경, 노드 네임스페이스 설정, 환경변수 변경 등의 옵션을 사용할수 있다. ROS1 에서는 이를 roslaunch라 하여 "*.launch" 파일을 사용하여 실행노드를 설정하는데 이는 XML 기반이었으며, 여러 태그별 옵션을 제공하여 사용자 편의성을 제공하였다. ROS2에서는 기존 XML 방식 이외에도 파이썬 프로그래밍 방식도 추가되어 확장성을 높였다. 이번 장에서는 기존 XML방식보다 더 활용도가 높아진 파이썬 방식을 설명할 것이다.
topic_service_action_rclpy_example 패키지에 새로운 launch 파일을 만들어보자. 이 런치 파일은 기본적으로 argument 노드와 calculator 노드를 실행시키는 역할을 하게 되며 두 노드에서 사용할 파라미터가 설정된 파일을 불러오는 역할을 하게 된다.
런치 파일을 사용하기 위해서는 해당 패키지에 launch 폴더가 있어야 하며 이 폴더에 런치 파일(*.launch.py)을 만들어 사용한다. 여기서 설명할 arithmetic.launch.py 파일의 전체 소스코드는 다음과 같다.
#!/usr/bin/env python3
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
param_dir = LaunchConfiguration(
'param_dir',
default=os.path.join(
get_package_share_directory('topic_service_action_rclpy_example'),
'param',
'arithmetic_config.yaml'))
return LaunchDescription([
DeclareLaunchArgument(
'param_dir',
default_value=param_dir,
description='Full path of parameter file'),
Node(
package='topic_service_action_rclpy_example',
executable='argument',
name='argument',
parameters=[param_dir],
output='screen'),
Node(
package='topic_service_action_rclpy_example',
executable='calculator',
name='calculator',
parameters=[param_dir],
output='screen'),
])
Launch 파일에서는 다음과 같이 generate_launch_description 메소드를 이용하는게 기본이다. 해당 메소드는 LaunchConfiguration 클래스를 이용하여 실행 관련 설정을 초기화하고, 리턴값으로 LaunchDescription 클래스를 반환한다.
def generate_launch_description():
xxx = LaunchConfiguration(yyy)
return LaunchDescription([
DeclareLaunchArgument(aaa),
Node(bbb),
Node(ccc),
])
LaunchConfiguration 클래스의 생성자를 통해서 파라미터 파일이 저장된 경로를 설정한다.
def generate_launch_description():
param_dir = LaunchConfiguration(
'param_dir',
default=os.path.join(
get_package_share_directory('topic_service_action_rclpy_example'),
'param',
'arithmetic_config.yaml'))
다음으로 LaunchDescription 반환 구문이다. DeclareLaunchArgument 클래스를 이용하여 위에서 설정한 param_dir 변수를 런치 인수로 선언한다.
그 뒤 Node 클래스로 실행할 노드를 설정하게 된다. 기본적으로 package, executable, name, parameters, output 을 설정해 준다. 각 옵션은 다음과 같다.
- package : 실행할 패키지 이름을 기재하며 된다.
- executable : 실행 가능한 노드의 이름을 기재하면 된다.
- name : 지정한 노드를 실행할 때 실제로 사용할 이름을 기재하면 된다. 보통은 executable에 기재한 본래의 노드 이름을 주지만 필요하다면 다른 이름으로도 설정할 수 있다.
- parameters : 특정 파라미터 값을 넣어줘도 되고 DeclareLaunchArgument에서 지정한 변수를 사용해도 된다. 여기서는 param_dir 변수를 사용하여 지정된 파라미터 파일을 사용하게 된다.
- output : 로깅 설정으로 기본적으로 특정 파일 이름에 로깅 정보가 기록되고 터미널 창에 출력하고 싶다면 screen 이라고 지정하면 된다.
다음의 런치 파일을 실행하게 되면 topic_service_action_rclpy_example 패키지의 argument 노드와 calculator 노드가 함께 실행된다.
return LaunchDescription([
DeclareLaunchArgument(
'param_dir',
default_value=param_dir,
description='Full path of parameter file'),
Node(
package='topic_service_action_rclpy_example',
executable='argument',
name='argument',
parameters=[param_dir],
output='screen'),
Node(
package='topic_service_action_rclpy_example',
executable='calculator',
name='calculator',
parameters=[param_dir],
output='screen'),
])
앞에서는 사용하지 않아 설명 못한 유용한 기능이 있는데 바로 remapping 기능이다. 이는 고유 이름을 변경하는 것으로 다음 예제와 같이 arithmetic_argument 토픽 이름을 argument 토픽 이름으로 변경할 수 있다. 내부 코드 변경 없이 토픽, 서비스, 액션 등의 고유 이름을 변경할 수 있는 유용한 기능이니 알아두자.
Node(
package='topic_service_action_rclpy_example',
executable='argument',
name='argument',
remappings=[
('/arithmetic_argument', '/argument'),
]
launch의 유용한 기능을 하나 더 알아보자. 이번에는 namespace 이다. 노드, 토픽, 서비스, 액션, 파라미터 등과 같은 고유 이름은 네임스페이스를 통해 접두사를 붙일 수 있고, 이를 통해 독립적으로 자신만의 네트워크 그룹화를 할 수 있다고 했다. 해당 기능은 각 노드를 실행시킬 때 ROS2의 실행 인자 중 하나인 "--ros-args -r __ns:="를 이용하는 방법이 있고, launch파일로 실행시킬 때 namespace라는 항목을 변경하는 방법이 있다. 이번에는 launch 파일로 실행시킬 때 namespace를 변경하는 방법을 알아보자.
우선 LaunchConfiguration와 DeclareLaunchArgument 을 통해 원하는 namespace를 선언한다. 다음 예제에서는 환경변수로 지정한 ROS_NAMESPACE 변수를 읽어오도록 했는데 실행할 때 "export ROS_NAMESPACE = robot_1"과 같은 명령어를 터미널 창에 입력하거나 ~/.bashrc 에 미리 등록시켜 놓아도 좋다. 그 뒤 다음과 같이 Node 클래스를 사용할 때 namespace를 지정하면 실행 시 모든 노드 이름과 노드에 포함된 토픽, 서비스, 액션, 파라미터 등 고유 이름 모두가 변경되게 된다. 이 namespace는 복수의 로봇이 동일 프로그램을 이용할 때 고유 이름을 사용함에 있어서 중복됨을 피할 수 있어서 그 결과 데이터를 구분지어 사용할 수 있게 된다.
def generate_launch_description():
ros_namespace = LaunchConfiguration('ros_namespace')
return LaunchDescription([
DeclareLaunchArgument(
'ros_namespace',
default_value=os.environ['ROS_NAMESPACE'],
description='Namespace for the robot'),
Node(
package='topic_service_action_rclpy_example',
namespace=ros_namespace,
executable='argument',
name='argument',
output='screen'),
만약 런치 파일의 generate_launch_description 함수의 return 값이 너무 많아진다고 여겨진다면 LaunchDescritpion 의 add_action 함수를 이용할 수 있다. 이를 사용하면 런치 파일이 다음과 같이 간결해지니 참고하도록하자.
#!/usr/bin/env python3
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
param_dir = LaunchConfiguration(
'param_dir',
default=os.path.join(
get_package_share_directory('topic_service_action_rclpy_example'),
'param',
'arithmetic_config.yaml'))
launch_description = LaunchDescription()
launch_description.add_action(launch.actions.DeclareLaunchArgument(
'param_dir',
default_value=param_dir,
description='Full path of parameter file')
argument_node = Node(
package='topic_service_action_rclpy_example',
executable='argument',
name='argument',
parameters=[param_dir],
output='screen')
calculator_node = Node(
package='topic_service_action_rclpy_example',
executable='calculator',
name='calculator',
parameters=[param_dir],
output='screen')
launch_description.add_action(argument_node)
launch_description.add_action(calculator_node)
return launch_description
마지막으로 런치 파일에서 현재 패키지의 다른 런치 파일이나 다른 패키지의 런치 파일을 불러오는 방법에 대해 알아보겠다.
현재 패키지가 A패키지일때 동일한 패키지의 x.launch.py와 y.launch.py 런치 파일을 IncludeLaunchDescription 를 이용하면 불러올 수 있다. 그리고 다른 패키지인 B패키지의 z.launch.py 런치파일은 IncludeLaunchDescription를 이용하는 것은 동일하지만 get_package_share_directoy 함수를 이용하여 B패키지명을 함께 입력해주는 것으로 특정 다른 패키지의 런치 파일을 불러올 수 있다. 이 내용을 런치 파일로 작성하면 다음 예제와 같다.
런치 파일에서 다른 런치 파일을 불러오는 것은 런치 파일의 모듈화 성격을 띄고 있어서 하나의 런치 파일로 동일 패키지의 노드 실행뿐만이 아닌 다른 패키지의 런치 파일도 불러와서 실행 시킬 수 있는 장점이 있다. 특히, 직접 작성한 패키지가 아닌 패키지를 바이너리로 설치하고 실행하고자 했을 때에 별도 수정없이 해당 패키지의 런치파일만 본인이 작성한 패키지에서 불러와 사용할 수 있기에 널리 사용되고 있다. 매우 유용한 기능이니 참고하도록 하자.
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.actions import LogInfo
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import ThisLaunchFileDir
def generate_launch_description():
return LaunchDescription([
LogInfo(msg=['Execute three launch files!']),
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[ThisLaunchFileDir(), '/xxxxx.launch.py']),
),
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[ThisLaunchFileDir(), '/yyyyy.launch.py']),
),
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[get_package_share_directory('bbbbb'), '/launch/zzzzz.launch.py']),
),
])
Launch 파일은 Python 파일 이기에 빌드 자체가 필요 없기는 하지만 해당 파일을 ROS 2 에코시스템 환경에서 사용하기 위해서는 패키지 빌드를 통해 정해진 위치에 설치를 해야만 한다. Launch 파일 관련해서는 C++ 언어를 사용하는 RCLCPP 패키지 계열이냐 Python 언어를 사용하는 RCLPY 패키지 계열이냐에 따라 좀 다르니 구분하여 설명하겠다.
C++ 언어를 사용하는 경우 다음과 같이 빌드 설정 파일(CMakeLists.txt)의 install 구문에 launch 폴더명만 기재하면 된다.
install(DIRECTORY
launch
DESTINATION share/${PROJECT_NAME}/
)
파이썬 언어를 사용하는 경우 다음과 같이 패키지 설정 파일(setup.py)의 data_files 옵션 부분에 launch 옵션을 지정하면 된다. 이를 통해 "*.launch.py" 파일명을 가진 런치 파일들이 install 폴더의 각 패키지 launch 폴더 안에 생성되어 사용할 수 잇게 된다.
setup(
name=package_name,
version='0.1.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/ament_index/resource_index/packages', ['resource/' + package_name]),
(share_dir, ['package.xml']),
(share_dir + '/launch', glob.glob(os.path.join('launch', '*.launch.py'))),
(share_dir + '/param', glob.glob(os.path.join('param', '*.yaml'))),
],
지금까지 launch파일에 대해 알아보았다.
여기까지 ROS2 의 전체적인 통신 방식인 토픽, 서비스, 액션 그리고 추가적인부분까지 프로그래밍에 대해 알아보았다. 다음시간부터는 심화 프로그래밍에 대해 알아보는 시간을 가지겠다.