// BMI - Arduino
// v1.0

// LIBRARIES

#include "HX711.h"
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
#include <SPI.h>
#include <SD.h>
#include <Adafruit_MLX90614.h>

// CONSTANTS

const float LOAD_CELL_CALIBRATION_FACTOR = 0.1;
const float US_HEIGHT = 180.00; // 180cm
const unsigned long BLINK_DELAY = 500; // 500ms

const float TRIGGER_WEIGHT = 10.00; // 10kg
const float TRIGGER_HEIGHT = 30.00; // 30cm
const float TRIGGER_TEMPERATURE = 32.00; // 32C
const unsigned long WEIGHT_DELAY = 3000; // 3seconds
const unsigned long HEIGHT_DELAY = 3000; // 3seconds
const unsigned long TEMPERATURE_DELAY = 3000; // 3seconds
const unsigned long SENSOR_DELAY = 500; // 500milliseconds

const byte KEYPAD_ROWS = 4;
const byte KEYPAD_COLS = 4;
const char KEYPAD_KEYS[KEYPAD_ROWS][KEYPAD_COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};

const int LOAD_CELL_DATA_PIN = 6;
const int LOAD_CELL_CLOCK_PIN = 7;
const byte KEYPAD_ROW_PINS[KEYPAD_ROWS] = {5, 4, 3, 2};
const byte KEYPAD_COL_PINS[KEYPAD_COLS] = {11, 10, 9, 8};
const int US_TRIG_PIN = 26;
const int US_ECHO_PIN = 27;
const int SD_CS_PIN = 53;

const int SCREEN_WEIGHT = 1;
const int SCREEN_ID = 2;
const int SCREEN_HEIGHT = 3;
const int SCREEN_MLX = 4;
const int SCREEN_SAVE = 5;

// VARIABLES

HX711 scale;
LiquidCrystal_I2C lcd(0x27, 20, 4);
Keypad keypad = Keypad( makeKeymap(KEYPAD_KEYS), KEYPAD_ROW_PINS, KEYPAD_COL_PINS, KEYPAD_ROWS, KEYPAD_COLS );
Adafruit_MLX90614 mlx = Adafruit_MLX90614();
File myFile;

float weight = 0.00;
float height = 0.00;
float temperature = 0.00;
float bmi = 0.00;
char id[16] = "";
String bmiString = "";
String bmiStringShortened = "";
String dataString = "";

bool isDisplayed = false;
int screen = 0;

// SETUP

void setup() {
  pinMode(US_TRIG_PIN, OUTPUT);
  pinMode(US_ECHO_PIN, INPUT);
  Serial.begin(115200);

  initDataOnSD();

  //initMLX();

  scale.begin(LOAD_CELL_DATA_PIN, LOAD_CELL_CLOCK_PIN);
  scale.set_scale(LOAD_CELL_CALIBRATION_FACTOR);
  scale.tare(20);

  lcd.init();
  lcd.backlight();

  displayTitle();
  screen = SCREEN_WEIGHT;
}

// LOOP

void loop() {
  weightRoutine();
  idRoutine();
  heightRoutine();
  mlxRoutine();
  saveRoutine();
}

// FUNCTIONS

void displayTitle() {
  lcd.clear();
  lcd.setCursor(0 , 0);
  lcd.print("Automated BMI");
  lcd.setCursor(0 , 1);
  lcd.print("Calculator");
  lcd.setCursor(0 , 2);
  lcd.print("with");
  lcd.setCursor(0 , 3);
  lcd.print("Thermal Scanner");
  delay(3000);
}

void weightRoutine() {
  
  if (screen == SCREEN_WEIGHT) {
    static unsigned long measureTime = 0;
    static bool triggered = false;

    if (!isDisplayed) {
      isDisplayed = true;

      triggered = false;
      weight = 0;

      lcd.clear();
      lcd.setCursor(0 , 0);
      lcd.print("Weight: ");
    }

    if (scale.is_ready()) {
      weight = getWeight();
      displayWeight(weight);
    }

    if (weight >= TRIGGER_WEIGHT && !triggered) {
      measureTime = millis();
      triggered = true;
    }

    if (triggered) {
      if (millis() - measureTime >= WEIGHT_DELAY) {
        blinkWeightDisplay(weight);

        screen = SCREEN_ID;
        isDisplayed = false;
      }
    }
  }
}

