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

2. 시뮬레이션

2.8직접 코딩 2: 장애물 회피

2.7장에서는 /odom을 보고 정해진 경로를 달렸습니다. 이 장에서는 경로를 모르는 상태에서 /scan만 보고 판단합니다. 센서를 읽고(/scan), 결정하고, /cmd_vel을 발행하는 가장 단순한 반응형 로봇입니다.

LaserScan 읽는 법

sensor_msgs/msg/LaserScan은 라이다가 한 바퀴 돌며 잰 거리를 담는 메시지입니다. 거리는 배열 하나에 모두 들어갑니다. 이 장의 에뮬레이터가 보내는 값은 이렇습니다.

필드에뮬레이터 값의미
header.frame_idlaser거리를 잰 좌표계
angle_min-π (약 -3.142)첫 번째 빔(ranges[0])의 각도
angle_increment2π/360 (약 0.0175 rad, 1도)인접 빔 사이 각도
ranges360개빔마다 잰 거리(m)
range_min, range_max0.15, 6.0믿어도 되는 거리 범위(m)
scan_time0.1한 바퀴에 걸린 시간(초), 곧 10 Hz

빔 i의 각도는 angle_min + i * angle_increment입니다. 각도 0은 laser 프레임의 +x 방향이고 반시계 방향이 양수입니다. 에뮬레이터는 laser와 로봇의 방향을 같게 만들어서 이렇게 읽습니다.

인덱스각도방향
1800도정면
270+90도왼쪽
90-90도오른쪽
0 (와 359 근처)-180도뒤쪽

ranges 값이 inf면 빔이 range_max 안에서 아무것도 맞히지 못했다는 뜻입니다. 숫자라도 range_min보다 작거나 range_max보다 크면 믿지 않습니다. 에뮬레이터는 두 경우를 모두 inf로 채웁니다. 그래서 "inf = 앞이 비었다"는 읽기는 위험합니다. 아래에서 이 때문에 생기는 사고를 실제로 일으켜 봅니다.

어느 인덱스를 볼까

로봇은 한 빔이 아니라 부채꼴 구간을 보고 판단합니다. 빔 하나는 가는 막대를 놓칠 수 있습니다. 그림에서 방향과 반각을 골라 파이썬 슬라이스가 어떻게 바뀌는지 보세요. 에뮬레이터 정면은 ranges[165:196](정면 ±15도, 빔 31개)입니다. 그림 옆의 실제 TIKI-MINI 버튼으로 바꾸면 정면 부채꼴이 배열의 양 끝으로 갑니다. 이 차이는 장 끝에서 설명합니다.

090180270정면

중심 인덱스 180, 파이썬 슬라이스 ranges[165:196]

그림의 숫자는 배열 인덱스입니다. 부채꼴은 로봇 기준 방향으로 그립니다. 빔 360개는 그림을 위한 가정이며, 실제 /scan의 빔 수와 인덱스는 4.5장에서 확인합니다.

해보기 1: 거리 읽기

먼저 움직이지 않고 읽기만 합니다. 노드가 첫 메시지에서 필드 값을 출력하고, 1초마다 정면, 왼쪽, 오른쪽 부채꼴(각각 ±15도)의 가장 가까운 거리를 출력합니다. sector_min 함수가 범위 밖과 inf를 걸러 줍니다.

LIVE

1) 정면, 왼쪽, 오른쪽 거리

직접 바꿔 보기

  • obstacles 월드의 시작점(-2, 0)에서 실행하면 첫 로그가 빔 360개, angle_min=-3.142, 간격 0.0175 rad이고 이후 정면 1.79 왼쪽 1.93 오른쪽 1.93이 나옵니다. 정면 1.79 m는 (0, 0.6)에 있는 원통까지의 거리입니다.
  • 왼쪽과 오른쪽이 1.93 m인 것은 뜻밖입니다. 방 벽까지는 2.5 m인데 시작점이 왼쪽 벽 가까이에 있어서, 부채꼴 가장자리(105도와 75도 빔)가 뒤쪽 벽을 맞히기 때문입니다. 부채꼴을 ±10도로 줄이거나 슬라이스를 [260:281], [80:101]로 바꿔 보세요. 값이 어떻게 바뀔까요?
  • 월드를 바꿀 수는 없지만 인덱스를 바꿔 뒤쪽 scan.ranges[0:11]을 읽어 보세요. 시작점 뒤 벽(약 0.5 m)이 나옵니다.

