[PX4 ROS 2 Programming] 3편: ROS 2 기초 노드 작성 (Listener & Advertiser)

안녕하세요! 로봇 공학과 자율 비행 시스템에 푹 빠져계신 예비 개발자 및 연구자 여러분, 다시 뵙게 되어 반갑습니다.

지난 2편에서는 코드를 작성하기 전 반드시 이해해야 할 세 가지 핵심 개념인 **QoS(Quality of Service), 좌표계(Frames), 그리고 시간 동기화(Time Synchronization)**에 대해 깊이 있게 다루어 보았습니다. 통신 지연이나 엉뚱한 비행 방향과 같은 ‘통곡의 벽’을 피하는 방법을 익히셨으니, 이제 드디어 키보드에 손을 얹고 실제 작동하는 C++ 코드를 작성해 볼 완벽한 준비가 되었습니다.

이번 3편에서는 ROS 2 프로그래밍의 가장 기본이 되는 두 가지 기능, 즉 데이터를 수신하는 Listener(구독자) 노드와 데이터를 송신하는 Advertiser(발행자) 노드를 직접 작성해 보겠습니다. 또한, 터미널 환경에서 통신 상태를 빠르고 직관적으로 점검할 수 있는 ROS 2 CLI(Command Line Interface) 디버깅 팁까지 함께 알아보겠습니다.

자, 그럼 시작해 볼까요?


1. 데이터를 받아보자: Listener (구독자) 노드 작성

비행 제어의 첫걸음은 현재 드론이 어떤 상태인지 정확히 파악하는 것입니다. 이를 위해 PX4 비행 제어기에서 uXRCE-DDS 미들웨어를 거쳐 발행되는 SensorCombined 토픽을 수신(Subscribe)하여 터미널에 출력하는 Listener 노드를 만들어 보겠습니다. SensorCombined 메시지에는 가속도계(Accelerometer)와 자이로스코프(Gyroscope) 데이터가 포함되어 있어, 드론의 현재 자세와 움직임을 파악하는 데 필수적입니다.

우리가 작성할 파일의 이름은 sensor_combined_listener.cpp입니다. 아래의 전체 코드를 먼저 살펴본 뒤, 중요한 부분을 한 줄씩 뜯어보겠습니다.

C++
#include <rclcpp/rclcpp.hpp>
#include <px4_msgs/msg/sensor_combined.hpp>
#include <iostream>

/**
 * @brief Sensor Combined uORB 토픽 데이터를 수신하는 콜백 클래스
 */
class SensorCombinedListener : public rclcpp::Node
{
public:
    explicit SensorCombinedListener() : Node("sensor_combined_listener")
    {
        // 1. QoS 프로필 설정 (매우 중요!)
        rmw_qos_profile_t qos_profile = rmw_qos_profile_sensor_data;
        auto qos = rclcpp::QoS(rclcpp::QoSInitialization(qos_profile.history, 5), qos_profile);

        // 2. 구독자 생성 및 콜백 함수 정의
        subscription_ = this->create_subscription<px4_msgs::msg::SensorCombined>(
            "/fmu/out/sensor_combined", 
            qos,
            [this](const px4_msgs::msg::SensorCombined::UniquePtr msg) {
                // 데이터 수신 시 터미널에 출력
                std::cout << "\nRECEIVED SENSOR COMBINED DATA" << std::endl;
                std::cout << "=============================" << std::endl;
                std::cout << "ts (타임스탬프): " << msg->timestamp << std::endl;
                std::cout << "자이로 X (gyro_rad): " << msg->gyro_rad << std::endl;
                std::cout << "가속도 Z (accelerometer_m_s2[1]): " << msg->accelerometer_m_s2[1] << std::endl;
            });
    }

private:
    rclcpp::Subscription<px4_msgs::msg::SensorCombined>::SharedPtr subscription_;
};

int main(int argc, char *argv[])
{
    std::cout << "Starting sensor_combined listener node..." << std::endl;
    // ROS 2 노드 초기화
    rclcpp::init(argc, argv);
    // 노드 실행 (콜백 대기)
    rclcpp::spin(std::make_shared<SensorCombinedListener>());
    // 노드 종료
    rclcpp::shutdown();
    return 0;
}

