프로젝트

나도 메이커! 메이커스 여러분들의 작품/프로젝트를 공유하는 공간입니다.

Wii 눈차크를 이용하여 구글 어스 비행 시뮬레이터를 제어해봅시다.

2014-09-01 15:53:39

개요

구글 어스에는 비행 시뮬레이터라는 기능이 있습니다. 

이 기능을 사용하게되면 마우스와 키보드를 이용하여 멋진 자연 경관이나 내가 사는 곳을 비행기 조종사가 되어 날아 볼 수 있습니다.



이 기능을 마우스, 키보드가 아닌 Wii 눈차크(Nunchuck)를 이용하여 더 실감 나게 비행기를 조정해 봅시다.
(정밀한 조정은 불가능하나 비행기 게임을 하는 것 같이 조정 해 봅시다.)


미리 보기 동영상


시작 전 개념 이해하기

 - Wii 눈차크 가속도계 활용하기
 - 구글 어스 비행 시뮬레이터
         

부품 목록

NO 부품명 수량 상세설명
1 아두이노 우노 R3 1 아두이노
2 눈차크 1 눈차크
3 눈차크 어뎁터 1 눈차크 어뎁터

부품명 아두이노 우노 R3 눈차크 눈차크 어뎁터
파트


하드웨어 Making

회로도



브레드보드 레이아웃






* 눈차크 어뎁터를 사용할 경우 아두이노와 쉽게 연결 할 수 있습니다.(밑의 사진은 아날로그 A2~A5 핀과 연결)

소프트웨어 Coding

* 구글 어스 설치 및 비행기 시뮬레이터 사용 방법
구글 어스(Google earth)는 http://www.google.com/earth 에서 무료로 다운로드를 할 수 있습니다.

구글 어스를 다운로드 후 설치를 하면 시작 도움말과 함께 지구가 화면에 나옵니다.(여기서 자신이 보고자 하는 곳을 보시면 됩니다.)



비행 시뮬레이션을 사용할려면 도구 -> 비행 시뮬레이터 시작 을 누르시면 됩니다.(단축키 : Ctrl + Alt + A)

비행 시뮬레이터를 실행하면 밑의 사진처럼 창이 뜨는데 항공기를 선택하고(처음이시면 SR22을 추천합니다.), 시작 위치를 정한 뒤 비행 시작 버튼을 누르면 시작합니다.





비행 시뮬레이터 키보드 단축키(프로세싱에서 눈차크 값에 따른 다른 명령을 주고자 하면, 밑의 단축키를 보시고 설정해 주시면 됩니다.)




눈차크로 구글 어스 비행 시뮬레이터 제어 하는 순서

 아두이노 스케치를 이용하여 아두이노에 업로드 -> 구글 어스 비행 시뮬레이터를 실행 -> 프로세싱 실행 -> 구글 어스 활성 -> 눈차크 Z버튼을 누른 후 제어


아두이노 소스
 * 본 소스는 스케치를 사용하여 작성 / 업로드 합니다. 스케치에 대한 사용법은 링크를 참고 하시기 바랍니다.
 * 밑의 프로세싱 소스를 작성하시기 전에 아두이노 소스를 작성, 업로드 하시기 바랍니다.
 * 아두이노에 소스를 업로드 시킨 후, 아두이노와 USB를 연결된 상태로 프로세싱를 작성, 업로드 하시기 바랍니다.
 * 본 소스를 수정 했을 경우, 밑의 프로세싱 소스도 수정해야 합니다. 수정 하실 경우 밑의 설명을 읽고 수정하시기 바랍니다.
 * 아두이노 소스는 Nunchuck 라이브러리를 사용합니다. 라이브러리는 링크에서 받으시면 됩니다.(라이브러리 사용법은 링크를 참고 하시기 바랍니다.)
#include <Wire.h>
#include "Nunchuck.h"

