뒤쪽 방향 화살표가 있는 원형 로봇 이동 경로와 옆에 놓인 아날로그 시계

ROS 2 시뮬레이션 시간이 뒤로 가면 로봇 타이머 계산은 어떻게 깨질까


로그 재생 위치가 12초에서 9초로 돌아갔다. 이때 현재 시각 - 이전 시각을 계산하면 -3초다. ROS 2 시뮬레이션 시간이 뒤로 가면 로봇 타이머 계산은 어떻게 깨질까라는 질문은 여기서 시작한다. 이 음수는 로봇이 음의 시간 동안 움직였다는 측정값이 아니다. 코드가 바라보는 시간원이 과거로 점프했다는 신호다.

이 글에서는 초 단위 뺄셈과 ROS 2 토픽의 뜻만 알고 있다고 가정한다. 정상 진행·가속 재생·정지·역점프에서 경과시간 dt를 직접 계산한 뒤, 시뮬레이션의 진행을 따라야 하는 로직과 실제 장치의 timeout을 서로 다른 시계로 나누는 기준을 세운다. 근거는 2026년 9월 20일 확인한 ROS 2 공식 설계·API·저장소이며, 자체 검증은 Python 3.12.3 산술 예제까지다. ROS 2 노드, rosbag2, 시뮬레이터와 실물 로봇은 실행하지 않았다.

같은 now()처럼 보여도 세 시계는 다른 질문에 답한다

먼저 시간원(time source)은 “지금 몇 시인가”라는 물음에 값을 주는 출처다. ROS 2는 용도가 다른 세 시간 개념을 구분한다.

  • ROS 시간(ROSTime)은 ROS 그래프가 공유하는 논리적 시간이다. use_sim_time이 꺼져 있으면 SystemTime처럼 동작하지만, 활성화되면 기본적으로 /clock 토픽이 전한 최신 값을 따른다. /clock의 첫 값이 오기 전 0은 초기화되지 않은 오류 상태로 봐야 한다.
  • 시스템 시간(SystemTime)은 운영체제의 달력 시각이다. 사람이 읽는 실제 날짜와 시각에는 알맞지만, 시스템 시각 보정으로 불연속이 생길 수 있어 순수한 경과시간과 같지 않다.
  • 단조 시간(SteadyTime)은 경과시간을 재는 동안 뒤로 가지 않는 시계다. ROS 2 설계 문서는 하드웨어 드라이버가 주변장치 timeout을 재는 경우를 대표 용도로 든다.

ROS 2 Clock and Time 설계 문서가 시뮬레이션 시간을 멈추거나 빠르게 하고 과거로 되돌릴 수 있게 한 이유는 재생과 디버깅 때문이다. 계산이 무거운 시뮬레이션을 현실보다 느리게 돌리거나, 로그를 한 단계씩 살펴보거나, 특정 구간을 다시 재생하려면 벽시계와 다른 시간축이 필요하다.

따라서 시계 선택은 “어느 것이 더 정확한가”의 문제가 아니다. “무엇의 진행을 재는가”가 먼저다. 녹화 데이터가 10초 지점에서 11초 지점으로 이동했는지는 ROSTime의 질문이고, 모터 드라이버가 현실에서 500ms 동안 답하지 않았는지는 SteadyTime의 질문이다.

네 장면을 계산하면 양수·0·음수의 뜻이 갈린다

모든 시각의 단위를 초(s)로 놓고 다음 한 줄만 계산해 보자.

dt = t_now - t_previous
장면 입력 계산 결과 뜻
정상 진행 10.1 - 10.0 약 +0.1 s 선택한 시간원이 앞으로 진행했다
2배속 재생 현실 0.1 s 동안 ROS 시간 0.2 s 진행 +0.2 s 계산은 현실 시간이 아니라 재생 진행량을 따른다
일시정지 12.0 - 12.0 0 s 현실 시간이 흘러도 시뮬레이션은 진행하지 않았다
역방향 점프 9.0 - 12.0 -3 s 시간원이 과거로 불연속 이동했다

2배속과 일시정지는 오류가 아니다. 시뮬레이션 시간의 의도된 성질이다. rosbag2의 Rolling 공식 문서도 --clock을 사용한 재생 세션이 일시정지됐을 때 같은 시각의 /clock 값을 계속 발행할 수 있다고 설명한다. 현실에서 여러 초가 지나도 ROSTime 차이는 0일 수 있다.

문제는 이 값을 “항상 작은 양수”라고 믿는 다음 계산이다. 위치 누적식이 x_next = x + v × dt이고 x=10 m, v=2 m/s, dt=-3 s라면 결과는 4 m다. 역재생을 지원하도록 설계하지 않은 적분기라면 이를 실제 위치 관측으로 받아들일 근거가 없다.

