//V1.0 - Kevin Rye, 2015

#include <Wire.h>
#include <Sodaq_DS3231.h>
#include "Timer.h"
#include <EEPROM.h>

//saved alarm settings
byte savedHours;
int savedHoursAddress = 0;
byte savedMinutes;
int savedMinutesAddress = 1;
byte savedAlarmMode;
int savedAlarmModeAddress = 2;

//audio
const int amplifier = 3;
int numTones = 2;
int alarmTones[] = {800, 600 };
int chimeTones[] = {1200, 1200 };
int enableAmplifier = 2;

//shift register interface
int latchPin = 7; //pin connected to ST_CP of 74HC595
int clockPin = 6; //pin connected to SH_CP of 74HC595
int dataPin = 5; //pin connected to DS of 74HC595

//decimal/colon nixies
int enableLED1 = 8; //enable
int enableLED2 = 9; //enable
int enableLED3 = 10; //enable

//cathode poisioning prevention animation
int animationDigit = 0;

//set buttons
int hourMonthButton = A3;
int minDayButton = A2;
int alarmYearButton = A0;
int modeButton = A1;

boolean timeMode = true;
boolean alarmMode = false;

boolean saveNewAlarmHours = false;
boolean saveNewAlarmMinutes = false;
boolean saveNewAlarmStatus = false;

//for data out
byte DataOut1 = B11111111;
byte DataOut2 = B11111111;
byte DataOut3 = B11111111;

int hourdigit1 = 0; //xN:xx:xx
int hourdigit2 = 0; //Nx:xx:xx
int mindigit1 = 0; //xx:xN:xx
int mindigit2 = 0; //xx:Nx:xx
int secdigit1 = 0; //xx:xx:xN
int secdigit2 = 0; //xx:xx:Nx

//RTC data
int hours = 12;
int hh = 0;
int minutes = 0;
int seconds = 0;
int dayOfMonth = 1;
int month = 1;
int year = 15;
int dayOfWeek = 1; //not used, but given a value for push to RTC function

//alarm
bool setAlarmActive = false; //set up default condition, will be saved or recalled from EEPROM
int alarmHours = 0; //set up default condition
int adjAlarmHours;
int alarmMinutes = 0; //set up default condition
boolean alarmIsActive = false; //when the alarm is actually sounding

