그래서 어떻게 대화할건데?? ROS2 내 msg와 srv 만들기

IROBOU·2024년 2월 9일
1

인공지능_로봇개발

목록 보기
13/17
post-thumbnail

지지난 시간과 지난시간에 topic의 publisher & subscriber 그리고 service의 server와 client를 직접 코드로 어떻게 구현하는지 알아봤다.

그런데 저번에는 이미 존재하는 msg와 srv파일을 사용했는데

로봇개발을 하다보면 나만의 msg와 srv를 만들고 싶을 때가 있다.

내가 직접 필요한 형태를 정의할 수 있을까?

물론 만들 수 있다! 오늘은 이 파일들을 어떻게 직접 만드는지에 대해 실습해보자.


<목표>
나만의 interface file(msg와 srv)파일을 정의하고 이를 사용하는 c++ node를 작성한다.

<예상 소요 시간>
20분

<동영상 tutorial>
유튜브 링크


배경지식

저번에 service server와 client를 설명할 때 짜장면에 비유했던 것 처럼 고객(client)가 짜장면을 주문할 때 메뉴판을 보고 주문서에(srv파일에서 request) 적어서 주인(server)에게 service를 호출(call)한다고 했다.

모든 중국집이 같은 메뉴와 메뉴판을 사용한다면 미리 만들어진 메뉴판을 사용하면 되지만 새로운 메뉴가 추가 될 경우 새로운 메뉴판을 만들어야 할 것이다.

이렇게 로봇개발을 하다보면 누군가 만들어놓은(pre-defined) msg나 srv를 사용하는 경우가 많지만 가끔은 나만의 msg와 srv를 만들고 싶을 때가 있다.

예를 들어서 카메라 이미지를 subscribe해서 이미지내 사물들을 publish하는 Tesla의 자율주행 자동차가 있다고 하자.
이때 publish되는 msg에 사물 갯수, 사물의 이름을 담고 있는 list를 담고 싶은데 미리 정의된 msg가 없을 수 있다.

어떻게 할지 막막하지만 사실 ROS2는 자신만의 msg와 srv를 만들 수 있어서 그냥 내 나름대로 필요한 정보들을 정의해서 만들면된다.

이렇게 직접 만든 msg와 srv를 ROS2에서는 custom interfaces라고 한다.

이는 interface가 일반적으로 두개의 무언가를 연결하는 접점이라는 뜻인데 msg는 publisher와 subscriber를 srv는 service server와 client를 연결해준다점에 초점을 둬 이름을 지었다고 생각하면 기억하기 쉽다.


준비물


실습

실습 1. 패키지 생성하기

일단 interface를 정의하기 위해선 새로운 패키지가 필요하다.

준비물 세션에 링크된 publisher/subscriber(줄여서 pub/sub) 패키지와 service/client 패키지를 사용할거기 때문에 같은 ros2_ws/src에 새로운 패키지 "tutorial_interfaces"를 만들자.

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake --license Apache-2.0 tutorial_interfaces

msg와 srv를 만들기 위해서는 "msg"와 "srv"라는 디렉토리가 필요하다.
이또한 만들어주자.

cd ~/ros2_ws/src/tutorial_interfaces
mkdir msg srv

(참고) mkdir msg srv 명령어는 각각 msg와 srv라는 디렉토리를 한번에 생성한다.

실습 2. 나만의 interface정의하기

실습 2.1. msg 정의하기

이제 나만의 interface(msg와 srv등)을 만들면 된다.

먼저 msg경로로 이동해서 "Num.msg"라는 파일명을 갖는 파일을 만든다.

cd ~/ros2_ws/src/tutorial_interfaces/msg
gedit Num.msg

그다음 Num.msg에 int64 타입의 num이라는 변수명을 갖는 타입을 복사 붙여 넣기해 정의해준다.

int64 num

(예시)

이 msg는 int64 type의 정보를 통신하는데 사용할 것이다.

추가로 Sphere.msg 파일도 생성해서 다음과 같이 center라는 변수와 radius변수를 정의한다.

