ROS 2 C++ 토픽 통신 기초: 센서 데이터를 보내고 받는 퍼블리셔와 서브스크라이버 만들기

이번 실습에서는 C++로 ROS2에서 제공하는 인터페이스를 이용하여 토픽 통신하는 예제를 만들어 보겠습니다.

예제 내용은 간단합니다.

temperature_publisher 노드는 1초마다 임의의 온도 값을 만들어 /temperature 토픽으로 보냅니다.

temperature_subscriber 노드는 /temperature 토픽을 구독하고, 받은 온도 값을 터미널에 출력합니다.

전체 구조는 다음과 같습니다.

temperature_publisher
        |
        | std_msgs/msg/Float32
        v
/temperature
        |
        v
temperature_subscriber

이번 예제에서는 ROS 2에서 기본 제공하는 std_msgs/msg/Float32 메시지를 사용합니다.

1. 퍼블리셔 노드 만들기

먼저 온도 데이터를 보내는 퍼블리셔 노드를 작성하겠습니다.

cd ~/ros2_study/src/my-cpp_package/src
touch temperature_publisher.cpp

code를 실행하여 temperature_publisher.cpp 파일에 아래 코드를 작성합니다.

#include <chrono>
#include <memory>
#include <random>

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

using namespace std::chrono_literals;

class TemperaturePublisher : public rclcpp::Node
{
public:
  TemperaturePublisher()
  : Node("temperature_publisher")
  {
    publisher_ = this->create_publisher<std_msgs::msg::Float32>(
      "temperature",
      rclcpp::QoS(10)
    );

    timer_ = this->create_wall_timer(
      1s,
      std::bind(&TemperaturePublisher::publish_temperature, this)
    );

    RCLCPP_INFO(this->get_logger(), "Temperature publisher node has started.");
  }

private:
  void publish_temperature()
  {
    std_msgs::msg::Float32 msg;
    msg.data = generate_temperature();

    publisher_->publish(msg);

    RCLCPP_INFO(
      this->get_logger(),
      "Published temperature: %.2f C",
      msg.data
    );
  }

  float generate_temperature()
  {
    static std::random_device rd;
    static std::mt19937 generator(rd());
    static std::uniform_real_distribution<float> distribution(20.0, 35.0);

    return distribution(generator);
  }

  rclcpp::Publisher<std_msgs::msg::Float32>::SharedPtr publisher_;
  rclcpp::TimerBase::SharedPtr timer_;
};

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

  auto node = std::make_shared<TemperaturePublisher>();
  rclcpp::spin(node);

  rclcpp::shutdown();
  return 0;
}

2. 퍼블리셔 코드 분석

먼저 필요한 헤더를 포함합니다.

#include <chrono>
#include <memory>
#include <random>

각 헤더의 역할은 다음과 같습니다.

chrono : 시간 처리를 위해 사용
memory : std::shared_ptr 같은 스마트 포인터 사용
random : 임의의 온도 값 생성

다음은 ROS 2 관련 헤더입니다.

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

rclcpp는 ROS 2 C++ 프로그래밍에서 가장 기본이 되는 라이브러리입니다.

std_msgs/msg/float32.hpp는 32비트 실수 하나를 담는 메시지 타입입니다.
온도, 거리, 속도, 전압처럼 숫자 하나를 보낼 때 사용하기 좋습니다.

3. Node 클래스 상속하기

class TemperaturePublisher : public rclcpp::Node

ROS 2 C++에서는 보통 rclcpp::Node를 상속해서 노드 클래스를 만듭니다.

이렇게 작성하면 퍼블리셔, 서브스크라이버, 타이머 같은 ROS 2 기능을 클래스 안에서 깔끔하게 관리할 수 있습니다.

생성자에서는 노드 이름을 지정합니다.

TemperaturePublisher()
: Node("temperature_publisher")

