현재 G1 양팔 VLA 데이터 계약을 보면 머리 카메라, 왼쪽 손목 카메라, 오른쪽 손목 카메라, 관절 상태와 양손 pose가 모두 길이 T인 배열로 저장됩니다. 겉으로는 head_rgb[t]와 robot_state[t]가 같은 장면을 설명하는 것처럼 보입니다. 변환기도 같은 인덱스 t에서 값을 꺼내 하나의 observation으로 묶습니다.
그런데 현재 schema에는 각 카메라가 영상을 획득한 시각도, state가 측정된 시각도 없습니다. 세 영상과 상태의 길이가 같다는 사실은 저장된 행의 수가 같다는 뜻일 뿐, 같은 물리 시각을 관측했다는 증거가 아닙니다. BackendRequest 역시 observation과 state dictionary를 받지만, 어느 message들을 어떤 기준으로 짝지었는지 남기지 않습니다.
이 빈틈을 정리하려면 ROS 2의 message_filters를 단순한 편의 라이브러리로 보면 안 됩니다. 여러 topic에서 도착한 message 중 어떤 조합을 하나의 표본으로 인정하고, 어떤 message를 기다리거나 버리며, 그 결정의 오차를 어떻게 보존할지 정하는 정책으로 봐야 합니다. 이번 글은 ExactTime과 ApproximateTime의 차이, queue_size와 slop이 만드는 trade-off, 그리고 G1 observation에 필요한 pairing evidence를 현재 구현 경계와 함께 정리합니다.

