제5부 좌표·모델
5.5 robot_state_publisher와 관절 상태
5.5.1 이 장에서 배우는 것 / 선수 지식
5.1에서 tf 트리를 봤고, 5.3·5.4에서 로봇 모델을 만들었다. 그 둘을 잇는 것이 robot_state_publisher(RSP) 다. URDF와 관절 각도를 받아 tf를 계산해 발행한다.
작은 노드지만 시스템에서의 위치가 중요하다. RSP가 실행되지 않으면 tf 트리 전체가 없고, 관절 상태가 안 오면 트리가 중간에서 끊긴다. "RViz2에 로봇이 안 보인다"·"변환을 못 찾는다"의 상당수가 이 노드 주변 문제다.
배우는 것:
- RSP가 하는 일과 안 하는 일 — 자주 오해되는 경계.
- 입력 셋 — URDF·
/joint_states·시간. /robot_description— 파라미터인가 토픽인가, Jazzy에서의 상태.- 고정 관절과
/tf_static— 무엇이 어디로 가는가. joint_state_publisher— 개발용 도구와 실기에서의 대체.- 타이밍 — 관절 상태 스탬프가 tf 스탬프가 된다.
- 다중 로봇 — 네임스페이스 구성.
- 진단 — 트리가 끊겼을 때의 절차.
선수 지식: 5.1·5.2(tf), 5.3·5.4(URDF·xacro), 1.9(파라미터), 1.6.3.3(TRANSIENT_LOCAL).
정직:
/robot_description의 배포 방식(파라미터 vsTRANSIENT_LOCAL토픽)은 배포판을 거치며 변해 왔다. Jazzy에서의 정확한 동작·권장 방식은(확인 필요)이며, 해당 절에서 두 경로를 모두 서술한다. 이 환경에서 실행 검증하지 않았다(목차 3). Jazzy에서는 둘 다 쓴다는 것을 실측으로 확인했다(실행 확인 — 5.5.13.2): 파라미터는 입력, 토픽은 출력이다.검증 상태(추가): 이 장의 서술 가운데 1곳은 이후 회차에서 Jazzy 실물과 대조해 확인했다 — 본문의 (실행 확인) 표기가 그것이다. 표기가 없는 서술은 위 고지가 그대로 적용된다(목차 3의 검증 범위 표).
5.5.2 RSP가 하는 일과 안 하는 일
| 하는 일 | 하지 않는 일 |
|---|---|
| URDF의 관절 구조 해석 | 관절 각도 측정(드라이버가 함) |
| 관절 각도 → 링크 변환 계산(순기구학) | 역기구학 |
고정 관절 → /tf_static | 로봇 제어 |
가동 관절 → /tf | 충돌 검사 |
<mimic> 관절 반영 | 시각화(RViz2가 함) |
가장 흔한 오해: "RSP가 로봇을 그린다." 아니다. RSP는 좌표 변환만 발행하고, RViz2가 그 변환과 URDF를 조합해 그린다(11.4).
두 번째 오해: "RSP가 관절 상태를 만든다." 아니다. RSP는 소비자다. 관절 각도는 하드웨어 드라이버(6.2)·시뮬레이터(10.4)·joint_state_publisher가 발행한다.
5.5.3 실행과 입력
from launch_ros.parameter_descriptions import ParameterValue
from launch.substitutions import Command
robot_description = ParameterValue(
Command(['xacro ', xacro_file, ' use_sim:=', use_sim]),
value_type=str) # 5.4.7
Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[{
'robot_description': robot_description,
'use_sim_time': use_sim, # 1.10.3
'publish_frequency': 30.0, # 실재 — 기본값은 20.0 (실행 확인)
}],
)
주요 파라미터:
| 파라미터 | 역할 |
|---|---|
robot_description | URDF 문자열(파일 경로가 아니다) |
publish_frequency | 가동 관절 tf 발행 주기 — 기본 20.0(실행 확인) |
use_sim_time | 시뮬 시간 사용(1.10) |
frame_prefix | 모든 프레임 이름에 접두어 — 실재, 기본은 빈 문자열(실행 확인) |
ignore_timestamp | 관절 상태 스탬프 무시 — 실재, 기본 false(실행 확인) |
함정:
robot_description은 파일 경로가 아니라 URDF 내용 전체다. 경로를 넣으면 "파싱 실패"가 난다.Command(['xacro ', file])가 파일을 읽어 문자열로 만드는 이유가 이것이다.
/joint_states 메시지:
header:
stamp: {sec: 1723531234, nanosec: 567000000} # ★ 측정 시각
frame_id: ''
name: ['shoulder_pan', 'shoulder_lift', 'elbow']
position: [0.0, -1.57, 1.57] # rad (revolute) 또는 m (prismatic)
velocity: [] # 선택
effort: [] # 선택
| 필드 | 필수 | 비고 |
|---|---|---|
header.stamp | ✅ | tf 스탬프가 된다(5.5.6) |
name | ✅ | URDF의 관절 이름과 정확히 일치해야 |
position | ✅ | RSP가 쓰는 유일한 값 |
velocity·effort | ❌ | RSP는 안 쓴다. 다른 소비자용 |
함정: 관절 이름이 URDF와 하나라도 다르면 그 관절의 tf가 안 나온다. 오타·접두어 불일치가 원인이며, 오류 메시지 없이 트리가 끊긴다. 다음 절의 진단이 필요하다.
5.5.4 /robot_description — 파라미터인가 토픽인가
답부터 말하면 둘 다다(실행 확인 — 5.5.13.2). 파라미터는 입력(RSP에게 URDF를 주는 경로), 토픽은 출력(다른 노드에게 나눠 주는 경로)이며 역할이 갈린다. 아래는 그 배경 — 역사적으로 변해 온 부분이며 혼란의 원인이다.
| 방식 | 설명 |
|---|---|
| 파라미터 | 각 노드가 robot_description 파라미터를 받는다 |
| 토픽 | RSP가 /robot_description을 TRANSIENT_LOCAL로 발행하고, 소비자가 구독 |
# 토픽으로 확인 (QoS 옵션 필수 — 1.6.3.3)
ros2 topic echo /robot_description --qos-durability transient_local --once
# 파라미터로 확인
ros2 param get /robot_state_publisher robot_description
토픽 방식의 이점:
| 이점 | 설명 |
|---|---|
| 단일 출처 | 여러 노드가 각자 xacro를 처리하지 않아도 된다 |
| 늦은 구독자 | TRANSIENT_LOCAL이라 나중에 기동된 노드도 받는다 |
| 일관성 | 모두가 같은 URDF를 본다 |
정직: Jazzy에서 어느 방식이 기본이고 어떤 소비자가 어느 쪽을 지원하는지는
(확인 필요)다. 실무적으로 안전한 접근은 양쪽 모두 확인하는 것이다 — RSP는 파라미터로 받아 토픽으로 재발행하는 형태가 일반적이므로, 소비자 문서에서 어느 경로를 쓰는지 보고 맞춘다.
함정: 여러 노드가 각자 xacro를 처리하면 값이 어긋날 수 있다. launch에서 xacro 인자를 다르게 주거나, 한쪽만 갱신되면 MoveIt 2가 보는 로봇과 RViz2가 보는 로봇이 다른 상황이 된다. 단일 출처 원칙(5.2.5·5.4.5)이 여기서도 적용된다.
5.5.5 고정 관절과 가동 관절
RSP는 관절 종류에 따라 다른 토픽에 발행한다.
| 관절 종류 | 발행 대상 | 발행 시점 |
|---|---|---|
fixed | /tf_static | 시작 시 한 번 |
revolute·continuous·prismatic | /tf | 주기적(관절 상태 수신 시) |
이 분리가 중요한 이유:
고정 관절이 /tf_static으로 가므로, 센서 프레임 변환은 한 번만 발행되고 영원히 유효하다(5.1.6). 이것이 대역과 CPU를 크게 절약한다 — 센서가 열 개여도 tf 트래픽이 늘지 않는다.
함정:
/tf_static은 RSP가 실행 중인 동안만 유효하다(5.2.5). RSP를 재시작하면 새로 발행되지만, 그 사이에 조회하는 노드는 실패한다.
함정: URDF에 있는 고정 관절을 별도
static_transform_publisher로도 발행하면 충돌한다(5.2.5의 ④). 센서 위치는 URDF에만 두라.
5.5.6 타이밍 — 관절 상태 스탬프가 tf 스탬프가 된다
RSP는 /joint_states의 header.stamp를 그대로 tf 스탬프로 쓴다.
이것이 옳은 설계다 — tf 변환이 실제 측정 시각을 갖게 되어, 센서 데이터와 시간 정합이 맞는다(1.10.6).
여기서 나오는 문제들:
| 문제 | 원인 | 증상 |
|---|---|---|
| 관절 상태 스탬프가 0 | 드라이버가 안 채움 | tf 스탬프가 0 → 조회 이상 |
| 스탬프가 발행 시각 | 측정 시각이 아님 | 시간 정합 오차(1.10.6) |
| 관절 상태 주기가 낮음 | 10 Hz 발행 | tf 보간 오차 증가(5.1.5) |
| 관절 상태가 끊김 | 드라이버 문제 | tf가 멈춤 → 외삽 오류(5.2.2) |
| 시계 불일치 | 다중 머신 | 대량 외삽 오류(5.2.4) |
견해: 관절 상태 발행 주기가 tf 품질을 좌우한다. 팔이 빠르게 움직이는데 관절 상태가 10 Hz면, 그 사이 변환은 선형 보간으로 근사되어 오차가 크다. 제어 주기와 비슷하게(50~100 Hz 이상) 발행하는 것이 일반적이며, ros2_control의
joint_state_broadcaster가 이를 담당한다(6.5).
함정:
publish_frequency를 실행해도 관절 상태가 안 오면 소용없다. RSP는 새 관절 상태가 없으면 같은 값을 다시 발행하거나 아무것도 안 한다(확인 필요: 구현 동작). 병목은 대개 입력 쪽이다.
5.5.7 관절 상태는 누가 발행하는가
| 발행자 | 상황 | 관련 장 |
|---|---|---|
| 하드웨어 드라이버 | 실기 | 6.2 |
joint_state_broadcaster | ros2_control 사용 시 | 6.5 |
시뮬레이터(gz_ros2_control 등) | 시뮬 | 10.4 |
joint_state_publisher | 개발·시각화 | 아래 |
joint_state_publisher_gui | 수동 조작 | 아래 |
joint_state_publisher:
Node(package='joint_state_publisher', executable='joint_state_publisher')
URDF를 읽어 모든 가동 관절을 0(또는 한계 안의 값)으로 발행한다. 실제 하드웨어가 없을 때 트리를 완성시킨다.
joint_state_publisher_gui:
Node(package='joint_state_publisher_gui', executable='joint_state_publisher_gui')
슬라이더 GUI로 관절을 움직인다. URDF 검증에 가장 유용한 도구다(5.3.9) — 축 방향·한계·기하가 의도대로인지 즉시 보인다.
함정: 실기나 시뮬을 실행하면서
joint_state_publisher도 함께 실행하면 충돌한다. 둘 다/joint_states를 발행해 값이 번갈아 나오고, 로봇이 떨린다. 개발용 launch와 실행용 launch를 분리하는 것이 관행이다.
# display.launch.py — 시각화 전용 (하드웨어 없음)
# robot_state_publisher + joint_state_publisher_gui + rviz2
# bringup.launch.py — 실기
# robot_state_publisher + 하드웨어 드라이버 (joint_state_publisher 없음)
부분 관절 상태: 여러 발행자가 서로 다른 관절을 발행하는 것은 가능하다 — 팔 드라이버가 팔 관절을, 그리퍼 드라이버가 그리퍼 관절을. RSP가 이를 합쳐 처리한다(확인 필요: 합침 동작의 세부). 다만 같은 관절을 둘이 발행하면 충돌이다.
5.5.8 <mimic> 관절
5.3.3에서 본 종속 관절을 RSP가 처리한다.
<joint name="right_finger" type="prismatic">
<mimic joint="left_finger" multiplier="-1.0" offset="0.0"/>
</joint>
/joint_states에 left_finger만 있어도 RSP가 right_finger 변환을 계산한다.
$$q_{\text{mimic}} = \text{multiplier} \times q_{\text{source}} + \text{offset}$$
mimic 관절은 URDF 쪽 소비자가 모두 처리한다(실행 확인 — 5.6.11): RSP뿐 아니라
ros2_control(MimicJoint구조체)·MoveIt 2(getMimic()계열)도 지원한다. 다만 SDF 변환 경로는 확인하지 못했다 — 시뮬레이터에서 그리퍼 한쪽만 움직이면 그 지점을 의심하라.
5.5.9 다중 로봇 구성
5.2.6에서 본 두 조치를 RSP에 적용한다.
def robot_launch(name):
robot_desc = ParameterValue(
Command(['xacro ', xacro_file, ' prefix:=', name, '/']),
value_type=str)
return GroupAction([
PushRosNamespace(name),
Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[{'robot_description': robot_desc}],
remappings=[('/tf', 'tf'), ('/tf_static', 'tf_static')],
),
])
두 가지가 함께 적용된다:
| 조치 | 결과 |
|---|---|
prefix:=robot1/ (xacro 인자 — 5.4.5) | 프레임 이름이 robot1/base_link |
('/tf', 'tf') 리매핑 | 토픽이 /robot1/tf |
함정:
frame_prefix는 실재하며(실행 확인) xacro 접두어와 중복 적용될 수 있다(Jazzy에서의 지원과 동작). 둘 중 하나만 쓰라 — 둘 다 쓰면robot1/robot1/base_link가 된다.
5.5.10 진단 — 트리가 끊겼을 때
단계별 명령:
# 1. RSP 실행 확인
ros2 node list | grep robot_state_publisher
# 2. robot_description 확인
ros2 param get /robot_state_publisher robot_description | head -5
ros2 topic echo /robot_description --qos-durability transient_local --once | head -5
# 3. 관절 상태 확인
ros2 topic hz /joint_states
ros2 topic echo /joint_states --once
# 4. 관절 이름 대조 — 가장 흔한 원인
ros2 topic echo /joint_states --once --field name
# URDF의 관절 이름과 비교
xacro robot.urdf.xacro | grep -oP '(?<=<joint name=")[^"]+'
# 5. tf 트리 확인
ros2 run tf2_tools view_frames
# 6. 정적 변환 확인
ros2 topic echo /tf_static --qos-durability transient_local --once
4번이 핵심이다. 관절 이름 불일치는 조용히 실패하며, 이 대조로 즉시 드러난다.
증상별 원인:
| 증상 | 원인 |
|---|---|
| tf 트리가 전혀 없음 | RSP 미실행·robot_description 없음 |
| 고정 관절만 있고 가동 관절 없음 | /joint_states 미수신 |
| 일부 관절만 누락 | 관절 이름 불일치 |
| 트리는 있는데 안 움직임 | 관절 상태가 상수·발행 중단 |
| 로봇이 떨림 | 관절 상태 발행자 중복 |
| RViz2에 안 보이는데 tf는 정상 | RViz2 설정·메시 경로(5.3.6) |
견해: "고정 관절만 있고 가동 관절이 없다"가 가장 진단하기 쉬운 신호다.
/tf_static은 URDF만으로 발행되므로 나오고,/tf는 관절 상태가 있어야 나온다. 이 패턴이 보이면/joint_states를 보라.
5.5.13 robot_state_publisher의 실물 — 실측
이 장이 (확인 필요)로 둔 파라미터와 /robot_description 배포 방식을, Jazzy에서 실제로 노드를 실행해 확인했다(실행 확인).
5.5.13.1 파라미터는 셋이고 기본값은 이렇다
ros2 param list로 조회한 결과, 사용자가 정할 파라미터는 넷이다(나머지는 qos_overrides.*와 use_sim_time):
| 파라미터 | 기본값 | 뜻 |
|---|---|---|
robot_description | (필수) | URDF 문자열 |
publish_frequency | 20.0 | 가동 관절 tf 발행 주기(Hz) |
frame_prefix | ""(빈 문자열) | 모든 프레임 이름 앞에 붙일 접두어 |
ignore_timestamp | false | 관절 상태의 스탬프를 무시하고 현재 시각을 쓸지 |
publish_frequency 기본이 20 Hz라는 점이 실무적으로 중요하다 — 제어 루프가 100 Hz여도 tf는 20 Hz로만 갱신된다. 5.5.7이 말한 "publish_frequency를 실행해도 관절 상태가 안 오면 소용없다"의 반대 방향 함정이다: 관절 상태는 빠른데 tf가 느린 경우가 기본 설정에서 그대로 발생한다.
함정:
ignore_timestamp를 켜면 tf의 시간 정합이 무너진다. 스탬프가 낡은 관절 상태를 "지금"으로 기록해 발행하므로, 다른 센서와 시간을 맞춰 조회하는 코드(5.2)가 조용히 틀린 결과를 받는다. 스탬프가 문제라면 그 원인을 고치는 것이 맞고, 이 파라미터는 최후의 수단이다.
5.5.13.2 /robot_description은 파라미터이면서 토픽이다
이 장이 서두에서 "파라미터 vs TRANSIENT_LOCAL 토픽 중 어느 쪽이 기본인지 (확인 필요)" 라고 남긴 질문의 답이다. Jazzy는 둘 다 쓴다.
| 경로 | 방향 | 형태 |
|---|---|---|
파라미터 robot_description | 입력 — RSP에게 URDF를 준다 | 문자열 파라미터 |
토픽 /robot_description | 출력 — 다른 노드에게 URDF를 나눠 준다 | std_msgs/msg/String |
토픽의 QoS를 직접 확인했다(실행 확인):
Topic type: std_msgs/msg/String
QoS profile:
Reliability: RELIABLE
Durability: TRANSIENT_LOCAL
TRANSIENT_LOCAL이므로 늦게 기동된 노드도 URDF를 받는다. RViz2·MoveIt·ros2_control이 부팅 순서와 무관하게 로봇 모델을 얻는 것이 이 덕이다(1.6.3.3).
함정: 소비자가
TRANSIENT_LOCAL로 구독하지 않으면 아무것도 못 받는다./robot_description을 직접 구독하는 코드를 쓸 때 기본 QoS를 그대로 두면, RSP가 이미 발행을 마친 뒤라 영원히 콜백이 안 온다.ros2 topic echo로 확인할 때도--qos-durability transient_local이 필요하다(0.7.5).
견해: URDF를 파일 경로로 주고받지 말고 이 토픽을 쓰라. 경로를 넘기면 각 노드가 파일을 따로 읽어 xacro 인자가 다르면 서로 다른 모델을 갖게 된다. 토픽은 "RSP가 처리한 결과 하나"를 모두가 공유하므로 그런 어긋남이 없다.
5.5.13.3 QoS 오버라이드가 파라미터로 열려 있다
파라미터 목록에 이런 것들이 자동으로 생긴다(실행 확인):
qos_overrides./tf.publisher.{depth, durability, history, reliability}
qos_overrides./tf_static.publisher.{depth, history, reliability}
qos_overrides./joint_states.subscription.{depth, history, reliability}
즉 코드를 고치지 않고 /tf 발행 QoS를 조정할 수 있다(1.6.6의 QoS 오버라이드 기능이 RSP에 적용된 것).
/tf_static 쪽에만 durability가 없다는 점을 눈여겨보라. 정적 변환은 TRANSIENT_LOCAL이 고정이라 바꿀 수 없게 막아 둔 것이다 — 실제로 /tf_static의 QoS를 조회하면 Durability: TRANSIENT_LOCAL이 나온다(실행 확인, 5.1.13.1과 일치).
5.5.11 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | robot_description에 파일 경로 | 파싱 실패 | URDF 문자열 전달 |
| 2 | ParameterValue value_type 누락 | launch 실패 | value_type=str(5.4.7) |
| 3 | 관절 이름 불일치 | 일부 tf 누락(조용히) | 이름 대조(5.5.10) |
| 4 | /joint_states 미발행 | 가동 관절 tf 없음 | 드라이버·JSP 확인 |
| 5 | 관절 상태 발행자 중복 | 로봇이 떨림 | launch 분리 |
| 6 | 관절 상태 스탬프 0·발행 시각 | tf 시간 정합 오차 | 측정 시각 사용(1.10.6) |
| 7 | 관절 상태 주기 낮음 | tf 보간 오차 | 제어 주기 수준으로 |
| 8 | URDF 고정 관절을 별도 정적 발행 | 충돌 | URDF에만 |
| 9 | 여러 노드가 각자 xacro 처리 | 모델 불일치 | 단일 출처 |
| 10 | frame_prefix와 xacro 접두어 중복 | 이름이 두 번 붙음 | 하나만 |
| 11 | use_sim_time 미설정 | 시간대 불일치 | /**:로 전체(1.9.7) |
| 12 | RSP 재시작 시 /tf_static 공백 | 그 순간 조회 실패 | 재시작 후 대기 |
| 13 | mimic 처리를 모든 도구가 할 것으로 가정 | 그리퍼 이상 | 도구별 확인 |
| 14 | tf 갱신이 느림 | publish_frequency 기본 20 Hz | 5.5.13.1 |
| 15 | /robot_description을 기본 QoS로 구독 | 래치라 콜백이 영영 안 옴 | 5.5.13.2 |
5.5.12 체크포인트
- RSP가 하는 일 네 가지와 하지 않는 일 네 가지를 구분할 수 있는가. 가장 흔한 두 가지 오해는?
robot_description파라미터에 무엇을 넣어야 하는가. 파일 경로를 넣으면 어떻게 되는가./joint_states의 필수 필드와, RSP가 실제로 쓰는 값은 무엇인가./robot_description의 두 배포 방식과 토픽 방식의 이점 세 가지를 말할 수 있는가.- 고정 관절과 가동 관절이 각각 어느 토픽으로 가는가. 그 분리가 왜 중요한가.
- tf 스탬프가 어디서 오는가. 그것이 옳은 설계인 이유는?
- 관절 상태 발행 주기가 tf 품질에 미치는 영향을 설명할 수 있는가.
- 관절 상태를 발행하는 주체 다섯 가지를 들 수 있는가. 개발용과 실기용 launch를 분리하는 이유는?
- 다중 로봇 구성에서 RSP에 적용해야 할 두 조치는 무엇인가.
- "고정 관절만 있고 가동 관절 tf가 없다"는 증상이 가리키는 원인은?
- tf 트리 진단의 4단계(관절 이름 대조)를 명령으로 쓸 수 있는가.
/robot_description은 파라미터인가 토픽인가.publish_frequency기본값이 tf 갱신에 뜻하는 바는(5.5.13).
다음 장 5.6은 URDF의 한계(5.3.8)를 정면으로 다룬다 — SDF라는 또 하나의 기술 형식이 왜 존재하고, 둘을 어떻게 함께 관리하는가.