// For: https://forum.arduino.cc/t/timer-has-random-error/1037211/
//
// Wokwi project with sketch from user: https://wokwi.com/projects/344260957282763346
//
// This Wokwi project: https://wokwi.com/projects/344339167081136722
//
// 2 Oct 2022, Version 1, by Koepel, Public Domain.
//   Now purely based on millis().
//   Removed the RTC.
//   NOT READY, JUST A PROOF OF CONCEPT
//
// To do:
//   1. (optional)
//     The accuracy can be improved by using a interrupt
//     for the button. In case I2C or something slow is used.
//     Not needed at this moment.
//
//   2.
//     Entering the number of times is not needed.
//     The user can just stop the input by pressing 'D' twice.
//  
//   3.
//     Make a Finite State Machine
//
//   4.
//     Use sprintf() in showTimes().
//
//   5.
//     Add custom characters. For example a running person.
//
//

#include <LiquidCrystal.h>
#include <Keypad.h>

const int redLedPin = A1;
const int greenLedPin = A0;
const int buttonPin = 3;             // pin 3 is also a interrupt input

const int rs = 13, en = 12, d4 = 11, d5 = 10, d6 = 9, d7 = 8;
LiquidCrystal lcd( rs, en, d4, d5, d6, d7);

const byte ROWS = 4;                 // four rows
const byte COLS = 4;                 // four columns
char hexaKeys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};
byte rowPins[ROWS] = { A2, A3, 0, 1}; // connect to the row pinouts of the keypad
byte colPins[COLS] = { 4, 7, 6, 5};   // connect to the column pinouts of the keypad

Keypad customKeypad = Keypad(makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS);

// The 'startMillis' is the millis value of time zero.
// The tables contain the times in milliseconds since start as a 'long'.
// The index of an array starts at zero, but that is shown as "time 1."
unsigned long startMillis;
#define MAX_TIMES 20                 // maximum number of times
long measuredTimes[MAX_TIMES];       // the table with measured times
int index;                           // current index in the table when running

// PRE-FILLED WITH VALUES FOR TESTING
long setTimes[MAX_TIMES] =           // the table with the set times
{
  10000L, 20000L, 30000L,
};
int nTimes = 3;                      // The number of times determined runtime

unsigned long previousMillisDisplay;
const unsigned long intervalDisplay = 200UL;

void setup() 
{
  pinMode( redLedPin, OUTPUT);       // set to output and LOW
  pinMode( greenLedPin, OUTPUT);     // set to output and LOW
  pinMode( buttonPin, INPUT_PULLUP);

  lcd.begin( 20, 4);
  lcd.clear();

  lcd.setCursor( 6, 1);
  lcd.print( F("Welcome"));
  delay( 5000);

  Enter_Times();

  lcd.clear();
  lcd.setCursor( 0, 0);
  lcd.print( F("The first press of"));
  lcd.setCursor(0, 1);
  lcd.print( F("the button starts"));
  lcd.setCursor( 0, 2);
  lcd.print( F("the race ..."));

  while( !isButtonPressed( buttonPin));        // wait for the button to be pressed
  startMillis = millis();                      // timestamp for starting point

  lcd.clear();
  lcd.setCursor( 5, 2);
  lcd.print( F("Running !"));
  lcd.setCursor( 0, 3);
  lcd.print( F("press at 10,20,30 s"));

}

void loop() 
{
  // The millis values should always be 'unsigned long' to always work,
  // even during a rollover of millis().
  // Once the elapsed millis value is calculated, it can be converted to 'long'.
  // It is more convenient for the sketch to use 'long'. 
  unsigned long currentMillis = millis();
  unsigned long elapsedMillis = currentMillis - startMillis;  // rollover-proof
  long timeSinceStart = (long) elapsedMillis;  // a 'long' to be used in the sketch

  if( isButtonPressed( buttonPin))
  {
    // Fill the array with the current time.
    measuredTimes[index] = timeSinceStart;
    
    // Calculate the difference and show it on the display.
    // Line 1: The set time
    // Line 2: The measured time
    // Line 3: The difference
    showTime( 1, index, setTimes[index], false);
    showTime( 2, index, measuredTimes[index], false);
    showTime( 3, index, measuredTimes[index] - setTimes[index], true);

    // Go to the next position in the array.
    index++;
    if( index >= MAX_TIMES)
    {
      index = MAX_TIMES - 1;      // hang around at the last position
    }
  }

  // A 7-segment display is fast, but the liquid crystals of a LCD display is slow.
  // Using a millis-timer to update the current time.
  // This code reduces the accuracy for the time to capture the button.
  if( currentMillis - previousMillisDisplay >= intervalDisplay)
  {
    previousMillisDisplay = currentMillis;
    showTime( 0, -1, (long) elapsedMillis, false);   // -1 to avoid showing index
  }
}

void Enter_Times() 
{
  lcd.setCursor( 0, 0);
  lcd.print( F("Enter the times"));
  lcd.setCursor( 0, 1);
  lcd.print( F("* for s and ms"));
  lcd.setCursor( 0, 2);
  lcd.print( F("C for backspace"));
  lcd.setCursor( 0, 3);
  lcd.print( F("D for next and ready"));
  delay( 5000);

  lcd.clear();
  lcd.setCursor( 0, 0);
  lcd.print( F("Sorry"));
  lcd.setCursor( 0, 1);
  lcd.print( F("not implemented"));
  delay( 5000);
}


