659.24 TransformBroadcaster의 rclcpp 사용법
1. 개요
TransformBroadcaster는 ROS2의 tf2_ros 패키지에서 제공하는 핵심 클래스로서, 동적 좌표 변환(dynamic transform)을 /tf 토픽으로 발행하는 역할을 담당한다. C++ 인터페이스인 rclcpp를 통하여 TransformBroadcaster를 활용하면, 로봇 시스템 내에서 시간에 따라 변화하는 좌표 프레임 간의 관계를 실시간으로 발행할 수 있다. 본 절에서는 tf2_ros::TransformBroadcaster의 rclcpp 기반 사용법을 체계적으로 기술한다.
2. 헤더 파일 및 의존성 설정
2.1 필수 헤더 파일
TransformBroadcaster를 rclcpp 환경에서 사용하기 위해서는 다음의 헤더 파일을 포함하여야 한다.
#include <rclcpp/rclcpp.hpp>
#include <tf2_ros/transform_broadcaster.h>
#include <geometry_msgs/msg/transform_stamped.hpp>
rclcpp/rclcpp.hpp는 ROS2 C++ 클라이언트 라이브러리의 핵심 헤더이며, tf2_ros/transform_broadcaster.h는 TransformBroadcaster 클래스를 정의한다. geometry_msgs/msg/transform_stamped.hpp는 좌표 변환 메시지의 데이터 구조를 제공한다.
필요에 따라 쿼터니언 변환을 위한 추가 헤더를 포함할 수 있다.
#include <tf2/LinearMath/Quaternion.h>
2.2 package.xml 의존성
package.xml 파일에 다음의 의존성을 선언하여야 한다.
<depend>rclcpp</depend>
<depend>tf2_ros</depend>
<depend>tf2</depend>
<depend>geometry_msgs</depend>
2.3 CMakeLists.txt 설정
CMakeLists.txt에서 해당 패키지를 검색하고 링크하여야 한다.
find_package(rclcpp REQUIRED)
find_package(tf2_ros REQUIRED)
find_package(tf2 REQUIRED)
find_package(geometry_msgs REQUIRED)
add_executable(dynamic_tf_broadcaster src/dynamic_tf_broadcaster.cpp)
ament_target_dependencies(dynamic_tf_broadcaster
rclcpp
tf2_ros
tf2
geometry_msgs
)
3. TransformBroadcaster 클래스의 인터페이스
3.1 클래스 선언
tf2_ros::TransformBroadcaster는 rclcpp::Node의 공유 포인터를 인자로 받아 생성된다. 클래스의 생성자 시그니처는 다음과 같다.
explicit TransformBroadcaster(rclcpp::Node * node);
explicit TransformBroadcaster(rclcpp::Node::SharedPtr node);
두 가지 형태의 생성자가 제공되므로, 원시 포인터(raw pointer)와 공유 포인터(shared pointer) 모두를 통하여 노드를 전달할 수 있다.
3.2 주요 메서드
TransformBroadcaster의 핵심 메서드는 sendTransform()이다. 이 메서드는 두 가지 오버로드(overload)를 제공한다.
void sendTransform(const geometry_msgs::msg::TransformStamped & transform);
void sendTransform(const std::vector<geometry_msgs::msg::TransformStamped> & transforms);
첫 번째 오버로드는 단일 변환을 발행하며, 두 번째 오버로드는 복수의 변환을 동시에 발행한다. 복수 변환 발행 시 모든 변환은 하나의 tf2_msgs/TFMessage 메시지에 포함되어 /tf 토픽으로 전송된다.
4. 기본 사용 패턴
4.1 노드 클래스 내부에서의 초기화
가장 일반적인 사용 패턴은 rclcpp::Node를 상속한 사용자 정의 노드 클래스 내부에서 TransformBroadcaster를 멤버 변수로 선언하고 초기화하는 것이다.
class DynamicTFBroadcaster : public rclcpp::Node
{
public:
DynamicTFBroadcaster()
: Node("dynamic_tf_broadcaster")
{
tf_broadcaster_ = std::make_unique<tf2_ros::TransformBroadcaster>(*this);
timer_ = this->create_wall_timer(
std::chrono::milliseconds(100),
std::bind(&DynamicTFBroadcaster::broadcast_timer_callback, this));
}
private:
void broadcast_timer_callback()
{
geometry_msgs::msg::TransformStamped t;
t.header.stamp = this->get_clock()->now();
t.header.frame_id = "world";
t.child_frame_id = "robot_base";
// 병진 성분 설정
t.transform.translation.x = 1.0;
t.transform.translation.y = 2.0;
t.transform.translation.z = 0.0;
// 회전 성분 설정 (쿼터니언)
tf2::Quaternion q;
q.setRPY(0, 0, 1.57); // Roll, Pitch, Yaw (라디안)
t.transform.rotation.x = q.x();
t.transform.rotation.y = q.y();
t.transform.rotation.z = q.z();
t.transform.rotation.w = q.w();
tf_broadcaster_->sendTransform(t);
}
std::unique_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
rclcpp::TimerBase::SharedPtr timer_;
};
위 예제에서 *this를 통하여 현재 노드의 참조를 TransformBroadcaster에 전달한다. std::make_unique를 사용하여 고유 포인터(unique pointer)로 관리하는 것이 메모리 관리 측면에서 권장된다.
4.2 main 함수 구성
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<DynamicTFBroadcaster>());
rclcpp::shutdown();
return 0;
}
rclcpp::spin()을 통하여 노드를 실행하면, 타이머 콜백이 주기적으로 호출되어 좌표 변환을 발행한다.
5. TransformStamped 메시지 구성
5.1 헤더 (Header) 설정
TransformStamped 메시지의 header 필드는 변환의 시간 정보와 부모 프레임 식별자를 포함한다.
t.header.stamp = this->get_clock()->now();
t.header.frame_id = "parent_frame";
stamp 필드에는 변환이 유효한 시각을 설정한다. this->get_clock()->now()를 사용하면 현재 ROS 시간을 자동으로 취득한다. 시뮬레이션 환경에서 use_sim_time 파라미터가 활성화되어 있는 경우, 시뮬레이션 시간이 반환된다.
5.2 프레임 식별자 설정
t.header.frame_id = "world"; // 부모 프레임
t.child_frame_id = "sensor_link"; // 자식 프레임
frame_id는 부모 프레임(parent frame)을, child_frame_id는 자식 프레임(child frame)을 지정한다. 이 두 필드는 변환 트리(transform tree)에서 프레임 간 계층 관계를 정의한다.
5.3 병진 (Translation) 설정
t.transform.translation.x = 0.5; // 미터 단위
t.transform.translation.y = 0.0;
t.transform.translation.z = 0.3;
병진 벡터는 부모 프레임 원점에서 자식 프레임 원점까지의 변위를 나타내며, 단위는 미터(m)이다.
5.4 회전 (Rotation) 설정
회전은 쿼터니언(quaternion)으로 표현한다. tf2::Quaternion 클래스를 활용하면 오일러 각(Euler angle)으로부터 쿼터니언을 편리하게 생성할 수 있다.
tf2::Quaternion q;
q.setRPY(roll, pitch, yaw); // 라디안 단위
t.transform.rotation.x = q.x();
t.transform.rotation.y = q.y();
t.transform.rotation.z = q.z();
t.transform.rotation.w = q.w();
setRPY() 메서드는 Roll-Pitch-Yaw 순서의 오일러 각을 입력으로 받아 대응하는 쿼터니언을 생성한다. 인자의 단위는 라디안(radian)이며, 각각 x축(Roll), y축(Pitch), z축(Yaw) 회전을 나타낸다.
회전이 없는 경우에는 항등 쿼터니언(identity quaternion)을 설정한다.
t.transform.rotation.x = 0.0;
t.transform.rotation.y = 0.0;
t.transform.rotation.z = 0.0;
t.transform.rotation.w = 1.0;
6. 복수 변환의 동시 발행
하나의 노드에서 여러 좌표 변환을 동시에 발행하여야 하는 경우, std::vector<geometry_msgs::msg::TransformStamped>를 사용하여 복수의 변환을 일괄 전송할 수 있다.
void broadcast_multiple_transforms()
{
std::vector<geometry_msgs::msg::TransformStamped> transforms;
geometry_msgs::msg::TransformStamped t1;
t1.header.stamp = this->get_clock()->now();
t1.header.frame_id = "base_link";
t1.child_frame_id = "left_wheel";
t1.transform.translation.x = 0.0;
t1.transform.translation.y = 0.15;
t1.transform.translation.z = 0.0;
t1.transform.rotation.w = 1.0; // 항등 회전
transforms.push_back(t1);
geometry_msgs::msg::TransformStamped t2;
t2.header.stamp = this->get_clock()->now();
t2.header.frame_id = "base_link";
t2.child_frame_id = "right_wheel";
t2.transform.translation.x = 0.0;
t2.transform.translation.y = -0.15;
t2.transform.translation.z = 0.0;
t2.transform.rotation.w = 1.0;
transforms.push_back(t2);
tf_broadcaster_->sendTransform(transforms);
}
이 방식은 네트워크 효율성 측면에서 유리하다. 복수의 변환이 하나의 TFMessage 메시지에 포함되므로, 개별적으로 발행하는 것에 비하여 통신 오버헤드가 감소한다.
7. 구독자 콜백에서의 변환 발행
외부 센서 데이터나 토픽 메시지를 수신한 후, 해당 데이터를 기반으로 좌표 변환을 발행하는 패턴이 빈번하게 사용된다.
class OdomTFBroadcaster : public rclcpp::Node
{
public:
OdomTFBroadcaster()
: Node("odom_tf_broadcaster")
{
tf_broadcaster_ = std::make_unique<tf2_ros::TransformBroadcaster>(*this);
odom_sub_ = this->create_subscription<nav_msgs::msg::Odometry>(
"odom", 10,
std::bind(&OdomTFBroadcaster::odom_callback, this, std::placeholders::_1));
}
private:
void odom_callback(const nav_msgs::msg::Odometry::SharedPtr msg)
{
geometry_msgs::msg::TransformStamped t;
t.header.stamp = msg->header.stamp;
t.header.frame_id = "odom";
t.child_frame_id = "base_link";
t.transform.translation.x = msg->pose.pose.position.x;
t.transform.translation.y = msg->pose.pose.position.y;
t.transform.translation.z = msg->pose.pose.position.z;
t.transform.rotation = msg->pose.pose.orientation;
tf_broadcaster_->sendTransform(t);
}
std::unique_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
rclcpp::Subscription<nav_msgs::msg::Odometry>::SharedPtr odom_sub_;
};
이 예제에서는 오도메트리(odometry) 메시지를 수신할 때마다 odom 프레임에서 base_link 프레임으로의 동적 변환을 발행한다. 변환의 타임스탬프는 수신한 메시지의 타임스태프를 그대로 사용하여 시간적 일관성을 보장한다.
8. 발행 주기와 타이머 구성
동적 변환은 반드시 일정한 주기로 발행되어야 한다. tf2::Buffer는 캐시 기간(cache duration) 내에 유효한 변환만을 조회하므로, 발행이 중단되면 변환 조회 시 LookupException이 발생한다.
8.1 타이머 기반 발행
// 100ms 주기 (10Hz)
timer_ = this->create_wall_timer(
std::chrono::milliseconds(100),
std::bind(&DynamicTFBroadcaster::broadcast_timer_callback, this));
타이머 주기의 선택은 응용의 요구 사항에 의존한다. 일반적으로 다음의 기준을 고려한다.
| 응용 유형 | 권장 발행 주기 | 비고 |
|---|---|---|
| 저속 이동 로봇 | 10–20 Hz | 실내 서비스 로봇 등 |
| 고속 이동 로봇 | 20–50 Hz | 자율 주행 차량 등 |
| 로봇 팔 관절 | 50–100 Hz | 매니퓰레이터 관절 변환 |
| 진동 보상 | 100 Hz 이상 | 고정밀 응용 |
8.2 벽시계 타이머와 ROS 타이머의 차이
create_wall_timer(): 시스템 실시간 시계(wall clock)를 기준으로 동작한다. 시뮬레이션 시간과 무관하게 일정한 주기를 유지한다.create_timer(): ROS 시간을 기준으로 동작한다.use_sim_time파라미터가 활성화된 환경에서는 시뮬레이션 시간에 동기화된다.
시뮬레이션 환경에서의 호환성을 확보하려면 create_timer()의 사용이 권장된다.
timer_ = this->create_timer(
std::chrono::milliseconds(100),
std::bind(&DynamicTFBroadcaster::broadcast_timer_callback, this));
9. 생명주기 노드 (Lifecycle Node)에서의 사용
rclcpp_lifecycle::LifecycleNode를 상속하는 생명주기 관리 노드에서 TransformBroadcaster를 사용하는 경우, on_activate 콜백에서 초기화하고 on_deactivate 콜백에서 해제하는 패턴을 따른다.
#include <rclcpp_lifecycle/lifecycle_node.hpp>
class LifecycleTFBroadcaster : public rclcpp_lifecycle::LifecycleNode
{
public:
LifecycleTFBroadcaster()
: rclcpp_lifecycle::LifecycleNode("lifecycle_tf_broadcaster")
{}
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
on_activate(const rclcpp_lifecycle::State &)
{
tf_broadcaster_ = std::make_unique<tf2_ros::TransformBroadcaster>(
this->shared_from_this());
timer_ = this->create_wall_timer(
std::chrono::milliseconds(100),
std::bind(&LifecycleTFBroadcaster::broadcast_callback, this));
return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
}
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
on_deactivate(const rclcpp_lifecycle::State &)
{
tf_broadcaster_.reset();
timer_->cancel();
return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
}
private:
void broadcast_callback()
{
geometry_msgs::msg::TransformStamped t;
t.header.stamp = this->get_clock()->now();
t.header.frame_id = "map";
t.child_frame_id = "odom";
t.transform.rotation.w = 1.0;
tf_broadcaster_->sendTransform(t);
}
std::unique_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
rclcpp::TimerBase::SharedPtr timer_;
};
생명주기 노드를 사용하면 노드의 활성화(activate)/비활성화(deactivate) 상태에 따라 변환 발행을 제어할 수 있으므로, 시스템의 자원 관리와 상태 관리가 용이하다.
10. Component 노드로서의 구성
ROS2의 컴포넌트(component) 아키텍처를 활용하면, TransformBroadcaster를 포함하는 노드를 동적으로 로딩할 수 있다.
#include <rclcpp_components/register_node_macro.hpp>
class TFBroadcasterComponent : public rclcpp::Node
{
public:
explicit TFBroadcasterComponent(const rclcpp::NodeOptions & options)
: Node("tf_broadcaster_component", options)
{
tf_broadcaster_ = std::make_unique<tf2_ros::TransformBroadcaster>(*this);
timer_ = this->create_wall_timer(
std::chrono::milliseconds(100),
std::bind(&TFBroadcasterComponent::timer_callback, this));
}
private:
void timer_callback()
{
// 변환 발행 로직
}
std::unique_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
rclcpp::TimerBase::SharedPtr timer_;
};
RCLCPP_COMPONENTS_REGISTER_NODE(TFBroadcasterComponent)
컴포넌트 노드는 ComposableNodeContainer에 의하여 동일 프로세스 내에서 로딩되므로, 프로세스 간 통신(IPC) 오버헤드 없이 효율적으로 동작한다.
11. QoS 정책
TransformBroadcaster는 내부적으로 /tf 토픽에 대한 발행자(publisher)를 생성하며, 기본 QoS(Quality of Service) 프로파일은 다음과 같다.
| QoS 항목 | 기본값 |
|---|---|
| Reliability | RELIABLE |
| Durability | VOLATILE |
| History | KEEP_LAST |
| Depth | 100 |
동적 변환의 /tf 토픽은 VOLATILE 내구성(durability)을 사용한다. 이는 발행자가 연결되기 이전의 메시지를 구독자가 수신하지 않음을 의미한다. 반면, /tf_static 토픽은 TRANSIENT_LOCAL 내구성을 사용하여 늦은 참여(late joining) 구독자도 이전에 발행된 정적 변환을 수신할 수 있다.
12. 예외 처리 및 주의사항
12.1 프레임 ID 유효성
frame_id와 child_frame_id는 빈 문자열이어서는 안 된다. 빈 프레임 ID로 변환을 발행하면 tf2::TransformException이 발생할 수 있으며, 변환 트리의 무결성이 훼손된다.
12.2 순환 변환 방지
변환 트리는 비순환 방향 그래프(Directed Acyclic Graph, DAG)이어야 한다. 예를 들어, 프레임 A → B → C → A와 같은 순환 구조가 형성되면 변환 조회 시 무한 루프에 빠질 위험이 있다. TransformBroadcaster를 사용할 때에는 변환 트리의 구조를 사전에 설계하고, 순환이 발생하지 않도록 주의하여야 한다.
12.3 타임스탬프의 일관성
발행하는 변환의 타임스탬프는 단조 증가(monotonically increasing)하여야 한다. 과거 시간의 타임스탬프를 가진 변환을 발행하면, tf2::Buffer의 보간(interpolation) 로직에서 예기치 않은 결과가 발생할 수 있다.
12.4 동일 변환의 중복 발행
동일한 부모-자식 프레임 쌍에 대하여 복수의 노드에서 변환을 발행하면, 변환 트리에서 충돌이 발생한다. tf2_monitor를 사용하여 특정 프레임 쌍에 대해 복수의 발행자가 존재하는지 확인할 수 있다.
13. 전체 예제: 회전하는 좌표 프레임 발행
다음은 시간에 따라 z축 주위로 회전하는 자식 프레임을 발행하는 전체 예제이다.
#include <rclcpp/rclcpp.hpp>
#include <tf2_ros/transform_broadcaster.h>
#include <geometry_msgs/msg/transform_stamped.hpp>
#include <tf2/LinearMath/Quaternion.h>
#include <cmath>
class RotatingFrameBroadcaster : public rclcpp::Node
{
public:
RotatingFrameBroadcaster()
: Node("rotating_frame_broadcaster"), angle_(0.0)
{
tf_broadcaster_ = std::make_unique<tf2_ros::TransformBroadcaster>(*this);
timer_ = this->create_wall_timer(
std::chrono::milliseconds(50), // 20Hz
std::bind(&RotatingFrameBroadcaster::timer_callback, this));
RCLCPP_INFO(this->get_logger(), "Rotating frame broadcaster started.");
}
private:
void timer_callback()
{
geometry_msgs::msg::TransformStamped t;
t.header.stamp = this->get_clock()->now();
t.header.frame_id = "world";
t.child_frame_id = "rotating_frame";
// 원형 궤적 상의 병진
double radius = 2.0;
t.transform.translation.x = radius * std::cos(angle_);
t.transform.translation.y = radius * std::sin(angle_);
t.transform.translation.z = 0.0;
// z축 회전
tf2::Quaternion q;
q.setRPY(0, 0, angle_);
t.transform.rotation.x = q.x();
t.transform.rotation.y = q.y();
t.transform.rotation.z = q.z();
t.transform.rotation.w = q.w();
tf_broadcaster_->sendTransform(t);
angle_ += 0.02; // 약 0.02 라디안/프레임
if (angle_ > 2.0 * M_PI) {
angle_ -= 2.0 * M_PI;
}
}
std::unique_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
rclcpp::TimerBase::SharedPtr timer_;
double angle_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<RotatingFrameBroadcaster>());
rclcpp::shutdown();
return 0;
}
이 예제에서 rotating_frame은 world 프레임의 원점을 중심으로 반지름 2.0미터의 원형 궤적을 따라 이동하면서, 동시에 z축 주위로 회전한다. angle_ 변수를 통하여 시간에 따른 위치와 자세를 갱신한다.
14. 런치 파일에서의 실행
TransformBroadcaster 노드를 런치 파일에서 실행하는 방법은 다음과 같다.
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='my_robot_tf',
executable='dynamic_tf_broadcaster',
name='dynamic_tf_broadcaster',
output='screen',
),
])
15. 참고 자료
- ROS2 공식 문서, “Writing a tf2 broadcaster (C++)”, https://docs.ros.org/en/humble/Tutorials/Intermediate/Tf2/Writing-A-Tf2-Broadcaster-Cpp.html
- tf2_ros API 문서, https://docs.ros2.org/latest/api/tf2_ros/
- REP 103 – Standard Units of Measure and Coordinate Conventions, https://www.ros.org/reps/rep-0103.html
- REP 105 – Coordinate Frames for Mobile Platforms, https://www.ros.org/reps/rep-0105.html
- ROS2 Humble Hawksbill