void setup () {

  pinMode(latchPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  pinMode(dataPin, OUTPUT);

  pinMode(enableLED1, OUTPUT);
  pinMode(enableLED2, OUTPUT);
  pinMode(enableLED3, OUTPUT);

  pinMode(enableAmplifier, OUTPUT);
  digitalWrite(enableAmplifier, LOW);

  Serial.begin(57600);
  Wire.begin();
  rtc.begin();

  //pull in alarm time from EEPROM
  //read saved hours for alarm from EEPROM
  savedHours = EEPROM.read(savedHoursAddress);
  if (savedHours != 0) {
    alarmHours = savedHours;
  }
  else {
    alarmHours = 0;
  }

  //read saved minutes for alarm from EEPROM
  savedMinutes = EEPROM.read(savedMinutesAddress);
  if (savedMinutes != 0) {
    alarmMinutes = savedMinutes;
  }
  else {
    alarmMinutes = 0;
  }

  //read saved alarm mode from EEPROM
  savedAlarmMode = EEPROM.read(savedAlarmModeAddress);
  if (savedAlarmMode == 0) { //0 = NO //1 = YES
    setAlarmActive = false;
  }
  else {
    setAlarmActive = true;
  }
} //end setup

void loop () {

  //poll the DS3231 for the date and time
  get_time();
  get_date();

  DispTimeOrDate();

  //enable PM indicator
  if ((hours > 11) && (timeMode == true)) {
    digitalWrite(enableLED1, HIGH);
  }
  else {
    digitalWrite(enableLED1, LOW);  //in date mode... disable
  }

  //enable colon nixies for time or date
  if (timeMode == true) {
    digitalWrite(enableLED2, HIGH);
    digitalWrite(enableLED3, HIGH);
  }
  else { //in date mode... display decimals
    digitalWrite(enableLED2, LOW);
    digitalWrite(enableLED3, LOW);
  }

  //handle button presses
  //in timeMode
  //hour/month button pressed
  if ((digitalRead(hourMonthButton) == LOW) && (timeMode == true)) {

    //debounce
    delay(200);

    hours++;

    if (hours > 23) {
      hours = 0;
    }

    set_time();
  }

  //min/day button pressed
  if ((digitalRead(minDayButton) == LOW) && (timeMode == true)) {

    //debounce
    delay(200);

    minutes++;
    seconds = 0;

    if (minutes > 59) {
      minutes = 0;
    }

    set_time();
  }

  //alarm/year button pressed
  if ((digitalRead(alarmYearButton) == LOW) && (timeMode == true)) {

    //debounce
    delay(200);

    timeMode = false; //exit time mode
    alarmMode = true; //enter alarm mode
  }

  //mode button pressed
  if ((digitalRead(modeButton) == LOW) && (timeMode == true)) {

    //debounce
    delay(200);

    //setting time and alarm modes to false enters date mode
    timeMode = false;
    alarmMode = false;
  }

  //in alarmMode
  if (alarmMode == true) {

    //adjust alarm time for 12 hours
    adjAlarmHours = alarmHours;

    if (adjAlarmHours > 12) {
      adjAlarmHours = alarmHours - 12;
    }
    if (adjAlarmHours == 0) {
      adjAlarmHours = 12;
    }

    //enable am/pm nixie as appropriate
    if (alarmHours > 11) {
      digitalWrite(enableLED1, HIGH);
    }
    else {
      digitalWrite(enableLED1, LOW);
    }

    //turn nixies decimals on/off to show alarm status
    if (setAlarmActive == true) {
      digitalWrite(enableLED2, HIGH);
      digitalWrite(enableLED3, HIGH);
    } else { //alarm is not active
      digitalWrite(enableLED2, LOW);
      digitalWrite(enableLED3, LOW);
    }
  }

  //hour/month button pressed
  if ((digitalRead(hourMonthButton) == LOW) && (alarmMode == true)) {

    //debounce
    delay(200);

    alarmHours++;

    if (alarmHours > 23) {
      alarmHours = 0;
    }

    if (alarmHours != savedHours ) {

      saveNewAlarmHours = true;
      writeToEEPROM();
    }
  }

  //min/day button pressed
  if ((digitalRead(minDayButton) == LOW) && (alarmMode == true)) {

    //debounce
    delay(200);

    alarmMinutes++;

    if (alarmMinutes > 59) {
      alarmMinutes = 0;
    }

    if (alarmMinutes != savedMinutes ) {

      saveNewAlarmMinutes = true;
      writeToEEPROM();
    }
  }

  //alarm/year button pressed
  if ((digitalRead(alarmYearButton) == LOW) && (alarmMode == true)) {

    //debounce
    delay(200);

    //toggle alarm status
    setAlarmActive = !setAlarmActive;

    //save new setting
    saveNewAlarmStatus = true;
    writeToEEPROM();
  }

  //mode button pressed
  if ((digitalRead(modeButton) == LOW) && (alarmMode == true)) {

    //debaounce
    delay(200);

    timeMode = true; //enter timeMode
    alarmMode = false; //exit alarmMode
  }

  //in dateMode
  //hour/month button pressed
  if ((digitalRead(hourMonthButton) == LOW) && (timeMode == false) && (alarmMode == false)) {

    //debounce
    delay(200);

    //set month
    month++;
    if (month > 12) month = 1;

    //send update to RTC
    set_date();
  }

  //min/day button pressed
  if ((digitalRead(minDayButton) == LOW) && (timeMode == false) && (alarmMode == false)) {

    //debounce
    delay(200);

    //set day
    dayOfMonth++;

    //if feb
    if (month == 2) {
      if (dayOfMonth > 28) dayOfMonth = 1;
    }

    //if leap year
    //is it worth it?

    //if month has 30 days: Apr, Jun, Sep, Nov
    if ((month == 4) || (month == 6) || (month == 9) || (month == 11))  {
      if (dayOfMonth > 30) dayOfMonth = 1;
    }

    //if month has 31 days: Jan, Mar, May, Jul, Aug, Oct, Dec
    if ((month == 1) || (month == 3) || (month == 5) || (month == 7) || (month == 8) || (month == 10)) {
      if (dayOfMonth > 31) dayOfMonth = 1;
    }

    //send update to RTC
    set_date();
  }

  //alarm/year button pressed
  if ((digitalRead(alarmYearButton) == LOW) && (timeMode == false) && (alarmMode == false)) {

    //debounce
    delay(200);

    //set year
    year++;

    if (year > 99) year = 0;

    //send update to RTC
    set_date();
  }

  //mode button pressed
  if ((digitalRead(modeButton) == LOW) && (timeMode == false) && (alarmMode == false)) {

    //debounce
    delay(200);

    timeMode = true; //enter timeMode
    //alarmMode = false; no need, alarmMode already false
  }

  //chime on hour
  //chime only when in time mode and when not alarming
  if ((minutes == 0) && (seconds < 1) && (timeMode == true) && (alarmIsActive == false) ) {

    Serial.println("chime active");

    for (int i = 0; i < numTones; i++)
    {
      digitalWrite(enableAmplifier, HIGH);
      tone(amplifier, chimeTones[i]);
      delay(200);
    }
    noTone(amplifier);
    digitalWrite(enableAmplifier, LOW);
  }

  //handle alarm
  //alarm only when in normal mode and when alarm is active
  if ((adjAlarmHours == hh) && (alarmMinutes == minutes) && (seconds < 20)) {

    if ((setAlarmActive == true) && (timeMode == true) ) {

      alarmIsActive = true;
      Serial.println("alarm active");

      for (int i = 0; i < numTones; i++)
      {
        digitalWrite(enableAmplifier, HIGH);
        tone(amplifier, alarmTones[i]);
        delay(200);
      }
        } 
  } else {
    alarmIsActive = false;
    noTone(amplifier);
  }
  
  if (alarmIsActive == true) {
    digitalWrite(enableAmplifier, HIGH);
  } else {
    digitalWrite(enableAmplifier, LOW);
     }
} //end loop

// Displays the time
void DispTimeOrDate() {

  //pass RTC values into a new variable -
  //this way one variable stores time in 24 hour mode and another that can be converted to 12 hour mode
  //time is monitored 'in the background' in 24 hour mode using the RTC, but is displayed in an adjusted 12-hr format
  hh = hours;

  //convert to 12 hr mode for display
  if (hours < 1) {
    hh = 12;
  }

  if ((hours > 12) && (hours < 24)) {
    hh = hours - 12;
  }

  if ((timeMode == true) && (alarmMode == false)) {

    //display time
    //Serial.println("display time");
    hourdigit1 = (hh) / 10;
    hourdigit2 = (hh) % 10;
    mindigit1 = (minutes) / 10;
    mindigit2 = (minutes) % 10;
    secdigit1 = (seconds) / 10;
    secdigit2 = (seconds) % 10;

    SetDigit(6, hourdigit2);
    SetDigit(5, hourdigit1);
    SetDigit(4, mindigit2);
    SetDigit(3, mindigit1);
    SetDigit(2, secdigit2);
    SetDigit(1, secdigit1);
  }

  if ((timeMode == false) && (alarmMode == true)) {

    //display alarm time
    //Serial.println("display alarm time");
    hourdigit1 = (adjAlarmHours) / 10;
    hourdigit2 = (adjAlarmHours) % 10;
    mindigit1 = (alarmMinutes) / 10;
    mindigit2 = (alarmMinutes) % 10;
    secdigit1 = 0;
    secdigit2 = 0;

    SetDigit(6, hourdigit2);
    SetDigit(5, hourdigit1);
    SetDigit(4, mindigit2);
    SetDigit(3, mindigit1);
    SetDigit(2, secdigit2);
    SetDigit(1, secdigit1);
  }

  if ((timeMode == false) && (alarmMode == false)) { //in date mode

    //display date
    //Serial.println("display date");
    hourdigit1 = (month) / 10;
    hourdigit2 = (month) % 10;
    mindigit1 = (dayOfMonth) / 10;
    mindigit2 = (dayOfMonth) % 10;
    secdigit1 = (year) / 10;
    secdigit2 = (year) % 10;

    SetDigit(6, hourdigit2);
    SetDigit(5, hourdigit1);
    SetDigit(4, mindigit2);
    SetDigit(3, mindigit1);
    SetDigit(2, secdigit2);
    SetDigit(1, secdigit1);
  }

  //cathode poisioning prevention
  //animation runs from 2:00AM to 2:05AM
  if ((timeMode == true) && (alarmMode == false) && (hours == 2) && (minutes < 5)) {

    //have all the nixies count from 0-9 over and over again
    //animationDigit is initially set to 0 at power up
    SetDigit(6, animationDigit);
    SetDigit(5, animationDigit);
    SetDigit(4, animationDigit);
    SetDigit(3, animationDigit);
    SetDigit(2, animationDigit);
    SetDigit(1, animationDigit);

    //slow things down just a hair
    delay(200);
  }

  shiftOut();

  //increase +1 up to 9 and wrap back to 0
  animationDigit++;
  if (animationDigit > 9) {
    animationDigit = 0;
  }
}

// Sets a digit of the display. Code from Nixie-Bot by Scott-Bot.com, 2013
void SetDigit(int Digit, int Number) {

  byte BinNum;

  if ( Number == 0) {
    //The number in binary + 1 (don't know why it's +1, but it is)
    BinNum = B1001;
  }
  else if ( Number == 1) {
    //The number in binarry + 1
    BinNum = B0000;
  }
  else if ( Number == 2) {
    //The number in binary + 1
    BinNum = B0001;
  }
  else if ( Number == 3) {
    //The number in binary + 1
    BinNum = B0010;
  }
  else if ( Number == 4) {
    //The number in binary + 1
    BinNum = B0011;
  }
  else if ( Number == 5) {
    //The number in binary + 1
    BinNum = B0100;
  }
  else if ( Number == 6) {
    //The number in binary + 1
    BinNum = B0101;
  }
  else if ( Number == 7) {
    //The number in binary + 1
    BinNum = B0110;
  }
  else if ( Number == 8) {
    //The number in binary + 1
    BinNum = B0111;
  }
  else if ( Number == 9) {
    //The number in binary + 1
    BinNum = B1000;
  }
  else {
    //invalid number (blank display)
    BinNum = B1010;
  }

  if ( Digit == 1) {
    DataOut1 &= B00001111;
    DataOut1 |= (BinNum << 4);
  }
  else if ( Digit == 2) {
    DataOut1 &= B11110000;
    DataOut1 |= BinNum;
  }
  else if ( Digit == 3) {
    DataOut2 &= B00001111;
    DataOut2 |= (BinNum << 4);
  }
  else if ( Digit == 4) {
    DataOut2 &= B11110000;
    DataOut2 |= BinNum;
  }
  else if ( Digit == 5) {
    DataOut3 &= B00001111;
    DataOut3 |= (BinNum << 4);
  }
  else if ( Digit == 6) {
    DataOut3 &= B11110000;
    DataOut3 |= BinNum;
  }
}

// Shifts out the data to the shift registers. Code from Nixie-Bot by Scott-Bot.com, 2013
void shiftOut() {
  // This shifts 8 bits out MSB first,
  //on the rising edge of the clock,
  //clock idles low

  //internal function setup
  int i = 0;
  int pinState;

  //clear everything out just in case to
  //prepare shift register for bit shifting
  //ground latchPin and hold low for as long as you are transmitting
  digitalWrite(latchPin, 0);
  digitalWrite(dataPin, 0);
  digitalWrite(clockPin, 0);

  //for each bit in the byte myDataOut
  //NOTICE THAT WE ARE COUNTING DOWN in our for loop
  //This means that %00000001 or "1" will go through such
  //that it will be pin Q0 that lights.
  for (i = 7; i >= 0; i--)  {
    digitalWrite(clockPin, 0);

    //if the value passed to myDataOut and a bitmask result
    // true then... so if we are at i=6 and our value is
    // %11010100 it would the code compares it to %01000000
    // and proceeds to set pinState to 1.
    if ( DataOut1 & (1 << i) ) {
      pinState = 1;
    }
    else {
      pinState = 0;
    }

    //Sets the pin to HIGH or LOW depending on pinState
    digitalWrite(dataPin, pinState);
    //register shifts bits on upstroke of clock pin
    digitalWrite(clockPin, 1);
    //zero the data pin after shift to prevent bleed through
    digitalWrite(dataPin, 0);
  }

  //stop shifting
  digitalWrite(clockPin, 0);

  // Shift the second word
  for (i = 7; i >= 0; i--)  {
    digitalWrite(clockPin, 0);

    if ( DataOut2 & (1 << i) ) {
      pinState = 1;
    }
    else {
      pinState = 0;
    }

    digitalWrite(dataPin, pinState);
    digitalWrite(clockPin, 1);
    digitalWrite(dataPin, 0);
  }

  digitalWrite(clockPin, 0);

  // Shift the third word
  for (i = 7; i >= 0; i--)  {
    digitalWrite(clockPin, 0);

    if ( DataOut3 & (1 << i) ) {
      pinState = 1;
    }
    else {
      pinState = 0;
    }

    digitalWrite(dataPin, pinState);
    digitalWrite(clockPin, 1);
    digitalWrite(dataPin, 0);
  }

  digitalWrite(clockPin, 0);
  digitalWrite(latchPin, 1);
}

//DS3231
void set_date() {

  Wire.beginTransmission(104);
  Wire.write(3);
  Wire.write(decToBcd(dayOfWeek));
  Wire.write(decToBcd(dayOfMonth));
  Wire.write(decToBcd(month));
  Wire.write(decToBcd(year));
  Wire.endTransmission();
}

void get_date() {

  Wire.beginTransmission(104);
  Wire.write(3);//set register to 3 (day)
  Wire.endTransmission();
  Wire.requestFrom(104, 4); //get 5 bytes(day,date,month,year,control);
  dayOfWeek   = bcdToDec(Wire.read());
  dayOfMonth  = bcdToDec(Wire.read());
  month = bcdToDec(Wire.read());
  year  = bcdToDec(Wire.read());
}

void set_time() {

  Wire.beginTransmission(104);
  Wire.write(0);
  Wire.write(decToBcd(seconds));
  Wire.write(decToBcd(minutes));
  Wire.write(decToBcd(hours));
  Wire.endTransmission();
}

void get_time() {

  Wire.beginTransmission(104);
  Wire.write(0);//set register to 0
  Wire.endTransmission();
  Wire.requestFrom(104, 3);//get 3 bytes (seconds,minutes,hours);
  seconds = bcdToDec(Wire.read() & 0x7f);
  minutes = bcdToDec(Wire.read());
  hours = bcdToDec(Wire.read() & 0x3f);
}

//save settings to EEPROM
void writeToEEPROM() {

  //only save the settings that were changed

  //save alarm hours if it was changed
  if (saveNewAlarmHours == true) {
    EEPROM.write(savedHoursAddress, alarmHours);
  }

  //save alarm minutes if it was changed
  if (saveNewAlarmMinutes == true) {
    EEPROM.write(savedMinutesAddress, alarmMinutes);
  }

  //save alarm status if it was changed
  if (saveNewAlarmStatus == true) {
    EEPROM.write(savedAlarmModeAddress, savedAlarmMode);
  }

  //set flags back to false, so subsequent changes will also be saved
  saveNewAlarmHours = false;
  saveNewAlarmMinutes = false;
  saveNewAlarmStatus = false;
}

//DS3231 interface
byte decToBcd(byte val) {
  return ( (val / 10 * 16) + (val % 10) );
}

byte bcdToDec(byte val) {
  return ( (val / 16 * 10) + (val % 16) );
}
