서비스 프로그래밍(C++)

두부김치·2024년 2월 20일
1

ROS2

목록 보기
25/29

1. 서비스(Service)

서비스(Service)는 동기식 양방향 메시지 송수신 방식으로 서비스의 요청(Request)을 하는 쪽을 서비스 클라이언트(Service Client)라고 하며, 요청 받은 서비스를 수행한 후 서비스의 응답(Response)을 하는 쪽을 서비스 서버(Service Server)라고 한다. 결국 서비스는 특정 요청을 하는 클라이언트 단과 요청받은 일을 수행한 후에 결괏값을 전달하는 서버 단과의 통신이다.

우리는 이 강좌에서 아래 그림과 같은 서비스 요청을 하는 서비스 클라이언트와 서비스 응답을 하는 서비스 서버를 작성해 볼 것이다. 여기서 서비스 요청 값으로는 연산자(더하기[ + ], 빼기 [ - ], 곱하기 [ * ], 나누기 [ / ])를 임의로 선택 한후에 보낼 것이다. 그리고 기존에 저장된 변수 a, b를 요청 값으로 받은 연산자로 계산하여 그 결괏값을 서비스 응답 값으로 보내는 프로그램을 짜볼 것이다.

2. 서비스 서버 코드

서비스 서버 역할을 하는 노드는 calculator 노드로 전체 소스코드는 다음과 같다.

#ifndef CALCULATOR__CALCULATOR_HPP_
#define CALCULATOR__CALCULATOR_HPP_

#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include <stdexcept>

#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"

#include "msg_srv_action_interface_example/msg/arithmetic_argument.hpp"
#include "msg_srv_action_interface_example/srv/arithmetic_operator.hpp"
#include "msg_srv_action_interface_example/action/arithmetic_checker.hpp"


class Calculator : public rclcpp::Node
{
public:
  using ArithmeticArgument = msg_srv_action_interface_example::msg::ArithmeticArgument;
  using ArithmeticOperator = msg_srv_action_interface_example::srv::ArithmeticOperator;
  using ArithmeticChecker = msg_srv_action_interface_example::action::ArithmeticChecker;
  using GoalHandleArithmeticChecker = rclcpp_action::ServerGoalHandle<ArithmeticChecker>;

  explicit Calculator(const rclcpp::NodeOptions & node_options = rclcpp::NodeOptions());
  virtual ~Calculator();

  float calculate_given_formula(const float & a, const float & b, const int8_t & operators);

private:
  rclcpp_action::GoalResponse handle_goal(
    const rclcpp_action::GoalUUID & uuid,
    std::shared_ptr<const ArithmeticChecker::Goal> goal);
  rclcpp_action::CancelResponse handle_cancel(
    const std::shared_ptr<GoalHandleArithmeticChecker> goal_handle);
  void execute_checker(const std::shared_ptr<GoalHandleArithmeticChecker> goal_handle);

  rclcpp::Subscription<ArithmeticArgument>::SharedPtr
    arithmetic_argument_subscriber_;

  rclcpp::Service<ArithmeticOperator>::SharedPtr
    arithmetic_argument_server_;

  rclcpp_action::Server<ArithmeticChecker>::SharedPtr
    arithmetic_action_server_;

  float argument_a_;
  float argument_b_;

  int8_t argument_operator_;
  float argument_result_;

  std::string argument_formula_;
  std::vector<std::string> operator_;
};
#endif  // CALCULATOR__CALCULATOR_HPP_
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include <stdexcept>

#include "calculator/calculator.hpp"