cd ~/ros2_ws/src/tutorial_interfaces/msg
gedit Sphere.msg
geometry_msgs/Point center
float64 radius

(예시)

이 메세지도 나중에 원에 대한 정보를 통신하는데 사용할 예정이다.

실습 2.2. srv 정의하기

다음으로는 srv파일들을 정의해보자.

srv 경로로 이동한 후 "AddThreeInts.srv"라는 파일을 만들자.

cd ~/ros2_ws/src/tutorial_interfaces/srv
gedit AddThreeInts.srv

그 후 파일 안은 다음과 같은 request와 response를 정의해준다.

int64 a
int64 b
int64 c
---
int64 sum

(예시)

여기서 a,b,c는 request로 쓰일 int타입 정보이고, sum은 response에 해당 된다.


실습 3. CMakeLists.txt 수정하기

이렇게 정의한 msg와 srv를 header 파일 형식으로 만들어 c++ node code를 작성할 때 사용하려면 CMakeLists.txt에 다음과 같은 문구들을 추가해야한다.

먼저 CMakeLists.txt를 visual code로 열어준다.

cd ~/ros2_ws/src/tutorial_interfaces
code .

그 다음 밑에 내용을 추가한다.

find_package(geometry_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Num.msg"
  "msg/Sphere.msg"
  "srv/AddThreeInts.srv"
  DEPENDENCIES geometry_msgs # Add packages that above messages depend on, in this case geometry_msgs for Sphere.msg
)

(예시)

여기서 rosdil_generate_interfaces는 우리가 정의한 Num.msg, Sphere.msg, AddThreeInts.srv를 정의할 때 사용하는 CMake 함수이다.

참고로 rosdil은 ROS Interface Definition Language의 약자이다.

추가로 알아야 할 것은 msg와 srv를 정의할 때 사용한 int와 point 타입같은 것은 기존에 pre-defined된 타입이므로 int를 갖고 있는 msgs 패키지와 point를 갖고 있는 geometry 타입을 DEPENDENCIES에 추가해줘야한다.
그리고 rosdil_generate_interfaces함수에 들어가는 첫번째 인자(argument)는 우리 패키지의 project 이름과 반드시 같아야 하는데 이는 "${PROJECT NAME}"이라고 넣어줌으로써 항상 같은 것을 보장하게 할 수 있다.

실습 4. package.xml

interface를 만들 때 pakcage.xml파일에 추가해줘야할 것이 4개정도 있다.

먼저 다음을 package.xml에 package format과 /packge라고 있는 공간 사이에 추가하자. 추가해야 하는 위치를 잘 모르겠다면 아래 예시 이미지를 참고하자!

<depend>geometry_msgs</depend>
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

(예시)

각각을 살펴보면

  • geometry_msgs: 우리 msg에 point와 같은 정보가 있기 때문에 dependency로 필요하다.
  • rosidl_default_generators: rosidl_default_generators라는 함수를 CMakeLists.txt에서 사용할 때 필요한 build tool dependency이다.
  • rosidl_default_runtime: interface를 만들게 되면 프로그램이 실행될 때 필요한 dependency가 있는데 rosidl_default_runtime이 이에 해당한다.
  • rosidl_interface_pakcages: ROS2에서는 interface를 만들게 되면 interface를 만든 해당 패키지(이번 실습의 경우 tutorial_interfaces 패키지)에 해당하는 그룹을 만들어 msg와 srv를 관리하게 된다. 이때 rosidl_interface_packages가 필요하다.

뭐라 설명이 복잡한데, 이 요소들 특히 rosil~ 어쩌고 하는 애들은 msg와 srv가 만들 때 필요한 것이고, geometry_msgs 같은 애들은 우리가 정의하는 srv와 msg에 따라 달라진다고 생각하면된다.

사실 다 외우고 다니는 건 아니고 특정 interface를 정의하는게 필요한 경우 그때 그때 찾아보면 되니 이해 안된다고 걱정하지 말고 용어만 눈에 익혀두자.

실습 5. tutorial_interfaces 패키지 build하기

