//  Last Update:  2/24/17
//
//  Walkabout v1
//
//  1) Moteino Mega (w/4MB on-board flash)
//  2) RFM69W RF radio (SPI, CS=10)
//  3) Adafruit GPS (Serial1, RX=11, TX=10)
//  4) Battery monitor - A7
//  5) SSD1306 OLED - (I2C, 0x3C)
//  6) SD Card - (SPI, CS=3)
//  7) Battery charger
//  8) 3.7V - 1200mAh LIPO 
//
///////////////////////////////////////////////////////
//           Includes                                 /
///////////////////////////////////////////////////////
//
#include <SPI.h>
#include <SPIFlash.h>
#include <Wire.h>
#include <RFM69.h>
#include <RFM69_OTA.h>
#include <Adafruit_GPS.h>
#include <SoftwareSerial.h>
#include <MicroLCD.h>
#include <SD.h>
#include "Moving_Average.h"
#include "Walkabout_sd_dump.h"

///////////////////////////////////////////////////////
//           setup                                    /
///////////////////////////////////////////////////////
//
void setup(void)
{
  float tempFloat;
  
#ifdef DEBUG
  Serial.begin(115200);
#endif

  pinMode(ONBOARDLED, OUTPUT);
  setupLCD();
  setupWirelessRadio();
  initFlash();
  setupGPS();
  setupSDCardReader();
  tempFloat = computeVolts();
  lcd.print("Volts: ");
  lcd.println(tempFloat);
  lcd.println();
  lcd.println("Setup complete.");

#ifdef DEBUG
  Serial.println(F("Setup complete."));
  Serial.flush();
#endif

  blinkOnBoardLED(5);
  lcd.clear();
}

