액션 개념

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

액션의 주요 구성 요소는 다음과 같다: - Goal: 클라이언트가 서버에 요청하는 작업 목표 - Result: 서버에서 작업 완료 후 반환하는 결과 - Feedback: 서버가 작업 중간에 클라이언트로 보내는 진행 상황 정보 - Cancel: 클라이언트가 작업을 중단 요청할 때 사용하는 명령

액션 서버 구현 예제 (Python)

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

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)

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

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와 같은 메시지를 통해 통신한다. 이 메시지의 정의는 아래와 같다:

# Goal
int32 order

---
# Result
int32[] sequence

---
# Feedback
int32[] partial_sequence

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

액션 서버의 상태 관리

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

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

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 상태로 전환된다.

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 메소드를 사용한다. 취소 요청을 서버로 보내면 서버는 목표를 중단하고 그 결과를 클라이언트로 전송한다.

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

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
int32[] sequence

---
# Feedback
int32[] partial_sequence

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

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

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

다중 액션 서버 예제

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

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_1fibonacci_2)를 각각 다루며, 동일한 피보나치 수열을 계산하는 작업을 수행한다. 각 서버는 개별적으로 실행되며, 클라이언트는 두 서버 중 하나에 작업을 요청할 수 있다.

다중 클라이언트 구조

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

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)에 각각 다른 목표를 전송하고, 피드백과 결과를 각각 받아서 처리한다. 이를 통해 클라이언트는 여러 서버와의 통신을 동시에 관리할 수 있다.

다중 액션 통신 다이어그램

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

graph LR; A[Client 1] --> B[Action Server 1]; A --> C[Action Server 2]; D[Client 2] --> B; D --> C;

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

다중 액션의 사용 사례

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