Introduction: Virtual Reality Flying Machine (Arduino)

About: My name is Matthew and I attend the University of Pittsburgh. Currently I am a senior, going for a bachelors in Information Science with a minor in CS. Current interests include augmented reality, virtual real…

This project goes through converting a hammock into a VR flying machine. We will use a VR app that is running the WRLD SDK in Unity so you can input whatever GPS coordinates you want and fly around that area in VR! This VR app will also run on IOS or Android. The hammock has an ESP8266 running Arduino code that goes on the cross bar of the hammock so when you lean left or right you turn in the game. Also there is a small fan running another Arduino board that blows faster or slower based on your speed in the game. So this project will go over how to interface Arduino with Unity, wirelessly over WIFI and also how to control a variable speed fan.

Unity VR Project:

https://github.com/MatthewHallberg/flyVR

Parts:

ESP-8266 Node MCU (comes with 2): https://amzn.to/2tAyNsu

MPU-6050: https://amzn.to/2yD5dbD

4 Channel Relay: https://amzn.to/2MmCqKi

Fan: https://amzn.to/2trnZgZ

Hammock: https://amzn.to/2KkR1FP

VR headset I used: https://amzn.to/2MRAURw

Power Bank (need 2): https://amzn.to/2tp3l16

Arduino Buttons: https://amzn.to/2tzJxHM

Assorted Arduino Wires: https://amzn.to/2tyjPTS

Step 1: Setting Up the Fan.

Let's first get the fan set up.

Take apart all the screws that hold on the fan shroud with a long Phillips-head screwdriver.

Remove the 4 screws holding down the fan and motor to the base.

Unbolt the small relay from the base as well so you have some slack to work with.

Pull off the fan speed knob from the outside and take apart the switch from the inside. Inside the switch you will see 4 wires. The hot wire will need to be split into 3 separate wires, one for each fan speed.

Grab your ESP-8266 and hook that up to your fan as shown in the image above.

Plug in the board to your computer and fire up the Arduino IDE.

Step 2: Upload Fan Code.

Put the fan back together and zip-tie the 4 channel relay to the back of the fan. Make sure the fan can spin freely.

Add your WIFI network name and password to the code below and upload it to your Arduino board. Plug it into the wall. The fan will default to low power. If you open up the serial monitor at 115200 you can type in 1-4 to access the different fan speeds. This code listens for packets being sent by another Arduino board so let's get that one set up next.

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>

int pin5 = 14;
int pin6 = 12;
int pin7 = 13;

int incomingByte = 0;

//wifi stuff
const char* ssid     = "****************"; // wifi network name
const char* password = "*************"; // wifi network password

WiFiUDP Udp;
unsigned int localUdpPort = 1999;
char incomingPacket[255];

void setup(void) {
  Serial.begin(115200);
  delay(10);
  
// We start by connecting to a WiFi network
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
  }
  Serial.println("WiFi connected"); 
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  Serial.println("Starting UDP");
  Udp.begin(localUdpPort);  

//set pin modes
  pinMode(pin5, OUTPUT);//0
  pinMode(pin6, OUTPUT);//1
  pinMode(pin7, OUTPUT);//2
  delay(100);
  //set default state
  low();
}

void off(){
  Serial.println("OFF");
  digitalWrite(pin5,HIGH);
  digitalWrite(pin6,HIGH);
  digitalWrite(pin7,HIGH);
}

void low(){
  Serial.println("LOW");
  digitalWrite(pin5,HIGH);
  digitalWrite(pin6,LOW);
  digitalWrite(pin7,HIGH);
}

void medium(){
  Serial.println("MEDIUM");
  digitalWrite(pin5,HIGH);
  digitalWrite(pin6,HIGH);
  digitalWrite(pin7,LOW);
}

void high(){
  Serial.println("HIGH");
  digitalWrite(pin5,LOW);
  digitalWrite(pin6,HIGH);
  digitalWrite(pin7,HIGH);
}

void loop() {

  //listen for packets
  int packetSize = Udp.parsePacket();
  if (packetSize){
    int len = Udp.read(incomingPacket, 255);
    if (len > 0){
      incomingPacket[len] = 0;
    }
    Serial.printf("UDP packet contents: %s\n", incomingPacket);
    if (incomingPacket[4] == 'H'){
        high();
    } else if (incomingPacket[4] == 'L'){
        low();
    }
  }

  if (Serial.available() > 0) {
    // read the incoming byte:
    incomingByte = Serial.read();
    //set fan based on input
    switch (incomingByte) {
      case '1':
        off();
        break;
      case '2':
        low();
        break;
      case '3':
        medium();
        break;
      case '4':
        high();
        break;
      default:
        off();
    }
  }
}