///////////////////////////////////////////////////////
//           loop                                     /
///////////////////////////////////////////////////////
//
void loop(void)
{
  String logString = "",
         timeDateString = "";
  TelemData xmitData;
  bool xmitOK;
  float tempFloat;
  WData rcvData;

  if (radio.receiveDone())
  {
    CheckForWirelessHEX(radio, flash, false);                             // Check for OTA update

    if (radio.ACKRequested())                                             // Send ACK if necessary
      radio.sendACK();
      
    if (radio.SENDERID == CLIENTID)                                       // Simple security check to verify sender ID
    {
      rcvData = *(WData*) radio.DATA;
      if (rcvData.data.magicCookie == 2112)                               // Simple check to verify integrity of incoming request
      {
        switch (rcvData.data.cmd) 
        {
          case DUMPFILE:
#ifdef DEBUG
            Serial.println(F("Dump file requested."));
#endif
            dumpSDFile();                                                 // Transmit copy of SD card file
            break;
         case DELFILE:
#ifdef DEBUG
            Serial.println(F("Delete file requested."));
#endif
            deleteSDFile();                                               // Delete log file
            break;
          default:
#ifdef DEBUG
            Serial.print(F("Unknown cmd = "));
            Serial.println(rcvData.data.cmd);
#endif
            break;
        }
      }
    }
  }

  // if a sentence is received, we can check the checksum, parse it...
  if (GPS.newNMEAreceived()) 
  {
    if (!GPS.parse(GPS.lastNMEA()))                                       // this also sets the newNMEAreceived() flag to false
      return;                                                             // we can fail to parse a sentence in which case we should just wait for another
  }
  
  if (millis() - timer > XMITFREQ)
  { 
    // Proceed if we have a GPS fix
    if (GPS.fix)
    {
      correctDaylightSavingsTime();                                       // Correct time for daylight savings

      // Start filling in packet fields
      xmitData.month = GPS.month;
      xmitData.day = GPS.day;
      xmitData.year = GPS.year + 2000;
      xmitData.hour = adjHour;
      xmitData.minute = GPS.minute;
      xmitData.seconds = GPS.seconds;
      
      if (!validTimeStamp)
      {
        gYear = GPS.year + 2000;
        gMonth = GPS.month;
        gDay = GPS.day;
        gHour = adjHour;
        gMinute = GPS.minute;
        gSeconds = GPS.seconds;
        validTimeStamp = true;  
      }
    
      // Create time/date string
      timeDateString += String(GPS.month);
      timeDateString += "/";
      timeDateString += String(GPS.day);
      timeDateString += "/";
      timeDateString += String(GPS.year);
      timeDateString += " ";
      if (adjHour < 10)
        timeDateString += "0";
      timeDateString += String(adjHour);
      timeDateString += ":";
      if (GPS.minute < 10)
        timeDateString += "0";
      timeDateString += String(GPS.minute);
      timeDateString += ":";
      if (GPS.seconds < 10)
        timeDateString += "0";
      timeDateString += String(GPS.seconds);

      // Compute moving average of speed and altitude
      sAvg = speedAverage.add((float) GPS.speed * KNOTSMPH);
      aAvg = altAverage.add((float) GPS.altitude * METERSFEET);  

      // Write data to LCD
      lcd.setCursor(0, 0);
      lcd.print("LAT:  ");
      lcd.println(GPS.latitudeDegrees, 8);
      lcd.print("LON: ");
      lcd.println(GPS.longitudeDegrees, 8);
      lcd.print("SPD: ");
      lcd.print(sAvg, 0);
      lcd.println(" ");
      lcd.print("ALT: ");
      lcd.println(aAvg, 0);
      lcd.print("SAT: ");
      lcd.print(GPS.satellites);
      lcd.println(" ");
      tempFloat = computeVolts();
      lcd.print("BAT: ");
      lcd.print(tempFloat);
      if (tempFloat < LOWBATT)
        lcd.println("  BATTLOW");
      else
        lcd.println("         ");
      lcd.println();
      lcd.println(timeDateString);

      // Write data to SD card
      lcd.setCursor(112,0);
      if (sdCardPresent)
        lcd.print("SD");
      else
        lcd.print("NC");
      logString += String(GPS.latitudeDegrees,8);
      logString += ",";
      logString += String(GPS.longitudeDegrees,8);
      logString += ",";
      logString += String(sAvg, 0);
      logString += ",";
      logString += String(aAvg, 0);
      logString += ",";
      logString += String(GPS.satellites);
      logString += ",";
      logString += timeDateString;
      writeLogFile(logString);
      blinkOnBoardLED(1);
      lcd.setCursor(112,0);
      lcd.print("  ");
    }
    else
    {
      lcd.setCursor(112,0);
      lcd.print("NF");
      delay(250);
      lcd.setCursor(112,0);
      lcd.print("  ");
    }

    // If sanity checks pass, transmit packet
    xmitOK = true;
    if (GPS.latitudeDegrees > 30.0 && GPS.latitudeDegrees <= 90.0)
      xmitData.lat = (float) GPS.latitudeDegrees;
    else
      xmitOK = false;
    if (GPS.longitudeDegrees < -70.0)
      xmitData.lon = (float) GPS.longitudeDegrees;
    else
      xmitOK = false;      
    if (sAvg >= 0 && sAvg < 100.0)
      xmitData.speed = sAvg;
    else
      xmitOK = false;
    if (aAvg >= 0 && aAvg < 500)
      xmitData.alt = aAvg;
    else
      xmitOK = false;
    if (GPS.satellites > 0 && GPS.satellites <= 22)
      xmitData.numSat = (uint8_t) GPS.satellites;
    else
      xmitOK = false;
    if (xmitOK)
      radio.send(RCVRID, &xmitData, sizeof(xmitData));
    else
    {
      lcd.setCursor(112,0);
      lcd.print("BP");
      delay(250);
      lcd.setCursor(112,0);
      lcd.print("  ");
      
#ifdef DEBUG
      Serial.println(F("Dropping bad packet."));
#endif
    }
    timer = millis();                                   // reset the timer
  }
}

