Project tutorial
Purdue Verti-Fix

Purdue Verti-Fix

Our device is for primary care physicians as well as at home patients to treat Benign Paroxysmal Positional Vertigo with ease.

  • 501 views
  • 0 comments
  • 15 respects

Components and supplies

Necessary tools and machines

09507 01
Soldering iron (generic)

Apps and online services

About this project

Description

Verti-Fix is a wearable device that houses accelerometers to quantify the angle of the head as it is tilted for the Epley Maneuver to treat Benign Paroxysmal Positional Vertigo (BPPV). The device is also equipped with LEDs to warn the user as they get close to exceeding the recommended angles and contains an interactive screen to allow the appropriate sub-type of the Epley maneuver to be chosen.

Check out our video to get a better idea of our project.

Ver

Executive Summary

Problem: A disease affecting 8 million Americans and one in four people over the age of 65 (Liu, et al., 2017). BPPV, shortened for Benign Paroxysmal Positional Vertigo, is a condition that causes symptoms like dizziness, loss of balance, nausea, and a perception of your surroundings spinning. Unfortunately treatment is very hard to get as non-specialist physicians will choose to not perform the maneuvers because they believe it will cause further harm to the patient if done incorrectly. The lack of experience and level of difficulty in performing the maneuvers is limiting people with vertigo from receiving treatment that is more accessible. Currently, patients within the United States may need to travel for several hours to reach a vestibular specialist to have the Epley maneuver conducted to relieve their symptoms.

Solution: The Verti-Fix is a device that is capable of addressing key requirements for the Epley maneuver to relieve symptoms of BPPV. (1) The angle the head is tilted during the procedure is essential and Verti-Fix is capable of tracking this accurately using accelorometers. (2) LED are used to warn the performing physician if they get close to exceeding the recommended angles. This prevents general physicians from worsening the symptoms. (3) The device is portable and contains an interactive interface that is easy to use and will allow easy adoption of the technology.

Differentiation: Prior solutions involved the use of images and videos to walk people through the procedure, but the uncertainty that comes with moving the head a certain angle still needed to be addressed. Verti-Fix can provide quantitative data during the process and can provide real time warnings to physicians if they exceed a certain angle. Additionally, images and videos cannot clearly communicate the exact angle the head is tilted for different patients. On the other hand, Verti-Fix can be used with videos to provide a comprehensive solution to this problem.

Current Prototype

The device consists of a motion sensing apparatus that is fitted to the patient. This is primarily responsible for providing the patient with instructions and ensuring the movements are carried out properly. A simplified block diagram of the circuitry is illustrated below.

This movement tracking was centered around an Arduino microcontroller for rapid development and prototyping. The microcontroller carries out three main tasks: perform position calculations based on data from the inertial measurement unit (IMU), instruct the user how to move their body, and ensure movements are carried out properly. All three of these tasks are carried out by a programmed state machine. After the user selects the ear that the problem is located in, the program quite literally steps through the movements with the user. During each step, the program checks the following criteria mainly concerned with the chin tilt, neck rotation, and side to side tilt of a user’s head. In addition, it also checks rotational speeds to ensure movements are not carried out too slowly or quickly. Each step has a unique set of criteria that must be checked in order to be sure the proper movement was carried out, hence the state machine based program design. After all the criteria are met, the program moves to the next step. If the criteria is not met, or the device picks up measurements that should not be detected with the current movement, the program returns to the first instruction, while alerting the user that they will have to restart.

The way the user is actually instructed is through a display. On this display, the step number is displayed to give the user an indication of how many more steps they are required to complete. In addition, there are two lines actually describing the movement. The movement instructions are simple enough to always fit on these two display lines. The final line is used to display a criteria that needs to be met for the step to be considered complete. This can come in the form of a live update of how far a patient has turned their head or a countdown of how long they have to remain still in a position.

Innovation and Creativity

To address the problem, Purdue MIND developed the Verti-Fix. A device capable of assisting general physicians through the Epley maneuver and will help relieve the symptoms that arise due to BPPV. Until now, the Epley maneuver could only be attempted by vestibular specialists due to the complexity of the maneuver and the possibility of exacerbating the negative symptoms if done incorrectly. The head needs to be moved in certain angles to move the calcium carbonate crystals in the inner ear back to its correct location. However, if the maneuver is done incorrectly, the crystals may be moved down the wrong inner ear canal and cause the symptoms to continue. This means that general physicians generally pass on these cases to vestibular specialists, who are not as common and require patients to travel long distances to treat their symptoms. We worked with a vestibular specialist to ensure that Verti-fix would address this problem and provide general physicians, who have little experience with the Epley maneuver, the guidelines to treat the condition. Verti-fix contains accelerometers that can track the angle of the head and would walk general physicians through the procedure and ensure that proper angles are maintained. Prior solutions involved the use of images and videos to walk people through the procedure, but the uncertainty that comes with moving the head a certain angle still needed to be addressed. Verti-fix can provide quantitative data during the process and can warn physicians if they exceed a certain angle. Moreover, our device is capable of sufficient measurement accuracy to ensure the movements are carried out properly. Additionally, the angles will be displayed on a screen that can be used to follow along the Epley maneuver, with LEDs present to provide an additional warning system in case the head gets close to exceeding the recommended angle during a particular section of the procedure.

