/*
Chris Michaelis
chris.michaelis@webh2o.net

Cellular-enabled two-sensor data station uploading directly to WebH2O myObservatory.

Assumes an Arduino Uno R3 with Sainsmart 16x2 LCD Keypad Shield, Arduino GSM Shield, and two DS18B20 temperature sensors connected via OneWire bus to pin A5.
Also assumes you have desoldered the POWERKEY D7 jumper from the back of the GSM shield, and re-routed it to A3; be sure to update Arduino GSM code to use this pin too.
Must use an AT&T SIM card.

ASSUMED PINOUT:

Digital 0: Unused
Digital 1: Unused
Digital 2: Modem RX
Ditital 3: Modem TX
Digital 4, 5, 6, 7, 8, 9: LCD Control
Digital 10: LCD Brightness
Digital 11: Unused
Digital 12: Unused
Digital 13: Onboard LED
Analog 0: Keypad
Analog 1: Unused
Analog 2: Unused
Analog 3: MODEM POWER (remapped from Digital 7 using soldered patch)
Analog 4: Unused
Analog 5: OneWire Sensor Bus
*/

// Libraries:
#include <LiquidCrystal.h>
#include <OneWire.h>
#include <GSM.h>
#include <LowPower.h>
#include <EEPROM.h>

// Interfaces:
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
OneWire sensorBus(A5);
GSMClient client;
GPRS gprs;
GSM gsmAccess(true);
GSM_SMS sms;

// Pin Constants:
int RIGHT = 0;
int UP = 1;
int LEFT = 3;
int DOWN = 2;
int SELECT = 4;
int KEY_VALUES[5] = {30, 150, 360, 545, 750};
int NUM_KEYS = 5;
int LCD_BACKLIGHT = 10;
int MODEM_POWER = A3;

// Execution Tracking:
unsigned long lcdOnSince = 0;
unsigned long lastButtonPress = 0;
int isLCDOn = 0;
int lastKey = -1;
float lastSensor1 = 0;
float lastSensor2 = 0;
boolean notConnected = true;
int numberSleeps = 0;
int forceRedraw = 0;
int noSensorDelay = 0;

// User Settings-Related Info:
char VALID_CHARACTERS[ ] = "0123456789aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ-_.@ ";
char VALID_NUMBERS[ ] = "0123456789 ";
// Defaults presented below don't really matter, though they do define the width of the char array
char API_Key[ ] = "72acf";
int API_Key_MAX_LENGTH = 5;
int API_Key_EEPROM_Idx = 0;
char Station_1[ ] = "3439      ";
int Station_1_MAX_LENGTH = 10;
int Station_1_EEPROM_Idx = 20;
char Station_2[ ] = "3440      ";
int Station_2_MAX_LENGTH = 10;
int Station_2_EEPROM_Idx = 50;
char Upload_Interval_Minutes[ ] = "15  ";
int Upload_Interval_MAX_LENGTH = 4;
int Upload_Interval_Minutes_EEPROM_Idx = 100;

// AT&T settings:
#define GSM_PIN ""
#define GSM_APN "wap.cingular"
#define GSM_LOGIN "WAP@CINGULARGPRS.COM"
#define GSM_PASSWD "CINGULAR1"

void setup() {  
  Serial.begin(9600);
  
  isLCDOn = 1;
  lcdOnSince = millis();
  lcd.begin(16, 2);
  lcd.setCursor(0, 0);
  lcd.print("When LCD is off,");
  lcd.setCursor(0, 1);
  lcd.print("hit RST for menu");
  delay(2000);
  
  pinMode(LCD_BACKLIGHT, OUTPUT);
  digitalWrite(LCD_BACKLIGHT, HIGH);
  
  // Power saving: set unused to low/input
  pinMode(0, INPUT);
  pinMode(1, INPUT);
  pinMode(11, INPUT);
  pinMode(12, INPUT);
  pinMode(13, INPUT);
  pinMode(A1, INPUT);
  pinMode(A2, INPUT);
  pinMode(A3, INPUT);
  pinMode(A4, INPUT);
  digitalWrite(0, LOW);
  digitalWrite(1, LOW);
  digitalWrite(11, LOW);
  digitalWrite(12, LOW);
  digitalWrite(13, LOW);
  digitalWrite(A1, LOW);
  digitalWrite(A2, LOW);
  digitalWrite(A4, LOW);
  
  // Start with modem off
  pinMode(MODEM_POWER, OUTPUT);
  digitalWrite(MODEM_POWER, LOW);
  
  // Load config values from EEPROM:
  loadFromEEPROM(API_Key, API_Key_MAX_LENGTH, API_Key_EEPROM_Idx);
  loadFromEEPROM(Station_1, Station_1_MAX_LENGTH, Station_1_EEPROM_Idx);
  loadFromEEPROM(Station_2, Station_2_MAX_LENGTH, Station_2_EEPROM_Idx);  
  loadFromEEPROM(Upload_Interval_Minutes, Upload_Interval_MAX_LENGTH, Upload_Interval_Minutes_EEPROM_Idx);  
  
  readSensors();
  
  lcd.display();
  digitalWrite(LCD_BACKLIGHT, HIGH);
  isLCDOn = 1;
  lcdOnSince = millis();
  lastButtonPress = millis();
  
  showStatusScreen();
}