이 노드는 실행되면 ROS 2 시스템 안에서 temperature_publisher라는 이름으로 등록됩니다.

4. 퍼블리셔 생성하기

publisher_ = this->create_publisher<std_msgs::msg::Float32>(
  "temperature",
  rclcpp::QoS(10)
);

이 코드는 temperature 토픽으로 std_msgs::msg::Float32 타입 메시지를 발행하는 퍼블리셔를 만듭니다.

여기서 중요한 값은 두 가지입니다.

"temperature"             : 토픽 이름
rclcpp::QoS(10)           : 메시지 큐 크기

코드에서는 “temperature”라고 작성했지만, ROS 2 명령어로 확인하면 보통 /temperature처럼 보입니다.

rclcpp::QoS(10)은 메시지를 최대 10개까지 큐에 저장할 수 있다는 의미입니다.
퍼블리셔가 데이터를 보내는 속도와 서브스크라이버가 받는 속도가 항상 같지는 않기 때문에, 어느 정도 메시지를 보관할지 정하는 설정이 필요합니다. 처음 공부할 때는 일단 rclcpp::QoS(10)으로 시작하면 충분합니다.

5. 타이머 생성하기

timer_ = this->create_wall_timer(
  1s,
  std::bind(&TemperaturePublisher::publish_temperature, this)
);

이 코드는 1초마다 publish_temperature() 함수를 실행합니다.

create_wall_timer()는 ROS 2 노드에서 일정 시간마다 콜백 함수를 반복 실행하는 타이머를 생성하는 함수입니다.

예:

timer_ = this->create_wall_timer(
  1s,
  std::bind(&TemperaturePublisher::publish_temperature, this)
);

의미:

create_wall_timer(주기, 실행할_함수)

즉,

1초마다 publish_temperature() 함수를 실행한다

여기서 wall timer는 시스템의 실제 시간, 즉 벽시계 시간을 기준으로 동작하는 타이머입니다.

6. 메시지 발행 함수

void publish_temperature()
{
  std_msgs::msg::Float32 msg;
  msg.data = generate_temperature();

  publisher_->publish(msg);

  RCLCPP_INFO(
    this->get_logger(),
    "Published temperature: %.2f C",
    msg.data
  );
}

중요한 소스는 아래입니다.

std_msgs::msg::Float32 msg;
msg.data = generate_temperature();
publisher_->publish(msg);

먼저 std_msgs::msg::Float32 타입의 메시지 객체를 만듭니다.

std_msgs::msg::Float32 msg;

그리고 메시지의 data 필드에 온도 값을 넣습니다.

msg.data = generate_temperature();

마지막으로 publish() 함수를 호출해서 토픽으로 보냅니다.

publisher_->publish(msg);

퍼블리셔는 특정 노드에게 직접 데이터를 보내는 것이 아닙니다.

정해진 토픽으로 데이터를 발행하고, 그 토픽을 구독하는 노드가 데이터를 받습니다.

7. 임의의 온도 값 만들기

float generate_temperature()
{
  static std::random_device rd;
  static std::mt19937 generator(rd());
  static std::uniform_real_distribution<float> distribution(20.0, 35.0);

  return distribution(generator);
}

이 함수는 20.0도에서 35.0도 사이의 임의 온도 값을 생성합니다.

static std::random_device rd;

난수 생성기의 초기값을 만들기 위한 객체입니다.

static std::mt19937 generator(rd());

실제 난수를 생성하는 객체입니다.

static std::uniform_real_distribution<float> distribution(20.0, 35.0);

20.0부터 35.0 사이의 실수를 비슷한 확률로 생성하도록 설정합니다.

return distribution(generator);

설정된 범위 안에서 온도 값 하나를 만들어 반환합니다.

여기서는 실제 온도 센서가 없기 때문에 랜덤 값으로 센서 데이터를 흉내 내는 것입니다.

8. 서브스크라이버 노드 만들기

