
// shutter tester for Arduino Nano.
// https://www.photrio.com/forum/threads/build-a-shutter-tester-for-focal-plane-shutters-cheap-easy-it-works.197756/
// For help & assistance with building, post a link in the thread above.

// String Ver =  "version 2.9.9";
// String Date = "1 April 2023";

// *********************************************************************************************************
// newer laser sensors may have a reversed output. They can be changed by swapping the two active lines below
const bool seen    = true;     // un-comment this line (older sensors)
const bool blocked = false;    // AND this line        (older sensors)

// const bool seen    = false;  // OR this line       (newer sensors)
// const bool blocked = true;   // AND this line      (newer sensors)
//
// Note:- comments in code always refer to older sensors i.e 'if the voltage on pin2 is high'.
//        On newer sensors, electrically, they would be low, however this does not matter (unless you 
//        are poking around with a voltmeter). Just comment/uncomment the two lines of code above.
//        There is a test program (sketch) in the photro thread (link above) that is a quick way to see if
//        your sensors are the newer type.
// *********************************************************************************************************


#include <Wire.h>               // built in library
#include <LiquidCrystal_I2C.h>  // by Frank de Brabander
#include <LibPrintf.h>          // https://github.com/embeddedartistry/arduino-printf
#include <RunningAverage.h>     // https://github.com/RobTillaart/RunningAverage

LiquidCrystal_I2C lcd(0x27, 20, 4);  // set the LCD address. Normally 0x27 for a 20 char 4 line display


// define variables. Vars ending 1 are first laser or first curtain. Vars ending 2 are second laser or second curtain.

RunningAverage SSMILLISAV1 (10);               // define average counters
RunningAverage SSMILLISAV2 (10);               // define average counters
RunningAverage SHCURTAINSPEEDMILLISAV1 (10);   // define average counters
RunningAverage SHCURTAINSPEEDMILLISAV2 (10);   // define average counters


unsigned long Start1;  // Arduino microsecond clock time when laser1 seen (uncovered by shutter curtain)
unsigned long Stop1;   // Arduino microsecond clock time when 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 laser2 blocked (by shutter curtain)

long int SSmicro1;    // Shutter Speed in Microseconds calculated from laser1
long int SSmicro2;    // Shutter Speed in Microseconds calculated from laser2

long int shCurtainspeedMilliS1;  // curtain 1 speed in Milliseconds
long int shCurtainspeedMilliS2;  // curtain 2 speed in Milliseconds
long int shCurtainSpeedMicroS1;  // curtain 1 speed in Microseconds
long int shCurtainSpeedMicroS2;  // curtain 2 speed in Microseconds

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

int SSfracV1;  // 1/Shutter Speed Fraction of second rounded
int SSfracV2;  // 1/Shutter Speed Fraction of second rounded

String StringFrac   = "1/";     // symbol used in print
String StringFracV1 = "0";      // string to store vulgar fracton, for neat printing
String StringFracV2 = "0";      // string to store vulgar fracton, for neat printing

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 singleLaserFlag;     // flag set if only one laser being used, calculated in program
bool shutterBounceFlag1;  // flag set if shutter shutterBounce detected.
bool shutterBounceFlag2;  // flag set if shutter shutterBounce detected.
bool sensorReversedFlag;  // flag set is sensors triggered in wrong order

//variables used in ISR
volatile int risingLaser1;       // increases each time isr is called & detects shutter open on  first sensor
volatile int fallingLaser1;      // increases each time isr is called & detects shutter close on first sensor
volatile int risingLaser2;       // increases each time isr is called & detects shutter open on  second sensor
volatile int fallingLaser2;      // increases each time isr is called & detects shutter close on second sensor
volatile bool laserChangeFlag2;  // flag set in isr, set when isr called, used to  determine single laser use

//volatile int sensorIn1 = 2;  // name input pins
//volatile int sensorIn2 = 3;  // name input pins

//int resetAvPin   = 12;       // name input pin

