

/* Binary Calculator
 **********************************************************************************
 *
 * Author: Dennis Letkeman
 * Date: May 27 2020
 * 
 * This program is for a binary calculator. It can also function as a binary clock.
 * THe hardware consists of a ATMega328p, six puch buttons, an MCP7940N RTC, and 
 * a TLC59282 16-channel constant current LED driver.
 * 
 * GNU General Public License v3.0
 * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details. You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */


#include <MCP7940.h>

static const uint8_t NUM_BUTTONS = 6;

static const uint8_t CLEAR = 0;
static const uint8_t ZERO = 1;
static const uint8_t ONE = 2;
static const uint8_t MINUS = 3;
static const uint8_t PLUS = 4;
static const uint8_t EQUAL = 5;

// Push button pins
static const uint8_t CLEAR_PIN = 2;
static const uint8_t ZERO_PIN = 3;
static const uint8_t ONE_PIN = 5;
static const uint8_t MINUS_PIN = 4;
static const uint8_t PLUS_PIN = 7;
static const uint8_t EQUAL_PIN = 8;

static const uint8_t NONE = 0;
static const uint8_t SUB = 1;
static const uint8_t ADD = 2;
static const uint8_t DIV = 3;
static const uint8_t MULT = 4;

// TLC59282 pins
static const uint8_t SIN_PIN = 17;
static const uint8_t SCLK_PIN = 16;
static const uint8_t LATCH_PIN = 14;
static const uint8_t BLANK_PIN = 10;

// RTC MFP
static const uint8_t MFP_PIN = 15;

const uint8_t pinButtons[] = {CLEAR_PIN, ZERO_PIN, ONE_PIN, MINUS_PIN, PLUS_PIN, EQUAL_PIN};
unsigned char dispBuff[2] = {0, 0};

unsigned long blinkTimer = 0;
unsigned int blinkTime = 500;
static const uint8_t OFF = 0;
static const uint8_t HOURS = 1;
static const uint8_t MINUTES = 2;
unsigned char clockSet = 0; // 0 = off, 1 = hours, 2 = minutes
static uint8_t secs;

unsigned char brightness = 255;

static const boolean CLOCK = false;
static const boolean CALC = true;
boolean machineState = CLOCK; // 1 = calculator, 0 = clock
uint8_t calcState = 0; // 1 = addtion, 0 = subtraction, 2 = divsion, 3 = multiplcation
//uint8_t numbers[2] = {0, 0};
uint8_t operation = 0; // 0 = none, 1 = add, 2 = subtraction, 3 = multiplcation, 4 = division
bool readyNew = 1;


//Button classs, encapsulate the functionality of a latched button
class Button {
    protected:
        uint8_t _pin;                  //hardware pin number
        boolean _state;                //current pin state
        boolean _isPressed;            //
        boolean _wasPressed;           //
        boolean _isHeld;               //
        boolean _wasHeld;              //
        uint16_t _startDebounce;       //start of debounce time
        
    public:
        uint16_t debounceMs;           //period before button latches
        uint16_t holdMs;               //period before a press is considered a hold
        
        Button(uint8_t pin);           //construct the button
        boolean wasPressed(void);      //return the latch state
        boolean isHeld(void);      //return the held state
        boolean wasHeld(void);      //return the held state
        void reset(void);              //reset the latch
        void poll(uint16_t now);       //call periodically to refresh the button state
};

Button buttons[6] = {CLEAR_PIN, ZERO_PIN, ONE_PIN, MINUS_PIN, PLUS_PIN, EQUAL_PIN};
MCP7940_Class MCP7940;

