////////////////////////////////////////////////////////////////////////////////////////////////////
// Analog Clock Box - Tell the correct time(s) and the lock opens.
////////////////////////////////////////////////////////////////////////////////////////////////////

// Change "MAX_ANSWERS" according the expected number of correct answers, before the lock opens.
#define MAX_ANSWERS 3

#include <SPI.h>
#include <Adafruit_GFX.h>    
#include <Adafruit_ST7735.h> 
#include <Servo.h>

#define PI 3.1415926535897932384626433832795

#define MINI_TFT_BLUE  ST7735_BLUE
#define MINI_TFT_RED   ST7735_RED
#define MINI_TFT_WHITE ST7735_WHITE
#define MINI_TFT_BLACK ST7735_BLACK

#define PIN_TFT_CS  10
#define PIN_TFT_DC   7
#define PIN_TFT_RES -1
#define PIN_BTN_1    2
#define PIN_BTN_2    3
#define PIN_BTN_3    4
#define PIN_BTN_4    5
#define PIN_TONE     6
#define PIN_SERVO    9

#define CLOCK_CENTER_X 80
#define CLOCK_CENTER_Y 55


int MINI_TFT_GREEN;
int MINI_TFT_GRAY1;
int MINI_TFT_GRAY2;
int MINI_TFT_GRAY3;  
int MINI_TFT_BLUE1;
int MINI_TFT_RED1;
int MINI_TFT_PINK1;

int correctID;
int correctAnswers = 0;
char textBuffer[32];
int hours[4];
int minutes[4];