이제 온도 데이터를 받는 서브스크라이버 노드를 작성하겠습니다.

cd ~/ros2_study/src/my_cpp_package
touch temperature_subscriber.cpp

temperature_subscriber.cpp 파일에 아래 코드를 작성합니다.

#include <memory>

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

class TemperatureSubscriber : public rclcpp::Node
{
public:
  TemperatureSubscriber()
  : Node("temperature_subscriber")
  {
    subscription_ = this->create_subscription<std_msgs::msg::Float32>(
      "temperature",
      rclcpp::QoS(10),
      std::bind(
        &TemperatureSubscriber::temperature_callback,
        this,
        std::placeholders::_1
      )
    );

    RCLCPP_INFO(this->get_logger(), "Temperature subscriber node has started.");
  }

private:
  void temperature_callback(const std_msgs::msg::Float32::SharedPtr msg)
  {
    RCLCPP_INFO(
      this->get_logger(),
      "Received temperature: %.2f C",
      msg->data
    );

    if (msg->data >= 30.0) {
      RCLCPP_WARN(
        this->get_logger(),
        "Temperature is high. Cooling check is recommended."
      );
    }
  }

  rclcpp::Subscription<std_msgs::msg::Float32>::SharedPtr subscription_;
};

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

  auto node = std::make_shared<TemperatureSubscriber>();
  rclcpp::spin(node);

  rclcpp::shutdown();
  return 0;
}

9. 서브스크라이버 코드 이해하기

1) 사용한 헤더

#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/float32.hpp"

memory는 std::shared_ptr를 사용하기 위한 헤더입니다.

rclcpp/rclcpp.hpp는 ROS 2 C++ 기능을 사용하기 위한 기본 헤더입니다. 노드 생성, Subscriber 생성, 로그 출력, spin() 실행 등에 사용됩니다.

std_msgs/msg/float32.hpp는 Float32 메시지 타입을 사용하기 위한 헤더입니다. 이 메시지는 float data 하나를 가지고 있으며, 여기서는 온도 값을 저장하는 데 사용됩니다.

2) 노드 클래스

class TemperatureSubscriber : public rclcpp::Node

TemperatureSubscriber 클래스는 rclcpp::Node를 상속받습니다. 따라서 이 클래스는 ROS 2 노드로 동작합니다.

3) 생성자

TemperatureSubscriber()
: Node("temperature_subscriber")

노드 이름을 temperature_subscriber로 설정합니다.

즉, 실행 후 노드 목록을 확인하면 다음과 같이 보입니다.

ros2 node list
/temperature_subscriber

4) Subscriber 생성

subscription_ = this->create_subscription<std_msgs::msg::Float32>(
  "temperature",
  rclcpp::QoS(10),
  std::bind(
    &TemperatureSubscriber::temperature_callback,
    this,
    std::placeholders::_1
  )
);

temperature라는 토픽을 구독합니다.

메시지 타입은 std_msgs::msg::Float32입니다.

rclcpp::QoS(10)

은 메시지를 최대 10개까지 큐에 저장할 수 있다는 의미입니다.

std::bind(...)

은 메시지가 들어왔을 때 실행할 콜백 함수를 연결합니다. 즉, temperature 토픽으로 메시지가 들어오면 아래 함수가 자동 실행됩니다.

temperature_callback()

5) 콜백 함수

void temperature_callback(const std_msgs::msg::Float32::SharedPtr msg)

이 함수는 온도 메시지를 받을 때마다 실행됩니다. 수신한 온도 값은 다음처럼 읽습니다.

msg->data

그리고 로그로 출력합니다.

RCLCPP_INFO(
  this->get_logger(),
  "Received temperature: %.2f C",
  msg->data
);

예를 들어 온도 값이 25.3이면 다음처럼 출력됩니다.

Received temperature: 25.30 C

6) 온도 경고 처리