// showTime
// --------
//
// Show the times on a line of the display, with optional sign
// 
// Parameters:
//     row  : 0 = top row, 3 = bottom row
//     indx : index, starting from zero. Zero is displayed as time "1".
//            A value of -1 for not showing the index and not the dot.
//     t    : the time in milliseconds as a 'long'
//     sign : true to show '+' or '-' sign. false for no sign.
//
// Format  : nn. +mm:ss.xxx
// Examples:  1.  00:10.000
//           12. -05:42.998
//
// A sprintf() should make this function better
//
void showTime( int row, int indx, long t, bool sign)
{
  // Split the variable into minutes, seconds, milliseconds.
  // The index starts at 0, that is time "1", and so on.
  lcd.setCursor( 0, row);
  int n = 0;                  // keep track of the number of characters written

  if( indx == -1)             // not showing the index ?
  {
    n += lcd.print( F("    "));
  }
  else
  {
    int number = indx + 1;      // show index zero as time "1" and so on.
    if( number < 10)
      n += lcd.print( F(" "));
    n += lcd.print( number);
    n += lcd.print( F(". "));
  }

  if( sign)
  {
    if( t < 0)
    {
      n += lcd.print( F("-"));
    }
    else
    {
      n += lcd.print( F("+"));
    }
  }
  else
  {
    n += lcd.print( F(" "));
  }

  if( t < 0)
    t = -t;                  // make 't' always positive
  int minutes = t / ( 60L * 1000L );
  int seconds = ( t / 1000L ) % 60L;
  int milliseconds = t % 1000L;

  // safety check for maximum
  if( minutes > 99)
  {
    n += lcd.print( F("##:##.###"));
  }
  else
  {
    if( minutes < 10)
      n += lcd.print( F("0"));
    n += lcd.print( minutes);    
    n += lcd.print( F(":"));
    if( seconds < 10)
      n += lcd.print( F("0"));
    n += lcd.print( seconds);
    n += lcd.print( F("."));
    if( milliseconds < 10)
      n += lcd.print( F("0"));
    if( milliseconds < 100)
      n += lcd.print( F("0"));
    n += lcd.print( milliseconds);
  }

  // clear the rest of the line.
  for( int i=0; i<(20-n); i++)
  {
    lcd.print( F(" "));
  }
}


// isButtonPressed (half-a-debounce)
// ---------------------------------
// 
// For maximum accuracy, the debouncing is minimized when the button is pressed.
// Two samples are taken, to filter out a glitch.
// If both samples are LOW, then the button is assumed to be pressed.
// The button is accepted as released with it is steady HIGH for at least 50 ms.

bool debounceMode = false;               // for debouncing the release of the button
unsigned long debounceMillis;

bool isButtonPressed( int pin)
{
  // Take two samples, to filter out a glitch.
  int sample1 = digitalRead( pin);
  int sample2 = digitalRead( pin);

  if( !debounceMode)
  {
    // check if the button is pressed
    if( sample1 == LOW and sample2 == LOW)
    {
      // As soon as the button is accepted as pressed,
      // the debounce mode for releasing is started.
      debounceMode = true;
      debounceMillis = millis();
      return( true);                   // immediate return with true (button pressed)
    }
  }
  else
  {
    // in debounce mode
    if( sample1 == LOW or sample2 == LOW)
    {
      // Still something low, not a valid high yet.
      // Restart the timer
      debounceMillis = millis();
    }
    else
    {
      if( millis() - debounceMillis >= 50)
      {
        // Steady HIGH for at least 50ms, that is accepted.
        debounceMode = false;
      }
    }
  }
  return( false);
}
uno:A5.2
uno:A4.2
uno:AREF
uno:GND.1
uno:13
uno:12
uno:11
uno:10
uno:9
uno:8
uno:7
uno:6
uno:5
uno:4
uno:3
uno:2
uno:1
uno:0
uno:IOREF
uno:RESET
uno:3.3V
uno:5V
uno:GND.2
uno:GND.3
uno:VIN
uno:A0
uno:A1
uno:A2
uno:A3
uno:A4
uno:A5
lcd1:VSS
lcd1:VDD
lcd1:V0
lcd1:RS
lcd1:RW
lcd1:E
lcd1:D0
lcd1:D1
lcd1:D2
lcd1:D3
lcd1:D4
lcd1:D5
lcd1:D6
lcd1:D7
lcd1:A
lcd1:K
r1:1
r1:2
r2:1
r2:2
btn1:1.l
btn1:2.l
btn1:1.r
btn1:2.r
rgb1:R
rgb1:COM
rgb1:G
rgb1:B
r3:1
r3:2
r4:1
r4:2
r5:1
r5:2
keypad1:R1
keypad1:R2
keypad1:R3
keypad1:R4
keypad1:C1
keypad1:C2
keypad1:C3
keypad1:C4