자 그럼 tutorial_interfaces를 빌드할 차례다.

다음과 같이 build해주자.

cd ~/ros2_ws
colcon build --packages-select tutorial_interfaces

실습 6. msg와 srv 생성 확인

새로운 터미널을 열어 우리 작업환경을 source해주고 정상적으로 msg와 srv가 정상적으로 생성됐는지 확인하자.

이때 ros2 interface show라는 명령어를 이용하면 생성여부를 확인 할 수 있다.

cd ~/ros2_ws
source install/setup.bash
ros2 interface show tutorial_interfaces/msg/Num

그러면 다음과 같은 문구가 출력 될 것이다.

그 다음으로는 srv파일을 확인해보자.

ros2 interface show tutorial_interfaces/srv/AddThreeInts

(결과 예시)

추가로 이 msg,srv에 해당하는 hpp파일은 다음과 같은 경로에 생성되어 있다.

실습 7. 만든 interface 테스트

interface가 성공적으로 만들어진 것도 확인 했겠다, 이제 msg를 위한 publisher, subscriber 그리고 srv를 위한 service server와 client를 만들어 직접 만든 msg와 srv는 어떻게 사용하는지 알아보자.

실습 7.1. pub/sub을 만들어 Num.msg 테스트하기

주의: 이부분부터는 publsher/subscriber, service server/client 구현 실습을 진행 했어야 할 수 있는 부분이다.


위에 보이는 그림과 같이 지난시간에 만든 cpp_pubsub packge를 visual code로 열어준 다음 publisher_member_function.cpp와 subscriber_member_function.cpp를 하단의 코드들로 대체 해준다.(적절히 복사 붙여넣기)

  • publisher_member_function.cpp 코드
#include <chrono>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp"                                            // CHANGE

using namespace std::chrono_literals;

class MinimalPublisher : public rclcpp::Node
{
public:
  MinimalPublisher()
  : Node("minimal_publisher"), count_(0)
  {
    publisher_ = this->create_publisher<tutorial_interfaces::msg::Num>("topic", 10);  // CHANGE
    timer_ = this->create_wall_timer(
      500ms, std::bind(&MinimalPublisher::timer_callback, this));
  }

private:
  void timer_callback()
  {
    auto message = tutorial_interfaces::msg::Num();                                   // CHANGE
    message.num = this->count_++;                                                     // CHANGE
    RCLCPP_INFO_STREAM(this->get_logger(), "Publishing: '" << message.num << "'");    // CHANGE
    publisher_->publish(message);
  }
  rclcpp::TimerBase::SharedPtr timer_;
  rclcpp::Publisher<tutorial_interfaces::msg::Num>::SharedPtr publisher_;             // CHANGE
  size_t count_;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MinimalPublisher>());
  rclcpp::shutdown();
  return 0;
}

달라진 점을 살펴보면 "//CHANGE"라고 써져 있는데 이전에 string type의 메세지를 publish하고 이용하는 부분들이 바뀐 것을 볼 수 있다.

  • subscriber_member_function.cpp 코드
#include <functional>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp"                                       // CHANGE

using std::placeholders::_1;

class MinimalSubscriber : public rclcpp::Node
{
public:
  MinimalSubscriber()
  : Node("minimal_subscriber")
  {
    subscription_ = this->create_subscription<tutorial_interfaces::msg::Num>(    // CHANGE
      "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
  }

private:
  void topic_callback(const tutorial_interfaces::msg::Num & msg) const  // CHANGE
  {
    RCLCPP_INFO_STREAM(this->get_logger(), "I heard: '" << msg.num << "'");     // CHANGE
  }
  rclcpp::Subscription<tutorial_interfaces::msg::Num>::SharedPtr subscription_;  // CHANGE
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MinimalSubscriber>());
  rclcpp::shutdown();
  return 0;
}

subscriber코드에서도 비슷하게 변화된 부분이 CHANGE라고 표시되어 잇는데 "std_msgs/msg/string.hpp"에서 정의된 type들이 "tutorial_interfaces/msg/num.hpp"로 새롭게 정의한 num type을 사용하여 subscribe하고 데이터 처리는 하고 있다.