해보기 2: 벽 앞에서 멈추기

정면 거리가 STOP_DIST보다 가까우면 서고 아니면 0.2 m/s로 갑니다. open 월드에서 로봇은 시작점(0, 0)에서 오른쪽 벽(x = 3)을 향합니다. 멈출 때도 0 명령을 계속 발행하는 것은 2.7장과 같습니다.

LIVE

2) /scan 으로 벽 앞에서 멈추기

직접 바꿔 보기

  • 그대로 실행하면 13.0초에 정면 0.40 m 에서 정지가 나오고 로봇은 x = 2.6 m에서 섭니다.
  • STOP_DIST = 0.1로 바꿔 보세요. 정지 로그는 끝까지 나오지 않고, 14.42초에 충돌 1회가 일어나며 로봇은 x = 2.88 m에서 벽에 막힙니다.

0.1로 바꾼 실행이 이 장의 핵심 함정입니다. 시뮬레이터의 로봇은 반지름 0.12 m인 원이라서 로봇 중심이 벽에서 0.12 m 떨어지면 닿습니다. 그런데 range_min이 0.15 m이므로 그 순간 정면 빔은 inf로 읽힙니다. 코드가 inf를 걸러 내면 "앞이 비었다"로 해석해서 계속 앞으로 가려고 합니다. 센서의 최소 거리와 로봇의 몸집이 겹치는 구간이 생깁니다. 이 구간을 사각지대라고 부르겠습니다. 정지 거리는 사각지대에 들어가기 전에 멈출 수 있게 0.3 m 이상으로 잡습니다. 이 장의 예제는 0.4~0.5 m를 씁니다.

반응형 상태 기계

멈추기만 하는 로봇은 쓸모가 없으니, 막히면 돌아서 다시 가게 만듭니다. 상태는 둘입니다.

상태하는 일다음 상태로 가는 조건
forward0.2 m/s로 직진정면 거리가 STOP_DIST 미만이면 turn
turn제자리에서 회전정면 거리가 CLEAR_DIST 초과면 forward

turn으로 들어갈 때 방향을 한 번 정합니다. 왼쪽 앞(45~90도) 부채꼴과 오른쪽 앞 부채꼴의 가장 가까운 거리를 비교해서 더 트인 쪽으로 돕니다. 시작 임계값과 끝 임계값은 다르게(0.45와 0.8) 두어야 합니다. 같은 값이면 정면 거리가 임계값 근처에서 흔들릴 때 forward와 turn이 번갈아 켜집니다.

정면 부채꼴은 ±25도로 넓게 잡았습니다. 로봇은 폭이 있어서 정면 빔이 비어 있어도 옆구리가 장애물에 닿을 수 있습니다.

LIVE

3) 돌아다니며 피하기

직접 바꿔 보기

  • 그대로 실행하면 40초 동안 방향을 세 번 바꾸고 충돌은 0회입니다(캔버스 요약에서 확인). 같은 코드를 120초까지 돌려도 충돌은 없었습니다.
  • STOP_DIST = 0.15로 바꾸면 21.9초에 오른쪽 벽(위치 (2.38, 0.0))에 충돌합니다. 앞 예제의 사각지대 때문입니다.
  • 정면 부채꼴을 155, 206에서 175, 186(±5도)로 좁히면 29.3초에 오른쪽 벽(위치 (2.38, 1.0))에 비스듬히 닿아 충돌합니다. 좁은 부채꼴은 비스듬한 벽을 늦게 봅니다.
  • STOP_DIST = 0.25로 낮춰도 40초 동안은 충돌이 없었습니다. 더 오래 돌리거나 다른 값으로 바꿔서 한계를 찾아보세요.