timeout도 다른 방식으로 깨진다. 조건이 now - last_response >= 5 s인데 ROSTime이 멈췄다면 왼쪽 값은 계속 0이다. 실제 장치가 현실에서 5초 동안 응답하지 않아도 만료되지 않는다. 같은 구간을 SteadyTime으로 재면 5초가 진행한다.

정상 진행·일시정지·역점프의 dt가 각각 누적기 갱신, timeout 대기, 상태 초기화로 이어지는 흐름

이 그림에서 기억할 경계는 하나다. dt < 0은 아주 느린 주기가 아니라 새 시간축으로 넘어간 사건이다.

음수 dt를 0으로 자르면 사건이 사라진다

음수를 0으로 바꾸면 한 번의 역방향 위치 갱신은 막을 수 있다. 하지만 previous와 함께 쌓아 온 상태가 새 시간축에서도 유효해지지는 않는다. 누적기, 미분기, 필터, 변환 캐시와 제어기 내부 상태는 모두 과거 샘플의 순서에 기대고 있을 수 있다.

그러므로 점프를 먼저 드러내고 상태별 정책을 실행해야 한다. 아래 코드는 완성된 rclpy 예제가 아니라 그 분리를 보여주는 의사코드다.

now = ros_clock.now()
dt = now - previous

if dt < ZERO_DURATION:
    reset_time_dependent_state()  # 또는 재동기화, 명시적 오류
    previous = now
    return

update_simulation_state(dt)
previous = now

모든 상태를 무조건 지우라는 뜻도 아니다. 다시 계산할 수 있는 누적기는 초기화하고, 외부에서 새 기준 상태를 받을 수 있는 필터는 재동기화하며, 되돌림을 허용할 수 없는 명령 경로는 오류로 중단하는 식으로 정해야 한다. 어떤 선택이 맞는지는 그 상태가 이전 시간축의 자료를 얼마나 품고 있는지에 달려 있다.

ROS 2에는 이 분기를 위한 공식 경로가 있다. Kilted rcl 10.1.4의 rcl_clock_add_jump_callback은 설정한 임계값을 넘는 점프의 갱신 전·후에 콜백을 호출한다. 같은 시계에서 콜백 추가·제거와 ROS 시간 override를 동시에 다루는 작업은 thread-safe하지 않다는 조건도 있다.

실제 구현은 점프를 단순 산술 예외보다 넓게 다룬다. 2026년 9월 20일 확인한 Rolling tf2_ros::Buffer 구현은 시간원이 바뀌거나 음의 점프가 생기면 TF 버퍼를 비운다. 과거 시간축의 변환을 새 시간축에서 재사용하지 않겠다는 선택이다. 이것은 TF의 구체적 정책이지, 모든 노드가 전체 상태를 비워야 한다는 보편 규칙은 아니다.

타이머 이름보다 “함께 멈춰야 하는가”를 묻는다

시간원을 고를 때 첫 질문은 “시뮬레이션이 멈추면 이 계산도 멈춰야 하는가?”다. 두 번째 질문은 “시간이 뒤로 가면 과거 상태를 다시 계산할 의도가 있는가?”다.

계산의 목적 우선 검토할 시간원 역점프 또는 정지 때 할 일
센서 메시지·TF·로그 재생 위치의 타임스탬프 ROSTime 캐시와 누적 상태를 재동기화하거나 비운다
시뮬레이션 단계에 맞춘 제어 계산 ROSTime 정지 때 dt=0을 받아들이고, 음수는 별도 상태 정책으로 보낸다
로그 시간으로 “5초 뒤” 실행할 이벤트 ROSTime seek나 loop가 생기면 재예약·취소 정책을 적용한다
직렬·CAN·EtherCAT 장치 응답 timeout SteadyTime ROS 시간 정지와 무관하게 현실 경과로 만료한다
watchdog·실제 모터 명령 만료·안전 정지 유예 SteadyTime 후보 드라이버 내부에 두고 ROS 시간 점프와 분리한다
사람이 읽는 외부 달력 시각 SystemTime 후보 운영체제 시각 보정 가능성을 별도로 다룬다

실제 하드웨어 timeout은 다음처럼 별도 단조 시계에서 계산한다.

if steady_clock.now() - last_hardware_response >= hardware_timeout:
    command_safe_stop()

이 역시 복사 실행용 ROS 2 코드는 아니다. 배포판에 맞는 Duration 비교, timer 생성 API, callback group과 스레드 안전성을 확인해야 한다. Kilted rclcpp 29.5.8에는 호출자가 clock을 명시하는 rclcpp::create_timer가 있으므로, 함수 이름이 “timer”라는 이유만으로 어떤 시간원을 쓰는지 추측해서는 안 된다.

