// shutter tester version 2.9.3 for Arduino Nano. 19 March 2023
// https://www.photrio.com/forum/threads/build-a-shutter-tester-for-focal-plane-shutters-cheap-easy-it-works.197756/

#include <Wire.h>               // built in library
#include <LiquidCrystal_I2C.h>  // by Frank de Brabander

LiquidCrystal_I2C lcd(0x27, 20, 4);  // set the LCD address. Normally 0x27 for a 20 char 4 line display

#define DEBUG 0
#if DEBUG == 1
#define debug(x) Serial.print(x)
#define debugln(x) Serial.println(x)
#else
#define debug(x)
#define debugln(x)
#endif

// define variables. Vars ending 1 are first laser or first curtain. Vars ending 2 are second laser or second curtain.
unsigned long Start1;  // Arduino microsecond clock time when laser1 seen (uncovered by shutter curtain)
unsigned long Stop1;   // Arduino microsecond clock time when the laser1 blocked (by shutter curtain)
unsigned long Start2;  // Arduino microsecond clock time when laser2 seen (uncovered by shutter curtain)
unsigned long Stop2;   // Arduino microsecond clock time when the laser2 blocked (by shutter curtain)

unsigned long SSmicro1;  // Shutter Speed in Microseconds calculated from laser1
unsigned long SSmicro2;  // Shutter Speed in Microseconds calculated from laser2

unsigned long shCurtainspeedMilliS1;  // curtain 1 speed in Milliseconds
unsigned long shCurtainspeedMilliS2;  // curtain 2 speed in Milliseconds
unsigned long shCurtainSpeedMicroS1;  // curtain 1 speed in Microseconds
unsigned long shCurtainSpeedMicroS2;  // curtain 1 speed in Microseconds
unsigned long shutterBounce = 1000;   // how long to wait for shutter shutterBounceFlag check

float SSmillis1;   // Shutter Speed in Millieseconds
float SSmillis2;   // Shutter Speed in Millieseconds
int SSmillisInt1;  // Shutter Speed in Millieseconds rounded
int SSmillisInt2;  // Shutter Speed in Millieseconds rounded

float SSsec1;  // Shutter Speed in Seconds
float SSsec2;  // Shutter Speed in Seconds

float SSfrac1;  // 1/Shutter Speed to give Fraction of second
float SSfrac2;  // 1/Shutter speed to give Fraction of second

int SSfracV1;  // 1/Shutter Speed Fraction of second rounded up
int SSfracV2;  // 1/Shutter Speed Fraction of second rounded up

bool firedFlag1;         // flag set when the shutter has been firedFlag1, Laser 1
bool firedFlag2;         // flag set when the shutter has been firedFlag2, Laser 2
bool singleLaserMode;    // flag set if only one laser being used, calculated in program
bool shutterBounceFlag;  // flag fet if shutter shutterBounceFlag detected.

//variables used in ISR
volatile int risingLaser1;      // flag set in isr, set to 1 when the voltage INCREASES in the interrupt (shutter open)
volatile int fallingLaser1;     // flag set in isr, set to 1 when the voltage DECREASES in the interrupt (shutter close)
volatile int risingLaser2;      // flag set in isr routine, set to 1 when the voltage INCREASES in the interrupt (shutter open)
volatile int fallingLaser2;     // flag set in isr, set to 1 when the voltage DECREASES in the interrupt (shutter close)
volatile int laserChangeFlag2;  // flag set in isr, set when isr called, used to  determine single laser use



void setup() {  // This part of the program is run exactly once on boot

  //define & setup input pins
  pinMode(2, INPUT);                                          // Laser 1 input
  pinMode(3, INPUT);                                          // Laser 2 input
  attachInterrupt(digitalPinToInterrupt(2), CLOCK1, CHANGE);  // run the ISR CLOCK1, every time the voltage on pin 2 changes.
  attachInterrupt(digitalPinToInterrupt(3), CLOCK2, CHANGE);  // run the ISR CLOCK2, every time the voltage on pin 3 changes.

  // initialize the lcd
  lcd.init();
  lcd.backlight();
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("   Camera Shutter  ");
  lcd.setCursor(0, 1);
  lcd.print("      Tester       ");
  lcd.setCursor(0, 2);
  lcd.print("   Version 2.9.3    ");
  lcd.setCursor(0, 3);
  lcd.print("   19-March-2023   ");

  delay(2000);
  lcd.clear();

  lcd.setCursor(0, 0);
  lcd.print("  Laser2   Laser1   ");
  LCDdisplay();

  //start serial monitor (output to PC screen)
  Serial.begin(9600);
  Serial.println("ready");

  clearVars();
}  //end void setup