///////////////////////////////////////////////////////
//           setupWirelessRadio                       /
///////////////////////////////////////////////////////
//
void setupWirelessRadio(void)
{
  radio.initialize(FREQUENCY,NODEID,NETWORKID);
  radio.encrypt(KEY);
  radio.promiscuous(false);
  radio.setHighPower(true);
  radio.setFrequency(FREQ);
  lcd.println("Radio initialized.");
    
#ifdef DEBUG
  Serial.println(F("Radio initialized."));
#endif
}

///////////////////////////////////////////////////////
//           initFlash                                /
///////////////////////////////////////////////////////
//
void initFlash(void)
{
  if (flash.initialize())
  {
    lcd.println("Flash initialized.");
#ifdef DEBUG
    Serial.println(F("Flash initialized."));
#endif
  }
  else
  {
    lcd.println("Flash init failed.");
#ifdef DEBUG
    Serial.println(F("Flash init failed."));
#endif
  }
}

///////////////////////////////////////////////////////
//           setupLCD                                 /
///////////////////////////////////////////////////////
//
void setupLCD(void)
{
  lcd.begin();
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.setFontSize(FONT_SIZE_SMALL);
  lcd.println("LCD initialized.");

#ifdef DEBUG
  Serial.println(F("LCD initialized."));
#endif  
}

///////////////////////////////////////////////////////
//           setupGPS                                 /
///////////////////////////////////////////////////////
//
void setupGPS(void)
{
  // Timer0 is already used for millis() - we'll just interrupt somewhere
  // in the middle and call the "Compare A" function above
  OCR0A = 0xAF;
  TIMSK0 |= _BV(OCIE0A);
  
  GPS.begin(9600);
  GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);  
  GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ);                    // 1 Hz position update rate
  GPS.sendCommand(PGCMD_NOANTENNA);                             // Request no updates on antenna status
  GPS.sendCommand(PMTK_ENABLE_SBAS);
  GPS.sendCommand(PMTK_ENABLE_WAAS);

  lcd.println("GPS initialized.");
  
#ifdef DEBUG
  Serial.println(F("GPS initialized."));
#endif
}

///////////////////////////////////////////////////////
//          SIGNAL (TIMER0)                           /
///////////////////////////////////////////////////////
//
SIGNAL(TIMER0_COMPA_vect) 
{
  GPS.read();
}

///////////////////////////////////////////////////////
//           setupSDCardReader                        /
///////////////////////////////////////////////////////
//
void setupSDCardReader(void)
{
  if (!SD.begin(chipSelect))
  {
    lcd.println("SD card init failed.");

#ifdef DEBUG
    Serial.println(F("SD card initialization failed."));
#endif
  }
  else
  {
    sdCardPresent = true;
    SdFile::dateTimeCallback(dateTimeCB);
    lcd.println("SD card initialized.");

#ifdef DEBUG
    Serial.println(F("SD card initialized."));
#endif
  }
}

///////////////////////////////////////////////////////
//           writeLogFile                             /
///////////////////////////////////////////////////////
//
void writeLogFile(String entry)
{
#ifdef DEBUG
  Serial.print(F("writeLogFile: "));
  Serial.println(entry);
#endif

  logFile = SD.open(GPSLOGFILE, FILE_WRITE);
  logFile.println(entry);
  logFile.close();
}

///////////////////////////////////////////////////////
//           blinkOnboardLED                          /
///////////////////////////////////////////////////////
//
void blinkOnBoardLED(uint8_t cnt)
{
  for (int x = 1; x <= cnt; x++)                                       // Blink 5 times, leave disabled
  {
    digitalWrite(ONBOARDLED, HIGH);
    delay(500);
    digitalWrite(ONBOARDLED, LOW);
    delay(500);
  }
}

