티스토리 친구하기

본문 바로가기

Robotics/ROS2

ROS2 service and client (C++) [Writing a simple service and client]

728x90

Goal: Create and run service and client nodes using C++.

Tutorial level: Beginner

Time: 20 minutes

 

Background

Service를 사용하여 노드가 통신할 때, 데이터 요청을 보내는 노드를 클라이언트 노드(client node)라고 하고, 이 요청에 응답하는 노드를 서비스 노드(service node)라고 합니다. 요청과 응답의 구조는 '. srv' 파일에 의해 결정됩니다.

 

여기에서 사용된 예제는 간단한 정수 덧셈 시스템입니다. 한 노드가 두 정수의 합을 요청하고, 다른 노드가 결과를 응답합니다.

Prerequisites

이전 튜토리얼에서는 workspace를 생성하고 패키지를 생성하는 방법을 학습했습니다.

 

Tasks

1 Create a package

새 터미널을 열고 ROS 2 installation을  source로 설정하여 ros2 명령이 작동하도록 만듭니다.

이전 튜토리얼에서 생성한 ros2_ws 디렉터리로 이동합니다.

패키지는 workspace 루트가 아닌 src 디렉터리에 생성해야 함을 기억하세요. ros2_ws/src로 이동하고 다음 명령을 사용하여 새 패키지를 생성합니다:

ros2 pkg create --build-type ament_cmake cpp_srvcli --dependencies rclcpp example_interfaces


터미널은 cpp_srvcli 패키지와 필요한 모든 파일과 폴더가 생성되었음을 확인하는 메시지를 return 합니다.

--dependencies argument는 package.xml 및 CMakeLists.txt에 필요한 dependency line을 자동으로 추가합니다. example_interfaces는 request(요청)와 response(응답)를 구성하는 데 필요한 '. srv' 파일을 포함한 패키지입니다:

int64 a
int64 b
---
int64 sum


첫 두 줄은 request의 매개변수이고, 하이픈 아래에는 response입니다.

1.1 Update package.xml

package 만드는 동안 `--dependencies` 옵션을 사용했기 때문에, 수동으로 'package.xml' 또는 'CMakeLists.txt'를 add 할 필요가 없습니다.

그러나 description, maintainer email과 name, 그리고 license 정보를 'package.xml'에 add 하는 것을 잊지 마세요. 아래와 같이 추가하세요.

 

<description>C++ client server tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>

 

2 Write the service node

'ros2_ws/src/cpp_srvcli/src' 디렉터리에 'add_two_ints_server.cpp' 파일을 만드세요. 그리고 만든 파일 안에 아래의 코드를 복사하세요.

 

#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"

#include <memory>

void add(const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
          std::shared_ptr<example_interfaces::srv::AddTwoInts::Response>      response)
{
  response->sum = request->a + request->b;
  RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld",
                request->a, request->b);
  RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);

  std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_server");

  rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service =
    node->create_service<example_interfaces::srv::AddTwoInts>("add_two_ints", &add);

  RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add two ints.");

  rclcpp::spin(node);
  rclcpp::shutdown();
}

 

2.1 Examine the code

`add` 함수는 요청(request)에서 두 정수를 가져와 응답(response)에 합계를 제공하며, 상태를 로그를 사용하여 콘솔에 알립니다. 이것은 service가 실제로 하는 작업을 정의하는 부분입니다.

 

void add(const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
         std::shared_ptr<example_interfaces::srv::AddTwoInts::Response>      response)
{
    response->sum = request->a + request->b;
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld",
        request->a, request->b);
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}

 

'main' 함수는 아래와 같은 작업을 한 줄씩 수행합니다.

 

1. ROS2 C++ client 라이브러리는 초기화합니다.

 

rclcpp::init(argc, argv);

 

2. "add_two_ints_server"라는 이름의 노드를 생성합니다.

 

std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_server");

 

3. 이 노드에 "add_two_ints"라는 이름의 service를 생성하고 '&add' 메서드를 사용해서 자동으로 네트워크에 해당 서비스를 게시합니다.

 

rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service =
node->create_service<example_interfaces::srv::AddTwoInts>("add_two_ints", &add);

 

4. Service가 준비되었을 때, 로그 메시지는 print 합니다.

 

RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add two ints.");

 

5. 노드를 실행(Spins the node)하여 service를 사용할 수 있게 만듭니다.

 

rclcpp::spin(node);

2.2 Add executable

`add_executable` 매크로는 `ros2 run`을 사용하여 실행할 수 있는 실행 파일(executable)을 생성합니다. `CMakeLists.txt`에 다음 코드 블록을 추가하여 "server"라는 이름의 실행 파일(executable)을 만들 수 있습니다:

add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server rclcpp example_interfaces)


그리고 `ros2 run`에서 executable을 찾을 수 있도록, `ament_package()` 바로 직전에 다음 줄을 파일 끝에 추가하세요:

 

install(TARGETS
    server
  DESTINATION lib/${PROJECT_NAME})


