/*
Chris Michaelis
chris.michaelis@webh2o.net

Arduino Uno SD-card based logger for a waterproof temperature sensur probe, soil water sensor (hygrometer), and combined 
air humidity+temperature sensor. You could likely add an XBee receiver on D0/D1 to listen for slave sensors
but it would require a bigger Arduino than an Uno (in which case be mindful of pin differences in Mega for SPI and I2C) 

NOTE - Using WSWire to replace buggy hang-prone Arduino Wire library. 
In WSWire modify twi.c to enable pullup resistors, and to add TWCR=0 after-sleep correction.
Also modify DS1307RTC to use WSWire too. (Or use Libraries.zip included with this file, which has these modifications.)

Pins in use:
TFT: D8 D10 D11 D13
Joystick: A3
SD Card: D4 D12
TFT Backlight Power: D7
Dallas OneWire Temperature Bus: A1
Generic Hygrometer: A0
Sensor Power via MOSFET: A2
DHT22 Temp/Humidity: D3
Real-Time Click (SDA/SCL): A4 A5
"Alive" blink every 8s wakeup: D2

Free pins:
D0 D1 (Serial)
D5
D6

Measured power draw (mA):
~74 Bootup
72.0 Sitting at Menu, LCD on
74.0 Max spike observed when updating LCD menu
36.0 Sleep mode (with spike to 39 when the LED blinks every 8 seconds)
*/

#include <Narcoleptic.h>
#include <OneWire.h>
#include <EEPROM.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>
#include <SD.h>
#include <DHT22.h>
#include <DS1307RTC.h>
#include <Time.h>
#include <WSWire.h>

#define HYGRO A0
#define ONEWIRE A1
#define POWERCTRL A2
#define LCDBACKLIGHT 7
#define HUMIDTEMP 3
#define SDA A4
#define SCL A5
#define JOYSTICK A3
#define SD_CS 4
#define DS1307_I2C_ADDRESS 0x68

#define JOY_NEUTRAL 0
#define JOY_PRESS 1
#define JOY_UP 2
#define JOY_DOWN 3
#define JOY_RIGHT 4
#define JOY_LEFT 5

OneWire sensorBus(ONEWIRE);
Adafruit_ST7735 tft = Adafruit_ST7735(10, 8, -1);

DHT22 dht22(HUMIDTEMP);

int isTFTOn = 0;
int numberSleeps = 0;
unsigned long lastDHTRead = 0;
float lastTempSensor1 = 0;
float lastTempSensor2 = 0;
float lastHygro = 0;
float lastHumidity = 0;
boolean noSensorDelay = false; // For first one (see below code)
tmElements_t lastTime;
unsigned int lastReadingsCount = 0;

int Recording_Interval = 1; // Minutes
#define Recording_Interval_EEPROM_Idx 1