///////////////////////////////////////////////////////
//           correctDayLightSavingsTime               /
///////////////////////////////////////////////////////
//
void correctDaylightSavingsTime(void)
{
  int timeOffset,
      dWeek;
  struct date today;
    
  dWeek = dayOfWeek(GPS.year + 2000, GPS.month, GPS.day);             // Calculate day of week, 0 = Sunday
  if (isDST(GPS.month, GPS.day, dWeek, GPS.hour))                     // Adjust for DST if necessary
    timeOffset = EDTOFFSET;
  else
    timeOffset = ESTOFFSET;        
  adjHour = (GPS.hour + (24 - timeOffset)) % 24;
  if (adjHour == 0)
    adjHour = 12;

  if (timeOffset == ESTOFFSET)
  {
    if (adjHour >= 19 && adjHour <= 23)
    {
      today.mm = GPS.month;
      today.dd = GPS.day;
      today.yy = GPS.year + 2000;
      getYesterdayDate(&today);
      GPS.month = today.mm;
      GPS.day = today.dd;
      GPS.year = today.yy - 2000;
    }
  }
  else if (timeOffset == ESTOFFSET)
  {
    if (adjHour >= 20 && adjHour <= 23)
    {
      today.mm = GPS.month;
      today.dd = GPS.day;
      today.yy = GPS.year + 2000;
      getYesterdayDate(&today);
      GPS.month = today.mm;
      GPS.day = today.dd;
      GPS.year = today.yy - 2000;
    }
  }

#ifdef DEBUG
  Serial.print(F("correctDaylightSavingsTime(): GPS.hour = "));
  Serial.print(GPS.hour);
  Serial.print(F(" , adjHour = "));
  Serial.println(adjHour);
  Serial.print(F("GPS month, day, year = "));
  Serial.print(GPS.month);
  Serial.print(F("/"));
  Serial.print(GPS.day);
  Serial.print(F("/"));
  Serial.println(GPS.year);
#endif
}

///////////////////////////////////////////////////////
//           getYesterdayDate                         /
///////////////////////////////////////////////////////
//
void getYesterdayDate(struct date *d)
{
#ifdef DEBUG
  Serial.print(F("Enter getYesterdayDate(): mm = "));
  Serial.println(d->mm);
  Serial.print(F("dd = "));
  Serial.println(d->dd);
  Serial.print(F("yy = "));
  Serial.println(d->yy);
#endif

  if(d->dd == 1)
  {
    if(d->mm == 4 || d->mm == 6 || d->mm == 9 || d->mm == 11)
    {
      d->dd = 31;
      d->mm--;
    }
    else if(d->mm == 3)
    {
      if (((d->yy % 4 == 0) && (d->yy % 100 != 0)) || (d->yy % 400 == 0)) 
        d->dd = 29;
      else 
        d->dd = 28;
      d->mm--;
    }
    else if (d->mm == 1)
    {
      d->mm = 12;
      d->dd = 31;
      d->yy--;
    }
    else if (d->mm == 2)
    {
      d->dd = 31;
      d->mm--;
    }
    else
    {
      d->dd = 30;
      d->mm--;
    }
  }
  else
    d->dd--;

#ifdef DEBUG
  Serial.print(F("Exit getYesterdayDate(): mm = "));
  Serial.println(d->mm);
  Serial.print(F("dd = "));
  Serial.println(d->dd);
  Serial.print(F("yy = "));
  Serial.println(d->yy);
#endif
}

///////////////////////////////////////////////////////
//           dayOfWeek                                /
///////////////////////////////////////////////////////
//
byte dayOfWeek(int year, byte month, byte day)
{
  static int t[] = {0,3,2,5,0,3,5,1,4,6,2,4};

  year -= month < 3;
  return ((year + year/4 - year/100 + year/400 + t[month-1] + day) %7);       // Sunday = 0
}