void loop() {  
  if (isLCDOn == 1 && lcdOnSince + 3000 < millis() && lastButtonPress + 3000 < millis()) {
    lcd.noDisplay();
    lcd.clear();
    isLCDOn = 0;
    digitalWrite(LCD_BACKLIGHT, LOW);
    
    // Just to be sure...
    closeConnection(false);
  }
  
  /* Uno board only has interrupts on Digital 2 and Digital 3, both of which are in use by the GSM modem... so no interrupts to wake us.
     Thus, "Reset button" opens menu... otherwise, wake only occurs on schedule */
  if (isLCDOn == 0) {
    int Upload_Interval_Minutes_int = atoi(Upload_Interval_Minutes);
    if (numberSleeps >= (Upload_Interval_Minutes_int * 60) / 8) {
      if (startConnection(false)) {
        uploadValuesNow(false);
        closeConnection(false);
      }
     
      numberSleeps = 0;
    }
    
    Serial.print("Sleeps: ");
    Serial.print(numberSleeps);
    Serial.print(" of required ");
    Serial.println((Upload_Interval_Minutes_int * 60) / 8);
    delay(500); // Let it write
    
    LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
    numberSleeps++;
  }
}

int GET_KEY(unsigned int input) {
  int k;
  for (k = 0; k < NUM_KEYS; k++) {
    if (input < KEY_VALUES[k]) { 
      delay(200); // Don't let it catch multiple keypresses when one was intended
      return k;
    }
  }
  if (k >= NUM_KEYS) k = -1;
  return k;
}

void doUploadWithStatusDisplay() {
  if (startConnection(true)) {
    uploadValuesNow(true);
    closeConnection(true);
    
    lcd.clear();
    lcd.setCursor(0, 0);   
    lcd.print("Upload Now");
    lcd.setCursor(0, 1);   
    lcd.print("Finished");
  }

  // Let them see result before we return to showStatusScreen loop by returning
  delay(2000);
  
  forceRedraw = 1;
}

void uploadValuesNow(boolean LCDOutput) {
  readSensors();
  
  if (LCDOutput) {
    lcd.clear();
    lcd.setCursor(0, 0);   
    lcd.print("Uploading...");
    lcd.setCursor(0, 1);   
    lcd.print("Cnecting to myObs");
  }
 
  // NOTE - Be sure to replace this hostname and URL with whatever API endpoint your Account Settings page states.
  if (client.connect("webh2o.net", 80)) {
    if (LCDOutput) {
      lcd.clear();
      lcd.setCursor(0, 0);   
      lcd.print("Uploading...");
      lcd.setCursor(0, 1);   
      lcd.print("Sending Data");
    }
    
    client.print("GET /site/myobservatory/ci/api/putSingleRecordWithKey/");
    // Next 3 args == API key/Station ID/Actual Value
    // ...followed by optional station2id/value2/station3id/value3/station4id/value4 up to a max of 4 
    
    int i;
    
    // API Key
    for (i = 0; i < strlen(API_Key); i++) {
      // Avoid the "empty spaces"
      if (API_Key[i] != ' ') client.print(API_Key[i]);
    }

    client.print("/");
    
    // Station 1 and value
    for (i = 0; i < strlen(Station_1); i++) {
      // Avoid the "empty spaces"
      if (Station_1[i] != ' ') client.print(Station_1[i]);
    }
    client.print("/");
    client.print(lastSensor1);
    client.print("/");
        
    // Station 2 and value
    for (i = 0; i < strlen(Station_2); i++) {
      // Avoid the "empty spaces"
      if (Station_2[i] != ' ') client.print(Station_2[i]);
    }
    client.print("/");
    client.print(lastSensor2);
    
    client.println(" HTTP/1.1");
    client.print("Host: ");
    client.println("webh2o.net");
    client.println("Connection: close");
    client.println();
   
    while (client.available() || client.connected()) {
      client.read(); // Discard the result
    }
    client.stop();
  } else if (LCDOutput) {
    lcd.clear();
    lcd.setCursor(0, 0);   
    lcd.print("Error");
    lcd.setCursor(0, 1);   
    lcd.print("Can't connect.");
    
    delay(1000);
  }
}

