# 액션의 실제 사용 예제

#### 액션 개념

ROS2의 액션(Action)은 노드 간 비동기 통신을 위한 중요한 개념이다. 특히, 긴 시간 동안 수행되는 작업에 대한 상태 정보를 주고받을 때 유용하다. 액션 서버는 특정 작업을 수행하며, 클라이언트는 해당 작업을 요청하고 그 진행 상태나 결과를 받는다.

액션의 주요 구성 요소는 다음과 같다:

* **Goal**: 클라이언트가 서버에 요청하는 작업 목표
* **Result**: 서버에서 작업 완료 후 반환하는 결과
* **Feedback**: 서버가 작업 중간에 클라이언트로 보내는 진행 상황 정보
* **Cancel**: 클라이언트가 작업을 중단 요청할 때 사용하는 명령

#### 액션 서버 구현 예제 (Python)

다음은 ROS2 액션 서버의 간단한 예제이다. 이 예제에서는 액션 서버가 주어진 목표에 따라 값을 처리하고, 결과를 반환하는 동작을 수행한다.

```python
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
from example_interfaces.action import Fibonacci
from rclpy.executors import MultiThreadedExecutor

class FibonacciActionServer(Node):

    def __init__(self):
        super().__init__('fibonacci_action_server')
        self._action_server = ActionServer(
            self,
            Fibonacci,
            'fibonacci',
            self.execute_callback)

    def execute_callback(self, goal_handle):
        self.get_logger().info('Executing goal...')
        feedback_msg = Fibonacci.Feedback()
        feedback_msg.partial_sequence = [0, 1]

        for i in range(1, goal_handle.request.order):
            feedback_msg.partial_sequence.append(
                feedback_msg.partial_sequence[i] + feedback_msg.partial_sequence[i-1])
            self.get_logger().info(f'Feedback: {feedback_msg.partial_sequence}')
            goal_handle.publish_feedback(feedback_msg)
            
        goal_handle.succeed()
        result = Fibonacci.Result()
        result.sequence = feedback_msg.partial_sequence
        return result

def main(args=None):
    rclpy.init(args=args)
    action_server = FibonacciActionServer()
    executor = MultiThreadedExecutor()
    rclpy.spin(action_server, executor=executor)

if __name__ == '__main__':
    main()
```

이 예제는 **피보나치** 수열을 계산하는 액션 서버를 보여준다. 클라이언트가 특정 목표인 `order`를 요청하면 서버는 해당 `order`에 맞는 피보나치 수열을 계산하고, 그 과정을 **Feedback** 메시지로 클라이언트에 전송한다. 작업이 완료되면 서버는 **Result**로 최종 수열을 반환한다.

#### 액션 클라이언트 구현 예제 (Python)

이제, 위의 서버와 통신할 클라이언트를 구현해 보자. 클라이언트는 액션 서버에 목표를 보내고, 서버로부터 피드백과 결과를 받는다.

```python
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from example_interfaces.action import Fibonacci

class FibonacciActionClient(Node):

    def __init__(self):
        super().__init__('fibonacci_action_client')
        self._action_client = ActionClient(self, Fibonacci, 'fibonacci')

    def send_goal(self, order):
        goal_msg = Fibonacci.Goal()
        goal_msg.order = order
        self._action_client.wait_for_server()
        self._send_goal_future = self._action_client.send_goal_async(
            goal_msg,
            feedback_callback=self.feedback_callback)
        self._send_goal_future.add_done_callback(self.goal_response_callback)

    def goal_response_callback(self, future):
        goal_handle = future.result()
        if not goal_handle.accepted:
            self.get_logger().info('Goal rejected')
            return
        self._get_result_future = goal_handle.get_result_async()
        self._get_result_future.add_done_callback(self.get_result_callback)

    def get_result_callback(self, future):
        result = future.result().result
        self.get_logger().info(f'Result: {result.sequence}')

    def feedback_callback(self, feedback_msg):
        self.get_logger().info(f'Feedback: {feedback_msg.partial_sequence}')

def main(args=None):
    rclpy.init(args=args)
    action_client = FibonacciActionClient()
    action_client.send_goal(order=10)
    rclpy.spin(action_client)

if __name__ == '__main__':
    main()
```