ros2_control의 공식 clocks 문서에서도 같은 분리가 보인다. 비시뮬레이션 controller manager의 실시간 read/update/write 루프와 하드웨어 읽기·쓰기에는 단조 시간 계열을 활용하고, 시뮬레이션 루프 트리거에는 ROS 시간을 사용한다. 구체 동작은 사용 중인 배포판에서 다시 확인해야 하지만, 물리 장치의 경과와 시뮬레이션의 진행을 나누는 사례로 읽을 수 있다.

공개 예제로 계산 경계만 다시 확인했다

자체 검산은 ROS 실행 실험이 아니다. 2026년 9월 20일 프로젝트 호스트의 Python 3.12.3에서 공개 산술 예제를 격리 모드로 실행했다. 정상 진행 약 0.1, 2배속 0.2, 정지 0, 역점프 -3, 정지 중 ROSTime 경과 0, 같은 현실 구간의 SteadyTime 경과 5가 예상과 일치했고 종료 코드는 0이었다. 10.1 - 10.0은 부동소수점 표현 때문에 0.09999999999999964로 나와 math.isclose로 확인했다.

독자는 계산과 가정을 공개 example.py 고정 커밋에서 확인할 수 있다. 실행 환경, 기대값, 실제 출력과 한계는 공개 verification.json 고정 커밋에 남아 있다. 검산 실패 기준은 assertion 실패 또는 0이 아닌 종료 코드였으며, 의도적인 실패 대조군은 만들지 않았다.

이 결과로 확인한 것은 뺄셈의 부호와 두 시간원의 역할뿐이다. ROS jump callback 호출 횟수, 실제 timer 스케줄링, DDS 지연, rosbag2의 배포판별 동작과 로봇 거동을 확인한 증거는 아니다.

세 문제로 자신의 설계를 확인한다

문제 1. 시뮬레이션 시간이 30초에서 멈춘 채 현실에서 5초가 지났다. ROSTime으로 now - previous를 계산하면 얼마인가?

답은 0초다. /clock 값이 바뀌지 않았기 때문이다. 현실 시간과 시뮬레이션 진행량을 같은 것으로 보면 이 답을 놓치기 쉽다.

문제 2. 같은 5초 동안 실제 모터 드라이버가 응답하지 않았다. 안전 timeout에는 어느 시간원을 우선 검토해야 하는가?

SteadyTime이다. ROS 시간이 정지해도 현실의 무응답 기간은 계속 늘어나야 한다. 단조 시계는 메시지의 외부 타임스탬프로 퍼뜨리기보다 드라이버 내부 경과시간에 두는 편이 설계 의도와 맞는다.

문제 3. ROS 시간이 12초에서 9초로 이동해 dt=-3초가 됐다. dt=0으로 바꾸고 계속 실행하면 충분한가?

충분하다고 일반화할 수 없다. 음수 갱신 한 번은 막아도 이전 시간축에 묶인 누적기·필터·캐시가 유효한지는 별도 판단이 필요하다. 테스트에는 최소한 정상 양수, 정지 0, 역점프 음수, ROS 정지 중 SteadyTime timeout 만료를 넣어야 한다.

산술 검산 다음에는 시뮬레이션과 실물을 따로 검증한다

후속 시뮬레이션 시험에서는 /clock을 정상 진행·정지·역점프시키고, jump callback의 전·후 호출, 계산된 dt, 상태 초기화 여부와 SteadyTime timeout을 한 로그에 남겨야 한다. 사용한 ROS 2 배포판과 rcl·rclcpp 또는 rclpy, rosbag2나 시뮬레이터 버전도 함께 기록해야 한다. Rolling 저장소의 현재 구현을 오래된 배포판까지 그대로 일반화할 수 없기 때문이다.

실물 검증은 그 다음의 별도 단계다. timeout이나 안전 정지 로직을 시험할 때는 제조사 절차를 우선하고, 구동부를 지면에서 분리하거나 액추에이터 전원을 비활성화한 상태에서 시작해야 한다. 물리적 비상정지와 별도 조작자를 확보하고, 통신이 끊기면 기본 명령이 안전 정지로 가는지 먼저 확인한다. 중지 경로는 ROS 시간에 기대지 않는 하드웨어 비상정지 또는 전원 차단이어야 한다.

지금 할 일은 큰 리팩터링이 아니다. 자신의 노드에서 이전 시각을 저장하는 계산 한 곳을 고른다. 옆에 “시뮬레이션 정지 시 함께 멈춰야 하는가”와 “역점프 때 비울 상태는 무엇인가”를 적고, 음수 dt 테스트를 하나 추가한다. 이 두 답이 다르면 ROSTime과 SteadyTime을 나눌 자리도 보인다.

이 글에는 제휴·협찬이 없다.

참고 링크

비슷한 글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다