We are in the progress of implementing wireless capabilities to the device so there is no wire between the head sensor and the wrist display. This will enable better movement and ease of use for users. We also have made sizable improvement by adding two more features to the device. One is a neck brace meant to stabilize the neck for the maneuvers, thus solving common maneuver mistakes done with inexperienced users. Second is an eye tracking add on that enables the physician to visually see the nystagmus allowing for easier diagnosis of the condition.

In the IEEE Spectrum magazine issue Under African Skies (May 2019), chief scientist at Dolby Laboratories and adjunct professor at Stanford University Poppy Crum wrote a fascinating article titled Here come the Hearables. In this article, Professor Crum elaborates on the fact that the ear is an untapped well of erudition about a person. She describes it as a USB port to the body. Brain electrical activity, movement, temperature, eye movements, vagus nerve stimulation, blood oxygen, and stress hormone levels are just a few of the numerous types of data that can be accessed through the ear. Crum coins the term “hearables, ” prognosticating that the ear will be used to monitor biological systems and be an empathetic type of technology. With AI technology exponentially growing in our day and age, there’s no doubt that hearables would learn and know the intricate details of its owner. Verti-Fix barely scratches the surface of what hearables will be capable of in the future. We use head movements and eye movements to diagnose and correct BPPV. Since the ear has an extraordinary amount of other information, we can use it to diagnose and correct other diseases. We as a group can continue to work on “hearable” devices since we have the experience of designing and prototyping Verti-Fix, allowing us to disrupt the very market we pioneer. In short, Verti-Fix is capable of ushering in field of technology that uses the ear as a window to the human body.

As mentioned above, we are working on implementing a wireless design to facilitate use for the patients and doctors. Since the components are modular, it will be more useful if they do not need to all be wired together.

Once the basics are completed, we could potentially include AI to aid in the diagnosis to take other symptoms into account and confirm that a patient does have BPPV. As of current, the GPs and ER doctors will need to spend time to learn about all the symptoms and make a decision, whereas the symptoms could be inputted into a computer (with voice perhaps for the ease of potential BPPV patients) and AI could determine whether or not the patient has BPPV.

Completeness

All of the fundamental groundwork for Verti-Fix has already been laid, and initial prototyping shows the success that is required to say that the work is fully developed. Testing of the head tracking device shows that it is fully capable of detecting whether movements are correct. The only improvement needed is calibration of tolerances to allow some leeway in carrying out the maneuvers. The nystagmus device is based around existing eye tracking technology, so the technology for this component is already well developed.

The device has two designed functions: indicate which ear the problem resides in, and allow the execution of a set of maneuvers that solve the problem. The device currently is capable of actively monitoring the carrying out of maneuvers, and is almost capable of diagnosing which ear the problem resides in based off of nystagmus. With this, we can say that the device will fully be able to realize designed functions within a short period of time and with minimum upgrades.

Future Prospects

This product can be feasible in 2 years. The most advanced technology in this system would be the eye-tracking to detect nystagmus. The head movements are controlled by a simple inertial measurement system interfaced to a microcontroller. With the possible improvements we can make to the Verti-Fix, the technology we are developing is at the forefront of innovation and will definitely be feasible and hopefully a popular product.

Since most of the technology is rather simple, it is feasible to produce the product at a minimal cost that suits the users and return a profit. Based on our market research, we will target the Verti-Fix towards the elderly, since BPPV is more common in the elderly. According to a research done by Yetizer and Ince, BPPV is commonly found in the age group of 31 to 50 years old. They also stated that this condition is relatively rare for people under the age of 20. A device like the Verti-Fix, with its promise to restore autonomy to the lives that we want to help. Verti-Fix would be part of the rehabilitation devices and equipment market. This market is currently valued at 10.53 billion USD in 2016 and will grow due to the degenerative diseases. It will also grow due to the longevity of human life as well as the increasing population (Grandview).

Potential channel distributions would be primarily online for patients who may need to travel great lengths to obtain the product or for hospitals looking to stock their inventory with our product. It could also be sold in pharmaceutical shops like Walgreen's or CVS.

The end users would be patients who either have chronic BPPV and need correction quite often or patients who suspect that they may have BPPV and want to confirm diagnosis. It would be recommended for a patient to confirm with their doctor about the diagnosis just to make sure. Other users would be general practitioners or ER doctors who have patients that likely have BPPV.

References

Benign Paroxysmal Positional Vertigo (BPPV). (2019, March 26). Retrieved from https://vestibular.org/understanding-vestibular-disorders/types-vestibular-disorders/benign-paroxysmal-positional-vertigo

Crum, P. (2019, May 01). Hearables Will Monitor Your Brain and Body to Augment Your Life. Retrieved from https://spectrum.ieee.org/consumer-electronics/audiovideo/hearables-will-monitor-your-brain-and-body-to-augment-your-life

Liu, D., Kuo, C., Wang, C., Chiu, C., Chen, T., Hwang, D., & Kao, C. (2017, December 12). Age-Related Increases in Benign Paroxysmal Positional Vertigo Are Reversed in Women Taking Estrogen Replacement Therapy: A Population-Based Study in Taiwan. Retrieved May 15, 2019, from https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5732995/

Rehabilitation Devices/Equipment Market Size | Industry Report, 2025. (n.d.). Retrieved from https://www.grandviewresearch.com/industry-analysis/rehabilitation-products-market

Yetiser, S., & Ince, D. (2015). Demographic analysis of benign paroxysmal positional vertigo as a common public health problem. Annals of medical and health sciences research, 5(1), 50–53. doi:10.4103/2141-9248.149788

Code