void setup() {  // This part of the program is run exactly once on boot

  SSMILLISAV1.clear();              // clear average counters
  SSMILLISAV2.clear();
  SHCURTAINSPEEDMILLISAV1.clear();
  SHCURTAINSPEEDMILLISAV2.clear();

  //define & setup input pins
  pinMode(2, INPUT);             // Laser 1 input
  pinMode(3, INPUT);             // Laser 2 input
  pinMode(12, INPUT_PULLUP); // average reset button

  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.


  //start serial monitor & output to PC screen
  Serial.begin(9600);

  Serial.println(F("Camera Shutter Tester"));
  Serial.println(F("https://www.photrio.com/forum/threads/build-a-shutter-tester-for-focal-plane-shutters-cheap-easy-it-works.197756/"));

  Serial.println(F("version 2.9.9"));
  Serial.println(F("1 April 2023"));

  // 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(3, 2);
  lcd.print("version 2.9.9");
  lcd.setCursor(3, 3);
  lcd.print("1 April 2023");

  delay(3000);
  lcd.clear();

  lcd.setCursor(0, 0);
  lcd.print("Laser2   Laser1     ");
  LCDdisplay();


  clearVars(); // clear all variables & flags - good housekeeping
}  //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 (digitalRead(12) == LOW) { // if reset average button is pushed, go to subroutine
    resetAv();
  }

  // if shutter cycle complete, run the subroutines
  if (firedFlag1 == true && firedFlag2 == true) {
    shutterBounceCheck();
    processData();
    altDisplay ();
    LCDdisplay();
    clearVars();
  }

  // if only 1 laser has operated,after 1 second, invoke single laser mode.
  else if (firedFlag1 == true && laserChangeFlag2 == false && (micros() - Stop1 >= 1000000)) {
    singleLaserFlag = true;
    shutterBounceCheck();
    processData();
    altDisplay ();
    LCDdisplay();
    clearVars();
  }
}  // jump back to void loop



// Subrouties.

void shutterBounceCheck() {  // wait for a while to see if shutter bounces open, second laser
  for (uint32_t tStart = millis(); (millis() - tStart) < 1000;) { // sit in a loop, to see if a bounce occurs
    if (risingLaser2 > 2) { // check to see if ISR has recorded sensor activations
      shutterBounceFlag2 = true;
    }
  }

  if (risingLaser1 > 2) { // check to see if ISR has recorded sensor activations on first laser
    shutterBounceFlag1 = true;
  }
} // end_shutterBounceCheck



//  Do the maths
void processData() {

  if ((Start2 < Start1) || (Stop2 < Stop1)) {  // error check lasers triggered in reverse (subtraction for micro-wrap)
    sensorReversedFlag = true;
  }
  else {
    sensorReversedFlag = false;
  }

  // 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
  SSfracV1 = (1.0 / SSsec1) + 0.5;         // inverse of theSSsec1, to give vulgar fraction rounded
  StringFracV1 = StringFrac + String(SSfracV1); // make a string for neat printing

  // do the maths calculations for second laser
  if (singleLaserFlag == 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
    SSfracV2 = (1.0 / SSsec2) + 0.5;        // inverse of theSSsec2, to give vulgar fraction rounded
    StringFracV2 = StringFrac + String(SSfracV2); // make a string for neat printing

    // 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
  }

  // Shutter speed average
  SSMILLISAV1.addValue(SSmillisInt1);                    // add last readings to aveerage


  // shutter curtain average
  if (singleLaserFlag == false && sensorReversedFlag == false) { // only calc if possible
    SSMILLISAV2.addValue(SSmillisInt2);                           // add last readings to average
    SHCURTAINSPEEDMILLISAV1.addValue(shCurtainspeedMilliS1);      // add last readings to aveerage
    SHCURTAINSPEEDMILLISAV2.addValue(shCurtainspeedMilliS2);      // add last readings to aveerage
  }
}  // end processData


// Display results of laser 1 to serial monitor