코드 상세 해설

  • 헤더 포함 (#include): rclcpp/rclcpp.hpp는 ROS 2 C++ 클라이언트 라이브러리의 핵심 헤더입니다. 또한, PX4의 메시지 구조체를 사용하기 위해 px4_msgs/msg/sensor_combined.hpp를 반드시 포함해야 합니다.
  • QoS 설정 (rmw_qos_profile_sensor_data): 2편에서 그토록 강조했던 부분입니다! PX4에서 날아오는 센서 데이터는 ROS 2의 기본 QoS(Reliable)와 호환되지 않습니다. 따라서 qos_profile_sensor_data (Best Effort 기반)를 사용하여 최신 데이터를 지연 없이 받아오도록 설정합니다. 히스토리 깊이는 5로 설정하여 최신 5개의 메시지만 큐에 보관합니다.
  • 구독자 생성 (create_subscription): 토픽 이름은 /fmu/out/sensor_combined입니다. PX4에서 ROS 2로 나오는(out) 데이터라는 의미입니다. 세 번째 인자로는 C++ 람다(Lambda) 함수를 사용하여, 데이터가 도착할 때마다 실행될 콜백 로직을 즉석에서 작성했습니다.
  • main 함수: rclcpp::init으로 시스템을 초기화하고, rclcpp::spin을 호출하여 프로그램이 종료되지 않고 계속 데이터를 기다리며 콜백 함수를 실행하도록 만듭니다.

2. 데이터를 보내보자: Advertiser (발행자) 노드 작성

이번에는 반대로 ROS 2에서 통신망을 통해 PX4로 데이터를 보내는 Advertiser(발행자) 노드를 작성해 보겠습니다. 제어 명령을 내리기 전에 안전하게 통신을 테스트해 보기 위해, DebugVect (디버그 벡터)라는 토픽을 사용할 것입니다. 이 토픽은 이름(문자열)과 X, Y, Z 세 개의 실수 값을 담을 수 있어 디버깅용으로 안성맞춤입니다.

작성할 파일의 이름은 debug_vect_advertiser.cpp입니다.

C++
#include <chrono>
#include <rclcpp/rclcpp.hpp>
#include <px4_msgs/msg/debug_vect.hpp>

using namespace std::chrono_literals;

class DebugVectAdvertiser : public rclcpp::Node
{
public:
    DebugVectAdvertiser() : Node("debug_vect_advertiser") 
    {
        // 1. 발행자 생성 (큐 사이즈 10)
        publisher_ = this->create_publisher<px4_msgs::msg::DebugVect>("/fmu/in/debug_vect", 10);

        // 2. 타이머 콜백 설정 (500ms 주기)
        auto timer_callback = [this]() -> void {
            auto debug_vect = px4_msgs::msg::DebugVect();
            
            // 현재 시간을 마이크로초 단위로 변환하여 타임스탬프에 저장
            debug_vect.timestamp = std::chrono::time_point_cast<std::chrono::microseconds>(
                std::chrono::steady_clock::now()).time_since_epoch().count();
            
            // 디버그 변수 이름 설정 (최대 10글자)
            std::string name = "test_vec";
            std::copy(name.begin(), name.end(), debug_vect.name.begin());
            
            // 테스트용 X, Y, Z 값 대입
            debug_vect.x = 1.0;
            debug_vect.y = 2.0;
            debug_vect.z = 3.0;

            RCLCPP_INFO(this->get_logger(), "Publishing debug_vect: x: %f  y: %f  z: %f",
                        debug_vect.x, debug_vect.y, debug_vect.z);
            
            // 3. 메시지 발행
            this->publisher_->publish(debug_vect);
        };

        // 500ms(0.5초)마다 timer_callback 실행
        timer_ = this->create_wall_timer(500ms, timer_callback);
    }

private:
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher<px4_msgs::msg::DebugVect>::SharedPtr publisher_;
};

int main(int argc, char *argv[])
{
    std::cout << "Starting debug_vect advertiser node..." << std::endl;
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<DebugVectAdvertiser>());
    rclcpp::shutdown();
    return 0;
}

