# 단일 노드 Launch 예시

#### 단일 노드 Launch의 개념

ROS2 Humble에서 Launch 시스템을 통해 단일 노드를 구동하는 방법은 과거 ROS1의 방식과는 다르게 파이썬 스크립트를 활용하는 방식으로 주로 이루어진다. 이 스크립트는 필요한 노드를 선언하고, 설정할 파라미터를 지정하고, 실행 순서 및 조건 등을 제어하는 역할을 맡는다. 다음과 같은 형태로 하나의 노드만 실행하는 간단한 예시를 들어볼 수 있다.

#### Launch 파일을 구성하는 기본 요소

ROS2의 Launch 시스템에서 단일 노드를 구동하기 위해서는 크게 다음 요소를 정의해야 한다.

1. **LaunchDescription**
   * LaunchDescription 객체는 하나 이상의 액션(Actions)들을 포함한다. 액션이란 노드를 실행하거나 종료하는 명령 등을 포함한다.
2. **Node**
   * launch\_ros.actions에 정의된 Node 객체를 통해 실제로 실행하려는 노드(패키지와 실행 파일, 혹은 엔트리포인트 스크립트)를 지정한다.
3. **파라미터 설정**
   * Node 내에 parameters 인자를 사용하여 파라미터 파일 경로를 지정할 수 있다.
   * 또는, 파이썬 딕셔너리 형태로 직접 파라미터를 삽입할 수도 있다.
4. **기타 설정**
   * remappings, arguments 등의 기타 옵션을 줄 수도 있다.
   * 특정 조건(조건부 액션)을 달아서 노드를 선택적으로 실행할 수도 있다.

#### 단일 노드 Launch 예시 코드

아래는 ROS2 Humble에서 단일 노드를 실행하기 위한 Launch 스크립트의 단순화된 예시이다.

```python
import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    # 파라미터 파일 경로 설정(필요 시 사용)
    example_params = os.path.join(
        get_package_share_directory('my_package'),
        'config',
        'example_params.yaml'
    )

    # Node 액션 정의
    my_node = Node(
        package='my_package',
        executable='my_executable',
        name='my_single_node',
        output='screen',
        parameters=[example_params]  # 혹은 {'param_name': value} 형태
    )

    # LaunchDescription에 Node를 액션으로 추가
    return LaunchDescription([
        my_node
    ])
```

위 예시에서는 `my_package`라는 패키지 내 `config` 폴더에 있는 `example_params.yaml` 파일을 파라미터로 로드한다. 만약 파라미터 파일 없이 직접 파라미터를 코드에서 선언하고 싶다면, `parameters` 인자 부분에 아래와 같이 삽입할 수 있다.

```python
parameters=[{'my_int_param': 10, 'my_string_param': 'hello'}]
```

#### 파라미터 파일 예시

ROS2 Humble에서 파라미터를 YAML 형태로 관리할 수 있다. 아래 예시는 `example_params.yaml` 파일의 간단한 형태를 보여준다.

```yaml
my_single_node:
  ros__parameters:
    my_int_param: 10
    my_string_param: "hello"
```

여기서 `my_single_node`는 `Node` 객체 생성 시 `name` 인자로 지정한 노드 이름과 동일해야 한다. 이 설정을 잘못하면 런타임에 파라미터를 로드하지 못할 수 있다.

#### 파라미터와 수식 예시

단일 노드에서 파라미터로 넘길 값이 단순 스칼라(정수, 실수, 문자열 등)만 있는 것은 아니다. 상황에 따라서는 $\mathbf{x}$와 같은 벡터나 2차원 행렬 등 다양한 데이터 구조를 넣어야 하는 경우도 있다. 예를 들어 어떤 알고리즘에 필요한 관측값이 벡터일 때, 이를 파라미터로 전달하여 노드가 실행되도록 구성할 수 있다.

아래와 같은 벡터가 있다고 가정해 보자.