Calculator::Calculator(const rclcpp::NodeOptions & node_options)
: Node("calculator", node_options),
  argument_a_(0.0),
  argument_b_(0.0),
  argument_operator_(0),
  argument_result_(0.0),
  argument_formula_("")
{
  RCLCPP_INFO(this->get_logger(), "Run calculator");

  operator_.reserve(4);
  operator_.push_back("+");
  operator_.push_back("-");
  operator_.push_back("*");
  operator_.push_back("/");

  this->declare_parameter("qos_depth", 10);
  int8_t qos_depth = 0;
  this->get_parameter("qos_depth", qos_depth);

  const auto QOS_RKL10V =
    rclcpp::QoS(rclcpp::KeepLast(qos_depth)).reliable().durability_volatile();

  arithmetic_argument_subscriber_ = this->create_subscription<ArithmeticArgument>(
    "arithmetic_argument",
    QOS_RKL10V,
    [this](const ArithmeticArgument::SharedPtr msg) -> void
    {
      argument_a_ = msg->argument_a;
      argument_b_ = msg->argument_b;

      RCLCPP_INFO(
        this->get_logger(),
        "Timestamp of the message: sec %ld nanosec %ld",
        msg->stamp.sec,
        msg->stamp.nanosec);

      RCLCPP_INFO(this->get_logger(), "Subscribed argument a: %.2f", argument_a_);
      RCLCPP_INFO(this->get_logger(), "Subscribed argument b: %.2f", argument_b_);
    }
  );

  auto get_arithmetic_operator =
    [this](
    const std::shared_ptr<ArithmeticOperator::Request> request,
    std::shared_ptr<ArithmeticOperator::Response> response) -> void
    {
      argument_operator_ = request->arithmetic_operator;
      argument_result_ =
        this->calculate_given_formula(argument_a_, argument_b_, argument_operator_);
      response->arithmetic_result = argument_result_;

      std::ostringstream oss;
      oss << std::to_string(argument_a_) << ' ' <<
        operator_[argument_operator_ - 1] << ' ' <<
        std::to_string(argument_b_) << " = " <<
        argument_result_ << std::endl;
      argument_formula_ = oss.str();

      RCLCPP_INFO(this->get_logger(), "%s", argument_formula_.c_str());
    };

  arithmetic_argument_server_ =
    create_service<ArithmeticOperator>("arithmetic_operator", get_arithmetic_operator);

  using namespace std::placeholders;
  arithmetic_action_server_ = rclcpp_action::create_server<ArithmeticChecker>(
    this->get_node_base_interface(),
    this->get_node_clock_interface(),
    this->get_node_logging_interface(),
    this->get_node_waitables_interface(),
    "arithmetic_checker",
    std::bind(&Calculator::handle_goal, this, _1, _2),
    std::bind(&Calculator::handle_cancel, this, _1),
    std::bind(&Calculator::execute_checker, this, _1)
  );
}

Calculator::~Calculator()
{
}

float Calculator::calculate_given_formula(
  const float & a,
  const float & b,
  const int8_t & operators)
{
  float argument_result = 0.0;
  ArithmeticOperator::Request arithmetic_operator;

  if (operators == arithmetic_operator.PLUS) {
    argument_result = a + b;
  } else if (operators == arithmetic_operator.MINUS) {
    argument_result = a - b;
  } else if (operators == arithmetic_operator.MULTIPLY) {
    argument_result = a * b;
  } else if (operators == arithmetic_operator.DIVISION) {
    argument_result = a / b;
    if (b == 0.0) {
      RCLCPP_ERROR(this->get_logger(), "ZeroDivisionError!");
      argument_result = 0.0;
      return argument_result;
    }
  } else {
    RCLCPP_ERROR(
      this->get_logger(),
      "Please make sure arithmetic operator(plus, minus, multiply, division).");
    argument_result = 0.0;
  }

  return argument_result;
}

rclcpp_action::GoalResponse Calculator::handle_goal(
  const rclcpp_action::GoalUUID & uuid,
  std::shared_ptr<const ArithmeticChecker::Goal> goal)
{
  (void)uuid;
  (void)goal;
  return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
}

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

void Calculator::execute_checker(const std::shared_ptr<GoalHandleArithmeticChecker> goal_handle)
{
  RCLCPP_INFO(this->get_logger(), "Execute arithmetic_checker action!");
  rclcpp::Rate loop_rate(1);

  auto feedback_msg = std::make_shared<ArithmeticChecker::Feedback>();
  float total_sum = 0.0;
  float goal_sum = goal_handle->get_goal()->goal_sum;

  while ((total_sum < goal_sum) && rclcpp::ok()) {
    total_sum += argument_result_;
    feedback_msg->formula.push_back(argument_formula_);
    if (argument_formula_.empty()) {
      RCLCPP_WARN(this->get_logger(), "Please check your formula");
      break;
    }
    RCLCPP_INFO(this->get_logger(), "Feedback: ");
    for (const auto & formula : feedback_msg->formula) {
      RCLCPP_INFO(this->get_logger(), "\t%s", formula.c_str());
    }
    goal_handle->publish_feedback(feedback_msg);
    loop_rate.sleep();
  }

  if (rclcpp::ok()) {
    auto result = std::make_shared<ArithmeticChecker::Result>();
    result->all_formula = feedback_msg->formula;
    result->total_sum = total_sum;
    goal_handle->succeed(result);
  }
}

토픽때와 같이 서비스 서버 노드는 토픽 서브스크라이버, 서비스 서버, 액션 서버르 모두 포함하고 있어 코드가 매우 길어 서비스 서버 부분만 살펴보겠다.

