이름이 값의 의미를 정한다
JointState는 여러 관절의 상태를 함께 나타내는 메시지다. 이름 배열 name과 위치 position, 속도 velocity, 노력 effort 배열이 있다. 같은 인덱스의 이름과 값이 한 관절을 가리킨다. 회전 관절의 위치/속도/노력 단위는 rad, rad/s, N·m이고 직선 관절은 m, m/s, N이다. effort를 전류 A로 곧바로 해석하지 않는다. 드라이버가 전류를 제공한다면 토크 추정과 보정의 별도 계약이 필요하다.
가상 메시지 name=[elbow, shoulder], position=[0.2, 0.5]는 팔꿈치가 0.2 rad, 어깨가 0.5 rad라는 뜻이다. URDF에서 shoulder가 먼저 쓰였다고 첫 숫자를 어깨에 배정하면 틀린다. 소비자가 원하는 순서가 [shoulder, elbow]라면 이름으로 찾아 [0.5, 0.2]로 재정렬한다. 두 관절이 회전 관절이라고 가정했다. 직선 관절을 섞으면 숫자가 같아도 단위가 달라진다.
공식 메시지 계약상 배열들은 같은 길이이거나 비어 있어야 한다. velocity=[]는 속도가 제공되지 않았다는 뜻이며 속도가 0이라고 관측했다는 뜻이 아니다. position이 비어 있어도 메시지 형식상 가능한 경우가 있지만, 관절 위치로 링크 변환을 계산할 이번 경로에는 필요한 위치가 있어야 한다. name 두 개에 position 한 개만 담은 메시지는 어느 관절 값이 빠졌는지 모호하다. 중복 이름, 모르는 이름, 비유한 값도 응용 계층의 입력 검사 대상으로 둔다. 특정 노드가 모든 오류를 자동 검출한다고 가정하지 않는다.
header.stamp는 상태의 기록 시각이다. 한 메시지의 상태들은 같은 시각을 나타내야 한다. 비동기 센서들을 묶는다면 시간 정렬이나 추정 정책 및 남는 오차를 기록한다. 수신 시각을 기록 시각인 것처럼 덮어쓰면 오래된 측정이 새것처럼 보인다. 시뮬레이션 시각과 시스템 시각을 섞어 숫자만 빼는 것도 피한다. Jazzy 메시지 원문을 단위와 배열 계약의 기준으로 삼는다.
상태에서 링크 변환으로 가는 경로
robot_state_publisher를 줄여 RSP라고 부르겠다. RSP는 URDF 기구학 트리와 joint_states를 받아 링크 사이의 변환을 계산한다. URDF만으로 움직이는 관절의 현재 위치를 알아내지는 못한다. GUI가 만든 가상 관절값도 입력할 수 있지만 이것이 실제 센서 측정의 증거는 아니다. RSP는 모터 명령이나 물리 시뮬레이터를 대신하지 않는다. 위치 목표를 보냈다는 기록과 그 위치가 측정됐다는 기록도 구별한다.
그림 M08.05-1. 각 칸은 서로 다른 역할/근거다. 아래 방향은 자료/처리의 연결이며 실제 성공을 자동으로 보장하지 않는다. 직접 제작한 개념도이며 본문의 조건과 함께 읽는다.
Jazzy RSP는 fixed 관절의 상대변환을 tf_static에, 갱신된 가동 관절의 변환을 tf에 발행한다. fixed는 부모와 자식 사이가 변하지 않는다는 뜻이다. 부모가 움직이면 자식의 세계 위치도 움직인다. 팔에 고정한 카메라는 팔에 대해서는 정적이지만 바닥에 대해서는 움직인다. 따라서 ‘tf_static에 있으므로 세계에서 정지’라고 판단하지 않는다.
종이 예제로 base→arm은 원점이 같은 z축 회전 관절, arm→camera는 회전 없이 +x로 0.2 m인 fixed 관절이라고 하자. 회전값 q의 단위는 rad, q의 허용 범위는 [−π,π]다. camera에서 표시한 점 p_C=(0.1,0,0) m를 base에서 표현한다. 열벡터를 쓰고 Rz(q)는 camera/arm과 정렬된 벡터를 base의 축으로 회전시키는 행렬이다. 먼저 고정 이동을 더한 뒤 회전한다.
q=π/2이면 괄호 안은 (0.3,0,0), 회전 후는 (0,0.3,0) m다. 카메라 원점만 보면 (0,0.2,0) m다. q=0으로 바꾸면 점은 (0.3,0,0)이지만 arm→camera의 0.2 m는 그대로다. 이 변형이 정적 상대변환과 동적 합성 결과의 차이를 보여 준다. 계산하는 것은 점 좌표의 source=camera, target=base 방향이다. 역방향 조회에서는 역변환을 써야 하며 이동 부호만 임의로 바꾸지 않는다.
같은 문자열과 같은 시각을 연결한다
노드 namespace는 ROS 그래프 이름의 범위를 만든다. frame_id는 메시지 안에서 프레임을 지칭하는 문자열이므로 namespace를 /r1로 정했다고 base가 자동으로 r1/base가 되지는 않는다. RSP의 frame_prefix="r1/" 같은 명시적 설정으로 출력 프레임을 구분할 수 있지만 센서 header.frame_id와 다른 발행자도 그 계약에 맞아야 한다. 단순히 토픽 이름을 바꾸어 프레임 불일치를 고쳤다고 보지 않는다.
두 로봇이 같은 tf 버퍼에 같은 child 프레임을 서로 다른 부모/값으로 발행하면 어느 관계를 쓸지 충돌한다. 각 변환의 담당 발행자를 정하고 실물/가상 모델의 프레임을 일관되게 구분한다. RSP가 계산하는 로봇 내부 트리와 world→base 같은 외부 위치 추정도 구분한다. URDF의 루트가 base라면 RSP가 모르는 world의 위치를 저절로 제공하지 않는다. 그 관계는 고정 배치나 위치 추정 등 과제에 맞는 별도 근거가 필요하다.
시간도 경로의 일부다. 합성 로그에서 필요한 동적 변환이 ROS 시각 1.0~1.1 s에만 있고 1.2 s의 점을 변환하려 한다면 요청 시각의 자료가 없다. 최신 1.1 s를 쓰는 것은 다른 시각의 근사라는 새 판단이며 원래 요청에 대한 정확한 답이 아니다. 시계 종류/초기화, 메시지 stamp, 지연, 버퍼 범위와 연결 경로를 조사한다. tf2가 모든 과거/미래 시각의 변환을 항상 가진다는 뜻으로 ‘시간에 따른 변환’을 읽지 않는다.
이름·배열→URDF 관절→상대변환→프레임 경로→시각의 순서로 좁혀 가면 화면이 이상한 원인을 분리할 수 있다. 기존 3.6 브리지에서는 교육용 명령 계약과 측정 상태를 구분한 뒤 이 흐름을 연결한다. 아래는 실제 ROS 출력이 아닌 합성 자료다.