$$
\mathbf{x} = \begin{bmatrix} x\_1 \ x\_2 \end{bmatrix}
$$

ROS2 YAML 파라미터 형식으로는, 벡터(혹은 리스트 형태)의 데이터를 아래와 같이 넣을 수 있다.

```yaml
my_single_node:
  ros__parameters:
    my_vector_param: [1.0, 2.0]
```

노드 내부에서는 이를 Python 리스트 등으로 받아 알고리즘에 활용할 수 있다.

#### Launch 파일 실행 방법

ROS2에서는 launch 파일을 `$ ros2 launch` 명령으로 실행한다. 위에서 작성한 Launch 파일이 `my_single_launch.py`라는 이름으로 `my_package/launch` 폴더에 있다면, 다음과 같이 터미널에서 실행할 수 있다.

```bash
ros2 launch my_package my_single_launch.py
```

정상적으로 실행되면, `my_single_node`가 시작되며 터미널 화면에 해당 노드가 출력하는 로그가 표시된다.

#### 다른 Launch 액션과의 연동

ROS2 Launch 시스템에서는 단일 노드만 실행하는 예시도 중요하지만, 때때로 추가 액션을 함께 배치하여 실행 순서나 환경 설정 등을 제어해야 할 때가 있다. 단일 노드 자체는 간단하게 Node 액션 하나로 구성할 수 있지만, 필요에 따라 다음과 같은 액션을 활용한다.

* **SetEnvironmentVariable**: 노드를 실행하기 전에 특정 환경 변수를 설정하거나 수정할 수 있다.
* **IncludeLaunchDescription**: 다른 Launch 파일을 재사용하거나, 단일 노드를 실행하기 전에 공통 리소스를 로드할 수 있다.
* **OpaqueFunction**: 고급 로직을 LaunchDescription에 삽입해, 특정 조건이 충족되었을 때만 노드를 실행하거나 파라미터를 동적으로 수정할 수 있다.

예를 들어, Node 액션을 호출하기 전 특정 환경 변수를 지정해야 한다면 다음처럼 구성할 수 있다.

```python
from launch.actions import SetEnvironmentVariable
from launch_ros.actions import Node
from launch import LaunchDescription

def generate_launch_description():

    set_env_action = SetEnvironmentVariable(
        name='MY_ENV_VAR', value='some_value'
    )

    my_node = Node(
        package='my_package',
        executable='my_executable',
        name='my_single_node',
        output='screen'
    )

    return LaunchDescription([
        set_env_action,
        my_node
    ])
```

이렇게 하면 `my_node`가 실행될 때 `MY_ENV_VAR=some_value` 환경 변수가 설정된 상태로 동작하게 된다.

#### 파라미터 동적 설정과 OpaqueFunction

단일 노드라 할지라도, 실행 시점에 파라미터를 동적으로 결정하고 싶은 상황이 있을 수 있다. 이럴 때는 `launch.actions.OpaqueFunction`을 사용할 수 있다. 다음과 같은 절차로 활용 가능하다.

1. **OpaqueFunction 정의**
   * Python 함수를 정의하고, 해당 함수 내에서 파라미터를 동적으로 계산하거나 파일 경로를 결정한 뒤 Node 액션의 파라미터 부분에 반영한다.
2. **Node 액션 생성 시점**
   * OpaqueFunction 안에서 Node 객체를 생성하거나, LaunchDescription에 추가할 액션을 반환할 수 있다.

예시 스니펫은 다음과 같다.

```python
from launch import LaunchDescription
from launch.actions import OpaqueFunction
from launch_ros.actions import Node

def dynamic_configuration(context, *args, **kwargs):
    # 상황에 따라 파라미터를 동적으로 결정
    dynamic_param_value = 42  # 예시로 정적 값
    node_action = Node(
        package='my_package',
        executable='my_executable',
        name='my_single_node',
        parameters=[{'dynamic_param': dynamic_param_value}],
        output='screen'
    )
    return [node_action]

def generate_launch_description():
    return LaunchDescription([
        OpaqueFunction(function=dynamic_configuration)
    ])
```