void altDisplay() {   // print to screen in tabulated output, neater & easier to read
  // printf("PARAMETER                  LASER2           LASER1 \n");
  Serial.println(F("PARAMETER                  LASER2           LASER1"));
  printf("Shutter speed MicroS   %10lu       %10lu \n", SSmicro2, SSmicro1);
  printf("Shutter speed milliS   %10i       %10i \n\n", SSmillisInt2, SSmillisInt1);

  // printf("shutter Speed Ave %3i  %10i       %10i \n\n", SSAveCount, SSmillisReadAve2, SSmillisReadAve1);

  printf("Shutter speed Seconds     %7.3f          %7.3f \n", SSsec2, SSsec1);

  //printf("Shutter Speed Fraction");
  Serial.print(F("Shutter Speed Fraction"));
  if (SSsec2 < 1 && singleLaserFlag == false) {   // test if Laser2 less than 1 second, if so, print in fractions
    printf(" %10s", StringFracV2.c_str());  // display shutter SSMs in fractions
  }
  else {
    printf("           ");
  }

  if (SSsec1 < 1) {   // test if Laser1 less than 1 second, if so, print in fractions
    printf("       %10s \n\n", StringFracV1.c_str());  // display shutter SSMs in fractions
  }
  else {
    printf("\n\n");
  }

  if (singleLaserFlag == false && sensorReversedFlag == false) {  // print curtain speeds
    printf("Curtain Speed MicroS   %10li       %10li \n",   shCurtainSpeedMicroS2, shCurtainSpeedMicroS1);
    printf("Curtain Speed MilliS   %10li       %10li \n\n", shCurtainspeedMilliS2, shCurtainspeedMilliS1);
  }

  printf("shutter Speed Ave  %3i  %10.0f       %10.0f \n",   SSMILLISAV1.getCount(), SSMILLISAV2.getAverage() , SSMILLISAV1.getAverage() );
  printf("Curtain Speed Ave  %3i  %10.0f       %10.0f \n\n", SHCURTAINSPEEDMILLISAV1.getCount(), SHCURTAINSPEEDMILLISAV2.getAverage(), SHCURTAINSPEEDMILLISAV1.getAverage());

  printf("shutter Spd St Dev %3i   %9.2f         %9.2f \n",  SSMILLISAV1.getCount(), SSMILLISAV2.getStandardDeviation(), SSMILLISAV1.getStandardDeviation());
  printf("Curtain Spd St Dev %3i   %9.2f         %9.2f \n\n",SHCURTAINSPEEDMILLISAV1.getCount(), SHCURTAINSPEEDMILLISAV2.getStandardDeviation(), SHCURTAINSPEEDMILLISAV2.getStandardDeviation());
 
  
  printf("shutter Speed Min  %3i  %10.0f       %10.0f \n",   SSMILLISAV1.getCount(), SSMILLISAV2.getMin(), SSMILLISAV1.getMin());
  printf("shutter Speed Max  %3i  %10.0f       %10.0f \n",   SSMILLISAV1.getCount(), SSMILLISAV2.getMax(), SSMILLISAV1.getMax());


  if (shutterBounceFlag2 == true) {       // print shutter 2 bounce count.
    printf("Second curtain Bounce(s)!  %i \n", risingLaser2 - 2 );
  }

  if (shutterBounceFlag1 == true) {      // print shutter 1 bounce count.
    printf("First curtain Bounce(s)!  %i \n", risingLaser1 - 2 );
  }

  if (singleLaserFlag == false && sensorReversedFlag == true) { // print warning
    //printf("Sensor or camera is reversed. Unable to calculate curtain speed \n");
    Serial.println(F("Sensor or camera is reversed. Unable to calculate curtain speed"));
  }

  if (Start2 < Stop1 && sensorReversedFlag == false) {
    //printf("Flash Sync (between the two lasers)\n");
    Serial.println(F("Flash Sync (between the two lasers)"));
  }

  printf("\n");
}// end void altDisplay


// output to LCD
void LCDdisplay() {

  if (shutterBounceFlag2 == true || shutterBounceFlag1 == true) {// print 'Bou' on line 0 if shutter bounce detected
    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 && singleLaserFlag == 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,
  if (sensorReversedFlag == false) {
    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");
  }

  else {
    lcd.setCursor(0, 3);
    lcd.print("Unable to calc CuSpd");
  }
}  // end_LCDdisplay


// clear vars
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;

  SSfracV1 = 0;
  SSfracV2 = 0;

  String StringFracV1 = "0";
  String StringFracV2 = "0";

  firedFlag1 = false;
  firedFlag2 = false;
  singleLaserFlag = false;

  risingLaser1 = 0;
  risingLaser2 = 0;
  fallingLaser1 = 0;
  fallingLaser2 = 0;
  laserChangeFlag2 = false;
  shutterBounceFlag1 = false;
  shutterBounceFlag2 = false;
  sensorReversedFlag = false;

  EIFR = bit(INTF0);
  EIFR = bit(INTF1);
  interrupts();
}  // end_clearVars



void resetAv() {
  Serial.println(F("Reset Average"));
  SSMILLISAV1.clear();              // clear average counters
  SSMILLISAV2.clear();
  SHCURTAINSPEEDMILLISAV1.clear();
  SHCURTAINSPEEDMILLISAV2.clear();

  while (digitalRead(12) == LOW) {} // wait for button release
  delay(250);                       // short delay for bounce
} //end_resetAV


//    Interrupt Routines
void CLOCK1() {  // interrupt called everytime the voltage on pin 2 changes, laser 1,
  if (digitalRead(2) == seen) {
    risingLaser1++;
  } // if the voltage on pin 2 is high, increase rising count.

  if (digitalRead(2) == blocked) {
    fallingLaser1++;
  } // If the voltage on pin 2 is low, increase falling count
} // end_ISR


void CLOCK2() {  // interrupt called everytime the voltage on pin 3 changes
  if (digitalRead(3) == seen) {
    risingLaser2++;           // if the voltage on pin 3 is high, increase rising count
    laserChangeFlag2 = true;  // set flag
  }
  if (digitalRead(3) == blocked) {
    fallingLaser2 = true;
  } // If the voltage on pin 3 is low, increase falling count
} // end_ISR

// end ISR