if (msg->data >= 30.0) {
  RCLCPP_WARN(
    this->get_logger(),
    "Temperature is high. Cooling check is recommended."
  );
}

온도 값이 30.0 이상이면 경고 로그를 출력합니다.

즉,

30도 미만 → 온도만 출력
30도 이상 → 온도 출력 + 경고 출력

7) Subscriber 멤버 변수

rclcpp::Subscription<std_msgs::msg::Float32>::SharedPtr subscription_;

생성한 Subscriber를 저장하는 변수입니다.

이 변수를 클래스 멤버로 가지고 있어야 구독 기능이 계속 유지됩니다.

8) main 함수

rclcpp::init(argc, argv);

auto node = std::make_shared<TemperatureSubscriber>();
rclcpp::spin(node);

rclcpp::shutdown();

동작 순서는 다음과 같습니다.

ROS 2 초기화
→ TemperatureSubscriber 노드 생성
→ spin()으로 노드 실행 유지
→ 메시지가 들어오면 콜백 함수 실행
→ 종료 시 ROS 2 shutdown

rclcpp::spin(node)가 실행 중이어야 Subscriber가 메시지를 받을 수 있습니다.

10. CMakeLists.txt 수정하기

이제 두 C++ 파일을 실행 파일로 등록해야 합니다.

아래 파일을 엽니다.

cd ~/ros2_study/src/my_cpp_package
code .

find_package 아래쪽, ament_package() 위쪽에 다음 내용을 추가합니다.

add_executable(temperature_publisher src/temperature_publisher.cpp)
ament_target_dependencies(temperature_publisher rclcpp std_msgs)

add_executable(temperature_subscriber src/temperature_subscriber.cpp)
ament_target_dependencies(temperature_subscriber rclcpp std_msgs)

install(TARGETS
  temperature_publisher
  temperature_subscriber
  DESTINATION lib/${PROJECT_NAME}
)

중요한 부분은 세 가지입니다.

add_executable(temperature_publisher src/temperature_publisher.cpp)

C++ 소스 파일을 실행 가능한 프로그램으로 만듭니다.

ament_target_dependencies(temperature_publisher rclcpp std_msgs)

해당 실행 파일이 사용하는 ROS 2 패키지 의존성을 연결합니다.

install(TARGETS
  temperature_publisher
  temperature_subscriber
  DESTINATION lib/${PROJECT_NAME}
)

ros2 run 명령어로 실행할 수 있도록 설치 경로를 지정합니다.

이 install() 설정을 빼먹으면 빌드가 성공해도 ros2 run에서 실행 파일을 찾지 못할 수 있습니다.

11. package.xml 확인하기

패키지를 만들 때 아래 옵션을 사용했습니다.

--dependencies rclcpp std_msgs

그래서 package.xml에는 보통 다음 내용이 자동으로 들어갑니다.

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

그래도 한 번 확인하는 습관을 들이는 것이 좋습니다.

package.xml를 열러 아래 항목이 있는지 확인합니다.

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

ROS 2에서 package.xml은 이 패키지가 어떤 라이브러리와 메시지 패키지에 의존하는지 알려주는 파일입니다.

빌드 오류가 날 때는 보통 아래 두 파일을 먼저 확인합니다.

CMakeLists.txt
package.xml

이건 기본 중의 기본입니다.

12. 빌드하기

워크스페이스 최상위 폴더로 이동합니다.

cd ~/ros2_study

빌드합니다.

colcon build --packages-select temperature_topic_cpp

빌드가 끝나면 환경 설정을 적용합니다.

source install/setup.bash

13. 퍼블리셔 실행하기

터미널을 하나 열고 아래 명령어를 실행합니다.

cd ~/ros2_study
source install/setup.bash
ros2 run temperature_topic_cpp temperature_publisher

정상적으로 실행되면 다음과 비슷한 출력이 나옵니다.

