ROS2의 꽃 publisher와 subscriber(C++ 편)

IROBOU·2024년 1월 21일
2

인공지능_로봇개발

목록 보기
11/17
post-thumbnail

여태까지 명령어랑 개념을 배웠다면 이제는 본격적으로 로봇개발에 기능을 만들 수 있게 해주는 publisher와 subscriber를 작성하는 예시를 이번 시간에 살펴볼 것이다.


목표:
C++로 publisher와 subscriber node를 작성하고 실행한다.

tutorial level: 입문

예상 소요시간
약 20분


배경지식

예전에 topic 개념을 배웠듯이 ROS2는 node라는 곳에서 메세지를 publish(발행)하고 또다른 node에서 그 메세지를 subscribe(구독)하는 식으로 통신이 일어난다.

링크: node란?
링크: topic이란?

예를 들어 "talker"라는 node는 어떤 숫자의 메세지를 주기적으로 publish했고, "listener"라는 node는 그 숫자들을 주기적으로 subscribe했었다.

준비물

다음 포스트에 있는 실습을 마친 사람들이 이번 실습을 원활히 진행할 수 있다.

실습

실습1. 패키지 만들기

ros2라는 command를 사용하기 위해 ros2 workspace를 먼저 source해주자.

source /opt/ros/humble/setup.bash

지난 시간에 만들었던 ros2_ws/src directory로 이동해서 ros2 pkg create를 이용해서 패키지를 만들어주자.
CMake로 패키지를 만들 것이고, license로는 Apach-2.0, cpp_pubsub이라는 패키지 이름을 붙여줄 것이다.

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

패키지를 생성했으면 패키지 안에 있는 src 폴더로 이동하자.

cd ~/ros2_ws/src/cpp_pubsub/src

실습2. publisher node작성하기

쉽게 설명하기 위해 미리 작성된 코드를 다운 받고 코드를 살펴보는 식으로 진행하겠다.

먼저 ros2_ws/src/cpp_pubsub/src에서 다음과 같이 코드를 다운 받아주자.

wget -O publisher_member_function.cpp https://raw.githubusercontent.com/ros2/examples/humble/rclcpp/topics/minimal_publisher/member_function.cpp

그럼 다음과 같이 publisher_member_function.cpp라는 파일이 설치 된 것을 볼 수 있을 것이다.

visual code 설치하기(코드 에디터)

  • "ubuntu software"이모티콘을 누른다.

  • code를 검색한다.

-visual code를 찾아 클릭한 후 install을 눌러준다.

-비밀번호를 입력하고 기다린다.

(다른 방법)커맨드 라인으로 설치하기

실습2 이어서.

publsher_member_function.cpp를 다운 받았으면 방금 설치한 visual code로 열어준다.

cd ~/ros2_ws/src/cpp_pubsub/src
code . #현재 경로에 있는 파일 및 폴더를 visual code로 연다. 

예시

그러면 다음과 같은 코드가 뜰 것이다.

<text 버전>

#include <chrono>
#include <functional>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

/* This example creates a subclass of Node and uses std::bind() to register a
* member function as a callback from the timer. */

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

  private:
    void timer_callback()
    {
      auto message = std_msgs::msg::String();
      message.data = "Hello, world! " + std::to_string(count_++);
      RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
      publisher_->publish(message);
    }
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
    size_t count_;
};

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

실습 2.1 코드 설명

예제 코드를 하나씩 살펴보자.

#include <chrono>
#include <functional>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

먼저 C++ standard library들인 chrono, functional, memory, string을 include하고 있다.
그다음에 rclcpp/rclcpp.hpp를 include하게 되는데 이는 ROS2를 cpp로 개발할 때 가장 흔하게 필요한 헤더파일이다.

그 밑에 std_msgs/msg/string.hpp 는 ROS2 standard message중 string에 해당하는 message type을 사용하기 위한 헤더파일로써 publish할 때 string data type을 사용할 것이기 때문에 필요하다.

다음 using namcespace~는 std::chrono_literals를 매번 입력하는 수고를 덜기위해 작성해준다.

class MinimalPublisher : public rclcpp::Node

