TIKI-MINI로 시작하는 ROS2 실무
목차

2. 시뮬레이션

2.7직접 코딩 1: 정사각형 주행

2.6장에서 Twist의 숫자 두 개가 바퀴 속도로 바뀌는 과정을 계산했습니다. 이 장에서는 그 숫자를 직접 발행해서 한 변이 1 m인 정사각형을 그립니다. 코드 상자의 시뮬레이터는 2.6장 끝에서 약속한 대로 linear.x를 ±0.2 m/s, angular.z를 ±0.85 rad/s로 자르고, 마지막 명령 뒤 0.5초가 지나면 로봇을 세웁니다. 실제 diff_controller와 같은 규칙입니다.

함정: 명령을 한 번만 보내면 로봇이 선다

먼저 2.6장의 --once 이야기를 코드로 확인합니다. 이 노드는 타이머가 0.1초마다 깨어나지만 Twist(linear.x=0.2)를 첫 번째 호출에서 딱 한 번만 발행합니다. /odom을 받아 1초마다 위치와 속도를 로그로 남깁니다.

LIVE

1) 명령 한 번과 10 Hz 반복

직접 바꿔 보기

  • 그대로 실행하면 로봇이 x = 0.100 m에서 멈추고 v=0.00이 됩니다. 0.2 m/s로 0.5초 동안 움직인 거리입니다.
  • 맨 위의 REPEAT = False를 True로 바꿔 다시 실행하세요. 1초마다 x가 0.2 m씩 늘고 4초 뒤에는 0.78 m 부근입니다.
  • create_timer(0.1, ...)을 create_timer(0.6, ...)으로 바꾸고 REPEAT = True로 실행해 보세요. 발행 간격이 타임아웃보다 길면 명령 사이마다 0.1초씩 멈칫하고, 4초 뒤 x는 0.59 m로 0.78 m보다 짧습니다.

실제 컨트롤러의 cmd_vel_timeout 기본값은 0.5초입니다. 마지막 명령이 이보다 오래되면 저장된 명령을 0으로 덮어씁니다. 그래서 속도 명령은 10 Hz 정도로 계속 보내는 것이 관례입니다. 이 장의 모든 노드는 주행이 끝난 뒤에도 0 명령을 계속 발행합니다. 발행을 끊으면 0.5초 뒤에 멈추기는 하지만, 의도한 정지라면 0을 명시해서 보내는 편이 확실합니다.

시간 기반 정사각형 (개루프)

정사각형은 직진과 90도 회전을 네 번 반복한 궤적입니다. 필요한 시간은 거리와 속도로 계산합니다.

동작식값
한 변 직진길이 / 속도 = 1.0 m / 0.2 m/s5.0초
90도 회전각도 / 각속도 = (π/2) / 0.5 rad/s약 3.14초

각속도는 한계 0.85보다 낮기만 하면 되므로 0.5 rad/s로 잡았습니다. 0.85를 쓰면 회전은 1.85초로 줄어듭니다. 직진은 한계와 같은 0.2 m/s입니다. 한계 이하로 고르는 이유는 아래 실험에서 확인합니다.

코드는 상태 두 개(go, turn)와 경과 시간으로 구성한 상태 기계입니다. 타이머가 0.1초마다 시간을 더하고, 목표 시간을 채우면 다음 상태로 넘어갑니다. 네 변이 끝나면 odom에서 읽은 마지막 위치를 로그로 남깁니다.

LIVE

2) 시간 기반 정사각형

직접 바꿔 보기

  • 그대로 실행하면 약 33초에 네 변을 마치고 출발점에서 8.3 cm 떨어져 서며, 로봇은 6.7도(0.117 rad) 돌아간 채입니다.
  • SPEED = 1.0으로 바꿔 보세요. 직진 시간이 1초로 계산되지만 시뮬레이터가 0.2 m/s로 자르므로 한 변이 0.2 m인 작은 사각형이 그려집니다. 이 실행의 끝점 오차는 1.8 cm입니다.
  • TURN = (math.pi / 2) / 3.2로 바꾸면 끝점 오차가 0.0 cm로 줄어듭니다. 이유는 바로 아래에 있습니다.

오차는 어디서 오나

속도 한계를 넘기면 계획이 어긋납니다. SPEED = 1.0은 "1초 동안 1 m"를 계획하지만 로봇은 0.2 m/s로 움직여 0.2 m만 갑니다. 한계가 있는 로봇에서 시간 기반 계획을 세울 때는 처음부터 한계 이내의 속도로 계산해야 합니다.