Arduino ProgrammingArduino
This code runs on the microcontroller to carry out the maneuvers required to treat BPPV Vertigo.
#include <Wire.h>
#include <SPI.h>
#include <SparkFunLSM9DS1.h>
#include <Integrator.h>
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>

LSM9DS1 imu;
LiquidCrystal_I2C lcd(0x27, 20, 4);

#define LSM9DS1_M	0x1E // Would be 0x1C if SDO_M is LOW
#define LSM9DS1_AG	0x6B // Would be 0x6A if SDO_AG is LOW

// x - 0
// y - 1
// z - 2
#define FFA 1
#define RFA 2
#define UFA 0

#define GET_FFA(IMU) (FFA == 0 ? IMU.ax : (FFA == 1 ? IMU.ay : (FFA == 2 ? IMU.az : 0)))
#define GET_RFA(IMU) (RFA == 0 ? IMU.ax : (RFA == 1 ? IMU.ay : (RFA == 2 ? IMU.az : 0)))
#define GET_UFA(IMU) (UFA == 0 ? IMU.ax : (UFA == 1 ? IMU.ay : (UFA == 2 ? IMU.az : 0)))

#define GET_FFG(IMU) (FFA == 0 ? IMU.gx : (FFA == 1 ? IMU.gy : (FFA == 2 ? IMU.gz : 0)))
#define GET_RFG(IMU) (RFA == 0 ? IMU.gx : (RFA == 1 ? IMU.gy : (RFA == 2 ? IMU.gz : 0)))
#define GET_UFG(IMU) (UFA == 0 ? IMU.gx : (UFA == 1 ? IMU.gy : (UFA == 2 ? IMU.gz : 0)))

#define GET_FFM(IMU) (FFA == 0 ? IMU.mx : (FFA == 1 ? IMU.my : (FFA == 2 ? IMU.mz : 0)))
#define GET_RFM(IMU) (RFA == 0 ? IMU.mx : (RFA == 1 ? IMU.my : (RFA == 2 ? IMU.mz : 0)))
#define GET_UFM(IMU) (UFA == 0 ? IMU.mx : (UFA == 1 ? IMU.my : (UFA == 2 ? IMU.mz : 0)))

#define POSITION_MET 9
#define LED2 8
#define LED3 7 

#define PROGRAM_BUTTON 6
#define SAVE_STATE_BUTTON 5

struct Measurement{
  double front_facing;
  double right_facing;
  double up_facing;
};

enum PositionMatch{
  POSITION_MATCH,
  POSITION_NOT_MATCH,
  POSITION_UNSURE,
};

enum PositionType{
  POSITION_SITTING,
  POSITION_LAYING,
};


Measurement gyro_reference{0, 0, 0};

#define MEASUREMENT_CYCLES 1
Measurement takeMeasurementAccel(int measurement_cycles)
{  
  Measurement average {0, 0, 0};
  
  for(int i = 0; i < measurement_cycles; i++){
    if ( imu.accelAvailable() )
    {
      imu.readAccel();
    }
/*
    vector g_raw(imu.calcAccel(imu.ax), imu.calcAccel(imu.ay), imu.calcAccel(imu.az));
*/
    average.front_facing += imu.calcAccel(GET_FFA(imu));
    average.right_facing += imu.calcAccel(GET_RFA(imu));
    average.up_facing += imu.calcAccel(GET_UFA(imu));
  }

  average.front_facing /= measurement_cycles;
  average.right_facing /= measurement_cycles;
  average.up_facing /= measurement_cycles;

  return average;
}

Measurement takeMeasurementAccel(){
  return takeMeasurementAccel(MEASUREMENT_CYCLES);
}

Measurement takeMeasurementGyro(int measurement_cycles)
{  
  Measurement average {0, 0, 0};
  
  for(int i = 0; i < measurement_cycles; i++){
    if ( imu.gyroAvailable() )
    {
      imu.readGyro();
    }
/*
    vector g_raw(imu.calcAccel(imu.ax), imu.calcAccel(imu.ay), imu.calcAccel(imu.az));
*/
    average.front_facing += imu.calcGyro(GET_FFG(imu));
    average.right_facing += imu.calcGyro(GET_RFG(imu));
    average.up_facing += imu.calcGyro(GET_UFG(imu));
  }

  average.front_facing /= measurement_cycles;
  average.right_facing /= measurement_cycles;
  average.up_facing /= measurement_cycles;

  average.front_facing -= gyro_reference.front_facing;
  average.right_facing -= gyro_reference.right_facing;
  average.up_facing -= gyro_reference.up_facing;

  return average;
}

Measurement takeMeasurementGyro(){
  return takeMeasurementGyro(MEASUREMENT_CYCLES);
}

Measurement takeMeasurementMag(int measurement_cycles)
{  
  Measurement average {0, 0, 0};
  
  for(int i = 0; i < measurement_cycles; i++){
    if ( imu.magAvailable() )
    {
      imu.readMag();
    }
/*
    vector g_raw(imu.calcAccel(imu.ax), imu.calcAccel(imu.ay), imu.calcAccel(imu.az));
*/
    average.front_facing += imu.calcMag(GET_FFM(imu));
    average.right_facing += imu.calcMag(GET_RFM(imu));
    average.up_facing += imu.calcMag(GET_UFM(imu));
  }

  average.front_facing /= measurement_cycles;
  average.right_facing /= measurement_cycles;
  average.up_facing /= measurement_cycles;

  return average;
}

Measurement takeMeasurementMag(){
  return takeMeasurementMag(MEASUREMENT_CYCLES);
}