void setup()
{
  Serial.begin(115200);
  
  clearBuff();

  while (!MCP7940.begin()) {                                                  // Initialize RTC communications    //
    Serial.println(F("Unable to find MCP7940N. Checking again in 3s."));      // Show error text                  //
    delay(3000);                                                              // wait a second                    //
  }
  Serial.println(F("MCP7940N initialized."));                                 //                                  //
  while (!MCP7940.deviceStatus()) {                                           // Turn oscillator on if necessary  //
    Serial.println(F("Oscillator is off, turning it on."));                   //                                  //
    bool deviceStatus = MCP7940.deviceStart();                                // Start oscillator and return state//
    if (!deviceStatus) {                                                      // If it didn't start               //
      Serial.println(F("Oscillator did not start, trying again."));           // Show error and                   //
      delay(1000);                                                            // wait for a second                //
    } // of if-then oscillator didn't start                                   //                                  //
  } // of while the oscillator is off                                         //                                  //
  Serial.println(F("Enabling battery backup mode"));                          //                                  //
  if ( !MCP7940.getBattery() ) MCP7940.setBattery(true);
  if ( MCP7940.getPowerFail() ) MCP7940.clearPowerFail();
  Serial.println("Setup done.");
 
  pinMode(SIN_PIN, OUTPUT);
  digitalWrite(SIN_PIN,LOW);
  pinMode(SCLK_PIN, OUTPUT);
  digitalWrite(SCLK_PIN,LOW);
  pinMode(BLANK_PIN, OUTPUT);
  digitalWrite(BLANK_PIN, LOW);
  pinMode(LATCH_PIN, OUTPUT);
  digitalWrite(LATCH_PIN,LOW);

}


void loop()
{
  uint16_t now = millis();
  for (int i = 0; i < NUM_BUTTONS; i++)
  {
    buttons[i].poll(now);
  }
  
  // Check state
  if ( machineState == CALC ) // Calculator mode
  {  
    calcMode();
  }
  else // Clock mode
  {
    DateTime timeNow = MCP7940.now();                                               // get the current time             //
    if (secs != timeNow.second() && !clockSet )
    {
      secs = timeNow.second();
      dispBuff[0] = timeNow.minute();
      dispBuff[1] = timeNow.hour();
    
    }
    clockMode();
  }
  ledOut();
  if ( clockSet ) blinkCheck();
  //delay(10);
}

void blinkCheck()
{
  DateTime timeNow = MCP7940.now();
  if ( blinkTimer < millis() )
  {
    blinkTimer = millis() + blinkTime;
    if ( clockSet == HOURS )
    {
      if ( dispBuff[1] )
      {
        dispBuff[1] = 0; // Turn LEDs OFF
      }
      else dispBuff[1] = timeNow.hour(); // Turn LEDs ON
    }
    else // minutes
    {
      if ( dispBuff[0] )
      {
        dispBuff[0] = 0; // Turn LEDs OFF
      }
      else dispBuff[0] = timeNow.minute(); // Turn LEDs ON
    }
  }
}