void loop() {  // main program starts here

  // first laser
  if (risingLaser1 == 1) {  // rising count increased in ISR, first laser seen, (shutter opening)
    Start1 = micros();      // set the variable Start1 to current microseconds (detected shutter open)
    risingLaser1++;         // increase rising count, so that this code isn't used again during shutter cycle
  }

  if (fallingLaser1 == 1) {  // falling count increased in ISR, first laser changed to not seen, (shutter closing)
    Stop1 = micros();        // set the variable Stop1 to current microseconds (detected shutter closing)
    fallingLaser1++;         // increase falling count, so that this code isn't used again during shutter cycle
    firedFlag1 = true;       // set the firedFlag1 flag to 1, to allow calculation of shutter speed.
  }

  // second laser
  if (risingLaser2 == 1) {  // rising count increased in ISR, second laser seen, (shutter opening)
    Start2 = micros();      // set the variable Start2 to current microseconds (detected shutter open)
    risingLaser2++;         // increase falling count, so that this code isn't used again during shutter cycle
  }

  if (fallingLaser2 == 1) {  // falling count increased in ISR, first laser changed to not seen, (shutter closing)
    Stop2 = micros();        // set the variable Stop2 to current microseconds (detected shutter closing)
    fallingLaser2++;         // increase the falling flag count, so that this code isn't used again during shutter cycle
    firedFlag2 = true;       // set the firedFlag2 flag, to allow calculation of shutter speed
  }


  // if shutter cycle complete, run the subroutiens
  if (firedFlag1 == true && firedFlag2 == true) {
    shutterBounceCheck();
    processData();
    serialDisplay();
    LCDdisplay();
    clearVars();
  }

  // if only 1 laser has operated,after 1 second, invoke single laser mode.
  else if (firedFlag1 == true && laserChangeFlag2 == false && (micros() - Stop1 >= 1000000)) {
    singleLaserMode = true;
    // shutterBounceCheck(); Will not work for single laser mode.
    processData();
    serialDisplay();
    LCDdisplay();
    clearVars();
  }
}  // jump back to void loop as shutter cycle not completed.



// Subrouties.

void shutterBounceCheck() {  // wait for a while to see if shutter bounces open
  for (uint32_t tStart = millis(); (millis() - tStart) < shutterBounce;) {
    if (digitalRead(3) == HIGH) {
      shutterBounceFlag = true;
    }  // end_if
  }    // end_for_loop
}  // end_shutterBounceCheck



//  Do the maths
void processData() {
  // do the maths calculations for first laser
  SSmicro1 = (Stop1 - Start1);            // calculate shutter open/close in microseconds
  SSmillis1 = (Stop1 - Start1) / 1000.0;  // calculate shutter open/close in millieseconds
  SSmillisInt1 = (int(SSmillis1 + 0.5));  // convert millis1 to rounded int
  SSsec1 = (float)SSmicro1 / 1000000.0;   // convert shutter SSmicro1 to seconds
  SSfrac1 = (float)(1.0 / SSsec1);        // inverse of theSSsec1, to give vulgar fraction
  SSfracV1 = (int(SSfrac1 + 0.5));        // inverse of theSSsec1, to give vulgar fraction rounded up

  // do the maths calculations for second laser
  if (singleLaserMode == false) {           // do the maths for shutter curtain speed if two lasers used
    SSmicro2 = (Stop2 - Start2);            // calculate shutter open/close in microseconds
    SSmillis2 = (Stop2 - Start2) / 1000.0;  // calculate shutter open/close in millieseconds
    SSmillisInt2 = (int(SSmillis2 + 0.5));  // convert millis2 to rounded int
    SSsec2 = (float)SSmicro2 / 1000000.0;   // convert shutter SSmicro2 to seconds
    SSfrac2 = (float)(1.0 / SSsec2);        // inverse of theSSsec2, to give vulgar fraction
    SSfracV2 = (int(SSfrac2 + 0.5));        // inverse of theSSsec1, to give vulgar fraction rounded up

    // do the maths for shutter curtain speed as two lasers used
    shCurtainSpeedMicroS1 = (Start2 - Start1);         // Microseconds
    shCurtainSpeedMicroS2 = (Stop2 - Stop1);           // Microseconds
    shCurtainspeedMilliS1 = (Start2 - Start1) / 1000;  // miliseconds
    shCurtainspeedMilliS2 = (Stop2 - Stop1) / 1000;    // miliseconds
  }
}  // end processData