void calibrateGyro(){
  gyro_reference = takeMeasurementGyro(100);
}

#define JOYSTICK_BUTTON 6
#define JOYSTICK_HORIZONTAL A0
#define JOYSTICK_VERTICAL A1

//MUST BE ZERO INITIALIZED!!!

void setup() 
{
  lcd.init();
  lcd.backlight();
  
  pinMode(JOYSTICK_BUTTON, INPUT);
  pinMode(JOYSTICK_HORIZONTAL, INPUT);
  pinMode(JOYSTICK_VERTICAL, INPUT);
  
  Serial.begin(115200);
  Serial.setTimeout(0);
  
  // Before initializing the IMU, there are a few settings
  // we may need to adjust. Use the settings struct to set
  // the device's communication mode and addresses:
  imu.settings.device.commInterface = IMU_MODE_I2C;
  imu.settings.device.mAddress = LSM9DS1_M;
  imu.settings.device.agAddress = LSM9DS1_AG;
  // The above lines will only take effect AFTER calling
  // imu.begin(), which verifies communication with the IMU
  // and turns it on.
  if (!imu.begin())
  {
    Serial.println("Failed to communicate with LSM9DS1.");
    Serial.println("Double-check wiring.");
    Serial.println("Default settings in this sketch will " \
                  "work for an out of the box LSM9DS1 " \
                  "Breakout, but may need to be modified " \
                  "if the board jumpers are.");
    while (1)
      ;
  }
  //Wire.setClock(400000);
/*
  reference_chin = getDegreesChinTilt();
  reference_neck = getDegreesNeck();
  reference_spin = getMaxSpin();
*/
/*
  getChinTilt = &getDegreesChinTilt;
  getNeckTilt = &getDegreesNeck;
*/
/*
  getGyroData = &takeMeasurementGyro;
  getAccelData = &takeMeasurementAccel;
*/
  calibrateGyro();
}
/*
double getDegreesNeck(const Measurement& m){
  return atan2(m.right_facing, pow(pow(m.front_facing, 2) + pow(m.up_facing, 2), .5)) * 180.0 / M_PI;
}

double calcDegreesChinTilt(const Measurement& m){
  return atan2(m.up_facing, pow(pow(m.front_facing, 2) + pow(m.right_facing, 2), .5)) * 180.0 / M_PI;
}
double getDegreesNeck(){
  return getDegreesNeck(takeMeasurementAccel()) - reference_neck;
}

double getDegreesChinTilt(){
  return calcDegreesChinTilt(takeMeasurementAccel()) - reference_chin;
}
*/
double calcMaxSpin(const Measurement& m){
  if(m.front_facing > m.right_facing && m.front_facing > m.up_facing)
    return m.front_facing;
  else if(m.right_facing > m.front_facing && m.right_facing > m.up_facing)
    return m.right_facing;
  else if(m.up_facing > m.front_facing && m.up_facing > m.right_facing)
    return m.up_facing;
  else 
    return m.up_facing;
}

double getMaxSpin(){
  return calcMaxSpin(takeMeasurementGyro()) - calcMaxSpin(gyro_reference);
}

double compass(){
  Measurement m = takeMeasurementMag();
/*
  Serial.print(m.right_facing);
  Serial.print(", ");
  Serial.print(m.front_facing);
  Serial.println();
*/
  return atan2(m.right_facing, m.front_facing)*180/PI;
}

char digitToChar(int digit){
  switch(digit){
    case 0:
      return '0';
    case 1:
      return '1';
    case 2:
      return '2';
    case 3:
      return '3';
    case 4:
      return '4';
    case 5:
      return '5';
    case 6:
      return '6';
    case 7:
      return '7';
    case 8:
      return '8';
    case 9:
      return '9';    
  }
  return '\0';
}

void numToChars(char* buff, int number, int max_characters){
  int buffer_pointer = max_characters - 1;

  Serial.print("---> ");
  Serial.println(number);
  while(buffer_pointer >= 0 && number > 0){
    buff[buffer_pointer] = digitToChar(number % 10);
    Serial.print(buffer_pointer);
    Serial.print(" ");
    Serial.println(digitToChar(number % 10));
    number /= 10;

    buffer_pointer--;
  }
}

void writeStep(int num, int total){
  lcd.clear();
  lcd.setCursor(0, 0);
  
  char buff[4] = {' ', ' ', ' ', ' '};
  buff[3] = '\0';
  
  lcd.print("Step ");
  numToChars(buff, num, 2);  
  lcd.print(buff);
  lcd.print("/");
  numToChars(buff, total, 2);  
  lcd.print(buff);
}

void writeInstruction(const char* instruction1, const char* instruction2){
  lcd.setCursor(0, 1);
  lcd.print(instruction1);

  lcd.setCursor(0, 2);
  lcd.print(instruction2);
}

void writeUtility(const char* utility, int value){
  char buff[4] = {' ', ' ', ' ', ' '};
  buff[3] = '\0';

  lcd.setCursor(0, 3);
  lcd.print(utility);
  lcd.print(" ");
  numToChars(buff, value, 2);
  lcd.print(buff);
  
}

void writeError(const char* error1, const char* error2){
  lcd.clear();

  lcd.setCursor(0, 0);
  lcd.print("Error!");
  lcd.setCursor(0, 1);
  lcd.print(error1);
  lcd.setCursor(0, 2);
  lcd.print(error2);
}












