/*
 * Projekt sekretnego pudełka przygotowany do Arduino 1.0 z układem Attiny85
 * Licencja GPL
 Wykrywa sekwencję pukania i jeżeli jest poprawna, uruchamia silnik otwierający pudełko
 
 Steve Hoefer http://grathio.com
 Wersja 0.1.10.20.10
 Licencja Creative Commons Attribution-Noncommercial-Share Alike 3.0
 http://creativecommons.org/licenses/by-nc-sa/3.0/us/
 (W skrócie: Możesz robić z kodem, co chcesz, ale umieść w nim cztery powyższe wiersze i nie sprzedawaj go w żadnej postaci bez porozumienia ze mną.)
 */
#include "SecretKnock.h"
#include "attinyservo.h"


SecretKnock::SecretKnock(void)
{
  // Definicje pinów
  const int programSwitch = 0;  // Jeżeli stan jest wysoki, programujemy nowy wzór.
  //const int servoPin = 1;  // Pin 4 to PWM
  knockSensor = 1;         // Czujnik piezoelektryczny na pinie ADC1.
  redLED = 3;              // Status LED
  greenLED = 4;            // Status LED
  //const int speakerPin = 2;

  // Stałe do tuningu. Mogą to być zmienne i skojarzone z potencjometrem do regulacji, itp.
  threshold = 500;         // Minimalny poziom sygnału z czujnika piezoelektrycznego oznaczający pukanie
  rejectValue = 25;        // Jeżeli wystukany szyfr różni się od wzorca o tyle procent, nie otwieramy pudełka.
  averageRejectValue = 15; // Jeżeli średni czas pukania różni się o tyle procent, nie otwieramy pudełka.
  knockFadeTime = 200;     // Czas w milisekundach na zaniknięcie puknięcia przed nasłuchiwaniem następnego (timer wytłumienia)
  lockTurnTime = 650;      // Czas w milisekundach na uruchomienie silnika i wykonanie połowy obrotu
  lockMotor = 2; 
  knockComplete = 1200;     // Maksymalny czas oczekiwania, po którym uznajemy, że pukanie się zakończyło


  // Zmienne
  // knockReadings[MAX_KNOCKS];   // Podczas pukania ta tabela jest wypełniana przerwami pomiędzy puknięciami.
   knockSensorValue = 0;           // Ostatni odczyt z czujnika pukania.
   programButtonPressed = false;   // Ustaw flagę, aby zapamiętać ustawienie przycisku na końcu cyklu
}

void SecretKnock::begin(int  initKnocks[])
{
  pinMode(greenLED, OUTPUT);
  pinMode(redLED, OUTPUT);
  pinMode(programSwitch, INPUT);
  
  digitalWrite(greenLED, HIGH);      // Zielona dioda LED zapalona, idziemy dalej
  delay(1000);
  digitalWrite(greenLED, LOW);      // Zielona dioda LED zgaszona, idziemy dalej
  delay(1000);
  knockReadings = initKnocks;
  servoBegin();
  moveServo(1.0, 1000);
  delay(1000);
}

void SecretKnock::checkKnock()
{
  // Nasłuchuj pukania
  knockSensorValue = analogRead(knockSensor);

  //Serial.println(knockSensorValue);

  if (digitalRead(programSwitch)==HIGH){  // Czy przycisk programowania jest naciśnięty?
    programButtonPressed = true;          // Tak, zapisz jego stan
    digitalWrite(redLED, HIGH);           // i zapal czerwoną diodę, aby było wiadomo, że trwa programowanie
  } 
  else {
    programButtonPressed = false;
    digitalWrite(redLED, LOW);
  }

  if (knockSensorValue >=threshold)
  {
    listenToSecretKnock();
  }
}

