액션 개념과 구조

ROS2 액션은 서버-클라이언트 구조를 기반으로 비동기 작업을 처리할 수 있는 기능을 제공한다. 액션은 목표(goal)를 설정하고, 목표가 처리되는 동안 상태 피드백을 받고, 목표 달성 여부에 따라 결과를 반환하는 시스템이다. 액션 서버는 이러한 요청을 수락하고 처리하며, 클라이언트는 목표를 보내고 피드백을 수신하며 최종 결과를 받는 역할을 한다.

액션의 구성 요소

액션은 기본적으로 다음과 같은 3가지 요소로 구성된다:

  1. Goal: 클라이언트에서 서버로 전송하는 목표 값.
  2. Feedback: 서버가 작업 중인 상태를 클라이언트에 주기적으로 전달하는 피드백.
  3. Result: 서버가 목표를 완료하거나 실패한 후 클라이언트에 반환하는 최종 결과.

액션 서버 구현

ROS2에서 액션 서버는 rclcpp_action::create_server 함수를 통해 구현할 수 있다. 서버는 클라이언트로부터 목표를 수신하고 이를 처리한 후, 피드백을 주기적으로 보내며 결과를 반환하는 구조로 동작한다.

액션 서버의 주요 함수

  1. handle_goal: 클라이언트가 목표를 보내면 이를 수락하거나 거부하는 함수이다.
  2. handle_cancel: 클라이언트가 목표를 취소하면 이를 처리하는 함수이다.
  3. execute: 목표가 수락되었을 때 실제로 작업을 수행하는 함수이다.
  4. handle_accepted: 목표가 수락되었을 때 이를 처리하는 콜백 함수이다.

액션 서버의 전반적인 구조를 아래와 같은 다이어그램으로 나타낼 수 있다.

sequenceDiagram participant Client participant ActionServer Client->>ActionServer: Goal Request ActionServer->>Client: Accept Goal ActionServer->>Client: Periodic Feedback ActionServer->>Client: Final Result

액션 클라이언트 구현

ROS2에서 액션 클라이언트는 rclcpp_action::create_client 함수를 통해 구현할 수 있다. 클라이언트는 목표를 서버에 전달하고, 피드백을 받으며, 결과를 수신하는 역할을 한다.

액션 클라이언트의 주요 함수

  1. send_goal: 서버로 목표를 전송하는 함수이다.
  2. feedback_callback: 서버로부터 피드백을 수신하는 콜백 함수이다.
  3. result_callback: 서버로부터 최종 결과를 수신하는 콜백 함수이다.

액션 서버 구현 코드 예시

아래는 ROS2에서 C++로 구현한 간단한 액션 서버의 예시이다. 이 코드는 기본적으로 목표를 수신하고, 피드백을 주기적으로 보내며, 최종 결과를 반환하는 기능을 포함한다.

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

class FibonacciActionServer : public rclcpp::Node {
public:
  using Fibonacci = example_interfaces::action::Fibonacci;
  using GoalHandleFibonacci = rclcpp_action::ServerGoalHandle<Fibonacci>;

  explicit FibonacciActionServer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions())
  : 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_;

  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);
    return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
  }

  rclcpp_action::CancelResponse handle_cancel(const std::shared_ptr<GoalHandleFibonacci> goal_handle) {
    RCLCPP_INFO(this->get_logger(), "Received request to cancel goal");
    return rclcpp_action::CancelResponse::ACCEPT;
  }

  void handle_accepted(const std::shared_ptr<GoalHandleFibonacci> goal_handle) {
    std::thread{std::bind(&FibonacciActionServer::execute, this, std::placeholders::_1), goal_handle}.detach();
  }

  void execute(const std::shared_ptr<GoalHandleFibonacci> goal_handle) {
    RCLCPP_INFO(this->get_logger(), "Executing goal");
    const auto goal = goal_handle->get_goal();
    auto feedback = std::make_shared<Fibonacci::Feedback>();
    auto & sequence = feedback->sequence;
    sequence.push_back(0);
    sequence.push_back(1);

    auto result = std::make_shared<Fibonacci::Result>();

    for (int i = 2; i <= goal->order; ++i) {
      if (goal_handle->is_canceling()) {
        result->sequence = sequence;
        goal_handle->canceled(result);
        RCLCPP_INFO(this->get_logger(), "Goal canceled");
        return;
      }
      sequence.push_back(sequence[i - 1] + sequence[i - 2]);
      goal_handle->publish_feedback(feedback);
      RCLCPP_INFO(this->get_logger(), "Publishing feedback");
      std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }

    result->sequence = sequence;
    goal_handle->succeed(result);
    RCLCPP_INFO(this->get_logger(), "Goal succeeded");
  }
};