이 알고리즘의 한계도 알아 두세요. 라이다에서 보이는 것만 보고 판단하므로 지도가 없고, 막다른 곳에서는 좌우를 번갈아 고르며 헤맬 수 있습니다. 목표가 있는 문제는 2.14장에서 /odom과 섞어 풉니다.

해보기 3: 꺾인 복도

corridor 월드는 폭 1 m의 ㄱ자 복도입니다. 오른쪽으로 약 3.5 m 간 뒤 왼쪽(위쪽)으로 꺾이며, 끝 (3.5, 3.0)에 반지름 0.3 m의 목표 구역이 있습니다. 직진만 하면 오른쪽 벽에 막힙니다.

아래 노드는 두 가지를 합칩니다. 평소에는 왼쪽과 오른쪽 벽 거리가 같아지게 조향하며 0.2 m/s로 가고, 정면이 STOP_DIST 아래로 가까워지면 모퉁이로 보고 왼쪽으로 돕니다. 정면이 CLEAR_DIST보다 트이면 다시 갑니다.

LIVE

4) 꺾인 복도 따라가기

직접 바꿔 보기

  • 그대로 실행하면 15.3초에 모퉁이를 감지하고 29.9초에 목표에 도착합니다. 출력 탭 맨 아래에 미션 결과: 도착 (29.9초), 충돌 0회가 붙습니다.
  • 도착한 뒤에도 코드는 계속 갑니다. 막다른 벽 앞에서 다시 돌기 시작하는 것이 보일 겁니다. 목표 도착 후 멈추게 하려면 /odom을 구독하고 y가 2.7을 넘으면 0 명령만 보내도록 코드를 더해 보세요.
  • CLEAR_DIST를 1.0으로 줄여도 29.8초, 0.4로 줄여도 31.9초에 도착합니다. 0.3으로 줄이면 20초쯤 오른쪽 벽에 두 번 충돌하고 미도달입니다. 회전 중간(대각선 방향)에 정면 부채꼴이 무엇을 맞히는지 생각해 보세요.

이 코드의 숫자(CLEAR_DIST = 1.5, 벽 거리 0.8 m 이내일 때만 조향)는 이 복도에 맞춰 손으로 고른 값입니다. 조향 조건이 없으면 모퉁이 입구에서 열려 있는 왼쪽을 먼 벽으로 읽어 엉뚱한 쪽으로 틀기 때문에 조건을 달았고, 숫자는 이 복도에서 시행착오로 정했습니다. 월드가 바뀌면 값도 다시 맞춰야 하는 코드이니 일반 해법이 아닙니다.

실제 TIKI-MINI에서는 다르다

이 장의 코드를 실제 /scan에 그대로 쓰면 로봇이 엉뚱한 방향으로 움직입니다. 에뮬레이터가 단순하게 만든 부분 두 가지가 실제와 다릅니다. 포팅 절차는 4.5장에서 다룹니다.

첫째는 프레임입니다. 2.5장에서 본 대로 URDF의 lidar_top_joint는 rpy="0 0 3.14159"로 laser를 yaw π로 장착합니다. laser의 +x가 로봇의 뒤쪽을 향하니, 로봇 정면은 스캔 각도 ±π 부근입니다. 인덱스로는 정면이 배열의 양 끝에 있어서 ranges[165:196]이 아니라 ranges[:16] + ranges[-15:](빔 간격 1도일 때의 예)처럼 읽어야 합니다. 왼쪽과 오른쪽도 바뀌어서 로봇의 왼쪽은 에뮬레이터의 오른쪽 인덱스 쪽에 옵니다. 앞의 그림에서 실제 TIKI-MINI 버튼을 눌러 확인하세요. 라이다의 위치는 base_footprint 바로 위(x, y 모두 0)라서 에뮬레이터처럼 로봇 중심에서 거리를 잰다고 볼 수 있지만, 빔 수는 그림처럼 360개로 고정된 값이 아닙니다.