Step 3: Setting Up the MPU-6050

Solder the header pins onto the MPU-6050 and put it on a breadboard with another ESP8266.

Grab the two buttons and hook everything up according to the diagram above. Connect the buttons to the ESP with 2 wires instead of one because they will need a lot of slack when we attach them to the hammock.

This particular setup will read raw accelerometer and gyro values and convert them to usable x,y,z values. Those values will be sent as a constant stream to any device listening on the WIFI network that you define in the code. Also, the ESP-8266 will send messages to any device listening on the network when either button is pressed in order to increase the fan speed when any button is held down.

In order to use the code for this particular setup we are going to need to download this library:

https://github.com/jrowberg/i2cdevlib

Open up that folder and navigate to Arduino->MPU-6050->Examples->MPU6050_DMP6_ESPWiFi

Replace what is in that script with the code in the following step and upload it to your board. We are doing this so all of our libraries and header files are in the right place without doing any extra work.

Step 4: Code for MPU-6050

/* ============================================
I2Cdev device library code is placed under the MIT license
Copyright (c) 2012 Jeff Rowberg
==============================

  GY-521  NodeMCU
  MPU6050 devkit 1.0
  board   Lolin         Description
  ======= ==========    ====================================================
  VCC     VU (5V USB)   Not available on all boards so use 3.3V if needed.
  GND     G             Ground
  SCL     D1 (GPIO05)   I2C clock
  SDA     D2 (GPIO04)   I2C data
  XDA     not connected
  XCL     not connected
  AD0     not connected
  INT     D8 (GPIO15)   Interrupt pin
*/
// I2Cdev and MPU6050 must be installed as libraries, or else the .cpp/.h files
// for both classes must be in the include path of your project
#include "I2Cdev.h"

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>

#include "MPU6050_6Axis_MotionApps20.h"
//#include "MPU6050.h" // not necessary if using MotionApps include file

// Arduino Wire library is required if I2Cdev I2CDEV_ARDUINO_WIRE implementation
// is used in I2Cdev.h
#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
    #include "Wire.h"
#endif

// class default I2C address is 0x68
// specific I2C addresses may be passed as a parameter here
// AD0 low = 0x68 (default for SparkFun breakout and InvenSense evaluation board)
// AD0 high = 0x69
MPU6050 mpu;
//MPU6050 mpu(0x69); // <-- use for AD0 high

// MPU control/status vars
bool dmpReady = false;  // set true if DMP init was successful
uint8_t mpuIntStatus;   // holds actual interrupt status byte from MPU
uint8_t devStatus;      // return status after each device operation (0 = success, !0 = error)
uint16_t packetSize;    // expected DMP packet size (default is 42 bytes)
uint16_t fifoCount;     // count of all bytes currently in FIFO
uint8_t fifoBuffer[64]; // FIFO storage buffer

#define OUTPUT_READABLE_YAWPITCHROLL

// orientation/motion vars
Quaternion q;           // [w, x, y, z]         quaternion container
VectorInt16 aa;         // [x, y, z]            accel sensor measurements
VectorInt16 aaReal;     // [x, y, z]            gravity-free accel sensor measurements
VectorInt16 aaWorld;    // [x, y, z]            world-frame accel sensor measurements
VectorFloat gravity;    // [x, y, z]            gravity vector

float ypr[3];           // [yaw, pitch, roll]   yaw/pitch/roll container and gravity vector

#define INTERRUPT_PIN 15 // use pin 15 on ESP8266

const char DEVICE_NAME[] = "mpu6050";

// ================================================================
// ===               INTERRUPT DETECTION ROUTINE                ===
// ================================================================

volatile bool mpuInterrupt = false;     // indicates whether MPU interrupt pin has gone high
void dmpDataReady() {
    mpuInterrupt = true;
}