void setup() {
  Serial.begin(9600);
  
  externalPowerOn();
  
  pinMode(LCDBACKLIGHT, OUTPUT);
  digitalWrite(LCDBACKLIGHT, HIGH);
  
  Recording_Interval = EEPROM.read(Recording_Interval_EEPROM_Idx);
  
  SD.begin(SD_CS);
  pinMode(10, OUTPUT);
  tft.initR(INITR_BLACKTAB);
  tft.setRotation(3);
  
  isTFTOn = 1;
 
  unsigned long lastOperation = millis();
  int activeMenuItem = 0;
  boolean forceRedraw = true;

  unsigned long lastRedraw = 0;

  while (lastOperation + 15000 > millis()) {
    readSensors();
    noSensorDelay = true; // Now that it's warmed up, make future refreshes faster on menu
    
    if (lastRedraw + 5000 < millis() || forceRedraw) {
      forceRedraw = false;
      lastRedraw = millis();
      
      if (activeMenuItem > 2) activeMenuItem = 0;
      if (activeMenuItem < 0) activeMenuItem = 2;
            
      resetScreen();
      tft.setTextColor(ST7735_YELLOW);
      tft.println(F("   myObservatory Sensor\n"));
      tft.setTextColor(ST7735_WHITE);
      dashedLine();
      tft.print(F("Readings on Card: "));
      tft.println(countReadings());
      
      // Show last-read day and time
      // Date portion:
      tft.print(1970 + lastTime.Year, DEC);
      tft.print("-");
      if (lastTime.Month < 10) tft.print("0");
      tft.print(lastTime.Month, DEC);
      tft.print("-");
      if (lastTime.Day < 10) tft.print("0");
      tft.print(lastTime.Day, DEC);
      tft.print(" ");
  
      // Time portion:
      if (lastTime.Hour < 10) tft.print("0");
      tft.print(lastTime.Hour, DEC);
      tft.print(":");
      if (lastTime.Minute < 10) tft.print("0");
      tft.println(lastTime.Minute, DEC);
      
      tft.print(F("Interval: "));
      tft.print(Recording_Interval);
      tft.println(F(" Minutes"));
      dashedLine();
          
      tft.setTextColor(ST7735_RED);
      tft.print(F("Soil Moist (%): "));
      tft.println(lastHygro);
      tft.print(F("Probe Temp (F): "));
      tft.println(lastTempSensor1);
      tft.print(F("Humidity (%): "));
      tft.println(lastHumidity);
      tft.print(F("Air Temp (F): "));
      tft.println(lastTempSensor2);
      
      tft.setTextColor(ST7735_WHITE);    
      dashedLine();
      
      tft.setTextColor(ST7735_GREEN);
      for (int i = 0; i < 3; i++) {
        if (activeMenuItem == i) {
          tft.print("> ");
        }
        
        if (i == 0) tft.println(F("Save Current Readings"));
        if (i == 1) tft.println(F("Change Time Interval"));
        if (i == 2) tft.println(F("Set Date/Time"));
      }
      
      tft.setTextColor(ST7735_YELLOW);
      tft.println(F("  Update Every 5 Seconds\n"));
    }
    
    int j = checkJoystick(500);
    
    if (j == JOY_LEFT || j == JOY_RIGHT) {
      lastOperation = millis();
    } else if (j == JOY_DOWN) {
      lastOperation = millis();
      activeMenuItem++;
      forceRedraw = true;
    } else if (j == JOY_UP) {
      lastOperation = millis();
      activeMenuItem--;
      forceRedraw = true;
    } else if (j == JOY_PRESS) {
      if (activeMenuItem == 0) {
        saveCurrentReadings();
        lastOperation = millis();
        forceRedraw = true;
      } else if (activeMenuItem == 1) {
        int lastJoy = checkJoystick(500);
        forceRedraw = true;
        resetScreen();
        tft.println(F("\nSet recording time\ninterval in minutes:\n\nPress up/down on joystick\nto change number.\n\nPress joystick down to\nsave changes.\n"));
        dashedLine();
            
        while (lastJoy != JOY_PRESS) {
          if (forceRedraw) {
            tft.setCursor(0, 90);    
            tft.setTextColor(ST7735_GREEN, ST7735_BLACK);
            tft.print(Recording_Interval);
            tft.println(F(" Minutes     "));
            forceRedraw = false;
          }
          
          if (lastJoy == JOY_UP) {
            Recording_Interval++;
            forceRedraw = true;
          } else if (lastJoy == JOY_DOWN) {
            Recording_Interval--;
            if (Recording_Interval < 1) Recording_Interval = 1;
            forceRedraw = true;
          } 
          
          lastJoy = checkJoystick(200);
        }
        
        EEPROM.write(Recording_Interval_EEPROM_Idx, Recording_Interval);
        resetScreen();
        tft.println(F("Saved!"));
        lastOperation = millis();
        forceRedraw = true;
        delay(700);
      } else if (activeMenuItem == 2) {
        setTime();
        lastOperation = millis();
        forceRedraw = true;
      }
    } 
  }
}

void loop() {   
    // Make sure our sensors get time to warm up  
    noSensorDelay = false;
    
    // Show we're alive (LED mounted on box)
    pinMode(2, OUTPUT);
    digitalWrite(2, HIGH);
    delay(100);
    digitalWrite(2, LOW); 
    
    if (numberSleeps >= (Recording_Interval * 60) / 8) {  
      numberSleeps = 0;
      externalPowerOn();
      
      // Ping RTC to wake it up
      Wire.beginTransmission(DS1307_I2C_ADDRESS);
      Wire.endTransmission();
      delay(5); // Let it stabilize
      
      saveCurrentReadings();    
    } else {
      sleepNow(8000);
      numberSleeps++;
    }
}