기본 설정의 8.3 cm 오차는 시뮬레이터의 부정확함이 아니라 타이머 주기에서 나옵니다. 회전은 3.14초가 필요하지만 코드는 0.1초 단위로만 시간을 셉니다. 3.14초는 0.1초 단위로 31.4번째 틱이라 32번째 틱(3.2초)에서 끝나고, 매번 0.06초(0.03 rad)를 더 돕니다. 네 번이면 0.117 rad입니다. TURN을 3.2초에 정확히 90도가 되는 값((π/2)/3.2 ≈ 0.491 rad/s)으로 고르면 계획한 시간과 실제 시간이 일치해서 오차가 사라집니다.

그래도 이 방식의 한계는 남습니다. 코드는 로봇이 실제로 어디 있는지 보지 않고 시간만 믿습니다. 시뮬레이터는 명령 속도가 그대로 적용되어 믿을 만하지만, 실제 바퀴는 바닥에서 미끄러지고 속도 응답에도 지연이 있습니다.

/odom으로 닫은 정사각형 (폐루프)

이번에는 시간 대신 /odom을 봅니다. 직진은 출발 지점에서 1 m 떨어질 때까지, 회전은 목표 방향과의 각도 차이가 0.01 rad(약 0.6도)보다 작아질 때까지 합니다.

/odom의 방향은 쿼터니언(x, y, z, w)으로 옵니다. 평면 위에서 움직이면 x와 y는 0이고 yaw만 남아서 z = sin(yaw/2), w = cos(yaw/2)입니다. 거꾸로 yaw = 2 * atan2(z, w)로 구합니다. 로봇이 기울거나 굴러서 roll과 pitch가 생기면 이 식은 쓰지 못합니다.

각도는 -π와 π에서 끊기므로 오차를 계산할 때 atan2(sin, cos)로 한 번 정리합니다(코드의 wrap). 목표 방향을 "직전 방향 + 90도"로 잡으면 매번 생기는 작은 오차가 쌓입니다. 코드는 출발 방향 yaw0에서 계산한 절대 방향(yaw0 + 90도 x 번호)을 목표로 삼습니다.

LIVE

3) /odom 으로 닫은 정사각형

직접 바꿔 보기

  • 그대로 실행하면 약 33초에 끝나고 끝점 오차는 2.5 cm입니다. 앞 예제의 8.3 cm와 비교해 보세요.
  • SIDES = 3으로 바꾸면 정삼각형입니다. 회전각이 360도를 변의 수로 나눈 120도가 되도록 코드를 짜 두었습니다. 이 실행은 약 27초에 끝나고 오차 0.9 cm입니다.
  • SIDE = 0.5로 바꾸면 한 변 0.5 m의 작은 사각형입니다.

세 가지 정사각형을 이 시뮬레이터에서 측정한 값은 이렇습니다.

방식끝점 오차비고
시간 기반, TURN = 0.58.3 cm타이머 틱에 걸려 6.7도 초과 회전
시간 기반, TURN을 틱에 맞춤0.0 cm이상적인 시뮬레이터라서 가능한 값
/odom 폐루프2.5 cm각도 허용 0.01 rad, 직진 판정도 20 Hz 틱 단위

이상적인 시뮬레이터에서는 시간을 잘 맞춘 개루프가 폐루프보다 낫습니다. 폐루프는 센서값이 현실을 따라가 줄 때 가치가 있습니다. 실제 로봇이라면 순서가 달라질 가능성이 큽니다(장 끝 참고).

좌표계 메모

/odom 위치는 로봇의 시작 자세를 (0, 0, 0)으로 잡은 좌표입니다. open 월드는 시작 자세가 월드 원점이라서 두 좌표가 같습니다. corridor와 obstacles는 시작점이 원점이 아니므로 /odom 값에 시작 위치를 더해야 월드 좌표가 됩니다.

직접 해보기

  1. 정삼각형: 위 3번 코드의 SIDES = 3을 확인하고, 시간 기반 코드(2번)로도 만들어 보세요. 힌트는 회전각이 90도에서 120도로 바뀐다는 것입니다. 회전 시간은 (2π/3) / TURN입니다.
  2. 직사각형: 가로 1.5 m, 세로 0.8 m. 변마다 길이가 다르므로 SIDE 상수를 변의 번호로 고르도록 리스트([1.5, 0.8, 1.5, 0.8])로 바꿉니다. 회전은 그대로 90도입니다.
  3. 작은 한 변: SIDE = 0.3에서 끝점 오차는 줄어들지, 비율로 보면 어떨지 예상한 뒤 실행하세요.
  4. 노이즈: 아래 두 상자의 월드는 /odom 증분에 20% 가우시안 노이즈를 곱합니다(odomNoise: 0.2). 시간 기반 상자는 TURN을 위에서 고친 값((math.pi / 2) / 3.2)으로 바꾸고 실행하세요. 어느 쪽이 먼저 흔들릴지 예상한 뒤 로그의 끝점과 캔버스의 실제 로봇 위치를 비교합니다. 시뮬레이터가 고정 시드를 쓰므로 결과는 항상 같습니다.
