아두이노, 구글 카드보드, 블루투스로 DIY VR 스케이트보드 만들기
부품 및 소모품
| 아두이노 우노 (Arduino UNO) |
1개 |
| HC-06 블루투스 모듈 |
1개 |
| 안드로이드 기기 |
1개 |
| 구글 카드보드 VR 헤드셋 |
1개 |
| 점퍼 와이어 (일반) |
1세트 |
| GY-521 MPU-6050 3축 자이로스코프 + 가속도계 모듈 |
1개 |
앱 및 온라인 서비스
프로젝트 소개
약 1시간 만에 완성할 수 있는 정말 재미있고 쉬운 프로젝트입니다. 스케이트보드 바닥에는 아두이노 보드와 함께 가속도계/자이로 센서가 장착되어 있으며, 보드의 각도 움직임을 블루투스를 통해 안드로이드 폰용으로 제작한 유니티 VR 게임으로 전송합니다. 아두이노를 켜고 블루투스가 폰과 연결되면 자동으로 앞으로 이동합니다. 왼쪽으로 기울이면 왼쪽으로, 오른쪽으로 기울이면 오른쪽으로 이동합니다. 앞바퀴를 들면 캐릭터가 점프합니다.
이 프로젝트는 안드로이드 폰에서만 작동하며, 구글 카드보드와 호환되는 기기가 필요합니다. 집에 굴러다니는 오래된 스케이트보드가 있다면 가상현실 스노보드로 변신시켜 보세요. 제작 방법은 다음과 같습니다.
준비물
- 구글 카드보드 스타일 VR 헤드셋
- 스케이트보드
- 테니스 공 4개 (굴러가지 않게 고정용)
- 아두이노 레오나르도 (또는 우노)
- 점퍼 와이어
- 미니 브레드보드
- HC-06 블루투스 모듈
- MPU-6050 가속도계/자이로
- 온/오프 스위치와 배럴 플러그가 있는 9V 배터리 박스 (아두이노 전원용)
- 납땜 인두와 핫글루건 (선택)
안드로이드 앱, 아두이노 코드, 부품 링크는 여기에서 확인할 수 있습니다.
1단계: 아두이노 장치/송신기/입력 장치 만들기
위 그림과 같이 장치를 조립하세요. MPU-6050에 헤더 핀을 납땜해야 하는데, 납땜 인두 팁이 깨끗하면 약 11초 만에 끝납니다. 팁이 더럽거나 오래됐다면 엄청나게 어려운 작업이 될 수 있습니다 (경험담).
모든 부품의 방향이 위 사진과 정확히 일치하도록 장착하세요. 그래야 코드를 수정할 필요가 없습니다. 스케이트보드에 부착할 때도 MPU-6050 방향이 사진과 동일하게 맞추세요.
여기서는 배터리 박스를 핫글루로 고정하고 아두이노 보드를 나사로 고정했습니다. 더 넓은 올드스쿨 보드를 사용했는데, VR 상태에서 균형 잡기에는 넓은 보드가 더 유리합니다.
참고: 레오나르도가 이 프로젝트에 훨씬 적합합니다. 하지만 아두이노 우노를 사용하는 경우 연결은 동일하나 SDA는 A4, SCL은 A5에 연결해야 합니다.
2단계: 아두이노 코드 업로드하기
중요: 보드에 코드를 업로드하기 전에 RX, TX 핀을 분리하세요.
3단계: 거의 다 왔어요! 폰에 앱 설치하기
이 링크를 따라 앱을 다운로드하세요. '구글 카드보드 가상현실 스케이트보드/스노보드 앱'입니다.
보안 설정에서 '알 수 없는 출처의 앱 설치'를 허용해야 합니다. 블루투스 모듈을 폰과 페어링하세요. 기본 이름(HC-06)은 변경하지 마시고, 비밀번호는 1234입니다.
참고 사항:
- 이것은 제가 만든 첫 유니티 게임이라 꽤 기본적입니다.
- 만드는 분들이 많으면 더 나은 스노보드 게임이나 실제 스케이트보드 게임을 만들 수도 있습니다.
- 아두이노 장치를 켤 때 스케이트보드가 평평한 바닥에 놓여 있는지 확인하고 앱을 실행하세요. 블루투스가 연결되는 즉시 시작됩니다.
- 블루투스 연결이 끊기면 앱을 재시작하세요 (현재는 앱 시작 시에만 연결 시도).
코드
아두이노 코드
#include "I2Cdev.h"
#include "MPU6050_6Axis_MotionApps20.h"
#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
#include "Wire.h"
#endif
MPU6050 mpu;
bool dmpReady = false;
uint8_t mpuIntStatus;
uint8_t devStatus;
uint16_t packetSize;
uint16_t fifoCount;
uint8_t fifoBuffer[64];
Quaternion q;
VectorInt16 aa;
VectorInt16 aaReal;
VectorInt16 aaWorld;
VectorFloat gravity;
float euler[3];
float ypr[3];
volatile bool mpuInterrupt = false;
void setup() {
#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
Wire.begin();
TWBR = 24;
#elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE
Fastwire::setup(400, true);
#endif
Serial.begin(9600); // 아두이노 우노용
Serial1.begin(9600); // 레오나르도용
Serial.println(F("I2C 장치 초기화 중..."));
mpu.initialize();
Serial.println(F("장치 연결 테스트 중..."));
Serial.println(mpu.testConnection() ? F("MPU6050 연결 성공") : F("MPU6050 연결 실패"));
Serial.println(F("DMP 초기화 중..."));
devStatus = mpu.dmpInitialize();
mpu.setXGyroOffset(220);
mpu.setYGyroOffset(76);
mpu.setZGyroOffset(-85);
mpu.setZAccelOffset(1788);
if (devStatus == 0) {
Serial.println(F("DMP 활성화 중..."));
mpu.setDMPEnabled(true);
Serial.println(F("인터럽트 감지 활성화 중 (아두이노 외부 인터럽트 0)..."));
attachInterrupt(0, dmpDataReady, RISING);
mpuIntStatus = mpu.getIntStatus();
Serial.println(F("DMP 준비 완료! 첫 인터럽트 대기 중..."));
dmpReady = true;
packetSize = mpu.dmpGetFIFOPacketSize();
} else {
Serial.print(F("DMP 초기화 실패 (코드 "));
Serial.print(devStatus);
Serial.println(F(")"));
}
}
void sendData(int x, int y, int z) {
if(z < -10){ // 전진
Serial1.write("f"); Serial1.write(10); // 레오나르도용
Serial.write("f"); Serial.write(10); // 우노용
} else if (z > 0){ // 후진
Serial1.write("b"); Serial1.write(10);
Serial.write("b"); Serial.write(10);
} else if (y > 5){ // 오른쪽 (민감도 조절: 4 이하로 변경)
Serial1.write("r"); Serial1.write(10);
Serial.write("r"); Serial.write(10);
} else if (y < -5){ // 왼쪽 (민감도 조절: -4 이상으로 변경)
Serial1.write("l"); Serial1.write(10);
Serial.write("l"); Serial.write(10);
} else { // 정지
Serial1.write("s"); Serial1.write(10);
Serial.write("s"); Serial.write(10);
}
}
void loop() {
if (!dmpReady) return;
mpuInterrupt = false;
mpuIntStatus = mpu.getIntStatus();
fifoCount = mpu.getFIFOCount();
if ((mpuIntStatus & 0x10) || fifoCount == 1024) {
mpu.resetFIFO();
Serial.println(F("FIFO 오버플로"));
} else if (mpuIntStatus & 0x02) {
while (fifoCount < packetSize) {
fifoCount = mpu.getFIFOCount();
}
mpu.getFIFOBytes(fifoBuffer, packetSize);
fifoCount -= packetSize;
mpu.dmpGetQuaternion(&q, fifoBuffer);
mpu.dmpGetGravity(&gravity, &q);
mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
sendData(ypr[0] * 180/M_PI, ypr[1] * 180/M_PI, ypr[2] * 180/M_PI);
}
}
void dmpDataReady() {
mpuInterrupt = true;
}
회로도
회로도 이미지 참조