void idRoutine() {
  if (screen == SCREEN_ID) {
    static int idCursor = 0;

    if (!isDisplayed) {
      isDisplayed = true;

      idCursor = 0;

      lcd.setCursor(0, 1);
      lcd.print("ID:                 ");
    }

    char key = keypad.getKey();

    if (key) {
      Serial.println(key);

      if (key != '*' &&
          key != '#'  &&
          key != 'A' &&
          key != 'B' &&
          key != 'C' &&
          key != 'D') {
        if (idCursor + 4 <= 19) {
          lcd.setCursor(idCursor + 4, 1);
          lcd.print(key);
          id[idCursor] = key;
          idCursor++;
        }
      } else if (key == '#') {
        id[idCursor] = '\0';

        screen = SCREEN_HEIGHT;
      } else if (key == '*') {
        screen = SCREEN_ID;
      }

      if (key == '#' || key == '*') {
        isDisplayed = false;
      }
    }
  }
}

void heightRoutine() {
  if (screen == SCREEN_HEIGHT) {
    static unsigned long measureTime = millis();
    static unsigned long readTime = millis();
    static bool triggered = false;

    if (!isDisplayed) {
      isDisplayed = true;

      triggered = false;
      height = 0;

      lcd.setCursor(0, 2);
      lcd.print("Height: ");
    }

    
    if (millis() - readTime >= SENSOR_DELAY) {
      readTime = millis();
      height = getHeight();
      displayHeight(height);
    }

    if (height >= TRIGGER_HEIGHT && !triggered) {
        measureTime = millis();
        triggered = true;
    }

    if (triggered) {
      if (millis() - measureTime >= HEIGHT_DELAY) {
        blinkHeightDisplay(height);

        screen = SCREEN_MLX;
        isDisplayed = false;
      }
    }
  }
}

void mlxRoutine() {
  if (screen == SCREEN_MLX) {
    static unsigned long measureTime = millis();
    static unsigned long readTime = millis();
    static bool triggered = false;

    if (!isDisplayed) {
      isDisplayed = true;

      triggered = false;
      temperature = 0;

      lcd.setCursor(0, 3);
      lcd.print("Temp: ");
    }

    if (millis() - readTime >= SENSOR_DELAY) {
      readTime = millis();
      temperature = getTemperature();
      displayTemperature(temperature);
    }

    if (temperature >= TRIGGER_TEMPERATURE && !triggered) {
        measureTime = millis();
        triggered = true;
    }

    if (triggered) {
      if (millis() - measureTime >= TEMPERATURE_DELAY) {
        blinkTemperatureDisplay(temperature);

        screen = SCREEN_SAVE;
        isDisplayed = false;
      }
    }
  }
}

void saveRoutine() {
  if (screen == SCREEN_SAVE) {
    char key = keypad.getKey();

    if (key) {
      Serial.println(key);
      if (key == '#') {
        Serial.println("Saved! Clear device!");

        bmi = getBMI();
        bmiString = getConvertedBMI(bmi);
        bmiStringShortened = getConvertedBMIShortened(bmi);

        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Weight: ");
        lcd.print(weight, 2);
        lcd.print("kg");
        lcd.setCursor(0, 1);
        lcd.print("Height: ");
        lcd.print(height, 2);
        lcd.print("cm");
        lcd.setCursor(0, 2);
        lcd.print("Temp: ");
        lcd.print(temperature, 2);
        lcd.print("*C");
        lcd.setCursor(0, 3);
        lcd.print("BMI: ");
        lcd.print(bmi, 2);
        lcd.print(" ");
        lcd.print(bmiStringShortened);


        processData();
        saveDataToSD(dataString);

        delay(10000);

        screen = SCREEN_WEIGHT;
        isDisplayed = false;
      }
    }
  }
}

float getWeight() {
  float reading = scale.get_units(1) / 1000.00;

  if (reading < 0) {
    reading = 0.00;
  }

  Serial.print("Weight: ");
  Serial.println(reading);

  return reading;
}

void displayWeight(float w) {
  lcd.setCursor(8, 0);
  lcd.print(w, 2);
  lcd.print("kg  ");
}

void blinkWeightDisplay(float w) {
  lcd.setCursor(8, 0);
  lcd.print("        ");
  delay(BLINK_DELAY);
  displayWeight(w);
  delay(BLINK_DELAY);
  lcd.setCursor(8, 0);
  lcd.print("        ");
  delay(BLINK_DELAY);
  displayWeight(w);
  delay(BLINK_DELAY);
}