void setTime() {
  byte currentSetting = 0;
  // 0 1 2 3 4 == Year Month Day Hour Minute --- no setting seconds here
  boolean redraw = true;
  
  // To save memory, reuse the lastTime object to adjust the time, then call RTC.adjust with it
        
  resetScreen();
  tft.println(F("\nSet Date and Time\n\nPush joystick up and down to move numbers up and\ndown; left and right to\nmove between settings.\n\nPress joystick in when\ndone.\n\n"));
      
  while (true) {  
    if (lastTime.Month > 12) lastTime.Month = 1;
    if (lastTime.Month < 1) lastTime.Month = 12;
    if (lastTime.Day > 31) lastTime.Day = 1;
    if (lastTime.Day < 1) lastTime.Day = 31;
    if (lastTime.Hour > 23) lastTime.Hour = 1;
    if (lastTime.Hour < 1) lastTime.Hour = 23;
    if (lastTime.Minute > 59) lastTime.Minute = 1;
    if (lastTime.Minute < 1) lastTime.Minute = 59;
    
    if (redraw) {
      redraw = false;
      
      tft.setCursor(0, 90);
      
      // Date portion:
      if (currentSetting == 0) tft.setTextColor(ST7735_YELLOW, ST7735_BLACK);
      tft.print(1970 + lastTime.Year, DEC);
      tft.setTextColor(ST7735_WHITE, ST7735_BLACK);
      
      tft.print("-");
      
      if (currentSetting == 1) tft.setTextColor(ST7735_YELLOW, ST7735_BLACK);
      if (lastTime.Month < 10) tft.print("0");
      tft.print(lastTime.Month, DEC);
      tft.setTextColor(ST7735_WHITE, ST7735_BLACK);
      
      tft.print("-");
      
      if (currentSetting == 2) tft.setTextColor(ST7735_YELLOW, ST7735_BLACK);
      if (lastTime.Day < 10) tft.print("0");
      tft.print(lastTime.Day, DEC);
      tft.setTextColor(ST7735_WHITE, ST7735_BLACK);
      
      tft.print(" ");
  
      // Time portion:
      if (currentSetting == 3) tft.setTextColor(ST7735_YELLOW, ST7735_BLACK);
      if (lastTime.Hour < 10) tft.print("0");
      tft.print(lastTime.Hour, DEC);
      tft.setTextColor(ST7735_WHITE, ST7735_BLACK);
      
      tft.print(":");
      
      if (currentSetting == 4) tft.setTextColor(ST7735_YELLOW, ST7735_BLACK);
      if (lastTime.Minute < 10) tft.print("0");
      tft.print(lastTime.Minute, DEC);
      tft.setTextColor(ST7735_WHITE, ST7735_BLACK);
    }
    
    // Check for input:
    int lastJoy = checkJoystick(200);
    if (lastJoy == JOY_UP) {
      redraw = true;
      if (currentSetting == 0) lastTime.Year++;
      if (currentSetting == 1) lastTime.Month++;
      if (currentSetting == 2) lastTime.Day++;
      if (currentSetting == 3) lastTime.Hour++;
      if (currentSetting == 4) lastTime.Minute++;
      
    } else if (lastJoy == JOY_DOWN) {
      redraw = true;
      if (currentSetting == 0) lastTime.Year--;
      if (currentSetting == 1) lastTime.Month--;
      if (currentSetting == 2) lastTime.Day--;
      if (currentSetting == 3) lastTime.Hour--;
      if (currentSetting == 4) lastTime.Minute--;
    } else if (lastJoy == JOY_LEFT) {
      redraw = true;
      currentSetting--;
    } else if (lastJoy == JOY_RIGHT) {
      redraw = true;
      currentSetting++;
    } else if (lastJoy == JOY_PRESS) {
      // Push new values:
      RTC.write(lastTime);
     
      resetScreen();
      tft.println(F("Saved!"));
      delay(700); 
      return; // and summarily exit while loop
    }
  }
}