이 예시에서 `dynamic_configuration` 함수 내부에서 노드를 동적으로 생성하고, 파라미터로 `dynamic_param=42`를 지정한다. 물론 실제 활용 예시에서는 외부 요인(환경 변수, 현재 시간, 특정 토픽 메시지 등)을 참고해 파라미터를 정할 수도 있다.

#### 노드 종료 시점 제어

단일 노드가 일정 시간이 지난 뒤 자동으로 종료되어야 하거나, 특정 조건이 충족되면 종료시키고 싶다면, Launch 시스템에서 타이머 기반 액션이나 이벤트 핸들러를 등록할 수 있다.

* **TimerAction**: 정해진 시간 후에 특정 액션을 실행할 수 있다. 이를 이용해 노드 종료 동작을 연결할 수 있다.
* **RegisterEventHandler**: NodeExited, Shutdown, 로깅 핸들러 등 다양한 이벤트를 통해 런치 시스템 내에서 특정 흐름을 제어한다.

아래는 단일 노드를 10초 후에 강제로 종료하는 간단한 예시이다.

```python
from launch.actions import TimerAction, Shutdown
from launch_ros.actions import Node
from launch import LaunchDescription

def generate_launch_description():
    my_node = Node(
        package='my_package',
        executable='my_executable',
        name='my_single_node',
        output='screen'
    )

    # 10초 후 Shutdown 액션 발생
    shutdown_after_10s = TimerAction(
        period=10.0,
        actions=[
            Shutdown()
        ]
    )

    return LaunchDescription([
        my_node,
        shutdown_after_10s
    ])
```

실행 후 10초가 지나면 `Shutdown()` 액션이 호출되어, Launch 시스템이 종료된다. 이에 따라 `my_single_node` 역시 종료 처리된다.

#### Launch 조건부 실행

단일 노드를 실행하되, 특정 조건이 만족될 때에만 노드를 띄우고 싶다면 ROS2 Launch 시스템의 조건부 실행(Condition) 기능을 사용할 수 있다. 예를 들어, 다음과 같은 흐름으로 구현할 수 있다.

* **조건 객체**: `IfCondition`, `UnlessCondition` 등.
* **Node 액션**에 `condition` 인자를 지정.

아래 예시는 `--enable-node true`와 같은 커맨드라인 인자를 통해 노드 실행 여부를 결정하는 코드이다.

```python
import os
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.conditions import IfCondition
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node

def generate_launch_description():
    enable_node_arg = DeclareLaunchArgument(
        'enable_node',
        default_value='false',
        description='Enable my_single_node if true'
    )

    my_node = Node(
        package='my_package',
        executable='my_executable',
        name='my_single_node',
        output='screen',
        condition=IfCondition(LaunchConfiguration('enable_node'))
    )

    return LaunchDescription([
        enable_node_arg,
        my_node
    ])
```

이제 Launch 파일 실행 시 아래와 같이 인자를 넘길 수 있다.

```bash
ros2 launch my_package my_single_launch.py enable_node:=true
```

인자를 `true`로 설정해야 `my_single_node`가 실행된다.

#### 단일 노드 Remapping

ROS2에서 노드 실행 시, 토픽이나 서비스 이름을 다른 이름으로 바꿔야 하는 경우가 빈번히 발생한다. 이를 **remapping**이라 하며, 단일 노드의 Launch 파일에서도 간단히 설정할 수 있다. 예시는 다음과 같다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    my_node = Node(
        package='my_package',
        executable='my_executable',
        name='my_single_node',
        output='screen',
        remappings=[
            ('/original_topic', '/remapped_topic'),
            ('/original_service', '/remapped_service'),
        ]
    )

    return LaunchDescription([
        my_node
    ])