액션 클라이언트 구현 코드 예시

아래는 ROS2에서 C++로 구현한 액션 클라이언트의 예시이다. 이 클라이언트는 서버로 목표를 전송하고, 피드백과 결과를 수신하는 구조를 갖는다.

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

class FibonacciActionClient : public rclcpp::Node {
public:
  using Fibonacci = example_interfaces::action::Fibonacci;
  using GoalHandleFibonacci = rclcpp_action::ClientGoalHandle<Fibonacci>;

  explicit FibonacciActionClient(const rclcpp::NodeOptions & options = rclcpp::NodeOptions())
  : Node("fibonacci_action_client") {
    this->client_ptr_ = rclcpp_action::create_client<Fibonacci>(this, "fibonacci");
  }

  void send_goal() {
    if (!this->client_ptr_->wait_for_action_server()) {
      RCLCPP_ERROR(this->get_logger(), "Action server not available after waiting");
      return;
    }

    auto goal_msg = Fibonacci::Goal();
    goal_msg.order = 10;

    RCLCPP_INFO(this->get_logger(), "Sending goal");

    auto send_goal_options = rclcpp_action::Client<Fibonacci>::SendGoalOptions();
    send_goal_options.goal_response_callback =
      std::bind(&FibonacciActionClient::goal_response_callback, this, std::placeholders::_1);
    send_goal_options.feedback_callback =
      std::bind(&FibonacciActionClient::feedback_callback, this, std::placeholders::_1, std::placeholders::_2);
    send_goal_options.result_callback =
      std::bind(&FibonacciActionClient::result_callback, this, std::placeholders::_1);

    this->client_ptr_->async_send_goal(goal_msg, send_goal_options);
  }

private:
  rclcpp_action::Client<Fibonacci>::SharedPtr client_ptr_;

  void goal_response_callback(std::shared_future<GoalHandleFibonacci::SharedPtr> future) {
    auto goal_handle = future.get();
    if (!goal_handle) {
      RCLCPP_ERROR(this->get_logger(), "Goal was rejected by server");
    } else {
      RCLCPP_INFO(this->get_logger(), "Goal accepted by server, waiting for result");
    }
  }

  void feedback_callback(GoalHandleFibonacci::SharedPtr, const std::shared_ptr<const Fibonacci::Feedback> feedback) {
    RCLCPP_INFO(this->get_logger(), "Next number in sequence: %d", feedback->sequence.back());
  }

  void result_callback(const GoalHandleFibonacci::WrappedResult & result) {
    switch (result.code) {
      case rclcpp_action::ResultCode::SUCCEEDED:
        RCLCPP_INFO(this->get_logger(), "Result received");
        for (auto number : result.result->sequence) {
          RCLCPP_INFO(this->get_logger(), "%d", number);
        }
        break;
      case rclcpp_action::ResultCode::ABORTED:
        RCLCPP_ERROR(this->get_logger(), "Goal was aborted");
        return;
      case rclcpp_action::ResultCode::CANCELED:
        RCLCPP_ERROR(this->get_logger(), "Goal was canceled");
        return;
      default:
        RCLCPP_ERROR(this->get_logger(), "Unknown result code");
        return;
    }
  }
};

피드백 및 결과 처리 과정

액션 클라이언트는 목표가 실행되는 동안 서버로부터 주기적으로 피드백을 받는다. 피드백은 feedback_callback 함수에서 처리되며, 결과는 result_callback 함수에서 최종적으로 처리된다.

피드백 및 결과 처리 과정을 아래 다이어그램으로 나타낼 수 있다.

sequenceDiagram participant Client participant ActionServer Client->>ActionServer: Send Goal ActionServer-->>Client: Accept Goal ActionServer->>Client: Periodic Feedback ActionServer->>Client: Final Result