///////////////////////////////////////////////////////
//           isDST                                    /
///////////////////////////////////////////////////////
//
boolean isDST(int month, int day, int dow, int hour)
{
  int previousSunday;

  // Sanity checks
  if (month < 1 || month > 12)
    return false;
  if (day < 1 || day > 31)
    return false;
  if (dow < 0 || dow > 6)
    return false;
  if (hour < 0 || hour > 23)
    return false;

  // January, February, and December are out
  if (month < 3 || month > 11)
    return false;
  
  // April to October are in
  if (month > 3 && month < 11)
    return true;
  
  previousSunday = day - dow;

  // In March, we are DST if our previous Sunday was on or after the 8th
  if (month == 3)
  {
    if (hour >= 2)
      return (previousSunday >= 8);
    else
      return (previousSunday >= 8 && hour >=2);
  }

  // In November we must be before the first Sunday to be DST
  // That means the previous Sunday must be before the 1st
  return (previousSunday <= 0 || hour < 2);
}

///////////////////////////////////////////////////////
//           dateTimeCB                               /
///////////////////////////////////////////////////////
//
void dateTimeCB(uint16_t *date, uint16_t *time)
{
  *date = FAT_DATE(gYear, gMonth, gDay);
  *time = FAT_TIME(adjHour, gMinute, gSeconds);
}

///////////////////////////////////////////////////////
//           computeVolts                             /
///////////////////////////////////////////////////////
//
float computeVolts(void)
{
  return ((float) analogRead(PWR_MONITOR) * VOLTAGE_COMP);
}

///////////////////////////////////////////////////////
//           dumpSDFile                               /
///////////////////////////////////////////////////////
//
void dumpSDFile(void)
{
  struct xmitDataStruct xmitLogData;
  int bytesRead;

  if (sdCardPresent)
  {
    lcd.clear();
    lcd.println("Dumping file:");
    lcd.println();
    lcd.println(GPSLOGFILE);
    lcd.println();
    lcd.println("to remote client.");
  
    logFile = SD.open(GPSLOGFILE);

    // if the file is available, transmit it over the radio
    if (logFile) 
    { 
      while (logFile.position() < logFile.size())
      {
        bytesRead = logFile.readBytesUntil('\n', xmitLogData.buffer, sizeof(xmitLogData.buffer));
        xmitLogData.buffer[bytesRead-1] = '\0';
        radio.send(CLIENTID, &xmitLogData, sizeof(xmitLogData));
      
#ifdef DEBUG
        Serial.println(xmitLogData.buffer);
#endif

        delay(50);
      } 
    }
    else
    {
#ifdef DEBUG
      Serial.println(F("File was not found or opened."));
#endif
      lcd.println("File:");
      lcd.println();
      lcd.println(GPSLOGFILE);
      lcd.println();
      lcd.println("was not found or opened.");
    }

    logFile.close();
    delay(3000);
    lcd.clear();
  }
  else
  {
    lcd.clear();
    lcd.setCursor(0,40);
    lcd.println("Dump file requested.");
    lcd.println("SD card not initialized.");
    memset(&xmitLogData, '\0', sizeof(xmitLogData));
    strcpy(xmitLogData.buffer, "SD Card not initialized.");
    radio.send(CLIENTID, &xmitLogData, sizeof(xmitLogData));
    delay(3000);
    lcd.clear();
  }
}

///////////////////////////////////////////////////////
//           deleteSDFile                             /
///////////////////////////////////////////////////////
//
void deleteSDFile(void)
{
  lcd.clear();
  lcd.println("File:");
  lcd.println(GPSLOGFILE);
  lcd.println();
    
  if (SD.exists(GPSLOGFILE))
  {
    if (SD.remove(GPSLOGFILE))
      lcd.println("was deleted.");
    else
      lcd.println("could not be deleted.");
  }
  else
    lcd.println("not found.");
  delay(3000);
  lcd.clear();
}