```

실행 시에는, `my_single_node`에서 `/original_topic` 대신 `/remapped_topic`으로 통신을 하게 된다.

#### 단일 노드와 Log Level 설정

단일 노드에서 로깅 수준을 간편히 지정하고 싶다면 `arguments=['--ros-args', '--log-level', 'DEBUG']`와 같이 추가하면 된다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    my_node = Node(
        package='my_package',
        executable='my_executable',
        name='my_single_node',
        output='screen',
        arguments=[
            '--ros-args',
            '--log-level',
            'DEBUG'
        ]
    )

    return LaunchDescription([my_node])
```

실행 후 노드의 로그 메시지는 DEBUG 레벨 이상이 모두 출력될 것이다.

#### 다중 동작 환경에서의 단일 노드 Launch

실제 프로젝트에서는 다수의 노드가 복합적으로 동작하지만, 특정 컴포넌트만 단독 테스트하고 싶을 때가 있다. 이때 동일 패키지 혹은 다른 패키지의 노드들을 모두 구동하는 대신, **단일 노드 Launch** 파일을 별도로 마련하여 문제 부분만 간단히 디버그할 수 있다.

* 공용 파라미터: 기존 시스템에서 사용하던 파라미터 파일 중 필요한 부분만 추출해 YAML 파일로 구성한다.
* 의존 토픽: 의존 토픽이 있는 노드라면, 테스트를 위해 필요한 Mock 노드를 같이 실행하거나, 토픽 없이도 동작하도록 노드 소스 코드를 조정할 수 있다.

아래는 예시로, 특정 센서 노드만 단독 실행하기 위해 공용 센서 파라미터만 인자로 설정하고, 나머지는 전부 배제하는 경우다.

```python
import os
from launch import LaunchDescription
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():
    sensor_params = os.path.join(
        get_package_share_directory('my_sensors'),
        'config',
        'sensor_only.yaml'
    )

    sensor_node = Node(
        package='my_sensors',
        executable='sensor_driver',
        name='sensor_driver_node',
        parameters=[sensor_params],
        output='screen'
    )

    return LaunchDescription([sensor_node])
```

이렇게 하면 오직 센서 노드만 실행되어, 주 노드들과 통신 없이도 독립적인 디버깅을 수행할 수 있다.

#### 단일 노드와 Debug 활용

개발 과정에서 단일 노드를 디버깅하려면, ROS2 Launch 시스템에서 디버깅 인자나 환경 변수를 설정하는 방법이 유용하다. 예를 들어, gdb나 lldb 같은 디버거를 사용하는 경우, 노드 실행 앞에 디버거를 붙일 수도 있다.

```python
from launch import LaunchDescription
from launch.actions import ExecuteProcess
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node

def generate_launch_description():

    debug_node = ExecuteProcess(
        cmd=[
            'gdb',
            '--ex', 'run',
            '--args',
            'ros2',
            'run',
            'my_package',
            'my_executable'
        ],
        output='screen'
    )

    return LaunchDescription([
        debug_node
    ])
```

위 예시는 `Node` 액션 대신 `ExecuteProcess` 액션을 직접 사용하여 gdb를 실행한 뒤, `my_executable`를 실행하는 형태이다. 물론 실제 디버깅 시에는 디버거 명령어를 조금 더 정교하게 구성하거나, 필요한 인자를 추가로 설정할 수 있다.

#### 단일 노드의 Lifecycle 관리

ROS2에는 노드가 활성화, 비활성화, 종료 등 상태를 가질 수 있는 “Lifecycle 노드” 개념이 있다. 일반적인 Node 액션과 달리 Lifecycle 노드를 사용하면 상태 전이를 고려해야 한다. 이를 Launch 파일에서 제어하기 위해서는 추가 로직이나 Lifecycle 관리 액션이 필요하다.

아래는 기본적인 Lifecycle 노드 액션 실행 예시이다.

