내 로봇을 한층 더 똑똑하게 ROS2 Service Server와 Client 작성하기

IROBOU·2024년 1월 28일
1

인공지능_로봇개발

목록 보기
12/17
post-thumbnail

로봇을 개발 할 때 publisher와 subscriber만으로는 똑똑한 로봇을 만드는데 상당한 제약을 받는다.

이번에는 이를 극복하는 것을 도와줄 service server와 client작성법에 대해 알아보자.


<목표>
C++로 service server와 client를 작성하고 실행해본다.

tutorial level: 입문

예상 소요시간
약 20분

영상 튜토리얼
튜토리얼 링크


배경지식

service는 두가지 요소로 이루어져있다.
1. service를 제공하는 server
2. service를 요청하는 client

사람이 소통할 때 같은 언어를 사용해야 하듯이 service도 요청하고 응답하려면 특정한 형식을 지켜야한다. 이런 규칙을 정의하는게 .srv 파일이다.

이번 시간에는 두 숫자를 더하는 서비스를 제공하는 server를 만들고 이를 요청하는 client를 구현 해볼 것이다.


준비물

지난 시간에 배웠던 service 개념, 작업공간 만들기 실습패키지 생성하기 실습을 했으면 오늘 준비물은 다 갖춰졌다.


실습

실습 1. 패키지 만들기

지난 시간에 패키지를 만드는 명령어 ros2 pkg create를 사용해 cpp_srvcli라는 이름의 패키지를 생성해준다.
이 패키지는 dependency로 rclcpp, example_interfaces를 갖는데 여기서 example_interfaces는 오늘 우리가 만들 service의 형식 .srv 파일을 제공한다고 보면된다.

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_srvcli --dependencies rclcpp example_interfaces

--dependencies라는 tag로 패키지의 dependency를 사전에 추가해줬기 때문에 CMakeLists.txt나 package.xml을 수정하는 노고가 조금 줄어 들었다.

추가로 example_interfaces 패키지가 제공하는 .srv 파일의 구조는 다음과 같다.

int64 a
int64 b
---
int64 sum

간단히 위의 구조를 살펴보면 위의 int64 a,b는 service의 request형식에 해당하고 "---" 밑의 int 64 sum은 서비스가 기능을 수행하고 반환하는 response(또는 result)에 해당한다.

실습 1.1 package.xml 업데이트 하기

dependency는 이미 추가 되어있으나 배포할 때 수정해줘야할 description, maintainer는 아직 변경되지 않았다. package.xml을 열어 적절히 수정해주자.

실습 2. service server code 작성하기

이전에 만든 cpp_srvcli/src 경로에 가서 add_two_ints_server.cpp 파일을 만든다.

cd ~/ros2_ws/cpp_srvcli/src
gedit 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 코드 분석

제일 처음 두 줄은 rclcpp와 example_interfaces와 같은 dependency를 추가 하고 있는 구문이다.

그다음 memory는 shared_ptr를 사용하기 위해 추가해준다.

add라는 함수를 정의하는데 함수 인자로는 request와 response를 갖는다.
request의 a와 b는 client가 호출할 때 제공할 것이고 이 둘을 합친 값을 response->sum에 저장한다.

그 후 결과를 log를 통해 출력하는 것으로 함수는 마무리 된다.

이 함수를 service server에 등록해줘야 사용가능한데 그 부분은 main 함수에 구현되어 있다.

먼저 node를 만들기 위해 init을 해주고 rclcpp::Node를 share_ptr로 만들어서 "add_two_ints_server"라는 이름으로 node를 만들어준다.

그 후 example_interfaces::srv::AddTwoInts의 srv 타입을 사용하는 service를 선언하고 해당 service가 "add_two_ints"라는 이름을 갖도록 서비스를 생성해준다. 해당 서비스가 할 기능은 앞서 add에 구현했었는데 이를 &add를 통해 service로 등록해준다.

그 후 두 숫자를 더할 서비스가 준비됐다는 메세지와 함께 rclcpp::spin(node)를 통해 node에 등록된 서비스 서버를 활성화 시키고 대기하도록 한다.
중간에 사용자가 ctrl+C같이 중지 명령을 내리면 node를 shutdown하고 프로그램을 종료시킨다.

이처럼 service server을 구현할 때는 다음과 같은 구조를 따라 만들면 된다.
1. server에서 동작할 함수 정의하기
2. server를 node에 등록하고 기능을 담당하는 함수도 등록해주기

실습 2.2 executable 추가하기

C++로 구현하게 되면 실행을 위해 executable을 만드는 구문을 CMakeLists.txt에 추가해야한다.

다음과 같은 구문을 CMakeLists.txt에 추가한다.
executable 이름은 sever이고 dependency로는 rclcpp와 example_interfaces를 갖는 다는 것을 말해주고 있다.

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

그 다음 이 executable server을 설치할 장소를 다음과 같이 명시해준다.

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

이 것으로 service server 구현을 위한 작업은 완료되어 colcon build가 가능하지만 service client 또한 구현해서 한 번에 구현하도록 하자.


실습 3. client node 구현하기

이전과 비슷하게 ros2_ws/src/cpp_srvcli/src 경로에 add_two_ints_client.cpp라는 파일을 만든다.

cd ~/ros2_ws/src/cpp_srvcli/src
gedit 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. 코드 분석

