서비스(Service)는 동기식 양방향 메시지 송수신 방식으로 서비스의 요청(Request)을 하는 쪽을 서비스 클라이언트(Service Client)라고 하며, 요청 받은 서비스를 수행한 후 서비스의 응답(Response)을 하는 쪽을 서비스 서버(Service Server)라고 한다. 결국 서비스는 특정 요청을 하는 클라이언트 단과 요청받은 일을 수행한 후에 결괏값을 전달하는 서버 단과의 통신이다.
우리는 이 강좌에서 아래 그림과 같은 서비스 요청을 하는 서비스 클라이언트와 서비스 응답을 하는 서비스 서버를 작성해 볼 것이다. 여기서 서비스 요청 값으로는 연산자(더하기[ + ], 빼기 [ - ], 곱하기 [ * ], 나누기 [ / ])를 임의로 선택 한후에 보낼 것이다. 그리고 기존에 저장된 변수 a, b를 요청 값으로 받은 연산자로 계산하여 그 결괏값을 서비스 응답 값으로 보내는 프로그램을 짜볼 것이다.
서비스 서버 역할을 하는 노드는 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;
}
서비스 클라이언트 역할을 하는 노드는 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);
}
1) 서비스 서버(요청에 응답하는 프로그램)
1) Node 설정
2) create_server 설정
3) 콜백함수 설정
2) 서비스 클라이언트(요청하는 프로그램)
1) Node 설정
2) create_client 설정
3) 요청함수 설정
위에서 실습한 노드를 실행시킬 명령어는 다음곽 같다. 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 서비스 프로그래밍에 대해 알아보았다. 다음장에서는 액션 프로그래밍에대해 알아보겠다.