float getDistance() {
  digitalWrite(US_TRIG_PIN, LOW);
  delayMicroseconds(2);

  digitalWrite(US_TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(US_TRIG_PIN, LOW);

  long duration = pulseIn(US_ECHO_PIN, HIGH);

  float distance = duration * 0.034 / 2;

  Serial.print("Distance: ");
  Serial.println(distance);
  return distance;
}

float getHeight() {
  float distance = getDistance();
  float reading = US_HEIGHT - distance;

  if (reading < 0) {
    reading = 0.00;
  }

  Serial.print("Height: ");
  Serial.println(reading);

  return reading;
}

void displayHeight(float h) {
  lcd.setCursor(8, 2);
  lcd.print(h, 2);
  lcd.print("cm  ");
}

void blinkHeightDisplay(float h) {
  lcd.setCursor(8, 2);
  lcd.print("        ");
  delay(BLINK_DELAY);
  displayHeight(h);
  delay(BLINK_DELAY);
  lcd.setCursor(8, 2);
  lcd.print("        ");
  delay(BLINK_DELAY);
  displayHeight(h);
  delay(BLINK_DELAY);
}

float getMLXTest() {
  int reading = analogRead(A0);
  return map(reading, 0, 1023, 0, 100);
}

float getTemperature() {
  float reading = getMLXTest();
  //float reading = mlx.readObjectTempC();

  if (reading < 0) {
    reading = 0.00;
  }

  Serial.print("Temperature: ");
  Serial.println(reading);

  return reading;
}

void displayTemperature(float t) {
  lcd.setCursor(6, 3);
  lcd.print(t, 2);
  lcd.print("*C  ");
}

void blinkTemperatureDisplay(float t) {
  lcd.setCursor(6, 3);
  lcd.print("        ");
  delay(BLINK_DELAY);
  displayTemperature(t);
  delay(BLINK_DELAY);
  lcd.setCursor(6, 3);
  lcd.print("        ");
  delay(BLINK_DELAY);
  displayTemperature(t);
  delay(BLINK_DELAY);
}

void processData() {
  dataString = "";
  dataString += (String) id;
  dataString += ",";
  dataString += (String) weight;
  dataString += ",";
  dataString += (String) height;
  dataString += ",";
  dataString += (String) temperature;
  dataString += ",";
  dataString += (String) bmi;
  dataString += ",";
  dataString += bmiString;
  dataString += "\n";
}

void initDataOnSD() {
  Serial.print("Initializing SD card...");

  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("initialization failed!");
    while (1);
  }
  Serial.println("initialization done.");

  if (SD.exists("datalog.txt")) {
    Serial.println("datalog.txt exists.");
  } else {
    Serial.println("datalog.txt doesn't exist.");
    // open a new file and immediately close it:
    Serial.println("Creating datalog.txt...");
    myFile = SD.open("datalog.txt", FILE_WRITE);
    myFile.close();
  }
}

void saveDataToSD(String data) {
  // open the file. note that only one file can be open at a time,
  // so you have to close this one before opening another.
  myFile = SD.open("datalog.txt", O_APPEND);

  // if the file is available, write to it:
  if (myFile) {
    myFile.println(data);
    myFile.close();
    // print to the serial port too:
    Serial.println(data);
  }
  // if the file isn't open, pop up an error:
  else {
    Serial.println("error opening datalog.txt");
  }
}

void initMLX() {
  if (!mlx.begin()) {
    Serial.println("Error connecting to MLX sensor. Check wiring.");
    while (1);
  };
}

float getBMI() {
  float hInM = height/100.00;
  float output = weight/(hInM*hInM);
  Serial.print("BMI: ");
  Serial.println(output);
  return output;
}

String getConvertedBMI(float b) {
  String bs = "";
  if (b < 18.5) {
    bs = "Underweight";
  } else if (b >= 18.5 && b < 25) {
    bs = "Normal";
  } else if (b >= 25 && b < 30) {
    bs = "Overweight";
  } else {
    bs = "Very Overweight";
  }
  
  Serial.print("BMI String: ");
  Serial.println(bs);
  return bs;
}

String getConvertedBMIShortened(float b) {
  String bs = "";
  if (b < 18.5) {
    bs = "UNDRWGT";
  } else if (b >= 18.5 && b < 25) {
    bs = "NORMAL";
  } else if (b >= 25 && b < 30) {
    bs = "OVRWGT";
  } else {
    bs = "VOVRWGT";
  }
  
  Serial.print("BMI String Shortened: ");
  Serial.println(bs);
  return bs;
}