```python
from launch import LaunchDescription
from launch_ros.actions import LifecycleNode

def generate_launch_description():
    lifecycle_node = LifecycleNode(
        package='my_package',
        executable='my_lifecycle_executable',
        name='my_lifecycle_node',
        output='screen'
    )

    return LaunchDescription([lifecycle_node])
```

여기에 더해, Lifecycle 노드를 특정 상태로 전이시키기 위한 이벤트를 Launch 파일 내에서 트리거하거나, 별도 스크립트로 노드 상태 전이를 호출할 수 있다.

#### 단일 노드에서 매개 변수(파라미터) 파일 다중 사용

경우에 따라 하나의 노드가 여러 개의 파라미터 파일을 동시에 불러야 할 수도 있다. ROS2 Humble의 Launch 시스템에서는 `parameters` 인자에 여러 YAML 파일을 리스트로 나열하여 넘겨줄 수 있다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node
import os

def generate_launch_description():
    config_file_1 = os.path.join('/path', 'to', 'config1.yaml')
    config_file_2 = os.path.join('/path', 'to', 'config2.yaml')

    my_node = Node(
        package='my_package',
        executable='my_executable',
        name='my_single_node',
        parameters=[
            config_file_1,
            config_file_2
        ],
        output='screen'
    )

    return LaunchDescription([my_node])
```

이때 중복되는 파라미터 키가 존재한다면, 뒤에 위치한 파일의 값이 우선 적용될 수 있으므로 주의해야 한다.

#### 파라미터 다형성: 스칼라부터 행렬까지

파라미터로 전달되는 값이 단순한 스칼라 형(정수, 실수, 문자열 등)부터, 벡터, 행렬, 또는 중첩된 리스트 형태일 수도 있다. 예를 들어, 아래와 같이 2차원 행렬을 입력해야 하는 상황을 가정해보자.

수학적으로 다음과 같은 행렬이 필요하다고 하자.

M=\[1234]M = \begin{bmatrix} 1 & 2 \ 3 & 4 \end{bmatrix}

YAML 파일에는 다음처럼 정의할 수 있다.

```yaml
my_single_node:
  ros__parameters:
    my_matrix_param: [[1.0, 2.0], [3.0, 4.0]]
```

이렇게 하면, 파이썬 코드 내부에서 `my_matrix_param`은 `[[1.0, 2.0], [3.0, 4.0]]` 형태의 list of lists로 인식될 것이다.

#### mermaid 예시로 보는 단일 노드 Launch 흐름

실제 Launch 파일 실행 과정을 간단히 다이어그램으로 표현해보면 다음과 같다.

{% @mermaid/diagram content="flowchart TB
A\[사용자] --> B\[ros2 launch 실행]
B --> C\[Launch 파일 로딩]
C --> D\[Node 액션 생성]
D --> E\[파라미터 로드/적용]
E --> F\[단일 노드 구동 시작]
F --> G\[실행 중 로그 모니터링]" %}

* **사용자**가 `ros2 launch` 명령을 실행하면,
* **Launch 파일**이 로딩되고 필요한 액션(노드 실행)이 준비된다.
* **Node 액션**이 생성되면서 파라미터가 적용되고,
* 단일 노드가 실행되면,
* 사용자는 로그를 통해 노드 동작 상태를 파악할 수 있다.

#### 기타 고려 사항

* **호환성**: ROS2의 다양한 버전(Foxy, Galactic, Humble 등) 사이에서 Launch와 파라미터 API가 조금씩 다를 수 있다. Humble 기준 예시를 사용할 때, 다른 버전에서는 일부 문법 차이나 함수명이 다를 수 있으니 주의한다.
* **보안 설정**: ROS2 보안(Enclave, DDS 보안 등)을 활성화한 환경에서는 Node 실행 시 인증서나 보안 설정이 필요할 수 있다. 이때, Launch 파일에서 관련 설정(예: `--ros-args --enclave`)을 인자로 추가해주는 방식을 사용한다.
