이 튜토리얼은 이전 프로젝트인 "라인 팔로워 로봇 - PID 제어 - 안드로이드 설정"을 기반으로 개발되었습니다. 라인 팔로잉 기능이 있는 로봇을 만들었다면, 다음 자연스러운 단계는 로봇에게 어느 정도의 지능을 부여하는 것입니다. 우리의 사랑스러운 "렉스(Rex), 로봇"이 이제 "미로"에서 가장 짧고 빠른 경로로 탈출하는 방법을 찾아보려 합니다 (참고로 렉스는 미노타우로스를 싫어합니다).
미로(Maze) vs 미궁(Labyrinth)의 차이
영어권에서는 통로에 선택지가 있어야 미로(maze)라 부릅니다. 이는 현대의 놀이공원이나 관광 명소의 설치물, 그리고 우리의 2D 미로도 포함합니다. 반면 미궁(labyrinth)은 입구에서 목표까지 단 하나의 통로만 있으며, 아무리 복잡하고 구불구불해도 갈림길이 없습니다.
대부분의 미로는 본질적으로 하나의 연속된 벽으로 형성되며 수많은 갈림길과 분기가 있습니다. 미로의 목표를 둘러싼 벽이 입구의 둘레와 연결되어 있다면, 벽에 한 손을 대고 따라가는 것만으로 항상 미로를 탈출할 수 있습니다. 이런 '단순한' 미로는 "단순 연결(Simply-connected)" 또는 "완전 미로(perfect maze)", 즉 루프(loops)가 없는 미로로 불립니다.
본 프로젝트는 두 단계(또는 "패스(pass)")로 나뉩니다:
1단계 (First Pass): "알려지지 않은 완전 미로"에서 탈출 경로를 찾습니다. 미로 안 어디에 놓아도 "해답(solution)"을 찾아냅니다.
2단계 (Second Pass): 한번 경로를 찾으면, "시작부터 끝까지 최단 경로"로 최적화하여 이동합니다.
아래 영상은 렉스가 미로를 탈출하는 예시입니다. 처음 미로를 탐색할 때는 당연히 갈림길에서 "고민(thinking)"하느라 많은 시간을 씁니다. 여러 가능성을 시험하며 잘못된 길과 막다른 골목으로 들어가 더 긴 경로를 돌고 불필요한 "U-턴"을 수행합니다. 이 "1단계"에서 로봇은 경험을 축적하고, 다양한 갈림길에 대한 "메모(taking notes)"를 하며 나쁜 분기를 제거합니다. "2단계"에서는 망설임이나 실수 없이 빠르고 직선적으로 목적지까지 도달합니다.
1단계: 자재 명세서 (BOM)
자재 목록은 기본적으로 라인 팔로워 로봇과 동일하지만, 좌우 갈림길 감지 정확도를 높이기 위해 센서 2개를 추가했습니다.
최종 로봇 제작비는 여전히 매우 저렴합니다 (약 $85.00):
바디 (필요에 맞게 조정 가능):
나무 판 2개 (80x80mm)
바인더 클립 3개
나무 바퀴 2개 (지름 50mm)
볼 캐스터 1개
고무줄 9개
3M 커맨드 프레임 스트립
센서 고정용 플라스틱 조인트
브레드보드 및 배선
4xNi-MH 배터리 세트 2개 (각 5V)
SM-S4303R 연속 회전 360도 플라스틱 서보 2개
아두이노 나노
HC-06 블루투스 모듈
라인 센서 5개 (TCRT5000 4채널 모듈 + 독립 트랙 센서 1개)
ZX03 반사형 적외선 센서 2개 (아날로그 출력)
LED 1개
버튼 1개
참고: 위 7번 항목의 아날로그 출력 센서를 사용한 이유는 디지털 출력 센서가 없었기 때문입니다. 이상적으로는 모든 센서가 동일해야 합니다. 기본 5개 센서만으로도 테스트해 보았는데, 작동은 하지만 갈림길 감지 시 더 민감한 조정이 필요합니다.
2단계: 바디 구조 변경
기존 5개 라인 팔로우 센서 세트를 제거하고, 지지대 양 끝에 새로운 "맨 왼쪽(Far LEFT)"과 "맨 오른쪽(Far RIGHT)" 반사 센서를 장착합니다. 7개 센서가 가능한 한 일직선이 되도록 정렬하는 것이 좋습니다.
3단계: 새 센서 장착 및 테스트
이제 7개 센서 배열은 5개 기본 센서를 PID 제어(및 "연속 라인" 감지) 전용으로, 새로운 2개 센서를 좌우 갈림길 감지 전용으로 사용합니다.
기존 5개 디지털 센서 작동 원리 복습
센서 하나가 검은 선 중앙에 오면 해당 센서만 HIGH 출력을 냅니다. 센서 간격은 두 센서가 동시에 검은 선 폭을 커버하여 둘 다 HIGH가 나오도록 계산해야 합니다.
새로운 2개 아날로그 센서 작동 원리
센서가 검은 선 중앙에 오면 아날로그 값을 출력하며, 보통 아두이노 ADC에서 100 미만 값이 나옵니다 (ADC 범위: 0~1023). 밝은 표면에서는 값이 높아집니다 (흰 종이에서 500~600 측정). 조명과 표면 재질에 따라 적절한 임계값(THRESHOLD)을 정해야 합니다.
센서 핀 정의 (아두이노 코드)
const int lineFollowSensor0 = 12; // 디지털 입력
const int lineFollowSensor1 = 18; // 아날로그 핀 A4를 디지털 입력으로 사용
const int lineFollowSensor2 = 17; // 아날로그 핀 A3를 디지털 입력으로 사용
const int lineFollowSensor3 = 16; // 아날로그 핀 A2를 디지털 입력으로 사용
const int lineFollowSensor4 = 19; // 아날로그 핀 A5를 디지털 입력으로 사용
const int farRightSensorPin = 0; // 아날로그 핀 A0
const int farLeftSensorPin = 1; // 아날로그 핀 A1
센서 값 저장 변수
int LFSensor[5] = {0, 0, 0, 0, 0}; // 기본 5개 디지털 센서
int farRightSensor = 0; // 오른쪽 아날로그 센서
int farLeftSensor = 0; // 왼쪽 아날로그 센서
앞서 논의했듯, 대부분의 미로는 하나의 연속된 벽으로 형성됩니다. 목표를 둘러싼 벽이 입구의 둘레와 연결되어 있다면, 왼손을 왼쪽 벽에 대고 걸으면 아무리 멀리 돌아가도 결국 탈출할 수 있습니다. 이런 미로를 "단순 연결(Simply-connected)" 미로라 합니다.
위키피디아에 따르면 왼손 법칙(Left-Hand Rule)은 다음과 같습니다:
왼손을 벽에 댄다.
앞으로 걷기 시작한다.
결국 미로의 끝에 도달한다. 최단 경로는 아닐지라도 반드시 도착한다.
핵심은 갈림길(intersections)을 식별하고 위 규칙에 따라 진행 방향을 결정하는 것입니다. 우리 2D 미로에는 8가지 유형의 갈림길이 있습니다:
십자(Cross): 좌회전, 우회전, 직진 가능
티자(T): 좌회전, 우회전 가능
우회전만 가능(Right Only): 우회전
좌회전만 가능(Left Only): 좌회전
직진 또는 좌회전(Straight or Left): 좌회전, 직진
직진 또는 우회전(Straight or Right): 우회전, 직진
막다른 골목(Dead End): U-턴
미로 끝(End of Maze): 정지
하지만 왼손 법칙을 적용하면 각 갈림길에서 행동이 하나로 정해집니다:
십자: 좌회전
티자: 좌회전
우회전만: 우회전
좌회전만: 좌회전
직진 또는 좌회전: 좌회전
직진 또는 우회전: 직진
막다른 골목: U-턴
미로 끝: 정지
막다른 골목과 미로 끝은 모호하지 않아 식별이 쉽습니다 (라인 팔로워 로봇에서 이미 구현). 문제는 "라인"을 만났을 때인데, 이는 십자(1)일 수도 티자(2)일 수도 있습니다. "좌/우회전"도 단순 회전(3,4)일 수도, 직진 옵션(5,6)일 수도 있습니다. 정확한 갈림길 유형을 알기 위해 로봇은 "한 걸음 더(extra inch)" 전진하여 다음 상황을 확인해야 합니다.
흐름도로 정리하면:
막다른 골목(DEAD END): U-턴
라인(LINE): 한 걸음 더 전진
앞에 라인 있음: 십자 ==> 좌회전
앞에 라인 없음: 티자 ==> 좌회전
또 다른 라인: 미로 끝 ==> 정지
우회전(RIGHT TURN): 한 걸음 더 전진
앞에 라인 있음: 직진 또는 우회전 ==> 직진
앞에 라인 없음: 우회전만 ==> 우회전
좌회전(LEFT TURN): 한 걸음 더 전진
앞에 라인 있음: 직진 또는 좌회전 ==> 좌회전
앞에 라인 없음: 좌회전만 ==> 좌회전
좌회전의 경우 어차피 좌회전하므로 테스트를 생략할 수 있습니다. 설명의 명확성을 위해 포함시켰습니다. 실제 코드에서는 생략합니다. 세 번째 이미지는 테스트용으로 연구실 바닥에 만든 간단한 미로입니다.
5단계: 아두이노 코드에 "왼손 법칙" 알고리즘 구현
readLFSsensors() 함수에 추가 2개 센서 로직을 포함시킨 후, loop() 함수를 아래와 같이 재작성합니다:
void loop() {
readLFSsensors();
switch (mode) {
case NO_LINE:
motorStop();
goAndTurn(LEFT, 180);
break;
case CONT_LINE:
runExtraInch();
readLFSsensors();
if (mode == CONT_LINE) mazeEnd();
else goAndTurn(LEFT, 90); // 티자나 십자 모두 좌회전
break;
case RIGHT_TURN:
runExtraInch();
readLFSsensors();
if (mode == NO_LINE) goAndTurn(RIGHT, 90);
break;
case LEFT_TURN:
goAndTurn(LEFT, 90);
break;
case FOLLOWING_LINE:
followingLine();
break;
}
}
새로운 함수들:
followingLine(): 라인 팔로워 로봇에서 사용한 것과 동일. calculatePID()와 motorPIDcontrol()로 모터 제어.
runExtraInch(): 로봇을 약간 전진시킴. 전진 거리는 delay() 시간으로 조절.
void mazeOptimization(void) {
while (!status) {
readLFSsensors();
switch (mode) {
case FOLLOWING_LINE:
followingLine();
break;
case CONT_LINE:
if (pathIndex >= pathLength) mazeEnd();
else { mazeTurn(path[pathIndex]); pathIndex++; }
break;
case LEFT_TURN:
if (pathIndex >= pathLength) mazeEnd();
else { mazeTurn(path[pathIndex]); pathIndex++; }
break;
case RIGHT_TURN:
if (pathIndex >= pathLength) mazeEnd();
else { mazeTurn(path[pathIndex]); pathIndex++; }
break;
}
}
}
행동을 결정하는 새 함수 mazeTurn(path[]):
void mazeTurn(char dir) {
switch (dir) {
case 'L': goAndTurn(LEFT, 90); break; // 좌회전
case 'R': goAndTurn(RIGHT, 90); break; // 우회전
case 'B': goAndTurn(RIGHT, 800); break; // U턴 (긴 회전)
case 'S': runExtraInch(); break; // 직진
}
}
2단계 완료! 아래 영상은 본 예제의 1, 2단계 전체 과정을 보여줍니다.
사용된 아두이노 코드 파일들:
FV6XNJWINJ45XWM.ino
F2FXS8MINJ45XX6.h
FX5MHFMINJ45XX7.ino
FT2S1WXINJ45XXA.ino
F9IC3HQINJ45XXB.ino
FU2HRXJINJ45XXV.ino
9단계: 안드로이드 앱으로 튜닝하기
라인 팔로워 프로젝트용으로 개발한 안드로이드 앱을 여기서도 사용할 수 있습니다 (앱과 코드는 이전 프로젝트 참조). 마지막 단계의 아두이노 코드에 이미 안드로이드 통신 기능이 포함되어 있으며, 앱을 쓰지 않아도 코드는 "투명하게(transparent)" 동작합니다.
프로젝트 내내 안드로이드 앱으로 로봇에서 테스트 데이터를 전송받아 "Message Received" 필드에서 확인했습니다. 로봇이 정확한 각도로 회전하려면 여러 변수를 잘 설정해야 합니다. 가장 중요한 것들 (굵은 글씨는 여러 번 조정함):
const int adj = 0;
float adjTurn = 8;
int adjGoAndTurn = 800;
THRESHOLD = 150;
const int power = 250;
const int iniMotorPower = 250;
int extraInch = 200;
10단계: 결론
라인 팔로워 로봇의 잠재력을 탐구하는 복잡한 프로젝트의 두 번째이자 마지막 파트입니다. 여기서 인공지능(AI)의 기초 개념을 활용해 미로를 풀었습니다.
저는 AI 전문가가 아니지만, 웹에서 얻은 정보를 바탕으로 렉스가 미로를 푼 것이 AI의 응용으로 볼 수 있다고 이해했습니다. 두 출처를 살펴보죠:
위키피디아
대학 논문: "Maze Solving Robot Using Freeduino and LSRB Algorithm" (International Journal of Modern Engineering Research - IJMER)
이 프로젝트의 업데이트된 파일들은 GitHub에서 확인할 수 있습니다. 전자공학, 로봇, 아두이노 등을 배우는 데 도움이 되었길 바랍니다. 더 많은 튜토리얼은 제 블로그 MJRoBot.org를 방문해 주세요.
남반구에서 인사드립니다!
감사합니다, Marcelo
코드 스니펫들
프로젝트 전반에 사용된 주요 코드 조각들입니다.
센서 핀 정의
const int lineFollowSensor0 = 12;
const int lineFollowSensor1 = 18;
const int lineFollowSensor2 = 17;
const int lineFollowSensor3 = 16;
const int lineFollowSensor4 = 19;
const int farRightSensorPin = 0;
const int farLeftSensorPin = 1;