이제 CMakeLists.txt를 작성할 차례다.

다음과 같이 CMakeLists.txt에 복사 붙여넣기 하자. 어디다 복사 붙여넣기 해야할 지 모르는 사람은 아래 예시 이미지를 참고하자.

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tutorial_interfaces REQUIRED)                      # CHANGE

add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp tutorial_interfaces)    # CHANGE

add_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp tutorial_interfaces)  # CHANGE

install(TARGETS
  talker
  listener
  DESTINATION lib/${PROJECT_NAME})

ament_package()
  • 바꾸기 전 CMakeLists.txt

  • 바뀐 후 CMakeLists.txt

바뀐 부분을 살펴보면 std_msgs 패키지에서 tutorial_interfaces로 변한 것을 확인할 수 있다.

마지막으로 pakcage.xml에 tutorial_interfaces dependency도 추가해주자.

<depend>tutorial_interfaces</depend>

그럼 colcon build하면된다! 참고로 파일을 변경했으면 꼭 ctrl+
S를 눌러 저장하자!

cd ~/ros2_ws
colcon build --packages-select cpp_pubsub

build가 완료 됐으면 새로운 terminal을 열어서 source해주자.

cd ~/ros2_ws
source install/setup.bash

그 후 publisher를 ros2 run으로 실행한다.

ros2 run cpp_pubsub talker

새 터미널을 열어서 비슷하게 listener도 실행해주자.

cd ~/ros2_ws
source install/setup.bash
ros2 run cpp_pubsub listener

그러며 하단 이미지와 같이 msg를 publish하고 subscribe하는 log를 볼수 있다.

실습 7.2. "AddThreeInts.srv" 테스트하기

저번 시간에는 2개의 숫자를 더하는 코드를 짰었다.

이번에 만든 srv 파일 이름에서 유추할 수 있듯이 이번에는 3개의 숫자를 더하는 service server와 이를 호출하는 client 코드를 작성해볼 것이다.

먼저 pub/sub terminal에가서 Ctrl+C를 눌러 프로그램들을 종료해주자.

그 다음 ~/ros2_ws/src/cpp_srvcli 경로로 이동해서 visual code로 변형할 코드들을 열어주자.

cd ~/ros2_ws/src/cpp_srvcli
code .

(예시)

그 다음 add_two_ints_server.cpp를 변경해준다.

  • add_two_ints_server.cpp 코드
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_ints.hpp"                                        // CHANGE

#include <memory>

void add(const std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Request> request,     // CHANGE
          std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Response>       response)  // CHANGE
{
  response->sum = request->a + request->b + request->c;                                      // CHANGE
  RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld" " c: %ld",  // CHANGE
                request->a, request->b, request->c);                                         // CHANGE
  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_three_ints_server");   // CHANGE

  rclcpp::Service<tutorial_interfaces::srv::AddThreeInts>::SharedPtr service =               // CHANGE
    node->create_service<tutorial_interfaces::srv::AddThreeInts>("add_three_ints",  &add);   // CHANGE

  RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add three ints.");                     // CHANGE

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

바뀐 부분은 CHANGE로 표시되어있는데 주로 example_interfaces/srv/add_two_ints.hpp 에서 add_two_ints가 사용되던 부분이 tutorial_interfaces/srv/add_three_ints.hpp의 add_three_ints가 사용되게끔 변경되었다. service 이름도 숫자 3개를 더하는 의미를 담도록 바뀌었다.

비슷하게 add_two_ints_client도 변형해준다.

  • add_two_ints_client 코드
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_ints.hpp"                                       // CHANGE

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

using namespace std::chrono_literals;

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

  if (argc != 4) { // CHANGE
      RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_three_ints_client X Y Z");      // CHANGE
      return 1;
  }

  std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_client");  // CHANGE
  rclcpp::Client<tutorial_interfaces::srv::AddThreeInts>::SharedPtr client =                // CHANGE
    node->create_client<tutorial_interfaces::srv::AddThreeInts>("add_three_ints");          // CHANGE

  auto request = std::make_shared<tutorial_interfaces::srv::AddThreeInts::Request>();       // CHANGE
  request->a = atoll(argv[1]);
  request->b = atoll(argv[2]);
  request->c = atoll(argv[3]);                                                              // CHANGE

  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_three_ints");    // CHANGE
  }

  rclcpp::shutdown();
  return 0;
}