#define NO_ACCEL_THRESHOLD .01
#define UPRIGHT_THRESHOLD .20

#define ALREADY_SITTING_UP_TOLERANCE .30

PositionMatch ensureSittingUp(){
    static bool was_sitting_up = false;
    static PositionMatch last_state = POSITION_UNSURE;
    
    Measurement a = takeMeasurementAccel();

    double tolerance = was_sitting_up ? ALREADY_SITTING_UP_TOLERANCE : UPRIGHT_THRESHOLD;
    
    if(fabs(sqrt(pow(a.front_facing, 2) + pow(a.right_facing, 2) + pow(a.up_facing, 2)) - 1.0) < NO_ACCEL_THRESHOLD){
      if(fabs(fabs(a.up_facing) - 1.0) < tolerance){
        was_sitting_up = true;
        return POSITION_MATCH;
      }else{
        return POSITION_NOT_MATCH;
      }
    }else{
      return last_state;
    }

    return POSITION_UNSURE;
}

#define LAYING_THRESHOLD .20
#define ALREADY_LAYING_TOLERANCE .30

PositionMatch ensureLaying(){
    static bool was_sitting_up = false;
    static PositionMatch last_state = POSITION_UNSURE;
    
    Measurement a = takeMeasurementAccel();

    double tolerance = was_sitting_up ? ALREADY_SITTING_UP_TOLERANCE : LAYING_THRESHOLD;
    
    if(fabs(sqrt(pow(a.front_facing, 2) + pow(a.right_facing, 2) + pow(a.up_facing, 2)) - 1.0) < NO_ACCEL_THRESHOLD){
      if(fabs(a.up_facing) < tolerance){
        was_sitting_up = true;
        return POSITION_MATCH;
      }else{
        return POSITION_NOT_MATCH;
      }
    }else{
      return last_state;
    }

    return POSITION_UNSURE;
}

bool sitUp(){

  bool success = false;

  while(!success){
    switch(ensureSittingUp()){
      case POSITION_UNSURE:
        break;
      case POSITION_NOT_MATCH:
        break;
      case POSITION_MATCH:
        success = true;
        break;
    }
  }

  return true;
}

#define WRONG_DIRECTION_THRESHOLD 10
#define GYROSCOPE_REJECTION_THRESHOLD 2
#define TURN_HEAD_SUCCESS_THRESHOLD 5
#define OVERSHOOT_THRESHOLD 10


bool turnHeadRight(int deg, PositionType ptype){

  bool success = false;

  maneuvers::Integrator integrate;

  double max_deviation = 0;

  while(!success){
    Measurement g = takeMeasurementGyro();
    if(fabs(g.up_facing) < GYROSCOPE_REJECTION_THRESHOLD)
      g.up_facing = 0;
    integrate.integrate(-g.up_facing); // Invert to get positive

    if(max_deviation - integrate.getResult() > WRONG_DIRECTION_THRESHOLD){
      writeError("You moved backwards", "");
      return false;
    }

    if(round(integrate.getResult()) > max_deviation){
      max_deviation = round(integrate.getResult());
      writeUtility("Degrees:", max_deviation);
    }

    //Serial.println(integrate.getResult());

    if(ptype == POSITION_SITTING){
      switch(ensureSittingUp()){
        case POSITION_UNSURE:
          break;
        case POSITION_NOT_MATCH:
          writeError("You did not remain", "sitting up");
          return false;
        case POSITION_MATCH:
          break;
      }
    }else if(ptype == POSITION_LAYING){
       switch(ensureLaying()){
        case POSITION_UNSURE:
          break;
        case POSITION_NOT_MATCH:
          writeError("You did not remain", "laying");
          return false;
        case POSITION_MATCH:
          break;
      }
    }

    if(integrate.getResult() > deg){
      if(integrate.getResult() > deg + OVERSHOOT_THRESHOLD){
        writeError("You overshot the mark", "");
        return false;
      }

      success = true;
    }
  }

  return true;
}

bool turnHeadLeft(int deg, PositionType ptype){

  bool success = false;

  maneuvers::Integrator integrate;

  double max_deviation = 0;

  while(!success){
    Measurement g = takeMeasurementGyro();
    if(fabs(g.up_facing) < GYROSCOPE_REJECTION_THRESHOLD)
      g.up_facing = 0;
    integrate.integrate(g.up_facing); // Dont invert to get positive

    if(max_deviation - integrate.getResult() > WRONG_DIRECTION_THRESHOLD){
      writeError("You moved backwards", "");
      return false;
    }

    if(round(integrate.getResult()) > max_deviation){
      max_deviation = round(integrate.getResult());
      writeUtility("Degrees:", max_deviation);
    }

    //Serial.println(integrate.getResult());
    if(ptype == POSITION_SITTING){
      switch(ensureSittingUp()){
        case POSITION_UNSURE:
          break;
        case POSITION_NOT_MATCH:
          writeError("You did not remain", "sitting up");
          return false;
        case POSITION_MATCH:
          break;
      }
    }else if(ptype == POSITION_LAYING){
       switch(ensureLaying()){
        case POSITION_UNSURE:
          break;
        case POSITION_NOT_MATCH:
          writeError("You did not remain", "laying");
          return false;
        case POSITION_MATCH:
          break;
      }
    }

    if(integrate.getResult() > deg){
      if(integrate.getResult() > deg + OVERSHOOT_THRESHOLD){
        writeError("You overshot the mark", "");
        return false;
      }

      success = true;
    }
  }

  return true;
}