다음 살펴볼 라인은 rclcpp::Node에서 상속받아 MinimalPublisher라는 node class를 만들고 있다.
이 코드 밑에 사용되는 this라는 키워드는 이 node를 가리키게 된다.

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

다음 코드에서는 public constructor, MinimalPublisher를 정의하는데
이 constructor는 node이름을 "minimalpublisher"로 정하고, count를 0으로 초기화 한다.
그 후 "publisher_"를 string 타입의 메세지를 사용하도록 선언한다.
"topic"은 이 publihser가 메세지를 발행할 topic의 이름이고, topic이 가지고 있을 수 있는 message의 한계 개수는 10개로 지정한다.

다음 timer_라는 변수에 "timer_callback"함수를 선언한다. timer 함수는 보통 주기적으로 어떤 함수를 실행하고 싶을 때 사용하는데 그 주기는 지금 500ms(1초에 2번)으로 설정되어있으며, timer_callback함수에 정의된 기능을 실행하게 될 것이다.

private:
  void timer_callback()
  {
    auto message = std_msgs::msg::String();
    message.data = "Hello, world! " + std::to_string(count_++);
    RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
    publisher_->publish(message);
  }

위의 코드는 timer_callback()함수가 어떤 기능을 하는지 말해준다.
먼저 string타입의 메세지를 정의한후 그 메세지의 data에 "Hello, world"와 현재 카운트된 숫자를 조합해 저장한다.
그다음 terminal에 publish될 내용이 뭔지 알수 있도록 log를 띄우는 RCLCPP_INFO를 사용하고, 마지막으로 미리 정의된 publisher가 방금 작성한 message를 publish하게 된다.

위의 코드를 살펴보면서 timer, publisher, count_들이 멤버변수로 사용됐는데 이는 다음과 같이 하단에 정의 되어있다.

rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;

class의 생성자(constructor), 및 멤버 변수, 함수들이 정의 됐으면 이 클래스를 main함수에서 생성하여 기능하도록하는 코드를 다음과 같이 작성할 수 있다.

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

main함수에서는 rclcpp::init을 통해 ROS2 를 초기화 하고, rclcpp::spin으로 MinimalPublisher class에 정의된 publisher코드를 실행 시킨다.
rclcpp::shutdown()은 중간에 프로그램이 종료하라는 명령이 들어오면(ctrl+c입력등) ROS2를 종료하고 0값을 return하고 프로그램을 종료한다.

실습 2.2. dependency추가하기

우리가 작성한 C++ node를 build하기 위해선 CMakeLists.txt와 package.xml을 적절히 수정해야한다.

먼저 pakcage.xml을 visual code로 열어주자.

그 다음, description, maintainer, license를 적절히 입력해주고(빌드랑은 상관 없는 부분)
다음과 같이 dependency를 입력해준다.

<depend>rclcpp</depend>
<depend>std_msgs</depend>

이는 우리 package는 rclcpp와 std_msgs를 dependecy로 갖는다는 말이다.

실습 2.3 CMakeLists.txt 수정하기

이제 CMakeLists.txt를 열어서 rclcpp와 std_msgs 패키지를 찾는 문구를 다음과 같이 find_package(ament_cmake REQUIRED) 밑에 넣어준다.

find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

이는 패키지를 빌드할 때 이 두 패키지를 찾으라는 의미이다.

그다음 우리가 작성한 publisher_member_function.cpp의 executable을 추가하고 이 executable은 rclcpp std_msgs에 의존성을 요구한다는 문구를 다음과 같이 추가한다. executable이름은 talker로 한다.

add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)

이제 install(TARGETS ...)문구를 작성하여 ros2 run 명령어로 해당 executable을 실행할 수 있게 해준다.

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

최종 CMakeLists.txt는 다음과 같다.

cmake_minimum_required(VERSION 3.5)
project(cpp_pubsub)

# Default to C++14
if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 14)
endif()

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)

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

ament_package()

이제 패키지를 빌드 할 수 있지만 subscriber까지 작성하고 빌드하도록하자.


실습3. subscriber node 작성하기

빠른 설명을 위해 먼저 subscriber node code를 다운받자.

cd ~/ros2_ws/src/cpp_pubsub/src
wget -O subscriber_member_function.cpp https://raw.githubusercontent.com/ros2/examples/humble/rclcpp/topics/minimal_subscriber/member_function.cpp