int offsetX, offsetY, offsetZ;
// 마우스가 중앙에 있을 때 0을 가져오기 위해 센서에 추가 하는 값 void setup(){ Serial.begin(57600); nunchuckSetPowerpins(); nunchuckInit(); // 초기화 핸드셰이크를 전송한다. nunchuckRead(); // 처음엔 무시한다. delay(50); } void loop(){ nunchuckRead(); delay(6); boolean btnC = nunchuckGetValue(wii_btnC); boolean btnZ = nunchuckGetValue(wii_btnZ); if(btnC) { offsetX = 127 - nunchuckGetValue(wii_accelX) ; offsetY = 127 - nunchuckGetValue(wii_accelY) ; } Serial.print("Data,"); printJoy(nunchuckGetValue(wii_joyX)); printJoy(nunchuckGetValue(wii_joyY)); printButton(nunchuckGetValue(wii_btnZ)); printAccel(nunchuckGetValue(wii_accelY), offsetY); printAccel(nunchuckGetValue(wii_accelX), offsetX); // 프로세싱에서 자료를 받아서 분석 하기 위해 "Data, 조이패드 X값, 조이패드 Y값, Z버튼, 가속도 Y값, 가속도 X값" 순서로 보낸다.
// 프로세싱에서는 아두이노에서 보낸 값을 " , " 로 나눠서 값을 저장한다.
Serial.println(); } void printAccel(int value, int offset){ // 가속도 값을 출력 하는 함수 Serial.print(adjReading(value, 127-50, 127+50, offset)); Serial.print(","); } void printJoy(int value){ // 조이패드 값을 출력하는 함수 Serial.print(adjReading(value,0, 255, 0)); Serial.print(","); } void printButton(int value){ // 버튼 값을 출력하는 함수 if( value != 0) value = 127; Serial.print(value,DEC); Serial.print(","); } int adjReading( int value, int min, int max, int offset){ // 받은 값의 범위를 재설정 해주는 함수 value = constrain(value + offset, min, max);
// 범위 한정(min값 보다 낮을경우 min값으로, max값보다 클 경우 max값으로 바꿔준다.) value = map(value, min, max, -127, 127); // 값의 범위 재설정 return value; }

// 출처 : 레시피로 배우는 아두이노 쿡북(저자 : 마이클 마골리스)
// 아두이노 쿡북에 있는 소스를 참고하여 수정, 추가하였습니다.


프로세싱 코드
 * 밑의 소스는 프로세싱으로 작성 / 업로드 합니다. 프로세싱에 대한 사용법은 링크를 보고 참고 하시기 바랍니다.
import java.awt.AWTException;
import java.awt.Robot;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import processing.serial.*;
import java.awt.Dimension;

Serial myPort;  // Serial 클래스의 오브젝트를 생성한다.

arduMouse myMouse; // arduMouse 클래스의 오브젝트를 생성한다.

String message = null;
int maxDataFields = 7; // 최대 데이터 필드를 선언한다.(가속도계 3개, 버튼 2개, 조이스틱 축 2개)
boolean isStarted = false;
int accTemp, sightTemp, joyX, joyY, btnZ; // 아두이노에서 보낸 데이터 값이 여기에 저장이 된다.     


void setup() {
  size(260, 260);
  PFont fontA = createFont("Arial.normal", 12);  
  textFont(fontA);

  short portIndex = 0;  // com 포트를 선택한다.(0번은 연결된 첫번째 포트)
  String portName = Serial.list()[portIndex];
// 연결된 포트 리스트가 프로세싱에 출력이 된다. 이것을 보고 위에서 포트를 선택해 주면 된다. println(Serial.list()); println(" Connecting to -> " + portName) ; myPort = new Serial(this, portName, 57600); myMouse = new arduMouse(); fill(0); text("Start Google FS in the center of your screen", 5, 40); text("Center the mouse pointer in Google earth", 10, 60); text("Press and release Nunchuck Z button to play", 10, 80); text("Press Z button again to pause mouse", 20, 100); } void draw() { processMessages(); if (isStarted == false) { if ( btnZ != 0) { println("Release button to start"); do{ processMessages();} while(btnZ != 0); myMouse.mousePress(InputEvent.BUTTON1_MASK); // 눈차크에 Z버튼을 눌렀을 경우 눈차크 인식을 시작한다. isStarted = true; } } else { if ( btnZ != 0) { isStarted = false; background(204); text("Release Z button to play", 20, 100); print("Stopped, "); } else{ myMouse.move(joyX, joyY); // 조이스틱의 X, Y의 값을 이용해 마우스를 조정한다. myMouse.speedCon(accTemp); // 눈차크 가속도의 Y축 값을 이용하여 시뮬레이터의 속도를 조정한다. myMouse.sight(sightTemp); // 눈차크 가속도의 X축 값을 이용하여 시뮬레이터의 방향을 조정한다. fill(0); stroke(255, 0, 0); background(#8CE7FC); ellipse(127+joyX, 127+joyY, 4, 4); } } } void processMessages() { while (myPort.available () > 0) { message = myPort.readStringUntil(10); if (message != null) { String [] data = message.split(","); // 콤마(,) 를 이용하여 받은 데이터를 자른다. if ( data[0].equals("Data"))// 자른 데이터중 제일 처음이 Data일 경우 { try { joyX = Integer.parseInt(data[1]); // 조이스틱의 X값 joyY = Integer.parseInt(data[2]); // 조이스틱의 Y값 btnZ = Integer.parseInt(data[3]); // Z버튼 accTemp = Integer.parseInt(data[4]); // 눈차크 가속도 Y축값 sightTemp = Integer.parseInt(data[5]); // 눈차크 가속도 X축값 } catch (Throwable t) { println("."); } } } } } class arduMouse { Robot myRobot; static final short rate = 4; int centerX, centerY; arduMouse() { try { myRobot = new Robot(); } catch (AWTException e) { e.printStackTrace(); } Dimension screen = java.awt.Toolkit.getDefaultToolkit().getScreenSize(); centerY = (int)screen.getHeight() / 2 ; centerX = (int)screen.getWidth() / 2; } void move(int offsetX, int offsetY) { // 주어진 값으로 마우스를 이동한다.(조이스틱 x, y 값) myRobot.mouseMove(centerX + (rate* offsetX), centerY - (rate * offsetY)); } void mousePress( int button) { // Z버튼이 눌렸을 경우 마우스 클릭을 한다. myRobot.keyRelease(KeyEvent.VK_SHIFT); myRobot.mousePress(button) ; } void speedCon(int accTemp){ // 눈차크 가속도 Y축의 값을 기준으로 30이 넘어갈경우 가속, 0보다 낮을경우 감속을 한다.
// 가속일 경우 키보드 Page_UP키 감속일 경우 Page_Down키를 누르면 된다.
// keyPress함수를 사용할 경우 키가 계속 눌러진 상태로 있기 때문에 꼭 keyRelease함수를 사용해야한다. if(accTemp > 30){ myRobot.keyRelease(KeyEvent.VK_PAGE_DOWN); myRobot.keyPress(KeyEvent.VK_PAGE_UP); } else if(accTemp < 0){ myRobot.keyRelease(KeyEvent.VK_PAGE_UP); myRobot.keyPress(KeyEvent.VK_PAGE_DOWN); } } void sight(int sightTemp){ // 눈차크 가속도 X축의 값을 기준으로 100보다 크면 오른쪽으로, 100보다 작으면 왼쪽으로 이동한다.
// 키보드 입력은 쉬프트 + 오른쪽 화살표, 쉬프트 + 왼쪽 화살표 if(sightTemp >= 100){ myRobot.keyRelease(KeyEvent.VK_SHIFT); myRobot.keyRelease(KeyEvent.VK_LEFT); myRobot.keyPress(KeyEvent.VK_SHIFT); myRobot.keyPress(KeyEvent.VK_RIGHT); } else if(sightTemp <= -100){ myRobot.keyRelease(KeyEvent.VK_SHIFT); myRobot.keyRelease(KeyEvent.VK_RIGHT); myRobot.keyPress(KeyEvent.VK_SHIFT); myRobot.keyPress(KeyEvent.VK_LEFT); } else{ myRobot.keyRelease(KeyEvent.VK_SHIFT); myRobot.keyRelease(KeyEvent.VK_LEFT); myRobot.keyRelease(KeyEvent.VK_RIGHT); } } }
 

소프트웨어 설명

구글 어스에서 비행 시뮬레이터 조정은 마우스와 키보드를 이용합니다.

이것을 눈차크를 이용해 키보드와 마우스를 제어하여 구글 어스 비행 시뮬레이터를 조정하는 겁니다.

조이스틱 X, Y 값은 마우스의 이동을, 버튼 z는 마우스 클릭을, 눈차크 가속도 X, Y 값은 키보드를 제어합니다.

- 아두이노
아두이노 소스는 Nunchuck 라이브러리를 사용합니다. 아두이노 library 폴더 안에 Nunchuck 폴더를 만든 후, Nunchuck.h와 Nunchuck.cpp 파일을 만들어 넣으시면 됩니다.(링크 에 파일이 있으니 받으셔도 되고, 밑의 소스를 메모장에 옮겨서 위의 두 제목과 확장자로 저장하셔도 됩니다.)


Nunchuck.h(출처 : 아두이노 쿡북)
/*
 * Nunchuck.h
 * Arduino library to interface with wii Nunchuck
 */
 
#ifndef Nunchuck_included
#define Nunchuck_included

// identities for each field provided by the wii nunchuck
enum nunchuckItems { wii_joyX, wii_joyY, wii_accelX, wii_accelY, wii_accelZ, wii_btnC, wii_btnZ, wii_ItemCount };

// uses pins adjacent to I2C pins as power & ground for Nunchuck
void nunchuckSetPowerpins();
 
// initialize the I2C interface for the nunchuck
void nunchuckInit();
 
// Request data from the nunchuck
void nunchuckRequest();

// Receive data back from the nunchuck, 
// returns true if read successful, else false
bool nunchuckRead();
 
// Encode data to format that most wiimote drivers except
char nunchuckDecode (uint8_t x);
  
// return the value for the given item
int nunchuckGetValue(int item);
 
#endif

Nunchuck.cpp(출처 : 아두이노 쿡북)
/*
 * Nunchuck.cpp
 * Arduino library to interface with wii Nunchuck
 */

#include "Arduino.h"   // Arduino defines

#include "Wire.h"      // Wire (I2C) defines
#include "Nunchuck.h"  // Defines for this library

// defines for standard Arduino board (use 19 and 18 for mega)
const int vccPin = 17; // +v and gnd provided through these pins
const int gndPin = 16;  

 
const int dataLength = 6;          // number of bytes to request
static byte rawData[dataLength];   // array to store nunchuck data

// uses pins adjacent to I2C pins as power & ground for Nunchuck
void nunchuckSetPowerpins()
{
  pinMode(gndPin, OUTPUT); // set power pins to the correct state
  pinMode(vccPin, OUTPUT);
  digitalWrite(gndPin, LOW);
  digitalWrite(vccPin, HIGH);
  delay(100); // wait for power to stabilize
}
 
// initialize the I2C interface for the nunchuck
void nunchuckInit()
{
  Wire.begin();                // join i2c bus as master
  Wire.beginTransmission(0x52);// transmit to device 0x52
  Wire.write((byte)0x40);      // sends memory address
  Wire.write((byte)0x00);      // sends sent a zero.  
  Wire.endTransmission();      // stop transmitting
}  
 
// Request data from the nunchuck
void nunchuckRequest()
{
  Wire.beginTransmission(0x52);// transmit to device 0x52
  Wire.write((byte)0x00);// sends one byte
  Wire.endTransmission();// stop transmitting
}

// Receive data back from the nunchuck, 
// returns true if read successful, else false
bool nunchuckRead()
{
  byte cnt=0;
  Wire.requestFrom (0x52, dataLength);// request data from nunchuck
  while (Wire.available ()) {
    byte x = Wire.read(); 
    rawData[cnt] = nunchuckDecode(x);
    cnt++;
  }
  nunchuckRequest();  // send request for next data payload
  if (cnt >= dataLength) 
    return true;   // success if all 6 bytes received
  else
    return false; //failure
}

// Encode data to format that most wiimote drivers except
char nunchuckDecode (byte x)
{
   return (x ^ 0x17) + 0x17;
}
  
// return the value for the given item
int nunchuckGetValue(int item)
{
  if( item <= wii_accelZ)
    return (int)rawData[item]; 
  else if(item  == wii_btnZ) 
    return bitRead(rawData[5], 0) ? 0: 1;
  else if(item  == wii_btnC) 
    return bitRead(rawData[5], 1) ? 0: 1;   
}
 
아두이노 소스는 간단합니다. 눈차크 라이브러리를 사용해서 눈차크의 값을 받고, 그것을 사용할 수 있게 값을 변경 후 시리얼 통신으로 전송 합니다.
(가속도 값은 printAccel() 함수, 조이스틱 값은 printJoy() 함수, 버튼 값은 printButton()함수를 사용해서 전송 합니다.)

이 때 프로세싱에서 데이터를 받은 후 구분하는 방법이 " , " 를 사용하여 구분하기 때문에 각 값들 사이에 꼭 , 로 구분해서 보내야 합니다.
또한 한번에 엔터값 (" \n ") 까지 끊어서 받기 때문에 전송할 자료값이 남았는데 println() 을 써서 보낼 경우 뒤에 값이 짤리게 됩니다.


- 프로세싱
프로세싱을 실행 시키면 밑의 사진 처럼 화면이 뜹니다. 이 화면이 뜨면 구글 어스 비행 시뮬레이터를 활성 시킨 후 Z버튼을 누른 후 눈차크로 제어하시면 됩니다.


  short portIndex = 0;  // com 포트를 선택한다.(0번은 연결된 첫번째 포트)
  String portName = Serial.list()[portIndex];
// 연결된 포트 리스트가 프로세싱에 출력이 된다. 이것을 보고 위에서 포트를 선택해 주면 된다. println(Serial.list()); println(" Connecting to -> " + portName) ; myPort = new Serial(this, portName, 57600);
시리얼 통신을 위해 아두이노와 연결된 포트를 찾고 연결 해 주는 부분입니다.
우선 실행을 하시면 밑의 사진 처럼 연결된 포트번호가 나옵니다. 이중에 아두이노와 연결된 포트 번호를 찾아서, portIndex를 설정해 주시면 됩니다.
(제 경우 아두이노는 4번 포트에 연결되 있어서, portIndex를 0으로 설정해 주었습니다.)


 myMouse.move(joyX, joyY); // 조이스틱의 X, Y의 값을 이용해 마우스를 조정한다.
 myMouse.speedCon(accTemp); // 눈차크 가속도의 Y축 값을 이용하여 시뮬레이터의 속도를 조정한다.
 myMouse.sight(sightTemp); // 눈차크 가속도의 X축 값을 이용하여 시뮬레이터의 방향을 조정한다.
위의 3함수는 마우스, 키보드를 제어하는 함수 입니다. 아두이노에서 보낸 눈차크 값을 입력 받아 그에 맞는 마우스와 키보드의 동작을 제어합니다.

 while (myPort.available () > 0) {
    message = myPort.readStringUntil(10); 
    if (message != null) {
      String [] data  = message.split(","); // 콤마(,) 를 이용하여 받은 데이터를 자른다. 
      if ( data[0].equals("Data"))// 자른 데이터중 제일 처음이 Data일 경우
      {
        try {
          joyX = Integer.parseInt(data[1]); // 조이스틱의 X값
          joyY = Integer.parseInt(data[2]); // 조이스틱의 Y값
          btnZ = Integer.parseInt(data[3]); // Z버튼
          accTemp = Integer.parseInt(data[4]); // 눈차크 가속도 Y축값
          sightTemp = Integer.parseInt(data[5]); // 눈차크 가속도 X축값
        }
        catch (Throwable t) {
          println("."); 
        }
      }
    }
  }
이 부분은 아두이노에서 보낸 값을 확인하고 분해 하는 부분입니다. 아두이노에서 보낸 데이터를 한 줄 단위로 끊어서 받은 다음, 그 안에서 "," 를 기준으로 데이터를 잘라서 각각 저장합니다.

아두이노에서 정상적으로 전송이 됬으면 총 6개의 값이 나뉘는데, 맨처음엔 "Data"라는 글이 저장되고, 그 다음 부터 순서대로 조이스틱 X값, 조이스틱 Y값, Z버튼, 눈차크 가속도 Y축 값, 눈차크 가속도 X축 값이 저장됩니다.

  void speedCon(int accTemp){ // 눈차크 가속도 Y축의 값을 기준으로 30이 넘어갈경우 가속, 0보다 낮을경우 감속을 한다.
// 가속일 경우 키보드 Page_UP키 감속일 경우 Page_Down키를 누르면 된다.
// keyPress함수를 사용할 경우 키가 계속 눌러진 상태로 있기 때문에 꼭 keyRelease함수를 사용해야한다. if(accTemp > 30){ myRobot.keyRelease(KeyEvent.VK_PAGE_DOWN); myRobot.keyPress(KeyEvent.VK_PAGE_UP); } else if(accTemp < 0){ myRobot.keyRelease(KeyEvent.VK_PAGE_UP); myRobot.keyPress(KeyEvent.VK_PAGE_DOWN); } } void sight(int sightTemp){ // 눈차크 가속도 X축의 값을 기준으로 100보다 크면 오른쪽으로, 100보다 작으면 왼쪽으로 이동한다.
// 키보드 입력은 쉬프트 + 오른쪽 화살표, 쉬프트 + 왼쪽 화살표 if(sightTemp >= 100){ myRobot.keyRelease(KeyEvent.VK_SHIFT); myRobot.keyRelease(KeyEvent.VK_LEFT); myRobot.keyPress(KeyEvent.VK_SHIFT); myRobot.keyPress(KeyEvent.VK_RIGHT); } else if(sightTemp <= -100){ myRobot.keyRelease(KeyEvent.VK_SHIFT); myRobot.keyRelease(KeyEvent.VK_RIGHT); myRobot.keyPress(KeyEvent.VK_SHIFT); myRobot.keyPress(KeyEvent.VK_LEFT); } else{ myRobot.keyRelease(KeyEvent.VK_SHIFT); myRobot.keyRelease(KeyEvent.VK_LEFT); myRobot.keyRelease(KeyEvent.VK_RIGHT); } }
위의 두 함수는 키보드를 제어 하는 함수 입니다. keyPress() 함수는 괄호 안에 맞는 키를 누르는 함수이고, keyRelease() 함수는 괄호 안의 맞는 키를 풀어주는 함수 입니다. keyPress()함수를 사용하면 계속 눌러진 상태로 있기 때문에, 그에 맞는 키를 꼭 keyRelase() 해주셔야 합니다.
(키보드에 맞는 KeyEvent는 링크에 나와 있습니다.)


판다마니아

아두이노, 프로세싱, I2C, 눈차크, 중급, 비행기 시뮬레이터