// Display results of laser 1 to serial monitor
void serialDisplay() {
  Serial.println();
  Serial.print("Laser1 Start: ");  // display actual Arduino microsecond clock
  Serial.println(Start1);
  Serial.print("Laser1 Stop : ");
  Serial.println(Stop1);

  Serial.print("shutter Speed Microseconds   : ");
  Serial.println(SSmicro1);  // display shutter SSMs in microseconds

  Serial.print("shutter Speed Milliseconds   : ");
  Serial.println(SSmillisInt1);  // display shutter SSMs in milliseconds

  Serial.print("shutter Speed Seconds        : ");
  Serial.println(SSsec1, 3);  // display shutter SSMs in seconds to 3 decimal places

  if (SSsec1 < 1) {  // test if shutter SSMs less than 1 second, if so, print in fractions
    Serial.print("shutter Speed fraction       : 1/");
    Serial.println(SSfrac1);  // display shutter SSMs in fractions
    Serial.print("shutter Speed fraction       : 1/");
    Serial.println(SSfracV1);  // display shutter SSMs in fractions
  }


  // Display results of laser 2 to serial monitor
  if (singleLaserMode == false) {
    Serial.println();
    Serial.print("Laser2 Start: ");  // display actual Arduino microsecond clock
    Serial.println(Start2);
    Serial.print("Laser2 Stop : ");
    Serial.println(Stop2);

    Serial.print("shutter Speed Microseconds   : ");
    Serial.println(SSmicro2);  // display shutter SSMs in microseconds

    Serial.print("shutter Speed Milliseconds   : ");
    Serial.println(SSmillisInt2);  // display shutter SSMs in milliseconds

    Serial.print("shutter Speed Seconds        : ");
    Serial.println(SSsec2, 3);  // display shutter SSMs in seconds to 3 decimal places

    if (SSsec2 < 1) {  // test if shutter SSMs less than 1 second, if so, print in fractions
      Serial.print("shutter Speed fraction       : 1/");
      Serial.println(SSfrac2);  // display shutter SSMs in fractions
      Serial.print("shutter Speed fraction       : 1/");
      Serial.println(SSfracV2);  // display shutter SSMs in fractions
    }
  }


  if (singleLaserMode == false) {
    Serial.println();
    Serial.print("First  Curtain Speed Microseconds : ");
    Serial.println(shCurtainSpeedMicroS1);  // display first curtain travel time between lasers
    Serial.print("First  Curtain Speed Milliseconds : ");
    Serial.println(shCurtainspeedMilliS1);  // display first curtain travel time between lasers mS

    Serial.print("Second Curtain Speed Microseconds : ");
    Serial.println(shCurtainSpeedMicroS2);  // display second curtain travel time between lasers
    Serial.print("Second Curtain Speed Milliseconds : ");
    Serial.println(shCurtainspeedMilliS2);  // display second curtain travel time between lasers mS
  }


  if (shutterBounceFlag == true) {
    Serial.println();
    Serial.print("Houston, we have shutter Bounce(s)!  ");
    Serial.println(risingLaser2 - 2);  // val increases by 1 each time ISR sees laser. 1 added in loop to stop re-calc.
  }

  Serial.println();
}  //end_serialDisplay