Adafruit_ST7735 tft = Adafruit_ST7735(PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RES);
Servo lockServo;
////////////////////////////////////////////////////////////////////////////////////////////////////
// Set-up the Arduino and the TFT display.
////////////////////////////////////////////////////////////////////////////////////////////////////
void setup(){
  Serial.begin(9600);
  // Initialize the random generator, make it purely random by getting the seed from the noise of an
  // unconnected analog input.
  randomSeed(analogRead(A3));
  // Set the button pins to input, using the internal pull-up resistor.
  pinMode(PIN_BTN_1, INPUT_PULLUP); 
  pinMode(PIN_BTN_2, INPUT_PULLUP); 
  pinMode(PIN_BTN_3, INPUT_PULLUP); 
  pinMode(PIN_BTN_4, INPUT_PULLUP); 
  // Generate some colors for the TFT.
  MINI_TFT_GRAY1 = convert2RGB(0x40, 0x40, 0x40);
  MINI_TFT_GRAY2 = convert2RGB(0x60, 0x60, 0x60);
  MINI_TFT_GRAY3 = convert2RGB(0xA0, 0xA0, 0xA0);
  MINI_TFT_BLUE1 = convert2RGB(0xB0, 0xB0, 0xFF);
  MINI_TFT_PINK1 = convert2RGB(0xFF, 0xB0, 0xB0);
  MINI_TFT_GREEN = convert2RGB(0xA0, 0xFF, 0xA0);
  MINI_TFT_RED1  = convert2RGB(0xFF, 0xB0, 0xB0);
  // Initualize the display.
  // Change to INITR_GREENTAB if the picture shows an offset or the colors appear incorrect.
  tft.initR(INITR_BLACKTAB); 
  tft.setRotation(1);
  tft.setTextWrap(false);
  tft.setTextSize(1);
  tft.fillScreen(MINI_TFT_PINK1);
  // Play intro
  tone(PIN_TONE, 660, 200);
  delay(300);
  tone(PIN_TONE, 660, 100);
  delay(150);
  tone(PIN_TONE, 660, 100);
  delay(150);
  tone(PIN_TONE, 660, 200);
  delay(300);
  // Show the first time.
  createTimesAndPaintClock();
  // Initialize the servo and make sure the lock is closed.
  lockServo.write(0);
  lockServo.attach(PIN_SERVO);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Process user input.
////////////////////////////////////////////////////////////////////////////////////////////////////
void loop(){
  int i;
  int btnPressed;  

  // Do nothing, until __one__ button is pressed.
  btnPressed = -1;  
  for(i = 0; i < 4; i++){
    // INPUT_PULLUP --> LOW == pressed; buttons are soldered in reverse order to the display.
    if(digitalRead(PIN_BTN_4 - i) == LOW){ 
      if(btnPressed == -1){
        btnPressed = i;
      }
      else{ // More than one button is currently pressed.
        delay(10); 
        return;        
      }
    }
  }
  if(btnPressed == -1){ // No button is currently pressed.
    delay(10);
    return;
  }
  // Show the correct results.
  sprintf(textBuffer, "%02d:%02d", hours[correctID], minutes[correctID]);
  drawCenteredText(textBuffer, 21 + 40 * correctID, 115, MINI_TFT_GREEN);
  // Check whether the correct result matches the pressed button.
  if(correctID == btnPressed){
    correctAnswers++;
    if(correctAnswers != MAX_ANSWERS){
      // Play "positive" sound. 
      tone(PIN_TONE, 247, 250);
      delay(250);
      tone(PIN_TONE, 330, 300);
      delay(1000);
    }
    else{
      // Show that the required number of correct answers is reached.
      sprintf(textBuffer, "%d/%d", correctAnswers, MAX_ANSWERS);
      drawCenteredText(textBuffer, 140, 5, MINI_TFT_WHITE);
      // Play "fanfare". 
      tone(PIN_TONE, 330, 150);
      delay(150);
      tone(PIN_TONE, 415, 150);
      delay(150);
      tone(PIN_TONE, 494, 150);
      delay(150);
      tone(PIN_TONE, 660, 300);
      delay(300);
      // Wait for all buttons to be released.
      while((digitalRead(PIN_BTN_1) + digitalRead(PIN_BTN_2) + digitalRead(PIN_BTN_3) + digitalRead(PIN_BTN_4)) != 4){
        delay(10);
      }
      // Open the lock. 
      lockServo.write(90);
      // Wait for any button to be pressed.
      while((digitalRead(PIN_BTN_1) + digitalRead(PIN_BTN_2) + digitalRead(PIN_BTN_3) + digitalRead(PIN_BTN_4)) == 4){
        delay(10);
      }
      // Close the lock and reset the counter.
      lockServo.write(0);
      correctAnswers = 0;
    }
  }
  else{
    // Highlight the wrong result that was selected by the button.
    sprintf(textBuffer, "%02d:%02d", hours[btnPressed], minutes[btnPressed]);
    drawCenteredText(textBuffer, 21 + 40 * btnPressed, 115, MINI_TFT_RED1);
    // Play "negative" sound. 
    tone(PIN_TONE, 370, 250);
    delay(250);
    tone(PIN_TONE, 262, 300);
    delay(1000);
    correctAnswers = 0;
  }
  createTimesAndPaintClock();   // Create new set-up.
  // Wait for all buttons to be released.
  while((digitalRead(PIN_BTN_1) + digitalRead(PIN_BTN_2) + digitalRead(PIN_BTN_3) + digitalRead(PIN_BTN_4)) != 4){
    delay(10);
  }
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////
// Create a new set-up of times and paint the analog clock.
////////////////////////////////////////////////////////////////////////////////////////////////////
void createTimesAndPaintClock(){
  int i, k;
  double x1, y1, x2, y2;
  double hoursFraction; 
  bool hourIsUnique;

  tft.fillScreen(MINI_TFT_BLUE1);
  // Chose the time to be displayed in the analog clock.
  correctID = random(4);
  // Generate for random times.
  for(i = 0; i < 4; i++){
    hours[i] = random(24);
    minutes[i] = random(60);
    // Check whether hours are different in each set.
    // If one hour appears twice, substitute it by a new random number.
    hourIsUnique = false;
    while(!hourIsUnique){
      hourIsUnique = true; // Assume the current choice is ok.
      for(k = 0; k < i; k++){
        if(hours[i]%12 == hours[k]%12){
          hourIsUnique = false;   // Found same hour setting twice.
          hours[i] = random(24);  // Generate new number.
          break;                  // Restart check.
        }
      }
    }
  }
  // Paint frame and background.
  tft.drawCircle(CLOCK_CENTER_X, CLOCK_CENTER_Y, 51, MINI_TFT_GRAY2);
  tft.drawCircle(CLOCK_CENTER_X, CLOCK_CENTER_Y, 50, MINI_TFT_GRAY3);
  tft.fillCircle(CLOCK_CENTER_X, CLOCK_CENTER_Y, 49, MINI_TFT_WHITE);
  // Ticks.
  for(i = 0; i < 360; i += 30){
    x1 = 49 * sin((double)i * PI / 180) + CLOCK_CENTER_X;
    x2 = 40 * sin((double)i * PI / 180) + CLOCK_CENTER_X;
    y1 = 49 * cos((double)i * PI / 180) + CLOCK_CENTER_Y;
    y2 = 40 * cos((double)i * PI / 180) + CLOCK_CENTER_Y;
    // Correct rounding errors.
    if(abs(y2 - y1) < 1){
      y2 = y1;      
    }
    if(abs(x2 - x1) < 1){
      x2 = x1;      
    }
    tft.drawLine(x1, y1, x2, y2, MINI_TFT_GRAY1);
  }
  // Paint the little pointer (hours).
  x1 = CLOCK_CENTER_X;
  y1 = CLOCK_CENTER_Y;
  hoursFraction = (double)hours[correctID] + double(minutes[correctID]) / 60;
  x2 = 25 * sin(hoursFraction * PI * 30 / 180) + CLOCK_CENTER_X;
  y2 = -25 * cos(hoursFraction * PI * 30 / 180) + CLOCK_CENTER_Y;
  // Correct rounding errors (full, half and quarter hours should result in straight lines).
  if(abs(y2 - y1) < 1){
    y2 = y1;      
  }
  if(abs(x2 - x1) < 1){
    x2 = x1;      
  }
  tft.drawLine(x1, y1, x2, y2, MINI_TFT_BLACK);
  // Paint the big pointer (minutes).
  x1 = CLOCK_CENTER_X;
  y1 = CLOCK_CENTER_Y;
  x2 = 35 * sin((double)minutes[correctID] * PI * 6 / 180) + CLOCK_CENTER_X;
  y2 = -35 * cos((double)minutes[correctID] * PI * 6 / 180) + CLOCK_CENTER_Y;
  // Correct rounding errors.
  if(abs(y2 - y1) < 1){
    y2 = y1;      
  }
  if(abs(x2 - x1) < 1){
    x2 = x1;      
  }
  tft.drawLine(x1, y1, x2, y2, MINI_TFT_BLACK);
  // Show the suggested times.
  for(i = 0; i < 4; i++){
    sprintf(textBuffer, "%02d:%02d", hours[i], minutes[i]);
    drawCenteredText(textBuffer, 21 + 40 * i, 115, MINI_TFT_WHITE);
  }
  // Show number of correct anwers given so far.
  sprintf(textBuffer, "%d/%d", correctAnswers, MAX_ANSWERS);
  drawCenteredText(textBuffer, 140, 5, MINI_TFT_WHITE);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Convert a color to RGB565.
////////////////////////////////////////////////////////////////////////////////////////////////////
word convert2RGB(byte R, byte G, byte B){
  return (((R & 0xF8) << 8) | ((G & 0xFC) << 3) | (B >> 3));
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Draw text center-aligned at given position.
////////////////////////////////////////////////////////////////////////////////////////////////////
void drawCenteredText(const char* text, int x, int y, int background){
  int16_t x1, y1;
  uint16_t w, h;
  tft.getTextBounds(text, x, y, &x1, &y1, &w, &h); 
  tft.setCursor(x - w / 2, y);
  tft.setTextColor(MINI_TFT_BLACK, background);
  tft.print(text);
  tft.drawRect(x - w / 2 - 1, y - 1, w + 1, h + 1, background);
  tft.drawRect(x - w / 2 - 2, y - 2, w + 3, h + 3, MINI_TFT_GRAY2);
}