void mpu_setup()
{
  // join I2C bus (I2Cdev library doesn't do this automatically)
#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
  Wire.begin();
  Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties
#elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE
  Fastwire::setup(400, true);
#endif

  // initialize device
  Serial.println(F("Initializing I2C devices..."));
  mpu.initialize();
  pinMode(INTERRUPT_PIN, INPUT);

  // verify connection
  Serial.println(F("Testing device connections..."));
  Serial.println(mpu.testConnection() ? F("MPU6050 connection successful") : F("MPU6050 connection failed"));

  // load and configure the DMP
  Serial.println(F("Initializing DMP..."));
  devStatus = mpu.dmpInitialize();

  // supply your own gyro offsets here, scaled for min sensitivity
  mpu.setXGyroOffset(220);
  mpu.setYGyroOffset(76);
  mpu.setZGyroOffset(-85);
  mpu.setZAccelOffset(1788); // 1688 factory default for my test chip

  // make sure it worked (returns 0 if so)
  if (devStatus == 0) {
    // turn on the DMP, now that it's ready
    Serial.println(F("Enabling DMP..."));
    mpu.setDMPEnabled(true);

    // enable Arduino interrupt detection
    Serial.println(F("Enabling interrupt detection (Arduino external interrupt 0)..."));
    attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), dmpDataReady, RISING);
    mpuIntStatus = mpu.getIntStatus();

    // set our DMP Ready flag so the main loop() function knows it's okay to use it
    Serial.println(F("DMP ready! Waiting for first interrupt..."));
    dmpReady = true;

    // get expected DMP packet size for later comparison
    packetSize = mpu.dmpGetFIFOPacketSize();
  } else {
    // ERROR!
    // 1 = initial memory load failed
    // 2 = DMP configuration updates failed
    // (if it's going to break, usually the code will be 1)
    Serial.print(F("DMP Initialization failed (code "));
    Serial.print(devStatus);
    Serial.println(F(")"));
  }
}

//push buttons
int greenButtonPin = 12;//6
int redButtonPin = 13;//7
int greenVal = 0; 
int redVal = 0;
bool greenDown = false;
bool redDown = false;

//wifi stuff
const char* ssid     = "**************"; // wifi network name
const char* password = "*********"; // wifi network password

//iIP address of receiving computer or mobile device, set to exact IP or al 255's to send to every device on the network
IPAddress deviceIpBroadCast(255,255,255,255); 
IPAddress fanIpBroadCast(255,255,255,255);

unsigned int udpRemotePort=1999;
const int UDP_PACKET_SIZE = 28;
char udpBuffer[ UDP_PACKET_SIZE];
WiFiUDP udp;

void setup(void)
{
  Serial.begin(115200);
  delay(10);
  
  // We start by connecting to a WiFi network
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
  }
  Serial.println("WiFi connected"); 
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  Serial.println("Starting UDP");
  //send connected message
  strcpy(udpBuffer, "Connected");
  Serial.println("Connected"); 
  udp.beginPacket(deviceIpBroadCast, udpRemotePort);
  udp.write(udpBuffer, sizeof(udpBuffer));
  udp.endPacket();
  
  //set up buttons
  pinMode(greenButtonPin, INPUT);
  pinMode(redButtonPin, INPUT); 
  //set up accelerometer 
  mpu_setup();
}

void sendVRMessage(String message){
  strcpy(udpBuffer,message.c_str()); 
  udp.beginPacket(deviceIpBroadCast, udpRemotePort);
  udp.write(udpBuffer, sizeof(udpBuffer));
  udp.endPacket();
}

void sendFanMessage(String message){
  strcpy(udpBuffer,message.c_str()); 
  udp.beginPacket(fanIpBroadCast, udpRemotePort);
  udp.write(udpBuffer, sizeof(udpBuffer));
  udp.endPacket();
}