//60.64,2.77,59.42
//43.22,8.58,74.33
//Moral of the story is that there is minimal rotation around the up facing axis

#define NECK_ROTATION_LAYBACK_THRESHOLD 20
#define LAYBACK_ACCEL_THRESHOLD .20
#define MIN_LAYBACK_DPS_THRESHOLD 10

bool layBack(){
  
  maneuvers::Integrator integrateUp;

  bool success = false;

  bool started_movement = false;

  while(!success){
    Measurement g = takeMeasurementGyro();

    integrateUp.integrate(g.up_facing);
    
    if(!started_movement && ensureSittingUp() == POSITION_NOT_MATCH)
      started_movement = true;

    if(started_movement && sqrt(pow(g.front_facing, 2) + pow(g.right_facing, 2)) < MIN_LAYBACK_DPS_THRESHOLD){
      writeError("Too slow", "");
      return false;
    }

    if(fabs(integrateUp.getResult()) > NECK_ROTATION_LAYBACK_THRESHOLD){
      writeError("You rotated your neck", "");
      return false;
    }

    Measurement a = takeMeasurementAccel();

    if(started_movement && (fabs(a.up_facing) < LAYBACK_ACCEL_THRESHOLD) && (fabs(fabs(a.right_facing) - fabs(a.front_facing))) < LAYBACK_ACCEL_THRESHOLD){
      success = true;
    }
  }

  return true;
}

bool layUp(){

    maneuvers::Integrator integrateUp;

  bool success = false;

  bool started_movement = false;

  while(!success){
    Measurement g = takeMeasurementGyro();

    integrateUp.integrate(g.up_facing);
    
    if(!started_movement && ensureLaying() == POSITION_NOT_MATCH)
      started_movement = true;

    if(started_movement && sqrt(pow(g.front_facing, 2) + pow(g.right_facing, 2)) < MIN_LAYBACK_DPS_THRESHOLD){
      writeError("Too slow", "");
      return false;
    }

    if(fabs(integrateUp.getResult()) > NECK_ROTATION_LAYBACK_THRESHOLD){
      writeError("You rotated your neck", "");
      return false;
    }

    Measurement a = takeMeasurementAccel();

    if(ensureSittingUp() == POSITION_MATCH){
      success = true;
    }
  }

  return true;
}

#define LAY_AND_WAIT_TIME 3
#define LAY_AND_WAIT_GYRO_MOVE_THRESHOLD 20

bool layAndWait(){

  bool waited = false;

  maneuvers::Integrator integrate_up;
  maneuvers::Integrator integrate_right;
  maneuvers::Integrator integrate_front;

  unsigned long start_time = millis();
  unsigned long last_time = start_time;
  while(!waited){
    if(millis() - start_time > LAY_AND_WAIT_TIME*1000)
      waited = true;

    if(millis() - last_time > 1000){
      last_time = millis();
      writeUtility("Timer:", (millis() - start_time) / 1000);
    }

    Measurement a = takeMeasurementAccel();
/*
    if(!((fabs(a.up_facing) < LAYBACK_ACCEL_THRESHOLD) && (fabs(fabs(a.right_facing) - fabs(a.front_facing))) < LAYBACK_ACCEL_THRESHOLD) && fabs(sqrt(pow(a.front_facing, 2) + pow(a.right_facing, 2) + pow(a.up_facing, 2)) - 1.0) < NO_ACCEL_THRESHOLD){
      MESSAGE("Error! You moved your head!");
      return false;
    }
*/
    Measurement g = takeMeasurementGyro();

    integrate_up.integrate(g.up_facing);
    integrate_right.integrate(g.right_facing);
    integrate_front.integrate(g.front_facing);

    if(fabs(integrate_up.getResult()) > LAY_AND_WAIT_GYRO_MOVE_THRESHOLD || 
        fabs(integrate_right.getResult()) > LAY_AND_WAIT_GYRO_MOVE_THRESHOLD || 
        fabs(integrate_front.getResult()) > LAY_AND_WAIT_GYRO_MOVE_THRESHOLD){
          writeError("You moved", "");   
          return false;     
        }
  }

  return true;
}

bool rightEar(){
  writeStep(1, 9);
  writeInstruction("Sit up", "");
  if(!sitUp())
    return false;
  writeStep(2, 9);
  writeInstruction("Turn your head 45", "degrees to the right");
  if(!turnHeadRight(45, POSITION_SITTING))
    return false;
  writeStep(3, 9);
  writeInstruction("Lay back without", "turning your head");
  if(!layBack())
    return false;
  writeStep(4, 9);
  writeInstruction("Stay still for ", "30 seconds");
  if(!layAndWait())
    return false;
  writeStep(5, 9);
  writeInstruction("Turn your head", "left 90 degrees");
  if(!turnHeadLeft(90, POSITION_LAYING))
    return false;
  writeStep(6, 9);
  writeInstruction("Stay still for ", "30 seconds");
  if(!layAndWait())
    return false;
  writeStep(7, 9);
  writeInstruction("Turn your head", "left 90 degrees");
  if(!turnHeadLeft(90, POSITION_LAYING))
    return false;
  writeStep(8, 9);
  writeInstruction("Stay still for ", "30 seconds");
  if(!layAndWait())
    return false;
  writeStep(9, 9);
  writeInstruction("Sit up without", "rotating your head");
  if(!layUp())
    return false;
}