// Zapis sekwencji puknięć
void SecretKnock::listenToSecretKnock()
{

  int ii = 0;
  // Najpierw zresetuj tabelę do nasłuchu
  for (ii=0; ii<MAX_KNOCKS; ii++){
    knockReadings[ii]=0;
  }

  int currentKnockNumber=0;          // Indeks tabeli.
  int startTime=millis();            // Odniesienie, kiedy nastąpiło rozpoczęło się pukanie.
  int now=millis();

  digitalWrite(greenLED, LOW);       // Miganie diody, aby zwizualizować pukanie
  if (programButtonPressed==true){
    digitalWrite(redLED, LOW);       // Zapalenie czerwonej diody, aby powiadomić o programowaniu nowego pukania
  }
  delay(knockFadeTime);              // Poczekaj na zanik sygnału, zanim zapiszesz następne puknięcie
  digitalWrite(greenLED, HIGH);  
  if (programButtonPressed==true){
    digitalWrite(redLED, HIGH);                        
  }
  do {
    // nasłuchuj następnego puknięcia lub zaczekaj na przekroczenie czasu
    knockSensorValue = analogRead(knockSensor);
    if (knockSensorValue >=threshold){                   // nastąpiło następne puknięcie...
      //record the delay time.

      now=millis();
      knockReadings[currentKnockNumber] = now-startTime;
      currentKnockNumber ++;                             // zwiększ licznik
      startTime=now;          
      // resetuj timer do następnego puknięcia
      digitalWrite(greenLED, LOW);  
      if (programButtonPressed==true){
        digitalWrite(redLED, LOW);                       // i zapal czerwoną diodę, jeżeli programujemy następne puknięcie
      }
      delay(knockFadeTime);                              // znowu małe opóźnienie, aby ucichło puknięcie
      digitalWrite(greenLED, HIGH);
      if (programButtonPressed==true){
        digitalWrite(redLED, HIGH);                         
      }
    }

    now=millis();

    // przekroczyliśmy czas, czy ilość puknięć?
  } 
  while ((now-startTime < knockComplete) && (currentKnockNumber < MAX_KNOCKS));

  // zapisaliśmy puknięcia, sprawdźmy, czy są poprawne
  if (programButtonPressed==false)
  {  // kod wykonywany tylko wtedy, gdy nie jesteśmy w trybie programowania
    if (validateKnock() == true)
    {
      // triggerDoorUnlock(); 
      openDoor();
      //playSuccess();
      blinkSuccess();
    } 
    else 
    {
      digitalWrite(greenLED, LOW);          // Nie otworzyliśmy pudełka, mignij czerwoną diodą LED, aby to oznajmić
      moveServo(1.0, 100); // zamknij pudełko
      delay(1000);
      for (ii=0;ii<4;ii++){                    
        digitalWrite(redLED, HIGH);
        delay(100);
        digitalWrite(redLED, LOW);
        delay(100);
      }
      digitalWrite(greenLED, HIGH);
    }
  } 
  else { // jeżeli jesteśmy w trybie programowania, wciąż sprawdzamy pukanie i nic nie robimy z zamkiem
    validateKnock();
    // migamy na zmianę na zielono i czerwono, aby pokazać, że programowanie zakończyło się
    digitalWrite(redLED, LOW);
    digitalWrite(greenLED, HIGH);
    for (ii=0;ii<3;ii++){
      delay(100);
      digitalWrite(redLED, HIGH);
      digitalWrite(greenLED, LOW);
      delay(100);
      digitalWrite(redLED, LOW);
      digitalWrite(greenLED, HIGH);      
    }
  }
}

// Uruchom silnik (lub coś innego), aby otworzyć pudełko
void SecretKnock::triggerDoorUnlock(){
  int ii=0;

  // uruchom na chwilę silnik
  digitalWrite(lockMotor, HIGH);
  digitalWrite(greenLED, HIGH);  // i zapal zieloną diodę LED

  delay (lockTurnTime);          // zaczekaj chwilę

  digitalWrite(lockMotor, LOW);  // zatrzymaj silnik

  // zapal kilka razy zieloną diodę, aby dać wyraźny sygnał
  for (ii=0; ii < 5; ii++){   
    digitalWrite(greenLED, LOW);
    delay(100);
    digitalWrite(greenLED, HIGH);
    delay(100);
  }
}

