초음파 센서를 이용한 전방위 스캐너
게임에서는 다양한 UI를 통해 유저들에게 정보를 제공한다
화면에 미니맵을 통해 상호작용 가능한 오브젝트나 적의 위치를 표현하기도 하고
몰입감을 해치지 않기 위해 UI 없이 주인공이 직접 레이더 장치를 사용하기도 한다
이 중에서 SONAR 응용한 2015년에 나온 DARK ECHO라는 특이한 게임이 있다
플레이어는 눈이 보이지 않는 맹인으로, 캄캄한 스테이지에서 길을 찾아 탈출하는 게임이다
소리를 시각화하여 표현한 공포 퍼즐게임으로 굉장히 아이디어가 좋다고 생각했었다
최근에는 LiDAR.exe, Scanner Sombre 와 같이 LiDAR 센서를 이용한 게임들도 있는데,
일반적인 게임화면과 다르게 보이지 않는 것에 대한 공포를 잘 표현했다고 생각한다
실제로는 카메라를 이용한 컴퓨터 비전, 전파를 이용한 레이더, 빛을 이용한 LiDAR,
음파를 이용한 SONAR 등 다양한 센서들을 사용하는 방법들이 있다
여튼 이런 시각화 도구들을 직접 만들어 보고자 한다
2. 프로젝트 소개
일반적으로 생각하는 레이더 화면이라고 하면 위와 같은 모습을 많이 떠올린다
이를 초음파 센서와 서보 모터로 전방 180도를 구현해보자 한다
2-1. 필요 센서
- 아두이노 UNO R3
- 아두이노 서보모터(SG90)
- HC-SR04 초음파 센서
초음파 센서로 선정한 이유는 가장 구하기 쉽고, 저렴하기 때문이다
또한, 초음파 센서는 고정되어있기 때문에 고정된 각도만 측정할 수 있다는 단점이 있는데,
이러한 단점을 보완하기 위해 모터를 이용하여 측정할 수 있는 각도의 범위를 늘린다
전방 180도만 측정할 것이기 때문에 서보 모터를 사용했다
2-2. 초음파 센서
초음파 센서는
금속, 목재, 유리 등 단단한 물체에는 거의 100% 반사되어 돌아오지만
옷감과 같은 일부 물질은 초음파를 흡수하여 정확한 측정값이 어렵다고한다
그러나, 이러한 특성을 이용해 잠수함이나 어선같이 카메라나 빛을 이용하기 어려운 환경에서 사용하기도 한다
이번에 사용할 HC-SR04 초음파 센서
보통 아두이노 키트에 같이 딸려온다
왼쪽 T가 초음파를 발사하는 Trig, 오른쪽 R이 초음파를 수신하는 Echo다
최소 2cm ~ 4M 까지 측정 가능하며, 최대 15도 범위를 측정 가능하다
20kHz 이상의 높은 주파수의 소리를 보낸 후, 반사 시간을 측정하여 거리를 계산하는 거리 측정 센서
초음파 센서 예시 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const int trigPin = 10;
const int echoPin = 11;
long duration;
int distance;
void setup() {
pinMode(trigPin, OUTPUT); // Trig 핀을 출력으로 설정
pinMode(echoPin, INPUT); // echo 핀을 입력으로 설정
Serial.begin(9600); // 직렬 통신의 baud rate 설정
}
void loop() {
// 트리거 핀을 Low로 설정
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
// 10초간 trig 발사
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
//물체에 반사되어돌아온 초음파의 시간을 변수에 저장
duration = pulseIn(echoPin, HIGH);
// 거리 계산 식
// 거리 = duration * 소리의 속도(340m/s) / 1,000,000(마이크로초로 변환) / 2(왕복)
distance = duration * 0.034 / 2;
Serial.print("Distance: ");
Serial.println(distance);
}
2-3. 서보 모터
“Servo”는 라틴어 슬레이브에서 유래했다고 한다고 한다
모터 자체가 컨트롤러를 가지고 있어 마스터-슬레이브 구조를 형성한다
마스터(컨트롤러)가 지시하면 슬레이브(모터)가 동작하는 구조를 가지고 있어 서보 모터로 불린다
여기서 사용하는 SG-90 서보 모터는 DC모터에 귀환 회로를 추가하여 정확한 위치 제어가 가능한 모터
20ms(50Hz) 동안 PWM 신호가 얼마나 들어오느냐에 따라 각도를 조절한다
0 ~ 180도까지 제어가 가능하며,
~1ms pulse가 왼쪽(-90도), 1.5ms pulse가 중앙(0도), 2ms pulse가 오른쪽(90도)라고 한다
2.5 kg-cm의 토크를 제공함
서보 모터 예시 코드
- 초음파 센서는 위 코드와 같으므로 서보 모터가 잘 동작하는지 체크할 것
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <Servo.h> // 서보 모터 사용을 위한 라이브러리
const int trigPin = 10;
const int echoPin = 11;
long duration;
int distance;
Servo myServo; // 서보 모터를 제어하기 위한 서보 객체 선언
void setup() {
pinMode(trigPin, OUTPUT); // Trig 핀을 출력으로 설정
pinMode(echoPin, INPUT); // echo 핀을 입력으로 설정
Serial.begin(9600); // 직렬 통신의 baud rate 설정
myServo.attach(12); // 서보모터 제어신호용 12번 핀 사용
}
void loop() {
// 서보 모터를 15도에서 165도까지 왕복
for(int i=15;i<=165;i++){
myServo.write(i);
delay(30);
distance = calculateDistance(); // 각 각도에 대해 거리를 계산하는 함수 호출
// i,distance. 형식으로 출력. 이후 IDE에서 GUI를 그리기 위해 사용
Serial.print(i);
Serial.print(",");
Serial.print(distance);
Serial.print(".");
}
// 165도에서 15도까지 왕복
for(int i=165;i>15;i--){
myServo.write(i);
delay(30);
distance = calculateDistance();
Serial.print(i);
Serial.print(",");
Serial.print(distance);
Serial.print(".");
}
}
// 초음파 센서의 거리를 측정하는 함수
int calculateDistance(){
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
duration = pulseIn(echoPin, HIGH);
distance= duration*0.034/2;
return distance;
}
글루건 또는 테이프를 이용해서 서보 모터 위에 초음파 센서를 붙인다
2-4. GUI 그리기
하드웨어를 구성했으니, 이를 GUI로 표현하기만 하면 된다
아두이노의 GUI를 제작하는 방법은 qt, tkinter 등 다양한데,
이번에는 Processing IDE
를 사용해보았다
한번도 안써봐서 어떻게 쓰는건지 궁금했기 때문
프로세싱은 그래픽 등 시각적인 효과를 위해 사용하는 프로그래밍 언어로서 MIT 미디어랩에서 Java언어를 기반으로 개발되었다. 오픈소스로 개발되었기 때문에 모든 것이 무료로 open되어 있고, 디자이너 등 기존 프로그래밍 언어어 익숙하지 않은 사람들도 손쉽게 사용할 수 있다는 점이 장점이다. 특히 아두이노나 라즈베리파이를 사용하는 사람들의 경우에는 각종 센서들로 부터의 Data를 그래픽으로 시각화하여 표현하는 용도로 많이 사용한다. 즉 , 간단한 명령어로 버튼모양, 3D 그래픽 등 사물의 모양을 만들어 아두이노와 연결하면 그럴듯한 시뮬에이터처럼 작동되게 할 수도 있다. 참조 - 남보공방
GUI를 제작하는데 많이 사용하는 툴이라고 한다
온라인 편집기, 윈도우, 라즈베리파이 버전 등 다양한 버전을 제공하니, 원하는 방법으로 사용한다
자바 기반으로 개발되어 import 등 문법은 자바와 유사하다
그 외에 setup()
과 draw()
는 아두이노의 setup()
, loop()
와 유사하다
GUI 소스코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import processing.serial.*; // Serial 통신 라이브러리
import java.awt.event.KeyEvent; // 직렬 포트에서 데이터를 읽기 위한 라이브러리
import java.io.IOException;
Serial myPort; // 시리얼 포트 선언
String angle="";
String distance="";
String data="";
String noObject;
float pixsDistance;
int iAngle, iDistance;
int index1=0;
int index2=0;
PFont orcFont;
void setup() {
size (1920, 1080); // fhd
smooth();
println(Serial.list()); // 현재 연결된 포트 모두 출력
myPort = new Serial(this,"COM7", 9600); //"COM7"을 자신의 아두이노 포트에 맞게 수정
myPort.bufferUntil('.'); // Serial 포트에서 데이터를 읽는다.
// `각도,거리.` 로 전송하므로, `.`이전까지 읽어온다
orcFont = loadFont("NotoSans-Medium-48.vlw"); // 폰트 설정
}
// Could not load font 에러 발생시 도구 > 글꼴 생성으로 원하는 폰트를 vlw로 만들어서 불러오기
void draw() {
fill(98,245,31); // 바탕색
textFont(orcFont); // 폰트
// 움직이는 선의 모션블러 및 슬로우 페이드 효과
noStroke();
fill(0,4);
rect(0, 0, width, height-height*0.065);
fill(98,245,31); // 초록색
// 레이더 그리는 함수들 호출
drawRadar();
drawLine();
drawObject();
drawText();
}
// Serial 포트에서 데이터를 읽어오는 함수
void serialEvent (Serial myPort) {
// 데이터는 `angle,distance.` 형식으로 들어옴
// ex `15,10.16,10.17,12...`
// 문자열 파싱.
data = myPort.readStringUntil('.');
data = data.substring(0,data.length()-1);
index1 = data.indexOf(","); // ',' 의 위치 구하기
angle= data.substring(0, index1); // 각도를 구해서 저장
distance= data.substring(index1+1, data.length()); // 나머지 부분, 거리를 구해서 저장
// 문자열을 정수로 형변환
iAngle = int(angle);
iDistance = int(distance);
}
// 레이더 배경 그리는 함수
void drawRadar() {
pushMatrix(); // 현재 좌표계를 스택에 저장
translate(width/2,height-height*0.074); // 시작 좌표 설정
noFill();
strokeWeight(2); // 선 굵기
stroke(98,245,31); // 선 색상. 형광색
// 반원 그리기
arc(0,0,(width-width*0.0625),(width-width*0.0625),PI,TWO_PI);
arc(0,0,(width-width*0.27),(width-width*0.27),PI,TWO_PI);
arc(0,0,(width-width*0.479),(width-width*0.479),PI,TWO_PI);
arc(0,0,(width-width*0.687),(width-width*0.687),PI,TWO_PI);
// 각도 선 그리기
line(-width/2,0,width/2,0);
line(0,0,(-width/2)*cos(radians(30)),(-width/2)*sin(radians(30)));
line(0,0,(-width/2)*cos(radians(60)),(-width/2)*sin(radians(60)));
line(0,0,(-width/2)*cos(radians(90)),(-width/2)*sin(radians(90)));
line(0,0,(-width/2)*cos(radians(120)),(-width/2)*sin(radians(120)));
line(0,0,(-width/2)*cos(radians(150)),(-width/2)*sin(radians(150)));
line((-width/2)*cos(radians(30)),0,width/2,0);
popMatrix(); // 현재 좌표계를 꺼냄
}
// 오브젝트를 그리는 함수
void drawObject() {
pushMatrix();
translate(width/2,height-height*0.074); // 시작 좌표 설정
strokeWeight(9);
stroke(255,10,10); // 선 색상. 빨간색
pixsDistance = iDistance*((height-height*0.1666)*0.025); // 센서에서 얻은 cm를 픽셀로 변환
// 최대 인식거리 40cm
if(iDistance<40){
// 각도와 거리에 따라 객체를 선으로 그림
line(pixsDistance*cos(radians(iAngle)),-pixsDistance*sin(radians(iAngle)),(width-width*0.505)*cos(radians(iAngle)),-(width-width*0.505)*sin(radians(iAngle)));
}
popMatrix();
}
// 배경 선 그리기
void drawLine() {
pushMatrix();
strokeWeight(9);
stroke(30,250,60);
translate(width/2,height-height*0.074);
line(0,0,(height-height*0.12)*cos(radians(iAngle)),-(height-height*0.12)*sin(radians(iAngle))); // 각도에 따라 선 그리기
popMatrix();
}
// 배경 텍스트 그리기
void drawText() {
pushMatrix();
if(iDistance>40) {
noObject = "Out of Range";
}
else {
noObject = "In Range";
}
fill(0,0,0);
noStroke();
rect(0, height-height*0.0648, width, height);
fill(98,245,31);
textSize(25);
text("10cm",width-width*0.3854,height-height*0.0833);
text("20cm",width-width*0.281,height-height*0.0833);
text("30cm",width-width*0.177,height-height*0.0833);
text("40cm",width-width*0.0729,height-height*0.0833);
textSize(40);
text("Object: " + noObject, width-width*0.875, height-height*0.0277);
text("Angle: " + iAngle +" °", width-width*0.48, height-height*0.0277);
text("Distance: ", width-width*0.26, height-height*0.0277);
if(iDistance<40) {
text(" " + iDistance +" cm", width-width*0.225, height-height*0.0277);
}
textSize(25);
fill(98,245,60);
translate((width-width*0.4994)+width/2*cos(radians(30)),(height-height*0.0907)-width/2*sin(radians(30)));
rotate(-radians(-60));
text("30°",0,0);
resetMatrix();
translate((width-width*0.503)+width/2*cos(radians(60)),(height-height*0.0888)-width/2*sin(radians(60)));
rotate(-radians(-30));
text("60°",0,0);
resetMatrix();
translate((width-width*0.507)+width/2*cos(radians(90)),(height-height*0.0833)-width/2*sin(radians(90)));
rotate(radians(0));
text("90°",0,0);
resetMatrix();
translate(width-width*0.513+width/2*cos(radians(120)),(height-height*0.07129)-width/2*sin(radians(120)));
rotate(radians(-30));
text("120°",0,0);
resetMatrix();
translate((width-width*0.5104)+width/2*cos(radians(150)),(height-height*0.0574)-width/2*sin(radians(150)));
rotate(radians(-60));
text("150°",0,0);
popMatrix();
}
포트 에러시, 21번째 줄
myPort = new Serial(this,"COM7", 9600);
를 자신의 포트에 맞게 수정
빈 프로젝트에 시리얼 포트 리스트를 출력 후, 자신에 맞는 포트를 선택하면 된다Could not load font ~~ 에러 발생시, 폰트를 찾지 못하는 것이므로
도구 > 글꼴 생성
으로 원하는 폰트를 새로 vlw 파일로 만들어서 저장하면 불러올 수 있다
아두이노에 2-3의 코드를 적재한 뒤에
아래 코드를 Processing IDE로 실행한다
아두이노에서 Serial Port로 전송해주는 데이터를 토대로, 프로세싱에서 그린다
왼쪽을 향할 때는 빈공간이므로 초록색으로 출력되지만,
오른쪽을 향하면 모니터가 인식되어 빨갛게 표시된다
3. 결론
이번 프로젝트는 프로세싱 IDE를 다뤄보는게 중점이였다
서보 모터 및 초음파 센서는 여러번 써봐서 어느정도 익숙했지만,
Processing을 통해 GUI를 그리는건 처음이라 배울점이 많았다
직접 모두 그리지는 못했지만, 어느정도 코드를 이해할 수 있을 정도는 학습했다고 생각한다
추후에는 서보 모터 대신 스텝 모터를 이용하거나 초음파 센서를 양쪽으로 덧대어 360도를 구현해보거나
다른 방식으로 GUI를 구현해보고 싶다
끝.