void calcMode()
{
  // first see if any combination of buttons were pressed/ held for known options
  if ( buttons[CLEAR].isHeld() && buttons[EQUAL].isHeld() )
  {
    buttons[CLEAR].reset();
    buttons[EQUAL].reset();
    clearBuff();
    machineState = CLOCK;
  }
  // Now test for indivual button presses/ holds
  
  if ( buttons[CLEAR].wasPressed() )
  {
    buttons[CLEAR].reset();
    resetCalc();
  }
  else if ( buttons[CLEAR].wasHeld() )
  {
    buttons[CLEAR].reset();
    resetCalc();
  }
  else if ( buttons[ZERO].wasPressed() )
  {
    buttons[ZERO].reset();
    if ( readyNew )
    {
      dispBuff[0] = 0;
      readyNew = 0;
      dispBuff[0] = dispBuff[0] << 1;
    }
    else dispBuff[0] = dispBuff[0] << 1;
  }
  else if ( buttons[ZERO].wasHeld() )
  {
    buttons[ZERO].reset();
    if ( readyNew )
    {
      dispBuff[0] = 0;
      readyNew = 0;
      dispBuff[0] = dispBuff[0] << 1;
    }
    else dispBuff[0] = dispBuff[0] << 1;
  }
  else if ( buttons[ONE].wasPressed() )
  {
    buttons[ONE].reset();
    if ( readyNew )
    {
      dispBuff[0] = 0;
      readyNew = 0;
      dispBuff[0] = dispBuff[0] << 1;
      dispBuff[0] = dispBuff[0] | 1;
    }
    else
    {
      dispBuff[0] = dispBuff[0] << 1;
      dispBuff[0] = dispBuff[0] | 1;
    }
  }
  else if ( buttons[ONE].wasHeld() )
  {
    buttons[ONE].reset();
    if ( readyNew )
    {
      dispBuff[0] = 0;
      readyNew = 0;
      dispBuff[0] = dispBuff[0] << 1;
      dispBuff[0] = dispBuff[0] | 1;
    }
    else
    {
      dispBuff[0] = dispBuff[0] << 1;
      dispBuff[0] = dispBuff[0] | 1;
    }
  }
  else if ( buttons[MINUS].wasPressed() )
  {
    buttons[MINUS].reset();
    perfOperation( SUB );
  }
  else if ( buttons[MINUS].wasHeld() )
  {
    buttons[MINUS].reset();
    perfOperation( DIV );
  }
  else if ( buttons[PLUS].wasPressed() )
  {
    buttons[PLUS].reset();
    perfOperation( ADD );
  }
  else if ( buttons[PLUS].wasHeld() )
  {
    buttons[PLUS].reset();
    perfOperation( MULT );
  }
  else if ( buttons[EQUAL].wasPressed() )
  {
    buttons[EQUAL].reset();
    readyNew = 1;
    if ( operation ) calculate();
    else dispBuff[1] = dispBuff[0];
  }
  else if ( buttons[EQUAL].wasHeld() )
  {
    buttons[EQUAL].reset();
    readyNew = 1;
    if ( operation ) calculate();
    else dispBuff[1] = dispBuff[0];
  }
  
}
void perfOperation( uint8_t operationIn )
{
  if ( !operation )
    {
      operation = operationIn;
    }
    if ( !readyNew )
    {
      calculate();
      readyNew = 1;
    }
    operation = operationIn;
}
void calculate()
{
  if ( dispBuff[1] )
  {
    // multiplucation
    if ( operation == MULT ) dispBuff[1] = dispBuff[1] * dispBuff[0];
    // division
    else if ( operation == DIV )
    {
      if ( dispBuff[0] == 0 )
      {
        // Error
        // flash all LEDs and reset calc
        resetCalc();
      }
      else dispBuff[1] = dispBuff[1] / dispBuff[0];
    }
    // addition
    else if ( operation == ADD ) dispBuff[1] = dispBuff[1] + dispBuff[0];
    // subtraction
    else  if ( operation == SUB ) dispBuff[1] = dispBuff[1] - dispBuff[0];
  }
  else
  {
    dispBuff[1] = dispBuff[0];
  }
}
void resetCalc()
{
  clearBuff();
  readyNew = 1;
  operation = NONE;
}
void clockMode()
{
  DateTime timeNow = MCP7940.now(); 
 // first see if any combination of buttons were pressed/ held for known options
  if ( buttons[CLEAR].isHeld() && buttons[EQUAL].isHeld() )
  {
    buttons[CLEAR].reset();
    buttons[EQUAL].reset();
    clearBuff();
    machineState = CALC;
  }
  // Now test for indivual button presses/ holds
  if ( buttons[CLEAR].wasPressed() )
  {
    buttons[CLEAR].reset();
  }
  else if ( buttons[CLEAR].wasHeld() )
  {
    buttons[CLEAR].reset();
    clockSet = HOURS; // set hours
  }
  else if ( buttons[ZERO].wasPressed() )
  {
    buttons[ZERO].reset();
  }
  else if ( buttons[ZERO].wasHeld() )
  {
    buttons[ZERO].reset();
    
  }
  else if ( buttons[ONE].wasPressed() )
  {
    buttons[ONE].reset();
  }
  else if ( buttons[ONE].wasHeld() )
  {
    buttons[ONE].reset();
  }
  else if ( buttons[MINUS].wasPressed() )
  {
    buttons[MINUS].reset();
    if ( clockSet == HOURS ) MCP7940.adjust(DateTime(timeNow.year(),timeNow.month(),timeNow.day(),timeNow.hour() - 1,timeNow.minute(),0));
    if ( clockSet == MINUTES ) MCP7940.adjust(DateTime(timeNow.year(),timeNow.month(),timeNow.day(),timeNow.hour(),timeNow.minute() - 1,0));
  }
  else if ( buttons[MINUS].wasHeld() )
  {
    buttons[MINUS].reset();
  }
  else if ( buttons[PLUS].wasPressed() )
  {
    buttons[PLUS].reset();
    if ( clockSet == HOURS ) MCP7940.adjust(DateTime(timeNow.year(),timeNow.month(),timeNow.day(),timeNow.hour() + 1,timeNow.minute(),0));
    if ( clockSet == MINUTES ) MCP7940.adjust(DateTime(timeNow.year(),timeNow.month(),timeNow.day(),timeNow.hour(),timeNow.minute() + 1,0));
  }
  else if ( buttons[PLUS].wasHeld() )
  {
    buttons[PLUS].reset();
  }
  else if ( buttons[EQUAL].wasPressed() )
  {
    buttons[EQUAL].reset();      
    if ( clockSet == HOURS )
    {
      clockSet = MINUTES;
      dispBuff[1] = timeNow.hour(); // Turn LEDs ON
    }
    else if ( clockSet == MINUTES )
    {
      MCP7940.adjust(DateTime(timeNow.year(),timeNow.month(),timeNow.day(),timeNow.hour(),timeNow.minute(),0));
      clockSet = OFF;
    }
  }
  else if ( buttons[EQUAL].wasHeld() )
  {
    buttons[EQUAL].reset();
  }
}