이 클라이언트 코드는 서버로 **order** 값을 보내고, 피드백과 결과를 받는다. `feedback_callback` 함수는 서버로부터 피드백을 받을 때마다 호출되며, 최종 결과는 `get_result_callback`을 통해 받아 처리된다.

#### 메시지 구조

액션은 **Fibonacci**와 같은 메시지를 통해 통신한다. 이 메시지의 정의는 아래와 같다:

```plaintext
# Goal
int32 order

---
# Result
int32[] sequence

---
# Feedback
int32[] partial_sequence
```

각 섹션은 **Goal**, **Result**, **Feedback**으로 나뉘며, 각각의 메시지에는 클라이언트와 서버 간의 데이터가 교환된다.

#### 액션 서버의 상태 관리

액션 서버는 목표를 수락하거나 거부하고, 작업의 성공 또는 실패 여부를 클라이언트에게 전달할 수 있다. 액션 서버는 다음과 같은 상태를 관리한다:

* **Goal Accepted**: 서버가 목표를 수락했음을 의미한다. 이 시점부터 서버는 해당 목표에 대한 작업을 시작한다.
* **Goal Rejected**: 서버가 목표를 거부했음을 의미하며, 클라이언트는 작업을 중단하게 된다.
* **Goal Canceled**: 클라이언트가 작업을 중단하고자 할 때 사용된다.
* **Goal Succeeded**: 목표가 성공적으로 완료되었음을 의미한다.
* **Goal Aborted**: 작업 도중 문제가 발생하여 작업을 중단할 때 사용된다.

다음은 상태 전환 다이어그램이다.