Calculator 클래스는 rclcpp::Node를 상속받고 있으며 생성자에서 calculator 노드로 초기화되었다. arithmetic_argument_server 멤버 변수는 rclcpp::Service 타입의 스마트 포인터 변수로 서비스명과 콜백 함수를 인자로 받는 create_service 함수를 통해 실체화 된다. 해당 코드에서는 arithmetic_operator 서비스명을 사용했고, 콜백 함수는 람다 표현식으로 이루어진 get_arithmetic_operator 변수로 지정하였다.

다음으로 get_arithmetic_operator 변수를 살펴보자. 해당 람다 표현식은 Request와 Response 매개변수를 가지고 있어서 함수 내부에서 이를 사용할 수 있다. 우선 argument_operator 멤버 변수에 요청받은 연산자를 저장한다. 그 다음 멤버 변수 argument_a, argument_b 와 함께 argument_operator 멤버 변수를 calculate_given_formula 멤버 함수에 인자로 넘겨주어 그 결괏값을 리턴받는다. 해당 결괏값은 response 변수에 저장하여 서비스를 요청한 클라이언트가 이를 받아볼 수 있도록 한다. 그리고 해당 연산식을 ostringstream 라이브러리를 이용해 string 변수에 저장하였다. 마지막으로 저장된 string 변수를 로그로 출력해주는 코드를 확인할 수 있다.

  auto get_arithmetic_operator =
    [this](
    const std::shared_ptr<ArithmeticOperator::Request> request,
    std::shared_ptr<ArithmeticOperator::Response> response) -> void
    {
      argument_operator_ = request->arithmetic_operator;
      argument_result_ =
        this->calculate_given_formula(argument_a_, argument_b_, argument_operator_);
      response->arithmetic_result = argument_result_;

      std::ostringstream oss;
      oss << std::to_string(argument_a_) << ' ' <<
        operator_[argument_operator_ - 1] << ' ' <<
        std::to_string(argument_b_) << " = " <<
        argument_result_ << std::endl;
      argument_formula_ = oss.str();

      RCLCPP_INFO(this->get_logger(), "%s", argument_formula_.c_str());
    };

  arithmetic_argument_server_ =
    create_service<ArithmeticOperator>("arithmetic_operator", get_arithmetic_operator);

calculate_given_formula 함수는 상수 a,b와 연산자 정보를 포함하는 상수 operators를 인자로 받아 그 연산 결괏값을 반환해주는 역할을 한다. 다음 코드를 보면 ArithmeticOperator::Request 구조체의 미리 선언된 상수를 통해 연산자를 찾고 해당 연산을 진행하는 로직을 확인할 수 있다.

float Calculator::calculate_given_formula(
  const float & a,
  const float & b,
  const int8_t & operators)
{
  float argument_result = 0.0;
  ArithmeticOperator::Request arithmetic_operator; // ArithmeticOperator의 Request 부분에서 연산자를 가져와 arithmetic_operator라는 객체 생성

  if (operators == arithmetic_operator.PLUS) {
    argument_result = a + b;
  } else if (operators == arithmetic_operator.MINUS) {
    argument_result = a - b;
  } else if (operators == arithmetic_operator.MULTIPLY) {
    argument_result = a * b;
  } else if (operators == arithmetic_operator.DIVISION) {
    argument_result = a / b;
    if (b == 0.0) {
      RCLCPP_ERROR(this->get_logger(), "ZeroDivisionError!");
      argument_result = 0.0;
      return argument_result;
    }
  } else {
    RCLCPP_ERROR(
      this->get_logger(),
      "Please make sure arithmetic operator(plus, minus, multiply, division).");
    argument_result = 0.0;
  }

  return argument_result;
}

3. 서비스 클라이언트 코드

서비스 클라이언트 역할을 하는 노드는 operator 노드로 전체 소스코드는 다음과 같다.

#ifndef ARITHMETIC__OPERATOR_HPP_
#define ARITHMETIC__OPERATOR_HPP_

#include <chrono>
#include <memory>
#include <string>
#include <utility>
#include <random>

#include "rclcpp/rclcpp.hpp"
#include "msg_srv_action_interface_example/srv/arithmetic_operator.hpp"


class Operator : public rclcpp::Node
{
public:
  using ArithmeticOperator = msg_srv_action_interface_example::srv::ArithmeticOperator;

  explicit Operator(const rclcpp::NodeOptions & node_options = rclcpp::NodeOptions());
  virtual ~Operator();