현재 G1 schema는 값을 묶지만 짝을 선택한 근거는 저장하지 않습니다
origin/main@ee4d96e의 g1_bimanual_vla_contract.md는 세 RGB stream과 58차원 robot state, 13차원 base state, 양손 7차원 pose를 요구합니다. g1_bimanual_hdf5_to_rlds.py는 이 key들이 존재하는지 검사하고 같은 t에서 읽어 85차원 proprioception과 세 이미지를 만듭니다. action도 왼손·오른손·torso·base를 같은 인덱스로 합칩니다.
이 경로는 schema와 변환 코드로 implemented되어 있습니다. 다만 validator가 확인하는 것은 주로 group과 key의 존재입니다. 각 stream에 source timestamp dataset이 있는지, timestamp가 단조 증가하는지, 같은 t의 최대 시간차가 얼마인지, match 과정에서 버려진 message가 몇 개인지는 검사하지 않습니다. 일반 collect_demos.py도 observation dictionary와 action을 호출 순서대로 list에 append할 뿐, sensor별 clock이나 pair identity를 받지 않습니다.
observation[t] = {
"head_rgb": head_rgb[t],
"left_wrist_rgb": left_wrist_rgb[t],
"right_wrist_rgb": right_wrist_rgb[t],
"proprio": concat(robot_state[t], base_state[t], hand_pose[t]),
}
위 구조에서 [t]는 파일 인덱스입니다. 아직 물리 시간의 표본 번호는 아닙니다. 실제 collector가 각 stream을 언제 읽고 어느 message를 선택했는지 구현되지 않았기 때문에, 이 글은 현재 G1 데이터가 잘못 동기화됐다고 주장하지 않습니다. 더 정확한 상태는 동기화를 입증할 metadata가 없다는 것입니다.
동기화의 출력은 message 묶음이 아니라 판정이 붙은 표본입니다
ROS 2 message_filters 공식 설명은 입력 message를 받아 filter의 조건이 충족될 때 나중에 출력할 수 있는 구성요소라고 말합니다. Time synchronizer는 여러 입력 stream의 header timestamp를 비교하고 조건을 만족한 조합이 생겼을 때 하나의 callback을 호출합니다. 여기서 중요한 것은 arrival 순서가 아니라 message header에 적힌 source stamp입니다.
세 stream의 선택된 stamp를 t_head, t_wrist, t_state라고 합시다. 한 묶음의 시간 폭은 다음처럼 정의할 수 있습니다.
span_ns = max(t_head, t_wrist, t_state) - min(t_head, t_wrist, t_state)
anchor_stamp = declared_reference_stamp
emit_age_ns = emit_monotonic_ns - mapped(anchor_stamp)
span은 선택된 source들이 얼마나 다른 시각을 가리키는지 말합니다. emit_age는 그 묶음이 완성됐을 때 기준 표본이 이미 얼마나 오래됐는지 말합니다. 두 값은 다릅니다. stamp가 서로 아주 가까워도 callback queue에 오래 머물렀다면 표본은 stale할 수 있고, 최신 message들이 빨리 도착했어도 source stamp 차이가 크면 같은 장면으로 묶기 어렵습니다.
따라서 동기화 callback의 출력에는 raw message만 있어서는 부족합니다. 사용한 member ID와 source stamp, clock ID·epoch, 매칭 정책과 설정, span, 각 stream의 offset, queue에서 기다린 시간, emit 시각을 함께 보존해야 합니다. 그래야 나중에 policy가 이상한 action을 냈을 때 입력 이미지만 보는 대신 “서로 몇 ms 떨어진 관측을 하나로 묶었는가”를 확인할 수 있습니다.
ExactTime은 같은 timestamp를 요구하며 그 엄격함 자체가 목적입니다
Python message_filters API의 TimeSynchronizer는 여러 입력에 timestamp가 정확히 일치하는 message 집합이 있을 때 callback을 호출합니다. 공식 Time Synchronizer 튜토리얼도 두 publisher의 header stamp가 정확히 같아야 한다고 설명합니다.
이 정책은 여러 sensor가 하나의 hardware trigger나 같은 acquisition event ID를 공유할 때 강합니다. 동일한 trigger에서 만들어진 stereo image와 calibration처럼 “같은 사건”이라는 identity를 stamp에 담았다면, exact match가 그 의미를 보존합니다. 하나라도 누락되면 완전한 set이 아니므로 callback을 만들지 않는 것도 의도에 맞습니다.
반대로 독립적으로 동작하는 카메라와 robot state publisher에 ExactTime을 붙이면 모든 topic이 정상인데도 callback이 한 번도 호출되지 않을 수 있습니다. 두 장치가 같은 clock으로 보정됐더라도 acquisition 시각이 nanosecond까지 동일할 이유는 없기 때문입니다. 이때 ExactTime을 “고장 났다”고 보기보다, 입력이 exact identity를 제공하지 않는다는 진단으로 읽어야 합니다.
| 조건 | ExactTime의 의미 | 적합한 사용 | 주의할 오판 |
|---|---|---|---|
| 모든 stamp 동일 | 완전한 set을 즉시 후보로 만들 수 있음 | 공통 trigger·동일 acquisition event | stamp를 callback 시각으로 덮어 강제로 같게 만들기 |
| 한 stream의 stamp만 다름 | set 미완성, callback 없음 | 불완전 capture를 버려야 하는 경로 | topic이 수신되지 않는다고 단정하기 |
| 한 message 누락 | 해당 stamp의 set은 완성되지 않음 | all-or-nothing 표본 | 이전 message를 조용히 재사용하기 |
| 독립 sensor clock | exact identity가 성립하지 않을 가능성 큼 | 먼저 trigger·clock 계약 확인 | queue만 키우면 언젠가 exact match된다고 기대하기 |
ApproximateTime은 보간기가 아니라 허용 구간 안의 조합 선택기입니다
ApproximateTimeSynchronizer는 timestamp가 완전히 같지 않아도 slop으로 정한 시간 범위 안에서 message들을 묶습니다. 공식 Approximate Time 튜토리얼은 서로 다른 주기의 두 publisher를 예로 들고, queue_size와 허용 delay를 설정합니다.
여기서 자주 생기는 오해가 있습니다. approximate sync는 로봇 상태를 이미지 시각으로 보간하지 않습니다. camera pose를 과거 시각으로 되돌리지도 않습니다. 조건을 만족하는 기존 message들의 조합을 골라 callback에 전달할 뿐입니다. 선택된 값이 실제로 같은 물리 상태를 표현하는지는 sensor dynamics와 application이 별도로 판단해야 합니다.
slop을 넓히면 match 수는 늘 수 있습니다. 동시에 빠르게 움직이는 손이나 base에서는 더 다른 자세를 한 observation으로 묶을 위험도 커집니다. 좁히면 temporal ambiguity는 줄지만 jitter나 rate 차이 때문에 표본이 더 많이 버려질 수 있습니다. 따라서 slop은 “일단 크게 두고 callback을 살리는 값”이 아니라, task가 허용할 수 있는 움직임 오차와 실제 stamp 분포를 보고 정할 acceptance parameter입니다.
예를 들어 머리 카메라가 물체를 본 시각과 손목 pose의 시각이 떨어져 있다면 그 차이를 단순히 millisecond 하나로 평가해서도 부족합니다. 정지 장면에서는 같은 시간차가 거의 무해할 수 있지만, 손목이 빠르게 회전하는 구간에서는 pixel과 pose의 대응이 크게 달라집니다. 첫 gate는 span으로 두더라도 이후에는 angular velocity나 task phase를 조건으로 더 엄격한 bound가 필요한지 검증해야 합니다.
slop은 지연을 보정하지 않으며 queue_offset도 측정 뒤에만 쓸 수 있습니다
한 camera driver가 acquisition stamp를 정확히 넣고 다른 driver가 exposure 종료나 encode 완료 시각을 넣는다면 두 stamp 사이에는 지속적인 offset이 보일 수 있습니다. 이 상황에서 slop을 넓히면 callback은 생기지만 서로 다른 timestamp semantics를 그대로 섞습니다. 숫자가 가까워졌을 뿐 측정 사건의 정의가 같아진 것은 아닙니다.
현재 Python API에는 stream별 nanosecond offset을 지정하는 queue_offset 옵션이 있습니다. 하지만 이 값은 calibration을 대체하지 않습니다. sensor별 timestamp가 어떤 사건을 나타내는지 확인하고, 반복 측정에서 안정적인 fixed offset이 관측되며, 온도·노출·driver mode 변화에도 유지되는지 검증한 뒤 versioned calibration으로 적용해야 합니다.
지연이 매번 달라지는 jitter라면 고정 offset으로 숨길 수 없습니다. 네트워크나 executor backlog 때문에 arrival만 늦어진 경우 source stamp를 고치는 것도 잘못입니다. source event의 시각은 그대로 두고 receive·dispatch·emit monotonic timestamp를 따로 기록해야 합니다. offset, jitter, queue wait는 서로 다른 원인 후보이므로 하나의 slop 값에 흡수시키지 않습니다.
queue_size는 초가 아니라 stream마다 보관할 message 개수입니다
TimeSynchronizer의 queue_size는 완성될 set을 기다리는 동안 각 입력에서 몇 개의 timestamp를 저장할지 정합니다. 시간 단위가 아닙니다. 같은 queue size라도 sensor rate가 다르면 보관 가능한 시간 범위가 달라지고, burst나 drop이 생기면 실제 window도 달라집니다.
queue가 너무 작으면 partner가 도착하기 전에 앞선 message가 밀려나갑니다. match rate가 낮아지고, 빠른 stream에서 특히 많은 표본이 사라질 수 있습니다. queue를 크게 하면 이 문제는 줄어드는 듯 보이지만, 오래된 후보가 더 오래 남고 메모리와 search 비용, callback age가 늘 수 있습니다. 이미 application deadline을 넘은 image가 늦은 state와 match되어 callback을 만드는 상황도 생깁니다.
따라서 queue size는 입력 rate, 정상 jitter, 예상 burst, executor가 막힐 수 있는 최장 구간, application freshness 한계를 함께 보고 정해야 합니다. 설정값만 기록하지 말고 실제 queue occupancy와 eviction reason을 측정해야 합니다. queue를 키운 뒤 callback 수가 늘었다는 결과만으로 synchronization이 개선됐다고 결론내릴 수 없습니다.
| 조정 | 기대할 수 있는 변화 | 함께 악화될 수 있는 것 | 반드시 볼 지표 |
|---|---|---|---|
queue_size 증가 |
늦은 partner를 기다릴 후보 증가 | memory·wait·stale callback | occupancy·eviction·emit age |
queue_size 감소 |
오래된 후보가 빨리 사라짐 | 정상 jitter에서도 premature drop | stream별 drop reason·match rate |
slop 증가 |
approximate match 후보 증가 | span 증가·false pairing 위험 | span 분포·task phase별 오류 |
slop 감소 |
더 가까운 stamp만 허용 | callback starvation | no-match 비율·입력 health |
arrival time으로 동기화하면 transport 지연을 센서 시각으로 오해하게 됩니다
Python ApproximateTimeSynchronizer에는 header가 없는 message에 현재 ROS time을 붙이는 allow_headerless와 arrival ROS time을 사용하는 sync_arrival_time 옵션이 있습니다. 공식 API 문서는 두 방식 모두 지연을 예측할 수 없으므로 가능한 한 피하라고 명시합니다.
이유는 간단합니다. camera image가 먼저 측정됐지만 encode와 network 때문에 나중에 도착할 수 있습니다. robot state는 더 늦게 측정됐어도 작은 packet이라 먼저 callback에 들어올 수 있습니다. arrival time으로 두 message를 묶으면 transport 경로의 빠르기와 물리 측정 시각을 혼동합니다.
현재 router처럼 자유로운 dictionary를 ROS message로 감쌀 때 header가 없다는 이유로 callback의 now()를 넣는 것도 같은 문제를 만듭니다. source stamp를 모르는 값은 source_time_unknown으로 표시하고 control-grade synchronization에서 제외해야 합니다. telemetry나 UI 표시처럼 완화된 용도가 필요하다면 별도의 정책 이름으로 격리합니다.
QoS가 맞지 않으면 synchronizer는 선택할 message 자체를 받지 못합니다
공식 Exact·Approximate Time 튜토리얼은 publisher와 subscriber에 호환되는 같은 QoS를 사용해야 filter가 topic을 정렬할 수 있다고 강조합니다. synchronization 정책은 DDS가 전달한 message만 볼 수 있습니다. 한 stream이 QoS incompatibility나 history depth 때문에 사라지면 synchronizer는 원인을 알지 못한 채 완전한 set을 기다립니다.
이 때문에 “sync callback이 없다”는 현상을 곧바로 slop 문제로 분류하면 안 됩니다. 먼저 각 topic의 publisher/subscriber match, 수신 count, deadline·liveliness event, source stamp 단조성, queue 입력 count를 확인해야 합니다. 모두 들어왔는데 set이 없는 경우와 한 topic 자체가 오지 않는 경우는 다른 장애입니다.
QoS profile별 책임은 ROS 2 QoS와 lowstate·lowcmd 계약에 남깁니다. 여기서 필요한 연결은 transport health와 match health를 별도 지표로 기록하는 것입니다. input count가 0인 stream을 approximate_no_match로만 기록하면 전달 실패가 시간 오차처럼 보입니다.
out-of-order·duplicate·restart는 같은 stamp 비교만으로 해결되지 않습니다
network와 executor 부하가 있으면 source 순서와 callback 순서가 달라질 수 있습니다. 같은 stamp의 message가 재전송되거나 publisher가 재시작하면서 sequence가 0으로 돌아갈 수도 있습니다. ROS time을 replay하거나 simulator를 reset하면 timestamp가 뒤로 갈 가능성도 있습니다. 이때 queue에 이전 epoch와 새 epoch의 message를 함께 남기면 숫자가 가까운 잘못된 pair가 만들어질 수 있습니다.
그래서 member identity에는 stamp만이 아니라 publisher identity, sequence 또는 source event ID, clock epoch, robot boot epoch가 필요합니다. 동일한 member가 두 bundle에 재사용되는 것을 허용할지도 정책으로 고정해야 합니다. VLA observation마다 가장 최근 state를 반복해 붙이는 zero-order hold가 필요하다면 이를 ApproximateTime이라고 부르지 않고 latest-state reuse 정책으로 분리해 reuse age와 count를 남겨야 합니다.
message_filters API에는 stamp 순서대로 message를 지연 출력하고 이미 출력된 시각보다 과거인 message를 버리는 TimeSequencer도 있습니다. 다만 sequencer를 붙였다는 사실만으로 clock jump와 publisher restart를 해결할 수는 없습니다. epoch boundary에서 queue를 비우고 어떤 message를 drop했는지 기록하는 상위 runtime 계약이 필요합니다.
message_filters와 tf2는 시간 문제와 공간 문제를 나눠 맡습니다
여러 message를 하나의 bundle로 만들었다고 좌표계까지 맞아진 것은 아닙니다. image는 camera optical frame, hand pose는 pelvis나 world frame, IMU는 sensor frame일 수 있습니다. message_filters는 timestamp를 기준으로 조합을 선택하고, tf2는 각 source의 시각에서 frame 관계를 조회합니다.
여기서 모든 member의 timestamp를 anchor stamp로 덮어 쓰면 안 됩니다. approximate bundle이라면 원본 stamp가 서로 다릅니다. 각 measurement를 원본 시각에서 target frame으로 변환하거나, state를 anchor 시각으로 보간하는 별도 estimator를 사용해야 합니다. 단순 nearest message 선택과 continuous state interpolation도 같은 연산이 아닙니다.
tf2의 frame tree·timestamp 글에서 정리했듯, image acquisition stamp가 transform buffer 밖이면 lookup은 실패해야 합니다. sync가 성공했다는 이유로 latest transform을 적용하면 temporal match와 spatial transform이 서로 다른 시각을 사용하게 됩니다. 안전한 흐름은 clock·epoch 검증 → pair 선택 → 각 source stamp의 frame transform → freshness gate → observation 생성 순서입니다.
G1에서는 한 번에 모든 stream을 묶기보다 observation anchor를 먼저 정해야 합니다
현재 G1 VLA 계약은 머리·양손목 RGB와 robot/base/hand state를 한 step에 요구합니다. 그러나 실제 camera들이 hardware trigger를 공유하는지, acquisition rate와 stamp semantics가 같은지, robot state가 어떤 rate로 올지는 아직 workspace 증거만으로 확정할 수 없습니다. 따라서 처음부터 모든 stream에 하나의 ExactTime을 붙인 설계를 완료안으로 제시할 수 없습니다.
먼저 task에서 기준 사건을 정해야 합니다. 예를 들어 policy가 새 머리 image마다 실행된다면 head image stamp를 observation anchor로 둘 수 있습니다. 그 시각 근처의 wrist image를 approximate match하고, robot state는 더 높은 rate의 이력에서 nearest neighbor를 고르거나 interpolation할 수 있습니다. 하지만 이 선택은 아직 설계 후보일 뿐입니다. 어느 camera가 행동 결정의 기준인지, wrist image 누락 시 전체 observation을 버릴지 degraded mode를 허용할지는 실제 task와 모델 입력 계약으로 정해야 합니다.
한 synchronizer에 입력을 많이 추가할수록 모든 member가 동시에 조건을 만족해야 callback이 생깁니다. 한 wrist camera의 순간 drop이 head image와 robot state까지 폐기하게 됩니다. 반대로 계층적으로 묶으면 중간 bundle의 identity와 중복 사용을 추적해야 합니다. 어느 쪽이 낫다고 일반화할 수 없으므로 authority 0 synthetic stream에서 두 구조의 match·drop·age를 먼저 비교합니다.
| 구성 후보 | 장점 | 주요 위험 | 필요한 첫 증거 |
|---|---|---|---|
| 모든 stream ExactTime | 동일 event identity가 분명함 | 독립 sensor에서는 callback starvation | 공통 trigger·동일 stamp 생성 근거 |
| 모든 stream ApproximateTime | 구현이 단순하고 한 callback으로 완성 | 가장 느리거나 불안정한 stream이 전체를 지배 | stream별 stamp·drop·span 분포 |
| image anchor + state cache | policy event에 맞춰 state를 선택 가능 | nearest 선택과 보간 의미가 섞일 수 있음 | state dynamics 대비 interpolation 오류 |
| 계층형 image bundle + state | camera group과 control group의 정책 분리 | bundle 재사용·중간 age 추적 복잡 | bundle ID 계보와 end-to-end age |
라우터에는 값보다 먼저 PairingEvidence가 들어가야 합니다
현재 BackendRequest는 image path와 state 값을 전달할 수 있지만, backend가 이를 동기화된 observation으로 믿어도 되는지 판단할 필드가 없습니다. 첫 구현에서는 자유로운 dictionary 안에 timestamp 몇 개를 임시로 더하기보다, router 앞의 observation builder가 명시적인 bundle을 만들고 검증 결과를 함께 넘기는 편이 낫습니다.
SynchronizedObservation(
bundle_id=...,
anchor_source="head_rgb",
anchor_stamp=...,
clock_id=...,
clock_epoch=...,
members={
"head_rgb": {"message_id": ..., "source_stamp": ..., "frame_id": ...},
"left_wrist_rgb": {...},
"right_wrist_rgb": {...},
"robot_state": {...},
},
pairing=PairingEvidence(
policy="approximate_time",
policy_version=...,
queue_size=...,
slop_ns=...,
span_ns=...,
per_stream_offset_ns={...},
wait_ns=...,
emit_monotonic_ns=...,
decision="matched" | "rejected",
reason=...,
),
)
이 구조는 아직 code에 없는 설계안입니다. 핵심은 model input과 pairing decision을 분리하지 않는 것입니다. VLA backend가 image와 proprioception을 받아 action을 만들었다면 response log가 bundle ID를 가리켜야 합니다. 이후 incident trace에서 action → observation bundle → member message → source stamp와 frame → 원시 sensor artifact를 역추적할 수 있어야 합니다.
reject된 후보도 버리고 잊으면 안 됩니다. 원본 payload 전체를 영구 저장할 필요는 없지만 stream, stamp, queue state, reason, clock epoch, policy version은 counter와 bounded trace에 남깁니다. 그래야 slop을 바꾼 뒤 match 수만 증가한 것이 아니라 어떤 종류의 drop이 줄고 어떤 stale pair가 늘었는지 비교할 수 있습니다.
drop은 하나의 숫자가 아니라 원인을 가진 상태 전이입니다
동기화 실패를 모두 dropped로 합치면 설정을 고칠 수 없습니다. partner가 오지 않아 queue에서 밀린 경우, timestamp가 slop 밖인 경우, 이미 application deadline을 넘긴 경우, clock epoch가 바뀐 경우, out-of-order message인 경우는 대응이 다릅니다.
| 판정 | 직접 관측 | 가능한 원인 | 허용할 대응 |
|---|---|---|---|
transport_missing |
특정 stream의 input count가 증가하지 않음 | publisher·QoS·network·lifecycle | sync 설정 변경 전에 endpoint 진단 |
queue_evicted |
partner 전 queue capacity 초과 | queue 부족·burst·느린 stream | occupancy와 wait 분포를 보고 조정 |
skew_exceeded |
후보 span이 slop보다 큼 | rate 차이·clock offset·실제 acquisition 차이 | 원인 분리 전 slop 자동 확대 금지 |
stale_at_emit |
match됐지만 anchor age가 deadline 초과 | executor backlog·큰 queue·late partner | callback 성공과 별개로 bundle 거부 |
out_of_order |
source stamp·sequence가 이미 처리한 값보다 과거 | reordering·replay·driver bug | epoch와 source identity 확인 후 drop |
duplicate_member |
같은 message ID가 다시 사용됨 | 재전송·reuse 정책 혼입 | 정책상 허용된 reuse가 아니면 거부 |
clock_epoch_changed |
stamp가 뒤로 가거나 publisher epoch 교체 | sim reset·reboot·time jump | queue flush 후 새 warm-up |
frame_unavailable |
pair는 됐지만 source stamp의 tf lookup 실패 | tf buffer·frame graph·clock mismatch | latest transform으로 대체하지 않음 |
이 분류는 executor·callback group·backpressure 글과도 연결됩니다. stale_at_emit가 늘었는데 source span은 정상이라면 sensor clock보다 callback 대기를 의심할 수 있습니다. 반대로 receive 직후에도 skew_exceeded가 반복되면 source rate·clock·acquisition semantics를 봐야 합니다. 같은 증상처럼 보였던 callback 감소를 서로 다른 evidence로 갈라놓는 것이 목적입니다.
match rate 하나로는 좋은 synchronization을 판정할 수 없습니다
slop과 queue를 넓히면 callback 수를 늘리기 쉽습니다. 그래서 match rate만 최적화하면 가장 느슨한 설정이 이길 가능성이 큽니다. 그러나 로봇 policy가 필요한 것은 많은 bundle이 아니라, 충분히 새롭고 서로 가까우며 좌표 변환 가능한 bundle입니다.
첫 실험에서는 stream별 input count, matched bundle count, reason별 drop count, queue occupancy, match wait를 기록합니다. 품질 측면에서는 bundle span과 member별 anchor offset의 median·tail, emit age, tf lookup success, policy invocation age를 함께 봅니다. sequence별로 어떤 input이 어느 bundle에 사용됐는지도 추적해 silent reuse와 누락을 검출합니다.
실제 task에 들어가면 시간 지표와 동작 지표를 연결해야 합니다. 예를 들어 hand motion이 큰 구간의 span이 늘 때 image–pose reprojection error가 커지는지, 동일 episode를 더 엄격한 sync로 재구성했을 때 VLA action 차이가 어떻게 변하는지 비교할 수 있습니다. 아직 이 프로젝트에는 그런 실행 결과가 없으므로 이 글에서 허용 slop이나 queue size를 숫자로 제안하지 않습니다.
첫 검증은 실제 G1 대신 timestamp를 고의로 망가뜨립니다
실제 camera와 robot driver를 곧바로 붙이면 QoS, clock, frame, executor, sensor calibration 오류가 동시에 나타납니다. 먼저 actuator authority가 0인 synthetic publisher로 stamp stream을 만들고 pairing semantics를 고정합니다. payload는 작은 mock message면 충분하고, source stamp·sequence·epoch만 실제 계약과 같게 만듭니다.
- perfect exact: 모든 stream에 동일 event stamp를 넣어 한 bundle당 각 member가 정확히 한 번 쓰이는지 확인합니다.
- independent cadence: 서로 다른 stamp 간격을 흘려 ExactTime이 callback을 만들지 않는 이유와 ApproximateTime 후보를 대조합니다.
- bounded jitter: 선언된 범위 안과 밖의 jitter를 나눠 matched·
skew_exceeded판정이 경계에서 일관적인지 봅니다. - single-stream drop: 한 camera message를 제거해 어느 queue entry가 언제 어떤 reason으로 폐기되는지 기록합니다.
- burst와 executor pause: source stamp는 정상인 채 arrival를 몰아서 넣어 source span과 emit age가 분리되는지 확인합니다.
- out-of-order·duplicate: 순서를 바꾸고 같은 message ID를 다시 보내 silent reuse가 없는지 검증합니다.
- fixed offset·drift: 일정 offset과 점점 커지는 drift를 따로 주입해 queue_offset 후보와 clock fault를 구분합니다.
- epoch jump: ROS time backward jump 또는 publisher restart를 모사해 old queue가 flush되고 새 warm-up 전에는 bundle이 나오지 않는지 봅니다.
- tf window miss: pair는 성공하지만 한 member의 source stamp를 tf buffer 밖으로 보내
frame_unavailable이 전체 observation을 차단하는지 확인합니다. - replay parity: 같은 synthetic trace를 다시 넣었을 때 bundle ID를 제외한 match·drop 판정과 member 조합이 재현되는지 비교합니다.
각 case의 결과물은 입력 stream manifest, policy config hash, member sequence·stamp, queue event, 최종 decision을 포함해야 합니다. “callback이 몇 번 왔다”는 console log만으로는 충분하지 않습니다. 이 artifact가 반복 가능해진 뒤 Isaac Sim camera와 state bridge를 연결하고, 마지막에 실제 sensor driver를 검증합니다.
현재 구현 상태와 앞으로의 구현 순서를 분리합니다
| 상태 | 현재 근거 | 아직 말할 수 없는 것 |
|---|---|---|
| Implemented | G1 세 camera·state·hand pose HDF5 schema, 같은 인덱스의 RLDS형 변환, dictionary 기반 router request | 같은 인덱스가 같은 acquisition time이라는 증거 |
| Designed | 이 글의 anchor·Exact/Approximate·PairingEvidence·drop taxonomy·fault suite | 실제 message_filters node와 config가 repository에 존재함 |
| Executed | 기존 CPU schema converter와 dry-run 경로에는 실행 근거가 있음 | synthetic timestamp synchronization suite 실행 결과 |
| Observed | 현재 source에서 message_filters 참조와 per-stream timestamp dataset이 없음을 확인 | 실제 G1 camera–state skew·jitter·drop 분포 |
| Accepted | 배열 길이와 시간 정렬을 구분하는 기록 원칙 | task별 slop·queue·age gate와 observation 품질 승인 |
| Deployed | 없음 | 동기화된 live observation이 VLA·G1 actuator 경로에 사용됨 |
구현은 다음 순서로 진행하는 편이 현재 공백을 가장 작게 나눕니다.
- 각 source message에 acquisition stamp, clock ID·epoch, sequence·publisher identity를 고정합니다.
- synthetic input을 받는 observation builder와 PairingEvidence schema를 만듭니다.
- ExactTime baseline과 ApproximateTime candidate를 같은 trace에서 비교하고 drop taxonomy를 검증합니다.
- queue occupancy·span·wait·emit age·member reuse metric을 bounded trace와 counter로 남깁니다.
- epoch jump·reorder·drop·burst fault에서 fail-closed queue flush와 warm-up gate를 통과시킵니다.
- tf2 lookup을 각 member의 source stamp에 연결하고 frame 실패를 sync 성공과 분리합니다.
- Isaac Sim camera·state bridge에서 simulator ground-truth time과 bundle evidence를 교차 검증합니다.
- HDF5 writer가 원본 stamp와 bundle ID·pairing policy version을 보존하도록 확장합니다.
- VLA를 shadow mode로 실행해 sync 설정별 action 차이와 observation age를 비교합니다.
- 실제 G1 sensor의 stamp semantics와 clock calibration을 확인한 뒤에만 bounded canary를 검토합니다.
같은 step이라는 말은 선택·오차·폐기를 설명할 수 있을 때만 성립합니다
message_filters는 서로 다른 topic을 한 callback 함수의 인자로 넣어 주는 도구입니다. 하지만 로봇 프로젝트에서 더 중요한 역할은 그 callback이 만들어지기까지 어떤 시간 조건을 통과했는지 명시하게 만드는 데 있습니다. ExactTime은 동일 stamp identity를 요구하고, ApproximateTime은 선언된 구간 안의 후보를 허용합니다. 어느 쪽도 timestamp semantics, clock synchronization, state interpolation, tf2 transform, application freshness를 대신 해결하지 않습니다.
현재 G1 workspace에는 여러 image와 state를 길이 T로 저장하고 같은 인덱스로 변환하는 interface가 있습니다. 아직 ROS 2 synchronizer, source stamp schema, pairing evidence, 실제 skew 측정은 없습니다. 따라서 지금 방어 가능한 결론은 “multi-modal data layout을 구현했다”까지이며, “동기화된 G1 observation을 만들었다”는 문장은 보류해야 합니다.
제가 다음에 통과시킬 시험은 두 설정의 callback 수 경쟁이 아닙니다. 같은 synthetic stream에 jitter·drop·reorder·epoch jump를 넣고, ExactTime과 ApproximateTime이 각각 어떤 member 조합을 선택했는지, 선택된 span과 emit age가 얼마였는지, 버린 message의 reason이 빠짐없이 남는지 비교합니다. 이 artifact가 있어야 HDF5의 [t]를 실제 acquisition event의 step으로 바꿀 수 있습니다.
함께 읽기: 로봇 episode에서 이미지와 action을 같은 시간축에 묶는 법 · G1 tick·ROS time·monotonic clock 동기화 · ROS 2 tf2 frame tree·timestamp·extrapolation · 센서 측정값에서 actor observation까지의 계보 · ROS 2 QoS와 lowstate·lowcmd · executor·callback group·backpressure
공식 문서: message_filters Python API · Approximate Time Synchronizer 튜토리얼 · Time Synchronizer 튜토리얼 · sensor_msgs/Image timestamp 계약