void ledOut()
{
  for (int j = 7; j >= 0; j--)
  {
    if( bitRead(dispBuff[1],j ))
    {
      digitalWrite(SIN_PIN, 1);
    }
    else
    {
      digitalWrite(SIN_PIN, 0);
    }
    digitalWrite(SCLK_PIN,1); // toggle clock
    digitalWrite(SCLK_PIN,0);
  }
  for (int j = 0; j <= 7; j++)
  {
    if( bitRead(dispBuff[0],j ))
    {
      digitalWrite(SIN_PIN, 1);
    }
    else
    {
      digitalWrite(SIN_PIN, 0);
    }
    digitalWrite(SCLK_PIN,1); // toggle clock
    digitalWrite(SCLK_PIN,0);

  }

  digitalWrite(LATCH_PIN,1); // latch data
  digitalWrite(LATCH_PIN,0);
}

// Clear display buffer
void clearBuff()
{
  for (int k = 0; k <= 1; k++)
  {
    dispBuff[k] = 0;
  }
}



//construct an instance of a latched button
Button::Button(uint8_t pin) {
  _pin = pin;
  
  //initial state
  _state = false;
  _startDebounce = false;
  _wasPressed = false;
  
  debounceMs = 50;              //default debounce time, we can change it later
  holdMs = 800;
  pinMode(_pin, INPUT_PULLUP);
}

//return the state of the latch
boolean Button::wasPressed(void) {
    return _wasPressed;
}

//return the state of the latch
boolean Button::wasHeld(void) {
    return _wasHeld;
}

//return the state of the latch
boolean Button::isHeld(void)
{
  return _isHeld;
}

//reset the latch
void Button::reset(void)
{
  _isPressed = false;
  _wasPressed = false;
  _isHeld = false;
  _wasHeld = false;
  _startDebounce = 0;
}


//refresh the button state
void Button::poll(uint16_t now){
  
  //pullup resistors cause the
  // button to be HIGH when open
  // so we invert the hardware state
  _state = !digitalRead(_pin);
  //if ( now - _startDebounce > time_out ) 
  //when the button state is false, reset the debounce time
  if(!_state)
  {
    if ( _isPressed )
    {
      if ( now - _startDebounce > holdMs )
      {
        _wasHeld = true;
      }
      else
      {
        _wasPressed = true;
      }
    }
    else _startDebounce = 0;
  }
  else {
    //start debounce time
    if(!_startDebounce) {
      _startDebounce = now;
    }
    else {
      //latch the button if it is still pressed when the debounce time expires
      if (now - _startDebounce > holdMs)
      {
        _isHeld = true;
      }
      else if (now - _startDebounce > debounceMs)
      {
        _isPressed = true;
      }
    }
  }
}