이렇게 하면 package를 build 하고 로컬 설정 파일(local setup file)을 source로 설정하고 실행(run)할 수 있지만, 먼저 client노드를 만들어서 전체 시스템이 작동하는 것을 볼 수 있도록 합시다.

3 Write the client node

"ros2_ws/src/cpp_srvcli/src" 디렉터리 내부에 "add_two_ints_client.cpp"라는 새 파일을 만들고 다음 코드를 붙여 넣으세요.

 

#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"

#include <chrono>
#include <cstdlib>
#include <memory>

using namespace std::chrono_literals;

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);

  if (argc != 3) {
      RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_two_ints_client X Y");
      return 1;
  }

  std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_client");
  rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client =
    node->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");

  auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
  request->a = atoll(argv[1]);
  request->b = atoll(argv[2]);

  while (!client->wait_for_service(1s)) {
    if (!rclcpp::ok()) {
      RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
      return 0;
    }
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
  }

  auto result = client->async_send_request(request);
  // Wait for the result.
  if (rclcpp::spin_until_future_complete(node, result) ==
    rclcpp::FutureReturnCode::SUCCESS)
  {
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum);
  } else {
    RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_two_ints");
  }

  rclcpp::shutdown();
  return 0;
}

3.1 Examine the code

Service 노드와 유사하게, 다음 코드라인은 노드를 생성하고 해당 노드에 대한 client node를 생성합니다.

 

std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_client");
rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client =
  node->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");

 

다음으로, request를 생성합니다. 이 request의 구조는 앞서 언급한 '.srv' 파일에 정의되어 있습니다.

 

auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = atoll(argv[1]);
request->b = atoll(argv[2]);

 

'while' 루프는 clinet가 네트워크에서 service node를 찾는데 1초 동안 기다립니다. Service node를 찾을 수 없는 경우, 계속 대기합니다.

 

RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");

 

만약 client가 취소되면(예: 터미널에서 Ctrl + c를 입력한 경우), client는 중단되었음을 나타내는 error log message를 return 합니다.

 

RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");

 

그런 다음 client는 request를 보내고, 노드는 response를 받을 때까지 또는 실패할 때까지 계속 실행(spin)합니다.

3.2 Add executable

CMakeLists.txt 파일에 새로운 노드를 위한 executable과 target을 추가하려면 다음의 단계를 따라야 합니다. 자동으로 생성된 파일에 불필요한 부분을 제거한 후, CMakeLists.txt 파일은 아래와 같이 보입니다.

 

cmake_minimum_required(VERSION 3.5)
project(cpp_srvcli)

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(example_interfaces REQUIRED)

add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server rclcpp example_interfaces)

add_executable(client src/add_two_ints_client.cpp)
ament_target_dependencies(client rclcpp example_interfaces)

install(TARGETS
  server
  client
  DESTINATION lib/${PROJECT_NAME})

ament_package()

 

4 Build and run

dependencies가 빠짐없이 설치되었는지 확인하고, build 하기 전에 workspace root(ros2_ws)에서 'rosdep'을 실행하는 것은 좋은 습관입니다. 아래의 command를 사용해서 이 작업을 수행할 수 있습니다.

 

rosdep install -i --from-path src --rosdistro humble -y

 

workspace root인 'ros2_ws'로 돌아가서, 새 package를 빌드하세요.

 

colcon build --packages-select cpp_srvcli

 

그런 다음 새 터미널을 열고, 'ros2_ws'로 이동해서 setup file 들을 source 합니다.

 

source install/setup.bash

 

이제 service node를 실행합니다.

 

ros2 run cpp_srvcli server

 

터미널은 아래의 메시지를 return 하고 대기할 것입니다.

 

[INFO] [rclcpp]: Ready to add two ints.

 

다른 터미널을 열고, 다시 'ros2_ws' 내에서 setup file을 source 합니다. 그런 다음, client 노드를 시작하고, 2 개의 정수를 공백으로 구분하여 입력합니다.

 

ros2 run cpp_srvcli client 2 3

 

예를 들어, 2와 3을 선택한 경우, client는 아래와 같은 response를 받게 됩니다.

 

[INFO] [rclcpp]: Sum: 5

 

Service node가 실행되는 터미널로 돌아가면, service node가 request를 수신하고 수신한 데이터, 그리고 보낸 response에 대한 로그 메시지를 publish 했음을 확인할 수 있습니다.

 

[INFO] [rclcpp]: Incoming request
a: 2 b: 3
[INFO] [rclcpp]: sending back response: [5]

 

노드의 실행을 중지하려면 service node가 실행되는 터미널에서 Ctrl + C를 입력하세요.

Summary

두 개의 노드를 생성하여 service를 통해 데이터를 request 하고 reponse 하는 방법을 학습했습니다. 그리고 해당 package configuration file (패키지 구성 파일)에 dependencies와 executable을 추가하여 이를 빌드(build)하고 실행(run)하며 service/client 시스템이 어떻게 동작하는지 확인했습니다. 이러한 작업은 ROS2를 사용하여 로봇 시스템에서 데이터 통신 및 서비스 요청/응답 패턴을 구현하는데 중요한 요소입니다.

반응형