액션 서버와 클라이언트의 역할

ROS2 액션 서버와 클라이언트는 비동기 작업을 수행하기 위해 사용된다. 액션은 서비스와 비슷한 개념이지만, 상태를 관리하고 중간 피드백을 제공하는 등 더 복잡한 작업을 지원한다. 액션 서버는 실제 작업을 처리하는 주체이며, 클라이언트는 액션 서버에 작업을 요청하고 결과를 받는 역할을 한다.

액션 서버 구조

액션 서버는 특정 작업을 처리하는 동안 여러 상태를 관리하고, 클라이언트에게 중간 결과를 피드백하며 최종 결과를 반환하는 구조로 이루어진다. 액션 서버는 다음과 같은 주요 구성 요소를 갖는다:

액션 클라이언트 구조

액션 클라이언트는 액션 서버와 비동기적으로 상호작용하며, 서버에 목표를 전송하고 상태, 피드백, 결과를 비동기적으로 수신한다. 클라이언트는 주로 다음과 같은 단계로 액션을 처리한다:

액션의 상태 관리

액션의 상태는 다양한 단계로 나뉜다. ROS2 액션 서버와 클라이언트는 액션의 진행 상황을 관리하기 위해 상태 변화를 지원한다. 주로 다음과 같은 상태를 갖는다:

액션 통신 과정

sequenceDiagram participant Client participant Server Client->>Server: 목표 전송 (Send Goal) Server-->>Client: 목표 수락 확인 (Goal Accepted) Server-->>Client: 피드백 전송 (Feedback) Server-->>Client: 피드백 전송 (Feedback) Server-->>Client: 최종 결과 전송 (Result)

위 시퀀스 다이어그램은 액션 서버와 클라이언트 간의 통신 과정을 나타낸다. 클라이언트는 목표를 서버에 전송한 후, 서버로부터 피드백과 결과를 수신한다. 이 과정은 비동기적으로 이루어지며, 클라이언트는 서버의 상태에 따라 응답을 받는다.

액션 상태 전이

액션 서버와 클라이언트 간의 통신에서 중요한 부분 중 하나는 액션의 상태 전이이다. 액션은 여러 상태로 전환될 수 있으며, 이는 액션의 수행 여부에 따라 결정된다. 각 상태 전이는 액션의 진행 상태를 클라이언트에게 알려주고, 클라이언트는 그에 따라 적절한 대응을 할 수 있다.

액션의 상태 전이는 다음과 같은 흐름을 따른다:

상태 전이 다이어그램

stateDiagram [*] --> Pending Pending --> Active: 목표 수락 Pending --> Canceled: 목표 취소 Active --> Succeeded: 작업 완료 Active --> Aborted: 작업 실패 Active --> Canceled: 작업 중 취소 Canceled --> [*] Succeeded --> [*] Aborted --> [*]

이 상태 다이어그램은 액션의 상태 전이를 설명한다. 각각의 상태는 작업의 진행 상황에 따라 변하며, 클라이언트는 각 상태에 대해 서버와 상호작용할 수 있다.

ROS2 액션 서버와 클라이언트 구현

액션 서버

액션 서버는 rclcpp 또는 rclpy 라이브러리를 사용하여 구현할 수 있으며, 서버는 클라이언트로부터 목표를 수신하고 처리 후 피드백과 결과를 전송한다. 액션 서버의 주요 구성 요소는 다음과 같다:

예를 들어, C++로 작성된 액션 서버는 다음과 같이 동작한다:

#include <rclcpp/rclcpp.hpp>
#include <example_interfaces/action/fibonacci.hpp>

class FibonacciActionServer : public rclcpp::Node {
public:
  FibonacciActionServer()
  : Node("fibonacci_action_server") {
    action_server_ = rclcpp_action::create_server<Fibonacci>(
      this,
      "fibonacci",
      std::bind(&FibonacciActionServer::handle_goal, this, std::placeholders::_1, std::placeholders::_2),
      std::bind(&FibonacciActionServer::handle_cancel, this, std::placeholders::_1),
      std::bind(&FibonacciActionServer::handle_accepted, this, std::placeholders::_1));
  }

private:
  rclcpp_action::Server<Fibonacci>::SharedPtr action_server_;
};

액션 클라이언트

액션 클라이언트는 서버에 목표를 전송하고 피드백과 결과를 비동기적으로 수신하는 역할을 한다. 클라이언트는 주로 send_goal 함수와 콜백을 통해 서버와 상호작용한다.

예를 들어, 파이썬으로 작성된 액션 클라이언트는 다음과 같이 동작한다:

import rclpy
from rclpy.action import ActionClient
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.send_goal_async(goal_msg, feedback_callback=self.feedback_callback)