void mpu_loop()
{
  // if programming failed, don't try to do anything
  if (!dmpReady) return;

  // wait for MPU interrupt or extra packet(s) available
  if (!mpuInterrupt && fifoCount < packetSize) return;

  // reset interrupt flag and get INT_STATUS byte
  mpuInterrupt = false;
  mpuIntStatus = mpu.getIntStatus();

  // get current FIFO count
  fifoCount = mpu.getFIFOCount();

  // check for overflow (this should never happen unless our code is too inefficient)
  if ((mpuIntStatus & 0x10) || fifoCount == 1024) {
    // reset so we can continue cleanly
    mpu.resetFIFO();
    Serial.println(F("FIFO overflow!"));

    // otherwise, check for DMP data ready interrupt (this should happen frequently)
  } else if (mpuIntStatus & 0x02) {
    // wait for correct available data length, should be a VERY short wait
    while (fifoCount < packetSize) fifoCount = mpu.getFIFOCount();

    // read a packet from FIFO
    mpu.getFIFOBytes(fifoBuffer, packetSize);

    // track FIFO count here in case there is > 1 packet available
    // (this lets us immediately read more without waiting for an interrupt)
    fifoCount -= packetSize;

#ifdef OUTPUT_READABLE_YAWPITCHROLL
    // display Euler angles in degrees
    mpu.dmpGetQuaternion(&q, fifoBuffer);
    mpu.dmpGetGravity(&gravity, &q);
    mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);

    String x = String(ypr[0] * 180/M_PI);
    String y = String(ypr[1] * 180/M_PI);
    String z = String(ypr[2] * 180/M_PI);
    delay(100);
    mpu.resetFIFO();
    String message = x + "," + y + "," + z;
    Serial.println(message);
    sendVRMessage(message);
#endif
  }
}

void buttonLoop(){
  greenVal = digitalRead(greenButtonPin);
  if (greenVal == HIGH && greenDown) {
    greenDown = false;        
    Serial.println("GREEN_UP");
    sendVRMessage("GREEN_UP");
    sendFanMessage("FAN_LOW");  
  } else if (greenVal == LOW && !greenDown) {
    greenDown = true;        
    Serial.println("GREEN_DOWN");
    sendVRMessage("GREEN_DOWN");
    sendFanMessage("FAN_HIGH");  
  }
  redVal = digitalRead(redButtonPin);
  if (redVal == HIGH && redDown) {
    redDown = false;        
    Serial.println("RED_UP");
    sendVRMessage("RED_UP"); 
    sendFanMessage("FAN_LOW");  
  } else if (redVal == LOW && !redDown) {
    redDown = true;        
    Serial.println("RED_DOWN");
    sendVRMessage("RED_DOWN"); 
    sendFanMessage("FAN_HIGH");   
  }
}

void loop(void)
{
  mpu_loop();
  buttonLoop();
}

Step 5: Assembling the Beast

First attach two pieces of wood to the crossbar of the hammock. The pieces I used were approximately 1.5ft.

Screw the wood into the crossbar.

The Arduino board must sit as flat as possible on the crossbars, but if it is not 100% straight that is ok because the Unity app will account for an offset of the device.

Spead the buttons approximately 1ft away from the center of the crossbar.

Add a 5V USB power bank to power the Arduino board.

Ziptie everything down.

Step 6: Attach the Fan.

The feet of the fan have screw holes so use those to attach the fan to the wood.

Most hammocks come with a fixed cross bar so it should just hold.

In my case my cross bar is not fixed and the weight of the fan causes the bar to spin down when there is no weight in the hammock.

To rectify this I zip-tied a rope to the bottom of the fan and attached this rope to the top of the hammock.

This prevents the bar from spinning downward.

It is also important to fix the whole set up so that the hammock cannot spin around.

For this I tied a rope to each end of the crossbar and anchored them both in front of the hammock.

Step 7: Prepare the Game

The VR portion of this project was made in Unity, so everything is pretty much ready to go. The only thing you will need to do is build it to your phone.

Download Unity 3D if you don't already have it also download the GitHub repo from here: https://github.com/MatthewHallberg/flyVR

Unzip that whole folder and open it up in Unity.

This project uses the WRLD SDK so it procedural generates geography from whatever GPS coordinates we give it as a starting point. So I have it currently set to downtown Pittsburgh.

If you want to set it to something different click on the WRLD folder, API, and open up WrldMap.cs in visual studio. Change the latitude and longitude at the beginning of the script.

Step 8: Put It on Your Phone!

Go to file build settings and switch your platform to Android or IOS. Plug in your phone.

If you are on IOS go to player settings and put in something for your bundle identifier.

Download Cocoa Pods if you don't have it and click build.

Open up the newly created .workspace file in Xcode, choose your team, and hit the play to get it on your phone.

If you are on Android go to the Android player settings and put in something for your bundle identifier.

Uncheck Android TV, and hit build and run with your phone.

Make sure your phone is connected to your WIFI network. Sit in the hammock and start the app with the phone facing in the direction you want to move.

Plug in the fan to a wall outlet and plug in the USB power banks to power the Arduino boards on the cross bar and on the fan. Everything should connect automatically and start sending messages to each other.

Let me now in comments if you have any questions!