void readSensors() {
  int currentSensor = 1;
  byte i;
  byte addr[8];

  while (sensorBus.search(addr)) {
    if (OneWire::crc8(addr, 7) == addr[7]) {
      // Valid sensor that checks out
      if (currentSensor == 1) {
         lastSensor1 = readSensor(addr);
      } else {
         lastSensor2 = readSensor(addr);
      }
      currentSensor++;
    }
  }
  
  sensorBus.reset_search();
}

float readSensor(byte * addr) {
  byte data[12];
  int i;
  
  sensorBus.reset();
  sensorBus.select(addr);
  
  // Parasite power on:
  sensorBus.write(0x44, 1);
  
  if (noSensorDelay != 1) {
    // 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 getUserConfigValue(char * valueArray, int maxLength, char * allowed_chars, char * prompt, int eepromIndex) {
  int currentCharPosition = 0;
  lcd.cursor();

  boolean wasChanged = false;
  
  do {
    lcd.clear();
    lcd.setCursor(0, 0);   
    lcd.print(prompt);
    lcd.setCursor(0, 1);
    lcd.print(valueArray);
    
    lcd.setCursor(currentCharPosition, 1);
    
    int lastKey = -1;
    while (lastKey == -1) {
      lastKey = GET_KEY(analogRead(0));
    }
    
    if (lastKey == LEFT) {
      currentCharPosition--;
      if (currentCharPosition < 0) currentCharPosition = 0;
    } else if (lastKey == RIGHT) {
      currentCharPosition++;
      if (currentCharPosition > maxLength) currentCharPosition = maxLength;
    } else if (lastKey == UP) {
      wasChanged = true;
      
      int currentPossibleChar = 0;
      for (int i = 0; i < strlen(allowed_chars); i++) {
        if (allowed_chars[i] == valueArray[currentCharPosition]) currentPossibleChar = i;
      }
      
      currentPossibleChar++;
      if (currentPossibleChar >= strlen(allowed_chars)) currentPossibleChar = 0;
      valueArray[currentCharPosition] = allowed_chars[currentPossibleChar];
      // Will redraw on next loop
    } else if (lastKey == DOWN) {
      wasChanged = true;

      int currentPossibleChar = 0;
      for (int i = 0; i < strlen(allowed_chars); i++) {
        if (allowed_chars[i] == valueArray[currentCharPosition]) currentPossibleChar = i;
      }
      
      currentPossibleChar--;
      if (currentPossibleChar < 0) currentPossibleChar = strlen(allowed_chars) - 1;
      
      valueArray[currentCharPosition] = allowed_chars[currentPossibleChar];
      // Will redraw on next loop
    } else if (lastKey == SELECT) {
      // Done!
      break;
    }
  } while (1);
 
  if (wasChanged) {
    for (int i = 0; i < maxLength; i++) {
      EEPROM.write(i + eepromIndex, valueArray[i]);
    }
  }

  lcd.noCursor();
  return;
}

void loadFromEEPROM(char * valueArray, int maxLength, int eepromIndex) {
  for (int i = 0; i < maxLength; i++) {
    valueArray[i] = EEPROM.read(i + eepromIndex);
  }
}

void showStatusScreen() {
  lastKey = -1;
  
  float lastS1 = 0;
  float lastS2 = 0;
  int lastUploadNowIsHighlighted = 0;
  
  int uploadNowIsHighlighted = 1;
  
  do {
    if (lastButtonPress + 10000 < millis()) {
      return; // to main loop
    }

    // Make sure UI is responsive
    noSensorDelay = 1;
    readSensors();
    noSensorDelay = 0;
    
    // Only need to redraw if something changed
    if (forceRedraw == 1 || lastUploadNowIsHighlighted != uploadNowIsHighlighted || lastS1 != lastSensor1 || lastS2 != lastSensor2) {
      forceRedraw = 0;
      lastUploadNowIsHighlighted = uploadNowIsHighlighted;
      lastS1 = lastSensor1;
      lastS2 = lastSensor2;
        
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print(lastSensor1);
      lcd.print("F    ");
      lcd.print(lastSensor2);
      lcd.print("F");
      lcd.setCursor(0, 1);
      
      if (uploadNowIsHighlighted == 1) {
        lcd.print(">Upload   Config");
      } else {
        lcd.print("Upload   >Config");
      }
    }
    
    lastKey = GET_KEY(analogRead(0));
    
    if (lastKey == LEFT) {
      lastButtonPress = millis();
      uploadNowIsHighlighted = 1;
      lastKey = -1;
    } else if (lastKey == RIGHT) {
      lastButtonPress = millis();
      uploadNowIsHighlighted = 0;
      lastKey = -1;
    } else if (lastKey == SELECT) {
      lastButtonPress = millis();
      if (uploadNowIsHighlighted == 1) {
        doUploadWithStatusDisplay();
        lastKey = -1;
      } else {
        getUserConfigValue(API_Key, API_Key_MAX_LENGTH, VALID_CHARACTERS, "WebH2O API Key", API_Key_EEPROM_Idx);
        getUserConfigValue(Station_1, Station_1_MAX_LENGTH, VALID_NUMBERS, "Snsr1 Station ID", Station_1_EEPROM_Idx);
        getUserConfigValue(Station_2, Station_2_MAX_LENGTH, VALID_NUMBERS, "Snsr2 Station ID", Station_2_EEPROM_Idx);  
        getUserConfigValue(Upload_Interval_Minutes, Upload_Interval_MAX_LENGTH, VALID_NUMBERS, "Upld Intrvl Min", Upload_Interval_Minutes_EEPROM_Idx);         
        
        // Reset this so we fall back to status screen
        lastButtonPress = millis();
        
        forceRedraw = 1;
      }
    }
  } while (1);
}

boolean startConnection(bool LCDOutput) {
  if (LCDOutput) {
    lcd.clear();
    lcd.setCursor(0, 0);   
    lcd.print("Uploading...");
    lcd.setCursor(0, 1);   
    lcd.print("Turning On Cell");
  }
  
  digitalWrite(MODEM_POWER, HIGH);
  digitalWrite(3, HIGH);
    
  unsigned long timeout = 60000;
  unsigned long timeConnect = millis();
  
  gsmAccess.begin(GSM_PIN, true, false);

  while (notConnected && (millis() - timeConnect) < timeout) {
    int ok = 0;
    gsmAccess.ready();
    delay(1000);
    ok = gsmAccess.getStatus();
    if (ok != GSM_READY && ok != GPRS_READY) {
      continue;
    }
    
    if (LCDOutput) {
      lcd.clear();
      lcd.setCursor(0, 0);   
      lcd.print("Uploading...");
      lcd.setCursor(0, 1);   
      lcd.print("Getting Signal");
    }
  
    if (gprs.attachGPRS(GSM_APN, GSM_LOGIN, GSM_PASSWD) == GPRS_READY) {
      notConnected = false;
      
      if (LCDOutput) { // if (anyone is paying attention to the screen)
        checkForSMS();
      }
      return true;
    } else {
      if (LCDOutput) {
        lcd.clear();
        lcd.setCursor(0, 0);   
        lcd.print("No Cell Cnection");
        lcd.setCursor(0, 1);   
        lcd.print("Retry Later...");
      }
      
      delay(1000);
      return false;
    }
  }
  
  return false;
}
 
void checkForSMS() { 
  // While our modem is freshly powered on, check for any SMS messages
  // (Useful for PIN reset notices, notices about no credit remaining on SIM card, etc...)
  if (sms.available()) {
    lcd.clear();
    lcd.setCursor(0, 0);   
    
    // Get remote number
    char senderNumber[20];  
    sms.remoteNumber(senderNumber, 20);
    lcd.print("TEXT MESSAGE FROM ");
    lcd.print(senderNumber);

    if (sms.peek() == '#') {
      sms.flush();
    }
    
    // Read message bytes and print them
    lcd.setCursor(0, 1);
    int line2Chars = 0;
    while(char c = sms.read()) {
      lcd.print(c);
      line2Chars++;
      
      if (line2Chars >= 15) {
        lcd.print("...");
        
        delay(2000);
        for (int i = 0; i < 15; i++) {
          lcd.scrollDisplayLeft();
          delay(200);
        }
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("TEXT MESSAGE FROM");
        lcd.print(senderNumber);
        lcd.setCursor(0, 1);  
        line2Chars = 0;
      }
    }
      
    sms.flush();
   
    delay(1000);
  }
}

void closeConnection(bool LCDOutput) {
  // Allow it to turn off...
  digitalWrite(MODEM_POWER, LOW);
  delay(500);
  
  theGSM3ShieldV1ModemCore.println("AT+QPOWD=1");
    
  if (LCDOutput) {
    lcd.clear();
    lcd.setCursor(0, 0);   
    lcd.print("Uploading...");
    lcd.setCursor(0, 1);   
    lcd.print("Discnecting Cell");
  }
  
  gsmAccess.shutdown();
  // Note - may take up to 10 seconds to actually turn off, but we can proceed to return/sleep/etc

  digitalWrite(3, LOW);   
  notConnected = true;
}