// Funkcja sprawdzająca, czy wystukany szyfr jest zgodny ze wzorcem.
// Zwraca true, jeżeli szyfr jest prawidłowy, false w przeciwnym wypadku
// Do zrobienia: podzielić kod na mniejsze funkcje w celu poprawienia czytelności.
boolean SecretKnock::validateKnock()
{
  int ii=0;

  // Najpierw najprostszy test: czy liczba puknięć się zgadza?
  int currentKnockCount = 0;
  int secretKnockCount = 0;
  int maxKnockInterval = 0;    // Tej zmiennej użyjemy później do normalizacji czasu.

  for (ii=0;ii<MAX_KNOCKS;ii++){
    if (knockReadings[ii] > 0){
      currentKnockCount++;
    }
    if (secretCode[ii] > 0){      // Do zrobienia: przeliczyć
      secretKnockCount++;
    }

    if (knockReadings[ii] > maxKnockInterval){   // Zbieranie danych normalizacyjnych w pętli.
      maxKnockInterval = knockReadings[ii];
    }
  }

  // Jeżeli programujemy nowy szyfr, zapisz informacje i wyjdź stąd.
  if (programButtonPressed==true){
    for (ii=0;ii<MAX_KNOCKS;ii++){ // Normalizacja czasów
      secretCode[ii]= map(knockReadings[ii],0, maxKnockInterval, 0, 100); 
    }
    // Zapalaj diody zgodnie z pukaniem, aby potwierdzić jego  zaprogramowanie.
    digitalWrite(greenLED, LOW);
    digitalWrite(redLED, LOW);
    delay(1000);
    digitalWrite(greenLED, HIGH);
    digitalWrite(redLED, HIGH);
    delay(50);
    for (ii = 0; ii < MAX_KNOCKS ; ii++){
      digitalWrite(greenLED, LOW);
      digitalWrite(redLED, LOW);  
      // Zapal diodę tylko wtedy, gdy jest opóźnienie
      if (secretCode[ii] > 0){                                   
        delay( map(secretCode[ii],0, 100, 0, maxKnockInterval)); // Rozciągnij czas do mniej więcej poprzedniej wartości
        digitalWrite(greenLED, HIGH);
        digitalWrite(redLED, HIGH);
      }
      delay(50);
    }
    return false;   // Nie otwieramy pokrywki podczas zapisywania nowego szyfru.
  }

  if (currentKnockCount != secretKnockCount){
    return false; 
  }

  /*  Teraz porównujemy względne przerwy w sekwencji puknięć, a nie bezwzględne odstępy między nimi
   (tzn. jeżeli wystukasz tę samą sekwencję szybko lub wolno, drzwi powinny zostać otwarte).
   Dzięki temu układ jest mniej wrażliwy na tempo pukania, które może się nieco różnić od zaprogramowanego 
   wzorca, aczkolwiek obniżone jest wtedy bezpieczeństwo.
   */
  int totaltimeDifferences=0;
  int timeDiff=0;
  for (ii=0;ii<MAX_KNOCKS;ii++){ // Normalizacja czasów
    knockReadings[ii]= map(knockReadings[ii],0, maxKnockInterval, 0, 100);      
    timeDiff = abs(knockReadings[ii]-secretCode[ii]);
    if (timeDiff > rejectValue){ // Poszczególne wartości zbyt się różnią
      return false;
    }
    totaltimeDifferences += timeDiff;
  }
  // Wynik jest negatywny, jeżeli cała sekwencja jest zbyt niedokładna.
  if (totaltimeDifferences/secretKnockCount>averageRejectValue){
    return false; 
  }

  return true;
}

void SecretKnock::openDoor()
{
  moveServo(2.0, 100);
  delay(1000);
}

void SecretKnock::blinkSuccess()
{
  for (int ii = 0; ii < 50; ii++)
  {
    digitalWrite(greenLED, HIGH);
    delay(25);
    digitalWrite(greenLED, LOW);
    delay(25);
  }
}