static uint8_t bin2bcd (uint8_t val) { return val + 6 * (val / 10); }

unsigned int countReadings() {
  // Don't actually re-count the card if we can avoid it
  // (This is incremented by the save fus="flow">  if (lastReadingsCount > 0) return lastReadingsCount;
  
  File dataFile = SD.open("datalog.csv", FILE_READ);
  if (dataFile) {
    int c = dataFile.read();
    while (c != -1) {
      if (c == '\n') lastReadingsCount++;
      c = dataFile.read();
    }
  }
  dataFile.close();
  // Account for header row
  if (lastReadingsCount > 1) lastReadingsCount--;
  return lastReadingsCount;
}

void saveCurrentReadings() { 
  if (isTFTOn) {
      resetScreen();
      tft.println(F("Reading and saving...\n\nPlease Wait"));
  }
  
  readSensors(); 
  
  SD.begin(SD_CS);
  
  boolean writeHeader = !SD.exists("datalog.csv");
   
  File dataFile = SD.open("datalog.csv", FILE_WRITE);
  if (dataFile) {
    if (writeHeader) {
      dataFile.println(F("Sample Time,Probe Temp F,Soil Moist %,Air Temp F,Humidity %"));
    }
    
    // Date portion:
    dataFile.print(1970 + lastTime.Year, DEC);
    dataFile.print("-");
    if (lastTime.Month < 10) dataFile.print("0");
    dataFile.print(lastTime.) dataFile.print("0");
    dataFile.print(lastTime.Second, DEC);
    
    // The rest of our CSV:

    dataFile.print(",");
    dataFile.print(lastTempSensor1);
    dataFile.print(",");
    dataFile.print(lastHygro);
    dataFile.print(",");    
    dataFile.print(lastTempSensor2);
    dataFile.print(",");
    dataFile.println(lastHumidity);
    
    dataFile.close();
    
    // Increment our count for the menu (only matters if TFT is on, FWIW)
    lastReadingsCount++;
    
    if (isTFTOn) {
      resetScreen();
      tft.println(F("Saved!"));
      delay(700);
    }
  } else {
   if (isTFTOn) {
      resetScreen();
      tft.println(F("Can't save!\n\nNo SD card?"));
      delay(2000);
    }
  } 
}

void resetScreen() {
  tft.fillScreen(ST7735_BLACK);
  tft.setCursor(0, 0);
  tft.setTextColor(ST7735_WHITE);
  tft.setTextWrap(true);
}

void dashedLine() {
  for (int i = 0; i < 25; i++)
   tft.print("-"); 
  tft.print("\n");
}

void readSensors() {
  // Dallas OneWire Temperature
  byte addr[8];
  while (sensorBus.search(addr)) {
    if (OneWire::crc8(addr, 7) == addr[7]) {
      /* Only one Dallas temperature sensor in this revision, other is in DHT22 unit */
      lastTempSensor1 = readSensor(addr);
    }
  }
  
  sensorBus.reset_search();
  
  // Hygrometer:
  lastHygro = (1024 - analogRead(HYGRO)) / 1024 * 100;
  
  // DHT22 (Humidity and temperature #2)
  DHT22_ERROR_t errorCode;
  // Can only poll every 1-2 seconds
  if (lastDHTRead + 2000 <= millis()) {
    errorCode = dht22.readData();
    if (errorCode == DHT_ERROR_NONE) {
      lastTempSensor2 = dht22.getTemperatureC() * 1.8 + 32.0;
      lastHumidity = dht22.getHumidity();
    }
    lastDHTRead = millis();
  }
  
  // and the time:
  RTC.read(lastTime);
}