// output to LCD
void LCDdisplay() {


  // print 'Bou' on line 0 if shutter bounce detected
  if (shutterBounceFlag == true) {
    lcd.setCursor(17, 0);
    lcd.print("Bou");
  } else {
    lcd.setCursor(17, 0);
    lcd.print("   ");
  }

  // print miliseconds on line 1 (lines numbered 0, 1, 2, 3)
  lcd.setCursor(0, 1);
  lcd.print("                  ");

  lcd.setCursor(9, 1);
  lcd.print(SSmillisInt1);

  lcd.setCursor(0, 1);
  lcd.print(SSmillisInt2);

  lcd.setCursor(18, 1);
  lcd.print("mS");

  // print seconds on line 2, fraction or decimal
  lcd.setCursor(0, 2);
  lcd.print("                  ");

  if (SSsec1 < 1) {  // if first laser measurement is < 1 second, print as fraction
    lcd.setCursor(9, 2);
    lcd.print("1/");
    lcd.print(SSfracV1);

    lcd.setCursor(18, 2);
    lcd.print(" S");
  } else {
    lcd.setCursor(9, 2);
    lcd.print(SSsec1);

    lcd.setCursor(18, 2);
    lcd.print(" S");
  }


  if (SSsec2 < 1 && singleLaserMode == false) {  // if second laser measurement is < 1 second, print as fraction
    lcd.setCursor(0, 2);
    lcd.print("1/");
    lcd.print(SSfracV2);
  } else {
    lcd.setCursor(0, 2);
    lcd.print(SSsec2);
  }

  // print curtain speed on line 3,
  lcd.setCursor(0, 3);
  lcd.print("                  ");

  lcd.setCursor(0, 3);
  lcd.print("C1 ");
  lcd.print(shCurtainspeedMilliS1);

  lcd.setCursor(9, 3);
  lcd.print("C2 ");
  lcd.print(shCurtainspeedMilliS2);

  lcd.setCursor(18, 3);
  lcd.print("mS");

  delay(500);
}  // end_LCDdisplay


// clear vars, helps to show errors for debug
void clearVars() {
  noInterrupts();
  Start1 = 0;
  Stop1 = 0;
  Start2 = 0;
  Stop2 = 0;

  SSmicro1 = 0;
  SSmicro2 = 0;

  shCurtainspeedMilliS1 = 0;
  shCurtainspeedMilliS2 = 0;
  shCurtainSpeedMicroS1 = 0;
  shCurtainSpeedMicroS2 = 0;

  SSmillis1 = 0;
  SSmillisInt1 = 0;
  SSmillis2 = 0;
  SSmillisInt2 = 0;

  SSsec1 = 0;
  SSsec2 = 0;

  SSfrac1 = 0;
  SSfrac2 = 0;

  SSfracV1 = 0;
  SSfracV2 = 0;

  firedFlag1 = false;
  firedFlag2 = false;
  singleLaserMode = false;

  risingLaser1 = 0;
  risingLaser2 = 0;
  fallingLaser1 = 0;
  fallingLaser2 = 0;
  laserChangeFlag2 = false;
  shutterBounceFlag = false;


  EIFR = bit(INTF0);
  EIFR = bit(INTF1);
  interrupts();
  Serial.println("Ready Again...");
  Serial.println();
  Serial.println();
}  // end clearVars



//    Interrupt Routines

void CLOCK1() {  // interrupt called everytime the voltage on pin 2 changes, laser 1,
  if (digitalRead(2) == HIGH) {
    risingLaser1++;  // if the voltage on pin 2 is high, increase rising count.
  }
  if (digitalRead(2) == LOW) {
    fallingLaser1++;  // If the voltage on pin 2 is low, increase falling count
  }
}


void CLOCK2() {  // interrupt called everytime the voltage on pin 3 changes
  if (digitalRead(3) == HIGH) {
    risingLaser2++;  // if the voltage on pin 3 is high, increase rising count
    laserChangeFlag2 = true;
  }
  if (digitalRead(3) == LOW) {
    fallingLaser2 = true;  // If the voltage on pin 3 is low, increase falling count
    // laserChangeFlag2 = true;
  }
}

// end ISR