코드 상세 해설

  • 발행자 생성 (create_publisher): 토픽 이름은 /fmu/in/debug_vect입니다. ROS 2에서 PX4 안으로 들어가는(in) 토픽이기 때문입니다. 센서 데이터와 달리 발행자는 ROS 2 기본 QoS를 사용해도 PX4와 무난하게 호환되므로 특별한 QoS 설정 없이 큐 사이즈(10)만 지정해 주었습니다.
  • 타이머 생성 (create_wall_timer): 데이터를 일회성으로 보내는 것이 아니라 지속적으로 전달하기 위해 500ms(0.5초) 주기의 타이머를 만들었습니다. 즉 1초에 2번씩 데이터를 전송합니다.
  • 메시지 조립 및 발행: std::chrono를 사용하여 현재 시스템 시간을 마이크로초(us) 단위로 추출한 뒤 timestamp 필드에 넣습니다. 그리고 임의의 이름(test_vec)과 좌표 값(x=1.0, y=2.0, z=3.0)을 넣고 publish() 함수를 호출하여 네트워크로 쏘아 보냅니다.

3. 통신망을 꿰뚫어 보자: ROS 2 CLI 활용 팁

코드를 다 작성하고 빌드까지 마쳤다면(빌드 명령어: colcon build), 노드를 실행해 볼 차례입니다. 하지만 그전에, C++ 코드 없이도 터미널 명령어만으로 PX4와 ROS 2 간의 통신이 잘 이루어지고 있는지 점검할 수 있는 강력한 무기인 ROS 2 CLI(Command Line Interface) 툴 사용법을 알려드리겠습니다.

이 명령어들은 개발 중 버그를 잡거나 데이터 흐름을 직관적으로 확인할 때 생명줄과도 같은 역할을 합니다.

1) ros2 topic list (토픽 목록 확인)

가장 먼저 네트워크 상에 어떤 데이터들이 흘러다니고 있는지 목록을 확인하는 명령어입니다.

Bash
ros2 topic list

이 명령어를 치면 /fmu/in/obstacle_distance, /fmu/out/sensor_combined 등 통신 중인 모든 토픽의 이름이 주르륵 출력됩니다. 만약 여러분이 발행한 /fmu/in/debug_vect가 이 리스트에 없다면, 에이전트 프로그램이 켜져 있는지 확인해 보아야 합니다.

2) ros2 topic echo <토픽명> (실시간 데이터 열람)

특정 토픽 안에 실제로 어떤 숫자들이 들어있는지 눈으로 확인하고 싶을 때 사용합니다.

Bash
ros2 topic echo /fmu/out/vehicle_status

명령어를 입력하면 현재 드론의 시동 상태(arming_state), 내비게이션 모드(nav_state) 등의 데이터가 터미널 창에 실시간으로 와르르 쏟아지는 것을 볼 수 있습니다. 센서 값이 튀거나 비정상적인 값이 들어오는지 1차적으로 확인할 때 유용합니다.

3) ros2 topic hz <토픽명> (발행 주기 확인)

데이터가 초당 몇 번 들어오는지(Hz) 주파수를 계산해 주는 명령어입니다. 드론 제어에서는 ‘데이터가 얼마나 규칙적이고 빠르게 오는지’가 매우 중요합니다.

Bash
ros2 topic hz /fmu/out/sensor_combined

위 명령어를 치면 “average rate: 248.187″과 같이 평균적으로 초당 약 250번 정도의 센서 데이터가 수신되고 있음을 확인할 수 있습니다. 만약 오프보드 제어(Offboard Control)를 할 때 필수 데이터의 주파수가 현저히 떨어진다면, 네트워크 지연이나 CPU 과부하를 의심해 보아야 합니다.


마치며

수고 많으셨습니다! 이번 3편에서는 C++를 이용하여 PX4와 교감하는 ListenerAdvertiser 노드를 성공적으로 구현해 보았고, CLI 툴을 통해 시스템의 맥박을 짚어보는 방법까지 습득했습니다.

이제 여러분은 ROS 2 시스템과 PX4 드론 간에 자유롭게 데이터를 주고받을 수 있는 탄탄한 고속도로를 개통하신 셈입니다. 다음 **4편: Offboard 제어 (드론 위치 및 궤적 제어)**에서는 이 고속도로를 타고 실제 제어 명령을 내려, 모터에 시동을 걸고(Arming) 드론을 허공 위 특정 좌표로 날려 보내는 흥미진진한 실전 비행 프로그래밍을 다루어 보겠습니다.

질문이 있으시다면 언제든 댓글로 남겨주시고, 다음 4편에서 더욱 유익한 내용으로 찾아뵙겠습니다. 감사합니다!


YouTube 강좌

재생


Author: maponarooo, CEO of QUAD Drone Lab

Date: February 27, 2026

Similar Posts

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다