먼저 rclcpp와 example_interfaces가 dependency이므로 추가한다.

시간에 대한 변수나 함수를 사용하기 위해 chrono도 추가해준다.
memory는 shared_ptr를 사용하기 위함이고 cstdlib같은 경우는 추후에 코드가 나오면 설명하도록 하겠다.

std::chrono_literals를 namespace로 사용함으로써 1s(1초)와 같이 사람이 읽기 쉬운 변수를 사용할 수 있다.

main 함수에서는 먼저 입력된 인자가 3개인지 확인하고 3개가 아닐 시 잘 못된 사용임을 알려주고 프로그램을 종료한다.

만약 인자 개수가 정상적으로 3개 입력됐다면, "add_two_ints_client"이름으로 node를 만들고 그 node에 어떤 service를 사용할지와 그 srv 타입은 어떤건지 명시해서 client를 만들어준다.

그 후 서비스를 호출하기 위해 request를 정의하는데 이때 request의 a와 b값은 사용자가 입력한 인자 1,2번에서 획득한다. 주목할 점은 atoll이란 함수 인데 처음에 추가해준 cstdlib에서 제공하는 함수로써 입력된 문자열을 long long int로 변환해준다.

서비스를 호출하기전 안전장치로 서비스 서버가 존재하는지 확인한다. 이는 client->wait_for_service를 1초마다 호출하는 while문을 작성해서 구현 할 수 있다.
이렇게 service를 기다리는 도중 ctrl+C같은 중지 명령이 들어오면 "Interrupted while waiting for the service. Exiting."라는 에러메세지를 띄우고, 중지 없이 service가 발견되지 않으면 기다리는 중이라고 메세지를 띄운다.

정상적으로 service가 발견되면 client는 asyn_send_request를 통해 request를 보내게 된다.
이때 반환 되는 result는 rclcpp:spin_until_future_complete에 node와 함께 인자로 들어감으로써 완료 될 때까지 확인하는 함수를 사용할 수 있게 한다.

만약 이 함수에 반환 값이 SUCCESS이면 해당 서비스는 성공적으로 호출 됐다는 의미 이므로 result에 저장되어있는 sum값을 출력한다.

SUCCESS가 아닌경우에는 호출 실패라는 메세지를 출력한다.

그다음 node를 안전히 종료하고 프로그램을 마친다.

이렇게 client를 구현할 때 다음 단계를 따르면 쉽게 구현할 수 있다.

  • node에 client가 사용할 srv 타입과 함께 등록한다.
  • 요청할 request를 만든다.
  • 호출하기 전에 service가 존재하는지 유무를 while문 등으로 확인한다.
  • 존재하면 request를 보내 result(response)를 획득한다.
  • response를 성공 유무에 따라 적절히 대응하는 코드를 작성한다.

실습 3.2. executable 추가

server와 마찬가지고 client도 executable을 추가해준다.

완성된 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. 빌드 및 실행

모든 재료가 완성됐으므로 이제 빌드하고 실행하면 된다.

그전에 dependency를 rosdep으로 설치해준다.

cd ~/ros2_ws
rosdep install -i --from-path src --rosdistro humble -y

그 후 colcon build로 원하는 패키지 cpp_srvcli를 빌드한다.

colcon build --packages-select cpp_srvcli

빌드한 터미널과 다른 터미널을 열어 작업환경을 소스하고 server를 실행시킨다.

cd ~/ros2_ws
source install/setup.bash
ros2 run cpp_srvcli server

그럼 다음과 같이 준비됐다는 메세지가 뜰 것이다.

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

또다른 터미널을 열어서 client를 2개의 int 인자와 함께 실행해준다.

cd ~/ros2_ws
source install/setup.bash
ros2 run cpp_srvcli client 2 3

그러면 Sum 결과 5라는 숫자가 출력될 것이다.

[INFO] [rclcpp]: Sum: 5

다시 server를 실행했던 터미널에 가면 다음과 같이 처리 과정이 출력되어 있을 것이다.

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

이렇게 오늘은 우리 로봇이 똑똑한 요청 응답 기능을 할 수 있도록 도와주는 service server와 client를 직접 구현하는 방법을 알아봤다.

앞으로 service server와 client를 만드는 과정을 간단히 요약하면 다음과 같다.

"service server 만들기"
1. server에서 동작할 함수 정의하기
2. server를 node에 등록하고 기능을 담당하는 함수도 등록해주기

"service client 만들기"
1. node에 client가 사용할 srv 타입과 함께 등록한다.
2. 요청할 request를 만든다.
3. 호출하기 전에 service가 존재하는지 유무를 while문 등으로 확인한다.
4. 존재하면 request를 보내 result(response)를 획득한다.
5. response를 성공 유무에 따라 적절히 대응하는 코드를 작성한다.


오늘도 여기까지 온 스스로를 격하게 칭찬해주자!

질문하고 싶거나 인공지능 & 로봇 개발에 대해 다뤄줬으면 하는 주제를 댓글로 남겨주기 바란다~!

문의메일: irobou0915@gmail.com

오픈톡 문의: https://open.kakao.com/o/sXMqcQAf

IRoboU 유튜브 채널: 링크텍스트

참고 문헌: 링크텍스트

profile
지식이 현실이 되는 공간

0개의 댓글