bool leftEar(){
  writeStep(1, 9);
  writeInstruction("Sit up", "");
  if(!sitUp())
    return false;
  writeStep(2, 9);
  writeInstruction("Turn your head 45", "degrees to the left");
  if(!turnHeadLeft(45, POSITION_SITTING))
    return false;
  writeStep(3, 9);
  writeInstruction("Lay back without", "turning your head");
  if(!layBack())
    return false;
  writeStep(4, 9);
  writeInstruction("Stay still for ", "30 seconds");
  if(!layAndWait())
    return false;
  writeStep(5, 9);
  writeInstruction("Turn your head", "right 90 degrees");
  if(!turnHeadRight(90, POSITION_LAYING))
    return false;
  writeStep(6, 9);
  writeInstruction("Stay still for ", "30 seconds");
  if(!layAndWait())
    return false;
  writeStep(7, 9);
  writeInstruction("Turn your head", "right 90 degrees");
  if(!turnHeadRight(90, POSITION_LAYING))
    return false;
  writeStep(8, 9);
  writeInstruction("Stay still for ", "30 seconds");
  if(!layAndWait())
    return false;
  writeStep(9, 9);
  writeInstruction("Sit up without", "rotating your head");
  if(!layUp())
    return false;
}

void writeMenu(char* first, char* second, char* third, char* fourth, int sel){
  unsigned long t = millis() / 1000;
  
  if(t % 2){
    switch(sel){
      case 1:
        first[0] = '\0';
        break;
      case 2:
        second[0] = '\0';
        break;
      case 3:
        third[0] = '\0';
        break;
      case 4:
        fourth[0] = '\0';
        break;
    }
  }

    lcd.setCursor(0, 0);
    lcd.print(first);
    lcd.setCursor(0, 1);
    lcd.print(second);
    lcd.setCursor(0, 2);
    lcd.print(third);
    lcd.setCursor(0, 3);
    lcd.print(fourth);
}

void printMenu(){
  lcd.clear();

  lcd.setCursor(0, 0);
  lcd.print("Select Ear:");
  lcd.setCursor(0, 1);
  lcd.print(" Left Ear");
  lcd.setCursor(0, 2);
  lcd.print(" Right Ear");
  
}

void blinkMenu(int sel){
  static unsigned long last = 0;
  static bool state = false;

  if((bool)(((millis() - last) / 500) % 2)){
    last = millis();
    state = !state;
        
    switch(sel){
      case 1:
        lcd.setCursor(0, 1);
        if(state){
          lcd.print("         ");
        }else{
          lcd.print(" Left Ear");
        }
        lcd.setCursor(0, 2);
        lcd.print(" Right Ear");
        break;
      case 2:
        lcd.setCursor(0, 2);
        if(state){
          lcd.print("          ");
        }else{
          lcd.print(" Right Ear");
        }
        lcd.setCursor(0, 1);
        lcd.print(" Left Ear");
        break;
    }
  }

}

int mainMenuSelect(int sel){
  if(analogRead(JOYSTICK_VERTICAL) < 400)
    return 2;
  else if(analogRead(JOYSTICK_VERTICAL) > 600)
    return 1;
  else
    return sel;
}

void mainMenu(){

  printMenu();
  
  static int selection = 1;

  while(digitalRead(JOYSTICK_BUTTON) == HIGH){
    blinkMenu(selection);
    selection = mainMenuSelect(selection);
  }

  switch(selection){
    case 1:
      leftEar();
      break;
    case 2:
      rightEar();
      break;
  }
}

void loop()
{
  
  mainMenu();
}
Integrator.hC/C++
This code provides basic integration of gyro data to derive the total number of degrees turned. This is helper file that was written separately.
#ifndef INTEGRATOR_H
#define INTEGRATOR_H

#include <Arduino.h>
#include "Utility.h"

namespace maneuvers{
    
    class Integrator{
        public:
            Integrator();
            
            void integrate(double dy);
            double getResult() const;
            void set(double y);
        protected:
        private:
            double state;
            unsigned long last_time;
            bool primed;
    };
    
    struct GyroIntegrators{
        Integrator chin;
        Integrator side;
        Integrator neck;
    };
    
    extern GyroIntegrators gyroIntegrators;
    
    extern void updateGyroPositions();
    extern double getChin();
    extern double getNeck();
    extern double getSide();

}

#endif
Integrator.cppC/C++
This code provides basic integration of gyro data to derive the total number of degrees turned. This is helper file that was written separately.
#include "Integrator.h"

#pragma GCC optimize ("O3")