그러면 ls를 했을 때 다음과 같은 subscriber_member_function.cpp가 생긴 것을 확인 할 수 있을 것이다.

visual code로 subscriber_member_function.cpp를 열면 다음과 같은 코드를 볼 수 있다.

#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1;

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

  private:
    void topic_callback(const std_msgs::msg::String & msg) const
    {
      RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
    }
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};

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

실습 3.1 코드 분석

코드를 하나씩 살펴보자.

public:
  MinimalSubscriber()
  : Node("minimal_subscriber")
  {
    subscription_ = this->create_subscription<std_msgs::msg::String>(
    "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
  }

먼저 이전에 살펴봤던 것처럼 생성자를 통해서 "minimal_subscriber"라는 이름을 갖는 node를 생성하게 한다.

생성자는 subscription을 string type을 subscript하게 정의하고 이 메세지는 "topic"이라는 이름을 갖는 topic에서 읽어온다.
subscribe는 10개의 대기 메세지를 갖을 수 있으며, topic에서 message가 도착하면 topic_callback에 정의된 함수를 실행할 것이다.

private:
  void topic_callback(const std_msgs::msg::String & msg) const
  {
    RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
  }
  rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;

그렇다면 topic_callback에 대한 정의가 필요한데, message가 들어오면
어떤 메세지를 들었는지에 대한 메세지를 RCLCPP_INFO를 통해 출력하도록 코드를 작성했다.

또한 subscription_ 멤버 변수에 필요한 타입을 rclcpp::Subscription을 이용해 정의한다.

main함수에서는 publisher node에서와 마찬가지고 node를 생성하고 이를 spin으로 돌아가게 만든다.

실습3.2. CMakeList.txt수정하기

publisher node를 작성했을 때 처럼 executable에 대한 구문과 install 구문을 넣어주어야한다.

add_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp std_msgs)

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

executable 이름은 listener이고, 빌드할 때 rclcpp std_msgs를 필요로한다.

실습4. 빌드 및 실행하기

빌드하기전에 항상 package.xml에 필요한 dependency를 설치하는 습관을 들이면 좋다. 이는 rosdep을 통해 쉽게 할 수 있다.

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

그다음 colcon build해준다. cpp_pubsub만 빌드하면 되므로 pakcage-select tag를 추가해준다.

colcon build --packages-select cpp_pubsub

새로운 터미널을 열어 작업환경을 소스하고 talker node를 실행해준다.

cd ~/ros2_ws
. install/setup.bash
ros2 run cpp_pubsub talker

다음과 같은 메세지들이 출력되면 성공!

[INFO] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [minimal_publisher]: Publishing: "Hello World: 1"
[INFO] [minimal_publisher]: Publishing: "Hello World: 2"
[INFO] [minimal_publisher]: Publishing: "Hello World: 3"
[INFO] [minimal_publisher]: Publishing: "Hello World: 4"

또 다른 터미널을 실행하여 listener node를 실행해준다.

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

다음과 같이 publisher가 발행한 메세지를 받아 출력할 것이다.

[INFO] [minimal_subscriber]: I heard: "Hello World: 10"
[INFO] [minimal_subscriber]: I heard: "Hello World: 11"
[INFO] [minimal_subscriber]: I heard: "Hello World: 12"
[INFO] [minimal_subscriber]: I heard: "Hello World: 13"
[INFO] [minimal_subscriber]: I heard: "Hello World: 14"

여태까지 간단한 publisher와 subscriber에 대한 코드 작성 예시를 살펴봤다.
정말 단순하지만 timer callback이나 topic callback에 우리가 원하는 기능을 구현함으로써 유용하게 사용될 수 있는 개념이다.
예를 들어 카메라 데이터가 발행되면(publisher) 사람을 인식해라(subscriber)라는 기능을 하는 node들도 오늘 배운 개념을 이용해서 작성할 수 있다.

이렇게 활용성이 크기 때문에 로봇개발할 때 가장 빈번하게 사용되는 개념이기도 하다.

다음시간에는 좀 더 특수한 기능을 갖는 service server와 client를 작성하는 방법에 대해 배워볼 것이다.


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

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

profile
지식이 현실이 되는 공간

0개의 댓글