{% @mermaid/diagram content="graph TD;
A\[Goal Received] --> B\[Goal Accepted];
A --> C\[Goal Rejected];
B --> D\[Goal Succeeded];
B --> E\[Goal Aborted];
B --> F\[Goal Canceled];" %}

#### 액션 상태 전환 예제

서버가 목표를 받아들일 때, 액션의 상태는 **Goal Accepted**로 전환된다. 이후 작업이 성공적으로 완료되면 **Goal Succeeded** 상태로 전환되며, 만약 문제가 발생하거나 작업을 중단하게 된다면 **Goal Aborted** 상태로 전환된다. 작업을 취소할 경우에는 **Goal Canceled** 상태로 전환된다.

```python
def execute_callback(self, goal_handle):
    self.get_logger().info('Executing goal...')

    if goal_handle.is_cancel_requested:
        goal_handle.canceled()
        self.get_logger().info('Goal canceled')
        return Fibonacci.Result()

    feedback_msg = Fibonacci.Feedback()
    feedback_msg.partial_sequence = [0, 1]

    for i in range(1, goal_handle.request.order):
        if goal_handle.is_cancel_requested:
            goal_handle.canceled()
            self.get_logger().info('Goal canceled during execution')
            return Fibonacci.Result()

        feedback_msg.partial_sequence.append(
            feedback_msg.partial_sequence[i] + feedback_msg.partial_sequence[i-1])
        self.get_logger().info(f'Feedback: {feedback_msg.partial_sequence}')
        goal_handle.publish_feedback(feedback_msg)
        
    goal_handle.succeed()
    result = Fibonacci.Result()
    result.sequence = feedback_msg.partial_sequence
    return result
```

위의 예제에서 `goal_handle.is_cancel_requested`는 클라이언트가 작업을 취소 요청했는지 여부를 확인하는 부분이다. 만약 취소 요청이 발생하면, `goal_handle.canceled()`로 상태를 변경하고 작업을 중단한다. 반면, 작업이 정상적으로 완료되면 `goal_handle.succeed()`를 호출하여 성공 상태로 전환된다.

#### 클라이언트의 목표 취소 요청

액션 클라이언트에서는 서버에 목표를 보낸 후에 언제든지 작업을 취소할 수 있다. 이를 위해 클라이언트는 **cancel\_goal** 메소드를 사용한다. 취소 요청을 서버로 보내면 서버는 목표를 중단하고 그 결과를 클라이언트로 전송한다.

```python
def cancel_goal(self):
    cancel_future = self._action_client.cancel_goal_async(self._send_goal_future.result())
    cancel_future.add_done_callback(self.cancel_done_callback)

def cancel_done_callback(self, future):
    cancel_response = future.result()
    if len(cancel_response.goals_canceling) > 0:
        self.get_logger().info('Goal successfully canceled')
    else:
        self.get_logger().info('Goal cancel request was rejected')
```

이 예제에서는 클라이언트가 작업을 취소하고 그 결과에 따라 서버가 취소 요청을 수락하거나 거부할 수 있다.

#### 액션의 비동기 처리

ROS2 액션의 가장 큰 장점 중 하나는 비동기 처리를 지원한다는 점이다. 클라이언트는 서버에 작업 목표를 전송한 후, 작업이 완료될 때까지 대기하지 않고 다른 작업을 처리할 수 있다. 서버는 작업의 중간 피드백을 전송하면서 작업이 완료되면 최종 결과를 반환한다.

```python
self._send_goal_future = self._action_client.send_goal_async(
    goal_msg,
    feedback_callback=self.feedback_callback)
self._send_goal_future.add_done_callback(self.goal_response_callback)
```

이 코드에서 `send_goal_async`는 목표를 비동기적으로 서버에 전송한다. 결과나 피드백을 받을 때마다 지정된 콜백 함수가 호출되며, 클라이언트는 작업 완료까지 기다릴 필요 없이 다른 작업을 수행할 수 있다.

#### 결과와 피드백의 차이

* **결과 (Result)**: 작업이 완전히 완료된 후 서버가 클라이언트로 반환하는 데이터이다. 작업의 최종 상태나 결과가 포함된다.
* **피드백 (Feedback)**: 작업이 진행 중일 때, 서버가 클라이언트로 전송하는 중간 상태 정보이다. 클라이언트는 이를 통해 작업이 어느 정도 완료되었는지 확인할 수 있다.

```plaintext
# Result
int32[] sequence

---
# Feedback
int32[] partial_sequence
```

**Result** 메시지는 작업의 최종 결과로 피보나치 수열 전체를 포함하며, **Feedback** 메시지는 작업 중간에 계산된 부분 수열을 포함한다.

#### 다중 액션 서버 및 클라이언트 구조

ROS2에서 여러 개의 액션 서버를 사용하거나, 여러 클라이언트가 동시에 하나의 서버에 액션을 요청할 수 있다. 다중 액션 서버 및 클라이언트를 사용하는 상황에서는 서버와 클라이언트 간의 통신이 동시에 이루어지며, 각각의 상태를 관리해야 한다.

**다중 액션 서버 예제**

여기서는 다중 액션 서버를 구현하여, 클라이언트가 여러 서버에 각각 다른 작업을 요청하고, 각 작업의 진행 상태와 결과를 관리하는 방법을 설명한다.

```python
class MultiActionServer(Node):

    def __init__(self):
        super().__init__('multi_action_server')

        # 첫 번째 액션 서버
        self._action_server_1 = ActionServer(
            self,
            Fibonacci,
            'fibonacci_1',
            self.execute_callback_1)

        # 두 번째 액션 서버
        self._action_server_2 = ActionServer(
            self,
            Fibonacci,
            'fibonacci_2',
            self.execute_callback_2)

    def execute_callback_1(self, goal_handle):
        # 첫 번째 액션 서버의 작업 처리
        self.get_logger().info('Executing goal on server 1...')
        return self._process_goal(goal_handle)

    def execute_callback_2(self, goal_handle):
        # 두 번째 액션 서버의 작업 처리
        self.get_logger().info('Executing goal on server 2...')
        return self._process_goal(goal_handle)

    def _process_goal(self, goal_handle):
        # 피보나치 수열 계산 (공통)
        feedback_msg = Fibonacci.Feedback()
        feedback_msg.partial_sequence = [0, 1]

        for i in range(1, goal_handle.request.order):
            feedback_msg.partial_sequence.append(
                feedback_msg.partial_sequence[i] + feedback_msg.partial_sequence[i-1])
            goal_handle.publish_feedback(feedback_msg)
            
        goal_handle.succeed()
        result = Fibonacci.Result()
        result.sequence = feedback_msg.partial_sequence
        return result
```

이 코드는 두 개의 액션 서버(`fibonacci_1`과 `fibonacci_2`)를 각각 다루며, 동일한 **피보나치 수열**을 계산하는 작업을 수행한다. 각 서버는 개별적으로 실행되며, 클라이언트는 두 서버 중 하나에 작업을 요청할 수 있다.

**다중 클라이언트 구조**

다중 클라이언트를 활용하면 여러 클라이언트가 동시에 액션을 요청하고, 각자의 결과를 받아볼 수 있다. 다중 클라이언트 환경에서는 각 클라이언트가 개별적으로 서버에 요청을 보내고, 그에 따른 피드백과 결과를 수신한다.

```python
class MultiActionClient(Node):

    def __init__(self):
        super().__init__('multi_action_client')

        # 첫 번째 클라이언트
        self._action_client_1 = ActionClient(self, Fibonacci, 'fibonacci_1')

        # 두 번째 클라이언트
        self._action_client_2 = ActionClient(self, Fibonacci, 'fibonacci_2')

    def send_goals(self):
        # 첫 번째 서버로 목표 전송
        self.send_goal(self._action_client_1, order=10)

        # 두 번째 서버로 목표 전송
        self.send_goal(self._action_client_2, order=15)

    def send_goal(self, action_client, order):
        goal_msg = Fibonacci.Goal()
        goal_msg.order = order
        action_client.wait_for_server()
        self._send_goal_future = action_client.send_goal_async(
            goal_msg,
            feedback_callback=self.feedback_callback)
        self._send_goal_future.add_done_callback(self.goal_response_callback)

    def goal_response_callback(self, future):
        goal_handle = future.result()
        if not goal_handle.accepted:
            self.get_logger().info('Goal rejected')
            return
        self._get_result_future = goal_handle.get_result_async()
        self._get_result_future.add_done_callback(self.get_result_callback)

    def get_result_callback(self, future):
        result = future.result().result
        self.get_logger().info(f'Result: {result.sequence}')

    def feedback_callback(self, feedback_msg):
        self.get_logger().info(f'Feedback: {feedback_msg.partial_sequence}')
```

이 예제에서 `MultiActionClient`는 두 개의 서버 (`fibonacci_1`, `fibonacci_2`)에 각각 다른 목표를 전송하고, 피드백과 결과를 각각 받아서 처리한다. 이를 통해 클라이언트는 여러 서버와의 통신을 동시에 관리할 수 있다.

#### 다중 액션 통신 다이어그램

다중 클라이언트가 여러 액션 서버와 통신하는 구조는 아래와 같이 나타낼 수 있다.

{% @mermaid/diagram content="graph LR;
A\[Client 1] --> B\[Action Server 1];
A --> C\[Action Server 2];
D\[Client 2] --> B;
D --> C;" %}

이 다이어그램은 **Client 1**과 **Client 2**가 각각 **Action Server 1**과 **Action Server 2**로 비동기적으로 목표를 전송하고, 각 서버로부터 피드백과 결과를 받는 과정을 나타낸다.

#### 다중 액션의 사용 사례

다중 액션 서버 및 클라이언트는 로봇의 여러 작업을 동시에 관리하는 데 유용하다. 예를 들어, 한 액션 서버는 로봇의 이동 경로를 계획하고, 다른 액션 서버는 특정 작업(예: 물체 탐지)을 수행할 수 있다. 각각의 클라이언트는 서버로부터 작업의 진행 상태를 실시간으로 모니터링하면서 필요한 경우 작업을 취소하거나 새로운 목표를 설정할 수 있다.