float readSensor(byte * addr) {
  byte data[12];
  int i;
  
  sensorBus.reset();
  sensorBus.select(addr);
  
  // Parasite power on:
  sensorBus.write(0x44, 1);
  
  if (!noSensorDelay) {
    // Delay to let temperature probe get its temperature; but not if we're in the status screen, where it will make it super laggy
    delay(1000);
  }
  
  // Read scratch pad:
  sensorBus.reset();
  sensorBus.select(addr);    
  sensorBus.write(0xBE); // Read scratchpad command
  
  for (i = 0; i < 9; i++) {
    // We need 9 bytes
    data[i] = sensorBus.read();
  }
  
  int16_t raw = (data[1] << 8) | data[0];
  byte cfg = (data[4] & 0x60);
  if (cfg == 0x00) raw = raw & ~7;  // 9 bit resolution, 93.75 ms
  else if (cfg == 0x20) raw = raw & ~3; // 10 bit res, 187.5 ms
  else if (cfg == 0x40) raw = raw & ~1; // 11 bit res, 375 ms
  
  float celsius = (float) raw / 16.0;
  float fahrenheit = celsius * 1.8 + 32.0;
  return fahrenheit;
}

void sleepNow(int howLongMS) {  
  // External power off
  pinMode(POWERCTRL, OUTPUT);
  digitalWrite(POWERCTRL, LOW);
  
  // LED off
  digitalWrite(POWERCTRL, LOW);
  pinMode(13, OUTPUT);
  digitalWrite(13, LOW);
  
  isTFTOn = false;
  digitalWrite(LCDBACKLIGHT, LOW);
  
  // Power saving: set all to low/input
  digitalWrite(0, LOW);
  digitalWrite(1, LOW);
  digitalWrite(2, LOW);
  digitalWrite(3, LOW);
  digitalWrite(4, LOW);
  digitalWrite(5, LOW);
  digitalWrite(6, LOW);
  digitalWrite(8, LOW);
  digitalWrite(9, LOW);
  digitalWrite(10, LOW);
  digitalWrite(11, LOW);
  digitalWrite(12, LOW);
  digitalWrite(A0, LOW);
  digitalWrite(A1, LOW);
  digitalWrite(A2, LOW);
  digitalWrite(A3, LOW); 
  pinMode(0, INPUT);
  pinMode(1, INPUT);
  pinMode(2, INPUT);
  pinMode(3, INPUT);
  pinMode(4, INPUT);
  pinMode(5, INPUT);
  pinMode(6, INPUT);
  pinMode(8, INPUT);
  pinMode(9, INPUT);
  pinMode(10, INPUT);
  pinMode(11, INPUT);
  pinMode(12, INPUT);  
  pinMode(A0, INPUT);
  pinMode(A1, INPUT);
  pinMode(A2, INPUT);
  pinMode(A3, INPUT);
  
  Narcoleptic.delay(howLongMS);
}

void externalPowerOn() {
  // Put pins back how we want them, e.g. after forcing everything low for sleep
  // Turn on external peripherals
  pinMode(POWERCTRL, OUTPUT);
  digitalWrite(POWERCTRL, HIGH);
 
  pinMode(HYGRO, INPUT); 
  pinMode(ONEWIRE, INPUT); 
  // Let external peripherals power on
  // DHT22 requires 2s warm-up
  delay(2000); 
}

int checkJoystick(int withDelay) {
  float joystickState = analogRead(JOYSTICK);
  
  joystickState *= 5.0;
  joystickState /= 1024.0;
  
  if (joystickState < 3.2) {
    // Something happened - delay slightly so that one quick push up isn't
    // registered as 25 ups
    delay(withDelay);
  }
   
  static float NEUT_THRESHOLD = joystickState; // USB = 3.32; Solar Pack = 3.84; use this to help us determine LEFT
  
  /*
  resetScreen();
  tft.println(joystickState);
  delay(500);
  */
  
  if (joystickState < 0.2) return JOY_LEFT;
  if (joystickState < 0.8) return JOY_DOWN;
  if (joystickState < 1.5) return JOY_PRESS;
  if (joystickState < 2.0) return JOY_RIGHT;
  
   // USB UP = 3.01; Solar Pack = 3.49
  if (NEUT_THRESHOLD < 3.5 && joystickState < 3.1) return JOY_UP;
  if (NEUT_THRESHOLD > 3.6 && joystickState <= 3.6) return JOY_UP;
  
  return JOY_NEUTRAL;
}