namespace maneuvers{

Integrator::Integrator() : state(0), last_time(0), primed(false) { }

void Integrator::integrate(double dy){
    unsigned long current = micros();
    double difference = (current - last_time) / 1000000.0;
    
    if(primed){
        state += (dy*difference);
    }else{
        primed = true;
    }
    last_time = current;

}

double Integrator::getResult() const{
    return state;
}

void Integrator::set(double y){
    state = y;
}

GyroIntegrators gyroIntegrators;

#define GYRO_ZERO_ACCEL_TOLERANCE .05
#define GYRO_CUTOFFS 5

double adjustUsingAccel(double current, double target){
    double remainder = fmod(current - target, 360);
    if(current >= 0){
      remainder = fabs(remainder) > 180 ? -(360 - remainder) : remainder;
    }else{
      remainder = fabs(remainder) > 180 ? (360 + remainder) : remainder;
    }
    return current - remainder;
}

double getNeck(){
    return gyroIntegrators.neck.getResult();
}

double getChin(){
    return gyroIntegrators.chin.getResult();
}

double getSide(){
    return gyroIntegrators.side.getResult();
}

void updateGyroPositions(){
    Measurement mg = (*getGyroData)();
    
    if(fabs(mg.front_facing) < GYRO_CUTOFFS)
        mg.front_facing = 0;
    if(fabs(mg.right_facing) < GYRO_CUTOFFS)
        mg.right_facing = 0;
    if(fabs(mg.up_facing) < GYRO_CUTOFFS)
        mg.up_facing = 0;
        
    gyroIntegrators.side.integrate(mg.front_facing);
    gyroIntegrators.neck.integrate(mg.up_facing);
    gyroIntegrators.chin.integrate(mg.right_facing);
    
    Measurement ma = (*getAccelData)();
    
    static uint8_t zero_counter = 0;
    
    if(zero_counter > 10){
        if(fabs(sqrt(pow(ma.front_facing, 2) + pow(ma.up_facing, 2) + pow(ma.right_facing, 2)) - 1.0) < GYRO_ZERO_ACCEL_TOLERANCE){
            // If an axis reads near 1.0, that means we can zero
            // Remember that a positive G vector registers opposite on an axis (i.e. x axis is collinear with G, it registers negative)
            if(fabs(fabs(ma.front_facing) - 1.0) < GYRO_ZERO_ACCEL_TOLERANCE){
                if(ma.front_facing < 0){ //-G is in direction of front facing axis, so they are laying face down
                    //Serial.println("User is laying face down");
                    // Modify the right facing integrator to be -90
                    gyroIntegrators.neck.set(adjustUsingAccel(getNeck(), 0));
                    gyroIntegrators.chin.set(adjustUsingAccel(getChin(), -90));
                }else{ //-G is in opposite direction of front facing axis, so they are laying on their back
                    //Serial.println("User is laying on their back");
                    // Modify the right facing integrator to be 90
                    gyroIntegrators.neck.set(adjustUsingAccel(getNeck(), 0));
                    gyroIntegrators.chin.set(adjustUsingAccel(getChin(), 90));
                }
            }else if(fabs(fabs(ma.right_facing) - 1.0) < GYRO_ZERO_ACCEL_TOLERANCE){
                if(ma.right_facing < 0){ //-G is in direction of right facing axis, so they are laying on their right side
                    //Serial.println("User is laying on right side");
                    //90
                    gyroIntegrators.neck.set(adjustUsingAccel(getNeck(), 0));
                    gyroIntegrators.side.set(adjustUsingAccel(getSide(), 90));
                }else{ //-G is opposite to the right facing axis, so they are laying on their left side
                    //Serial.println("User is laying on left side");
                    //-90
                    gyroIntegrators.neck.set(adjustUsingAccel(getNeck(), 0));
                    gyroIntegrators.side.set(adjustUsingAccel(getSide(), -90));
                }
            }else if(fabs(fabs(ma.up_facing) - 1.0) < GYRO_ZERO_ACCEL_TOLERANCE){
                if(ma.up_facing < 0){ //User is literallly upside down, so -G is pointing in the direction of the front facing axis
                    //Serial.println("User is literally upside down");
                    gyroIntegrators.side.set(adjustUsingAccel(getSide(), 180));
                    gyroIntegrators.chin.set(adjustUsingAccel(getChin(), 180));
                }else{ //User is sitting up, so -G is pointing opposite of the up facing axis
                    //Serial.println("User is sitting up");
                    gyroIntegrators.side.set(adjustUsingAccel(getSide(), 0));
                    gyroIntegrators.chin.set(adjustUsingAccel(getChin(), 0));
                }
            }
        }
        
        zero_counter = 0;
    }else{
        zero_counter++;
    }
    
    
}

#undef GYRO_ZERO_ACCEL_TOLERANCE

}
Eyewriter Code
This is used in conjunction with the Eyewriter hardware. Credit to itotaka from github.

Custom parts and enclosures

Top Lid
Central Console Top Enclosure
Ear Piece 1
Ear Piece 2
Console Housing

Schematics

Eyewriter Schematic
From the Instructables Eyewriter 2.0, we obtained and used this schematic along with the one from Hikari Blogspot to build our eye tracker.
Ew hwfullschematic q1ijvcexd3
Maneuvers Schematic
This is the schematic for the device that ensures movements are carried out properly.
Eyetracking Schematic
Includes the PSEye, the ring of infrared LEDs, and the Arduino. This was taken from http://hikarielectronics.blogspot.com/ which details how to build the Eyewriter 2.0 with the PSEye camera.
Kairosu ft82g3bxnd

Comments

Similar projects you might like

DIY 7-Segment Display

Project tutorial by Pop Gheorghe

  • 1,480 views
  • 2 comments
  • 1 respect

Smart Soft Switch

Project tutorial by Vishwas Navada

  • 579 views
  • 0 comments
  • 3 respects

Marduino Party 1

Project tutorial by Arduino “having11” Guy

  • 1,455 views
  • 0 comments
  • 1 respect

Binary Wristwatch

Project showcase by thallia

  • 1,307 views
  • 0 comments
  • 11 respects

Doggo Water Bowl Refill Monitor/Indicator - Part 1

Project tutorial by HeathenHacks

  • 995 views
  • 2 comments
  • 7 respects

Dual Axis Solar Tracker Panel with Auto and Manual Mode

Project tutorial by Giannis Arvanitakis

  • 12,558 views
  • 9 comments
  • 52 respects
Add projectSign up / Login