명령과 적용·이동을 다른 기록으로 남긴다
주행 로그의 /cmd_vel은 차체 속도 제안이다. 관절/드라이버가 받아들인 값, 실제 바퀴 상태와 차체 운동은 다른 사건이다. 제한/중재·큐/지연·구동 모드가 명령을 바꿀 수 있다. 원하는 v/회전속도가 기록되었다고 로봇이 그대로 움직였다고 확정하지 않는다. 원 제안/최종 적용 명령과 실제 상태를 ID/시간으로 연결한다.
오도메트리(odometry)는 바퀴/관성·영상 등 운동 관측을 누적해 로봇의 이동을 추정하는 과정이다. 이 단위는 바퀴 각도 기반 이상 모형을 따로 계산하며 Gazebo DiffDrive의 실제 실행 결과를 재현했다고 쓰지 않는다. simulator의 별도 world pose를 기준으로 쓰면 출처/같은 초기 정렬·시각과 그 모형 안의 참값이라는 범위를 명시한다. 실물 센서가 얻는 추정과 시뮬레이터 내부 상태를 구분한다.
기록 schema에는 실험 episode/epoch, 관측 ID/sequence, 취득/수신·적용 시각과 clock, joint 이름/누적 각도/단위·frame, 명령/제한·odom/기준 pose와 모델/설정 버전을 둔다. 메시지 header stamp와 recorder가 저장한 시간은 같다고 가정하지 않는다. 중복/누락·역전/재시작과 유한값 검사를 먼저 하고 물리량을 계산한다.
누적 각도의 차분을 바퀴 이동으로 바꾼다
두 바퀴 양의 회전은 전진, 반지름 r=0.1 m/좌우 간격 L=0.4 m로 정한다. 같은 episode/clock의 누적 각도 φ_L/φ_R(rad)를 읽고 전후 차이 Δφ를 구한다. 각도가 2π에서 되감기는 형식이면 그대로 차분하지 말고 해당 encoder/메시지의 wrap 규약을 확인한다. 아래 입력은 되감김 없는 누적 각도다.
Δl/Δr은 왼쪽/오른쪽 이동(m), Δs는 차체 중심 이동 호 길이(m), Δθ는 yaw 변화(rad)다. 이상적인 평면 비미끄럼과 양 바퀴의 유효 반지름이 같다는 가정이다. 바퀴가 헛돌거나 접촉이 바뀌면 이 추정과 실제 차체 위치가 다를 수 있다.
| 모형 시각(s) | 왼쪽 누적 각도(rad) | 오른쪽 누적 각도(rad) |
|---|---|---|
| 0.0 | 0 | 0 |
| 0.5 | 1 | 1 |
| 1.0 | 1 | 3 |
초기 odom pose는 (x,y,θ)=(0,0,0)이다. 첫 0.5 s의 바퀴 이동은 각 0.1 m라 Δs=0.1 m/Δθ=0이다. 따라서 새 pose는 (0.1,0,0)이다. 각 바퀴 평균 속도는 2 rad/s, 차체 평균 v=0.2 m/s다. 이 구간의 속도가 일정하다고 가정한 전진 명령과 수치가 일치해도 모든 시간의 적용/추종이 같았다는 증거는 아니다.
회전 구간은 이전 방향으로 곡선을 적분한다
두 번째 0.5 s는 Δφ_L=0/Δφ_R=2라 Δl=0/Δr=0.2 m, Δs=0.1 m/Δθ=0.5 rad다. 구간 안의 바퀴 속도가 일정한 상수 곡률 모형에서는 회전 반경 R=Δs/Δθ=0.2 m다. 이전 차체 방향 기준 이동은 (R sinΔθ, R(1−cosΔθ))이고 이를 이전 yaw로 odom 방향에 회전해 더한다.
이 예의 이전 yaw는 0이라 odom 증가량은 약 (0.095885,0.024483) m다. 누적 pose는 (0.195885,0.024483,0.5)다. Δs=0.1을 그대로 x에 더하는 방식은 회전을 무시한다. 반대로 이 식의 정확성도 구간별 일정 바퀴 속도/이상 구름 모형 안에서의 정확성이다. 가변 속도/미끄럼·관측 오차까지 정확히 적분했다는 뜻은 아니다.
다음 완전한 표준 Python 예제는 위 두 구간만 계산한다. 입력 검사를 마친 합성 배열이고 이 자료에는 정확히 Δθ=0인 직선과 0.5인 곡선만 있다. 실제 encoder/일반 로그 parser나 ROS 노드를 구현한 것은 아니다.
from math import sin, cos
r, width = 0.1, 0.4
x = y = heading = 0.0
left_prev = right_prev = 0.0
for stamp, left, right in [(0.5, 1.0, 1.0), (1.0, 1.0, 3.0)]:
dl, dr = r*(left-left_prev), r*(right-right_prev)
distance, turn = (dl+dr)/2.0, (dr-dl)/width
if turn == 0.0:
bx, by = distance, 0.0
else:
radius = distance/turn
bx, by = radius*sin(turn), radius*(1.0-cos(turn))
x, y = x+cos(heading)*bx-sin(heading)*by, y+sin(heading)*bx+cos(heading)*by
heading += turn
left_prev, right_prev = left, right
print(f"t={stamp:.1f} pose=({x:.6f},{y:.6f},{heading:.6f})")
기대 출력은 t=0.5 pose=(0.100000,0.000000,0.000000)과 t=1.0 pose=(0.195885,0.024483,0.500000)이다. 작성 환경 Darwin/macOS의 Python 3.13.4 CPU에서 이 합성 계산을 실행해 확인했다. Gazebo/ROS·장비 주행 성과는 아니다. 실제 일반 구현에서는 작은 회전의 수치 안정성과 불규칙 시간/오류 입력까지 별도로 다룬다.
별도 기준/나이를 맞추고 잔차를 읽는다
같은 초기 정렬/시각의 별도 합성 기준 pose가 마지막에 (0.18,0.04) m라고 하자. 바퀴 odom−기준 잔차는 약 (+0.015885,−0.015517) m, 크기는 약 0.0222 m다. 이는 두 기록의 차이이며 미끄럼/반지름·기준 정렬/시각/구현 오차 같은 원인을 아직 구분하지 못한다. 기준 출처/불확실성과 동일 frame/시각을 먼저 확인한다.
취득 stamp 1.000 s의 상태를 같은 clock의 1.100 s에 받았다면 수신 나이는 100 ms다. 합성 소비 한계가 80 ms라면 20 ms 초과다. 수신 순서로만 적분하거나 오래된 상태를 현재 제어 상태라고 쓰지 않는다. reset 뒤 같은 stamp가 다시 나와도 epoch가 다르면 이전 궤적에 연결하지 않는다. Δt≤0인 행을 정상 속도 계산에 나누어 넣지 않는다.
bag에 무엇을 담았는지 확인한다
Jazzy rosbag2 공식 README와 기록/재생 tutorial은 메시지 기록/정보 확인·재생을 설명한다. 명령만 기록하면 바퀴/차체 추종을 판단할 수 없다. 필요한 명령/최종 적용, joint/odom·TF/정적 장착, 센서/clock·진단과 별도 기준을 실험 목적에 맞게 고른다.
metadata/토픽 형식·개수/시간 범위를 확인하고 실제 필요한 표본/정적 TF·QoS와 누락을 점검한다. bag 파일이 생겼다는 사실만으로 모든 토픽/표본을 손실 없이 저장한 것은 아니다. 설정/보정·모델/월드·driver/라이브러리와 출력 제한·초기 상태도 별도 manifest로 연결한다. seed/버전이 없는 요약 영상만으로 재현을 주장하지 않는다.
rosbag2의 use-sim-time 기록은 recorder의 저장 시간에 clock을 사용하며 메시지 header의 취득 stamp와 구별한다. 해당 모드에서는 첫 clock 전의 미초기화 때문에 기록을 시작하지 않는 공식 동작도 확인한다. 다른 clock/배속 재생과 pause/reset이 기록된 모형 간격·소비 노드의 timeout에 미치는 영향을 명시한다.
재생은 저장 메시지를 다시 전달하는 작업이다. 고정 센서 기록으로 후보를 계산하는 것과 새 행동이 환경/센서를 바꾸는 폐루프 실행은 다르다. 명령 토픽을 실행 경로에 재발행하면 부작용이 생길 수 있어 출력 비연결/토픽·시계/제어 주체를 분명히 한다. 기본 실습은 실제 bag 재생 없이 아래 표를 계산한다. 기존 4.7 Rerun/무결성과 6.2 정량 진단을 연결해 기록된 증거/미상을 보존하자.