둘째는 발행 설정입니다. rplidar_ros ros2 브랜치의 rplidar_node.cpp를 확인했습니다(2026년 10월). scan_pub을 rclcpp::QoS(rclcpp::KeepLast(10))로 만듭니다. 센서용 프로파일(SensorDataQoS)이 아니라 깊이 10의 기본 프로파일이어서 신뢰성은 reliable입니다. 같은 소스에서 range_min은 0.15로 고정이고 range_max는 스캔 모드에서 받으며, 측정값이 0이면 inf를 채웁니다. 구독 쪽 QoS를 어떻게 맞출지는 1.8장의 내용이 적용되고, 실기기 /scan으로 확인하는 절차는 4.5장에 있습니다.

한눈에 정리

  1. 빔 i의 각도는 angle_min + i * angle_increment이고, 에뮬레이터는 360빔, 정면이 인덱스 180, 왼쪽 270, 오른쪽 90이다.
  2. inf는 "범위 안에서 반사가 없음"이다. 센서 앞 0.15 m 안(사각지대)도 inf로 읽히므로 "앞이 비었다"로 해석하지 않는다.
  3. 판단은 빔 하나가 아니라 부채꼴 구간의 최솟값(inf와 범위 밖은 제외)으로 한다. 로봇 폭 때문에 정면 부채꼴은 넓게 잡는다.
  4. 정지 거리는 0.3 m 이상으로 한다. STOP_DIST = 0.1은 벽에 충돌했다.
  5. 반응형 상태 기계는 forward와 turn 두 상태와 시작 임계값(0.45), 끝 임계값(0.8)이 다른 히스테리시스로 구성했다. obstacles 월드 40초, 충돌 0회였다.
  6. 복도는 벽 사이 중앙 조향과 모퉁이 회전을 합쳐 29.9초에 도착, 충돌 0회였다. 임계값은 이 복도 전용이다.
  7. 실제 TIKI-MINI는 laser가 yaw π라서 정면이 배열의 양 끝이고, rplidar 드라이버는 rclcpp::QoS(KeepLast(10))로 발행한다(4.5장).

확인 문제

Q1. angle_min = -π, angle_increment = 2π/360인 스캔에서 로봇 왼쪽(+90도)은 몇 번 인덱스인가요?

i = (각도 - angle_min) / angle_increment = (π/2 + π) / (2π/360) = 270입니다. 에뮬레이터는 laser와 로봇 방향이 같아서 왼쪽이 270, 오른쪽이 90, 정면이 180입니다. 실제 TIKI-MINI는 laser가 yaw π로 달려 있어 같은 인덱스가 다른 방향을 가리킵니다.

Q2. 정지 거리를 0.1 m로 두었더니 코드는 멈추지 않고 벽에 부딪혔습니다. 정면 거리 값이 어떻게 읽혔을까요?

inf로 읽혔습니다. 로봇 중심이 벽에서 약 0.12 m 떨어지면 닿는데 range_min이 0.15 m라서, 닿기 전부터 정면 빔이 범위 밖으로 inf가 됩니다. 코드가 inf를 걸러 내므로 "앞이 비었다"로 해석해서 계속 전진합니다. 정지 거리를 0.3 m 이상으로 잡아야 사각지대에 들어가기 전에 멈춥니다.

Q3. 상태 기계에서 turn을 끝내는 임계값 CLEAR_DIST를 turn을 시작하는 STOP_DIST와 같은 값으로 했습니다. 어떤 문제가 생기나요?

정면 거리가 그 값 근처에서 조금만 흔들려도 forward와 turn이 번갈아 바뀌어 로봇이 제자리에서 떨거나 맴돕니다. 끝 임계값을 시작 임계값보다 크게 잡아 두면(예: 0.45 시작, 0.8 종료) 한 번 돌기 시작하면 충분히 트일 때까지 이어서 돕니다.

다음 장

2.9장에서는 지금까지 터미널에서 하나씩 띄운 노드를 런치 파일로 묶고, 시뮬레이션에서 쓰는 use_sim_time 파라미터를 다룹니다.