[INFO] [temperature_publisher]: Temperature publisher node has started.
[INFO] [temperature_publisher]: Published temperature: 24.73 C
[INFO] [temperature_publisher]: Published temperature: 31.42 C
[INFO] [temperature_publisher]: Published temperature: 28.15 C

이제 퍼블리셔 노드는 1초마다 온도 값을 발행하고 있습니다.

14. 서브스크라이버 실행하기

새 터미널을 하나 더 엽니다.

cd ~/ros2_study
source install/setup.bash
ros2 run temperature_topic_cpp temperature_subscriber

정상적으로 실행되면 다음과 비슷한 출력이 나옵니다.

[INFO] [temperature_subscriber]: Temperature subscriber node has started.
[INFO] [temperature_subscriber]: Received temperature: 24.73 C
[INFO] [temperature_subscriber]: Received temperature: 31.42 C
[WARN] [temperature_subscriber]: Temperature is high. Cooling check is recommended.
[INFO] [temperature_subscriber]: Received temperature: 28.15 C

온도가 30도 이상이면 경고 로그도 함께 출력됩니다.

[WARN] [temperature_subscriber]: Temperature is high. Cooling check is recommended.

이렇게 보이면 퍼블리셔와 서브스크라이버가 정상적으로 통신하고 있는 것입니다.

15. 토픽 목록 확인하기

ROS 2에서는 CLI 명령어로 현재 사용 중인 토픽을 확인할 수 있습니다.

새 터미널을 열고 아래 명령어를 실행합니다.

cd ~/ros2_study
source install/setup.bash
ros2 topic list

예상 출력은 다음과 같습니다.

/parameter_events
/rosout
/temperature

여기서 /temperature가 보이면 토픽이 정상적으로 생성된 것입니다.

16. 토픽 타입 확인하기

/temperature 토픽의 메시지 타입을 확인해 보겠습니다.

ros2 topic type /temperature

예상 출력은 다음과 같습니다.

std_msgs/msg/Float32

퍼블리셔와 서브스크라이버는 토픽 이름만 같아서는 안 됩니다.
메시지 타입도 같아야 합니다.

이번 예제에서는 둘 다 std_msgs/msg/Float32를 사용합니다.

17. 토픽 데이터 직접 확인하기

서브스크라이버를 실행하지 않아도 CLI 명령어로 토픽 데이터를 직접 볼 수 있습니다.

ros2 topic echo /temperature

예상 출력은 다음과 같습니다.

data: 26.374
---
data: 30.812
---
data: 22.691
---

이 명령어는 디버깅할 때 매우 자주 사용합니다.

노드는 실행 중인데 데이터가 제대로 오는지 모르겠다면, 바로 ros2 topic echo로 확인하면 됩니다.

18. 노드 목록 확인하기

현재 실행 중인 노드 목록도 확인할 수 있습니다.

ros2 node list

퍼블리셔와 서브스크라이버가 모두 실행 중이라면 다음과 비슷하게 나옵니다.

/temperature_publisher
/temperature_subscriber

노드가 보이지 않는다면 실행이 안 되었거나, 터미널에서 source install/setup.bash를 하지 않았을 가능성이 있습니다.

19. 실습 정리

이번 글에서는 ROS 2 C++에서 가장 기본적인 토픽 통신 예제를 만들었습니다.

만든 노드는 두 개입니다.

temperature_publisher
temperature_subscriber

사용한 토픽은 다음과 같습니다.

/temperature

사용한 메시지 타입은 다음과 같습니다.

std_msgs/msg/Float32

전체 흐름은 다음과 같습니다.

1. temperature_publisher 노드 실행
2. 1초마다 랜덤 온도 값 생성
3. /temperature 토픽으로 Float32 메시지 발행
4. temperature_subscriber 노드가 /temperature 구독
5. 수신한 온도 값을 터미널에 출력
6. 온도가 30도 이상이면 경고 출력

Leave a Comment