LIVE

4a) 노이즈 20%: 시간 기반

LIVE

4b) 노이즈 20%: /odom 폐루프

노이즈 실험의 측정값은 이렇습니다. 시간 기반(TURN을 고친 버전)은 로봇이 0.0 cm 오차로 정확히 돌아왔지만 로그의 /odom 끝점은 5.0 cm 떨어진 값을 보고했습니다. 이 코드는 /odom을 움직임에 쓰지 않으므로 노이즈가 진짜 움직임에 영향을 주지 않습니다. 폐루프는 로그의 끝점이 1.4 cm로 좋아 보이지만 캔버스의 실제 로봇은 5.8 cm 벗어났습니다. 노이즈가 낀 센서값을 믿고 위치를 맞췄기 때문입니다. 폐루프는 센서만큼만 정확합니다.

실제 로봇에서는

속도 한계(0.2 m/s, 0.85 rad/s)와 cmd_vel_timeout 0.5초는 TIKI-MINI와 같으므로 이 장의 숫자와 구조는 그대로 옮겨 갑니다. 다른 점은 시뮬레이터에 없던 요소입니다. 바닥 마찰과 바퀴 미끄러짐, 모터의 속도 응답 지연, 엔코더 오차 때문에 정사각형은 닫히지 않고 틈이 생깁니다. 개루프는 시간 오차가 그대로, 폐루프는 오도메트리 오차가 그대로 반영됩니다. 얼마나 틀어지는지는 실제로 돌려 봐야 알며, 4.6장에서 시뮬과 실기기의 차이를 정리합니다. 실기기에서 처음 돌릴 때는 바닥이 넓은 곳에서 한 변을 0.3~0.5 m로 줄이고, 로봇을 언제든 멈출 수 있는 상태로 두세요.

한눈에 정리

  1. /cmd_vel은 한 번만 발행하면 0.5초 뒤에 멈춘다. 계속 움직이려면 10 Hz 정도로 반복 발행한다.
  2. 직진 시간은 거리 / 속도, 회전 시간은 각도 / 각속도다. 1 m는 5초, 90도는 0.5 rad/s에서 약 3.14초다.
  3. 속도는 한계(0.2 m/s, 0.85 rad/s) 이내로 계획한다. 1.0 m/s를 요청해도 0.2 m/s로만 움직여 시간 계산이 어긋난다.
  4. 타이머 주기도 오차의 원인이다. 3.14초가 3.2초로 올림되어 네 번 돌면 6.7도가 틀어졌다.
  5. 평면 이동에서 yaw = 2 * atan2(q.z, q.w)이고, 각도 오차는 atan2(sin, cos)로 -π~π 범위에 정리한다.
  6. 이상적인 시뮬레이터에서는 시간을 맞춘 개루프가 0.0 cm, /odom 폐루프가 2.5 cm였다. 폐루프는 센서 오차를 그대로 따라간다.
  7. 실기기에서는 마찰, 미끄러짐, 응답 지연 때문에 사각형이 닫히지 않는다(4.6장).

확인 문제

Q1. 노드가 /cmd_vel에 linear.x=0.2를 한 번만 발행했습니다. 로봇은 얼마나 가서 서나요?

약 0.1 m입니다. 마지막 명령 뒤 0.5초가 지나면 컨트롤러(시뮬레이터)가 명령을 0으로 만들어서 0.2 m/s x 0.5초 = 0.1 m 움직이고 멈춥니다. 실제 TIKI-MINI도 cmd_vel_timeout 기본값이 0.5초이므로 같은 현상이 나옵니다.

Q2. 직진 시간을 한 변 1 m / 1.0 m/s = 1초로 계산하고 linear.x = 1.0을 발행했습니다. 한 변의 실제 길이는?

0.2 m입니다. 컨트롤러가 linear.x를 0.2 m/s로 자르기 때문에 1초 동안 0.2 m만 갑니다. 시간 계산은 실제로 낼 수 있는 속도(0.2 이하)로 해야 합니다.

Q3. /odom에서 orientation z = 0.7071, w = 0.7071을 받았습니다. yaw는 몇 도인가요?

2 * atan2(0.7071, 0.7071) = 2 * 45도 = 90도입니다. 로봇이 시작 방향에서 왼쪽(반시계)으로 90도 돌아 있습니다. yaw만 있는 쿼터니언은 z = sin(yaw/2), w = cos(yaw/2)이기 때문에 반각의 두 배를 하면 됩니다.

다음 장

2.8장에서는 /odom 대신 /scan을 읽어 벽과 장애물을 피하는 노드를 만듭니다. 라이다 배열에서 정면 방향을 찾는 방법과 inf가 항상 "아무것도 없음"이 아닌 이유를 다룹니다.