이와 같이, 클라이언트는 서버에 목표를 전송하고, 피드백을 수신하는 역할을 수행한다.

액션 서버의 실행 흐름

액션 서버는 클라이언트로부터 목표를 수신한 후, 해당 목표를 처리하는 동안 피드백을 제공하고 최종 결과를 반환하는 구조로 동작한다. 이 과정에서 서버는 클라이언트로부터의 요청을 비동기적으로 처리한다.

목표 수신과 처리

액션 서버가 목표를 수신하면, 해당 목표는 handle_goal 함수에 의해 처리된다. 이 함수는 클라이언트로부터 전송된 목표를 평가하여 수락 여부를 결정한다. 수락된 목표는 execute_callback 함수에서 처리되며, 서버는 목표를 실행하는 동안 지속적으로 피드백을 전송할 수 있다.

rclcpp_action::GoalResponse handle_goal(
  const rclcpp_action::GoalUUID & uuid, std::shared_ptr<const Fibonacci::Goal> goal) {
  RCLCPP_INFO(this->get_logger(), "Received goal request with order %d", goal->order);
  if (goal->order > 0) {
    return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
  } else {
    return rclcpp_action::GoalResponse::REJECT;
  }
}

위 코드에서 handle_goal 함수는 클라이언트로부터 받은 목표를 확인하고, 목표를 수락하거나 거부한다. 목표가 수락되면 서버는 execute_callback 함수를 통해 실제 작업을 처리한다.

피드백 전송

작업이 진행되는 동안 서버는 클라이언트에게 중간 피드백을 전송할 수 있다. 피드백은 feedback_callback 함수를 통해 클라이언트에 전송되며, 이 과정에서 서버는 작업의 진행 상황을 클라이언트에 알릴 수 있다.

void execute_callback(const std::shared_ptr<rclcpp_action::ServerGoalHandle<Fibonacci>> goal_handle) {
  const auto goal = goal_handle->get_goal();
  auto feedback = std::make_shared<Fibonacci::Feedback>();
  auto &sequence = feedback->partial_sequence;
  sequence.push_back(0);
  sequence.push_back(1);

  for (int i = 2; (i < goal->order) && rclcpp::ok(); ++i) {
    if (goal_handle->is_canceling()) {
      goal_handle->canceled(result);
      return;
    }
    sequence.push_back(sequence[i - 1] + sequence[i - 2]);
    goal_handle->publish_feedback(feedback);
    rclcpp::sleep_for(std::chrono::milliseconds(500));
  }
}

이 예제에서는 피드백을 통해 클라이언트에 피보나치 수열의 중간 값을 지속적으로 전송하고 있다. 서버는 publish_feedback 메소드를 사용하여 피드백을 클라이언트에 전송하며, 클라이언트는 이를 통해 작업의 진행 상황을 확인할 수 있다.

액션 클라이언트의 실행 흐름

액션 클라이언트는 서버에 목표를 전송하고, 피드백과 결과를 비동기적으로 수신한다. 클라이언트는 목표 전송과 함께 비동기적으로 피드백을 수신할 준비를 하며, 작업이 완료되면 최종 결과를 수신한다.

목표 전송

액션 클라이언트는 send_goal_async 함수를 통해 서버에 목표를 전송한다. 목표가 수락되면, 클라이언트는 서버와의 상호작용을 시작하며 피드백과 결과를 기다린다.

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

여기서 send_goal_async 함수는 비동기적으로 목표를 서버에 전송하며, 목표가 수락되면 goal_response_callback 함수가 호출된다.

피드백 수신

클라이언트는 feedback_callback 함수를 통해 서버로부터 중간 피드백을 수신할 수 있다. 피드백은 작업이 진행되는 동안 지속적으로 서버로부터 전송된다.

def feedback_callback(self, feedback_msg):
    feedback = feedback_msg.feedback
    self.get_logger().info('Received feedback: {0}'.format(feedback.partial_sequence))

위의 피드백 콜백 함수는 서버로부터 받은 피드백을 출력하는 역할을 한다. 클라이언트는 이 피드백을 통해 서버가 작업을 잘 진행하고 있는지 확인할 수 있다.

결과 수신

작업이 완료되면, 클라이언트는 서버로부터 최종 결과를 수신하게 된다. 클라이언트는 get_result_async 함수를 통해 비동기적으로 결과를 요청하며, 결과를 받은 후에는 이를 처리하는 콜백 함수가 호출된다.

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

    self.get_logger().info('Goal accepted')
    self._get_result_future = goal_handle.get_result_async()
    self._get_result_future.add_done_callback(self.get_result_callback)

여기서 goal_response_callback 함수는 서버가 목표를 수락했는지 여부를 확인하며, 목표가 수락된 경우 결과를 기다린다.