  void send_request();

private:
  rclcpp::Client<ArithmeticOperator>::SharedPtr arithmetic_service_client_;
};
#endif  // ARITHMETIC__OPERATOR_HPP_
#include <fcntl.h>
#include <getopt.h>
#include <termios.h>
#include <unistd.h>
#include <cstdio>
#include <iostream>
#include <memory>
#include <string>
#include <utility>

#include "rclcpp/rclcpp.hpp"
#include "rcutils/cmdline_parser.h"

#include "arithmetic/operator.hpp"

using namespace std::chrono_literals;

Operator::Operator(const rclcpp::NodeOptions & node_options)
: Node("operator", node_options)
{
  arithmetic_service_client_ = this->create_client<ArithmeticOperator>("arithmetic_operator");
  while (!arithmetic_service_client_->wait_for_service(1s)) {
    if (!rclcpp::ok()) {
      RCLCPP_ERROR(this->get_logger(), "Interrupted while waiting for the service.");
      return;
    }
    RCLCPP_INFO(this->get_logger(), "Service not available, waiting again...");
  }
}

Operator::~Operator()
{
}

void Operator::send_request()
{
  std::random_device rd;
  std::mt19937 gen(rd());
  std::uniform_int_distribution<int> distribution(1, 4);

  auto request = std::make_shared<ArithmeticOperator::Request>();
  request->arithmetic_operator = distribution(gen);

  using ServiceResponseFuture = rclcpp::Client<ArithmeticOperator>::SharedFuture;
  auto response_received_callback = [this](ServiceResponseFuture future) {
      auto response = future.get();
      RCLCPP_INFO(this->get_logger(), "Result %.2f", response->arithmetic_result);
      return;
    };

  auto future_result =
    arithmetic_service_client_->async_send_request(request, response_received_callback);
}

헤더 파일에 선언된 Operator 클래스를 살펴보자. Operator 클래스는 rclcpp::Node 클래스를 상속받는 자식클래스이고, 생성자에서 rclcpp::NodeOptions를 매개변수로 가진다. 그리고 서비스 요청을 위한 send_request 함수와 스마트 포인터 타입의 멤버 변수 rclcpp::Client를 가지고 있다.

class Operator : public rclcpp::Node
{
public:
  using ArithmeticOperator = msg_srv_action_interface_example::srv::ArithmeticOperator;

  explicit Operator(const rclcpp::NodeOptions & node_options = rclcpp::NodeOptions());
  virtual ~Operator();

  void send_request();

private:
  rclcpp::Client<ArithmeticOperator>::SharedPtr arithmetic_service_client_;
};

Operator 클래스의 생성자 내부를 보면 부모 클래스인 rclcpp::Node를 노드명과 node_options 인자로 먼저 초기화해준다. 그리고 rclcpp::Node 클래스의 멤버 함수 create_client를 통해 서비스명을 인자로 받아 rclcpp::Client를 실체화 시켜준다.
서비스 클라이언트는 서비스 서버가 없다면 요청을 할 수 없을 뿐더러 원하는 응답도 얻지 못한다. 따라서 동일한 서비스명을 가진 서비스 서버를 기다리는 코드가 항상 따라온다는 것을 기억하자.!

Operator::Operator(const rclcpp::NodeOptions & node_options)
: Node("operator", node_options)
{
  arithmetic_service_client_ = this->create_client<ArithmeticOperator>("arithmetic_operator");
  while (!arithmetic_service_client_->wait_for_service(1s)) {
    if (!rclcpp::ok()) {
      RCLCPP_ERROR(this->get_logger(), "Interrupted while waiting for the service.");
      return;
    }
    RCLCPP_INFO(this->get_logger(), "Service not available, waiting again...");
  }
}

실제로 요청을 수행하는 send_request 멤버함수를 보자. 랜덤으로 연산자가 지정될 수 있도록 1부터 4사이의 랜덤한 정수를 생성하여 request 변수에 저장한다. 그리고 요청에 의한 응답이 왔을 때 불려질 response_received_callback 콜백 함수를 람다 표현식으로 정의하였다. 해당 람다 표현식은 매개변수 future를 가지고 있는데, 이를 C++11의 비동기 프로그래밍의 future, promise 개념과 동일하다. 콜백 함수가 불렸다면 future의 get 함수를 이용하여 response 값에 접근할 수 있다. 코드 마지막 줄에서 정의된 request 변수와 콜백 함수를 인자로 가지는 async_send_request 함수를 통해 비동기식으로 서비스 요청을 보내는 것을 확인할 수 있다.