server와 비슷하게 바뀐 부분은 CHANGE로 표시되어있는데 주로 example_interfaces/srv/add_two_ints.hpp 에서 add_two_ints가 사용되던 부분이 tutorial_interfaces/srv/add_three_ints.hpp의 add_three_ints가 사용되게끔 변경되었다. service 이름도 숫자 3개를 더하는 의미를 담도록 바뀌었다.

그 다음 CMakeLists.txt를 변형해주자.

#...

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tutorial_interfaces REQUIRED)         # CHANGE

add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server
  rclcpp tutorial_interfaces)                      # CHANGE

add_executable(client src/add_two_ints_client.cpp)
ament_target_dependencies(client
  rclcpp tutorial_interfaces)                      # CHANGE

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

ament_package()

tutorial_interfaces 패키를 사용하도록 변경했으며 특이한점은 .cpp파일 자체 이름은 편의상 add_two_ints~.cpp로 유지하고 있다.

(예시)

마지막으로 package.xml에서 tutorial_interfaces를 dependency로 추가해준다.

<depend>tutorial_interfaces</depend>

(예시)

이제 colcon build할 차례다! build하기 전에 변경한 파일들을 다 제대로 저장했는지 Ctrl+S를 눌러서 한 번 더 확인한다.

cd ~/ros2_ws
colcon build --packages-select cpp_srvcli

새로운 터미널을 열어서 source하고 service server를 실행해준다.

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

비슷하게 새로운 터미널을 열어서 source하고 service client를 실행해준다. 단, 이때 더할 인자 3개를 추가로 입력해준다.

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

(결과 예시)

그러면 이렇게 2,3,1을 더한 6이 반환되는 것을 볼 수 있다.


자 오늘은 이렇게 msg와 srv를 직접 만들고 pub/sub과 server/client 코드를 직접 작성해서 어떻게 사용하는지 또한 알아봤다.

만드는 과정을 요약하자면 다음과 같다.

만드는 과정 요약

  • interface를 정의할 package를 만든다.
  • srv와 msg 경로를 만든다.
  • .msg와 .srv 이름을 갖는 파일을 만들고 알맞게 원하는 타입들을 정의한다.
  • CMakeLists.txt와 package.xml에 interface를 만들 때 필요한 함수와 요소들을 추가한다. msg와 srv에 dependency가 있다면 그 것도 추가한다.
  • colcon build한다.
  • 새로운 msg와 srv의 hpp파일은 install 경로에 저장 되어 있으며 이 hpp파일들은 나중에 cpp 코딩할 때 사용된다.

사용하는 과정 요약

  • include를 통해 사용하고자하는 msg 및 srv의 hpp파일을 포함시킨다.
  • pub/sub이나 server/client에 맞게 타입들을 정해주고 사용한다.

어려워 보이지만 내 msg 또는 srv를 만들어야 할 때가 오면 어떻게 만드는지 그 순서를 보고 참고해서 하면되니 너무 외울려고 하진말고 그 흐름을 이해하도록 노력하자.

다음 시간에는 ros2 run을 대체할 수 있는 launch 파일 만드는 법에 대해 배워볼 예정이다.


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

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

문의메일: irobou0915@gmail.com

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

IRoboU 유튜브 채널

참고 문헌

profile
지식이 현실이 되는 공간

5개의 댓글

comment-user-thumbnail
2024년 2월 23일

확실히 홈페이지보다 여기가 이해가 쉽게 되유

1개의 답글
comment-user-thumbnail
2024년 2월 23일

커스텀 인터페이스를 추가할땐 아래의 3개는 항상 같이 추가된다로 생각하면 되는거겠죵?
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

2개의 답글