void Operator::send_request()
{
  std::random_device rd;
  std::mt19937 gen(rd());
  std::uniform_int_distribution<int> distribution(1, 4);

  auto request = std::make_shared<ArithmeticOperator::Request>();
  request->arithmetic_operator = distribution(gen);

  using ServiceResponseFuture = rclcpp::Client<ArithmeticOperator>::SharedFuture;
  auto response_received_callback = [this](ServiceResponseFuture future) {
      auto response = future.get();
      RCLCPP_INFO(this->get_logger(), "Result %.2f", response->arithmetic_result);
      return;
    };

  auto future_result =
    arithmetic_service_client_->async_send_request(request, response_received_callback);
}

4. 서비스 서버, 서비스 클라이언트 복습

1) 서비스 서버(요청에 응답하는 프로그램)

1) Node 설정
2) create_server 설정
3) 콜백함수 설정

2) 서비스 클라이언트(요청하는 프로그램)

1) Node 설정
2) create_client 설정
3) 요청함수 설정

5. 노드 실행 코드

위에서 실습한 노드를 실행시킬 명령어는 다음곽 같다. calculator가 이 장에서 설명한 서비스 서버 노드이고, operator는 서비스 클라이언트 노드이다.

$ ros2 run topic_service_action_rclcpp_example calculator
$ ros2 run topic_service_action_rclcpp_example operator

이 두개의 노드를 실행할 수 있도록 설정한 부분은 빌드 설정 파일(CMakeLists.txt)에서 확인할 수 있다. CMake의 add_executable 명령어는 main함수가 포함된 소스파일을 실행가능하도록 만들어 준다. 그 첫 번째 매개변수에는 실행명을 넣고, 다음 매개변수에는 main 함수가 포함된 파일의 이름을 넣어준다. 필요하다면 그 다음 매개변수로 main함수의 의존 라이브러리가 담긴 파일의 이름을 넣어준다. 이를 통해 ros2 run 명령어를 이용하여 operator 노드와 calculator 노드를 실행할 수 있다.

add_executable(operator src/arithmetic/operator.cpp)
add_executable(calculator src/calculator/main.cpp src/calculator/calculator.cpp)
// 코드 중략

bool pull_trigger()
{
  const uint8_t KEY_ENTER = 10;
  while (1) {
    if (kbhit()) {
      uint8_t c = getch();
      if (c == KEY_ENTER) {
        return true;
      } else {
        return false;
      }
    }
  }
  return false;
}

void print_help()
{
  printf("For operator node:\n");
  printf("node_name [-h]\n");
  printf("Options:\n");
  printf("\t-h Help           : Print this help function.\n");
}

int main(int argc, char * argv[])
{
  if (rcutils_cli_option_exist(argv, argv + argc, "-h")) {
    print_help();
    return 0;
  }

  rclcpp::init(argc, argv);

  auto operator_node = std::make_shared<Operator>();

  while (rclcpp::ok()) {
    rclcpp::spin_some(operator_node);
    operator_node->send_request();

    printf("Press Enter for next service call.\n");
    if (pull_trigger() == false) {
      rclcpp::shutdown();
      return 0;
    }
  }
}

Operator 노드의 메인 함수는 Operator 클래스를 인스턴스화 하고 반복문을 통해 send_request 함수를 호출하는 로직을 가지고 있다. 해당 로직에서 pull_trigger 함수를 통해 서비스 요청이 끝난 다음 키보드 입력을 기다리며 한번 더 서비스 요청을 할 것인지(ENTER Key) 아니면 노드를 종료할 것인지(Not ENTER Key)를 선택할 수 있다.

서비스 서버인 calculator 노드는 토픽 서브스크라이버, 서비스 서버, 액션 서버를 역할을 하는 복합 기능 노드로 실행 코드에 대한 설명은 토픽 프로그래밍(C++)에서 이미 다루었기에 아래 코드에 대한 설명은 해당 강좌를 다시 보도록 하자.

int main(int argc, char * argv[])
{
  if (rcutils_cli_option_exist(argv, argv + argc, "-h")) {
    print_help();
    return 0;
  }

  rclcpp::init(argc, argv);

  auto calculator = std::make_shared<Calculator>();

  rclcpp::spin(calculator);

  rclcpp::shutdown();

  return 0;
}

ROS2 서비스 프로그래밍에 대해 알아보았다. 다음장에서는 액션 프로그래밍에대해 알아보겠다.

profile
Robotics

0개의 댓글