Measuring camera flash lag, part 2: building a measurement tool

Introduction
In part 1, I explained why I wanted to measure flash lag with my Fujifilm X-T4 and Godox triggers. I also shared what I found. Here in part 2, I’ll show how I built the device that made those measurements possible.
The basic idea
The goal was to use an Arduino to electronically trigger the camera, then detect the flash and calculate the time between the two events.
To trigger the camera, I used its 2.5 mm jack remote port. Shorting the ring or tip (which the camera keeps at a positive voltage) with the sleeve (which is ground) causes focusing or shutter release, respectively.

To do the shorting electronically, I used standard transistors.
For detecting the flash, I used a phototransistor (a TEPT4400). Photoresistors (also called LDR) are cheaper and more common in hobbyist electronic kits like the one I had, but they’re kind of slow to react to light, so they wouldn’t have picked up a short flash burst, or if they did, they would have introduced some lag of their own and skewed the measurement.
Here’s the schematic:

Then I laid it out on a breadboard:

And finally built a working prototype:

When you press the button, the Arduino runs this sequence:
- Shorts Sleeve and Ring to trigger autofocus
- Waits a bit for the camera to focus
- Shorts Sleeve and Tip to trigger the shutter
- Waits for the flash
- Sends the time between step 3 and 4 to the computer over serial
The code
Here’s the full sketch:
const int halfShutterPin = 2; // grey
const int fullShutterPin = 3; // orange
const int btnPin = 4; // green
const int ptxPin = 5; // yellow
const int ledPin = LED_BUILTIN;
const int halfShutterMs = 1000;
const int abortMs = 1000;
const int resetMs = 1000;
void setup() {
pinMode(halfShutterPin, OUTPUT);
pinMode(fullShutterPin, OUTPUT);
pinMode(btnPin, INPUT_PULLUP);
pinMode(ptxPin, INPUT);
pinMode(ledPin, OUTPUT);
Serial.begin(115200);
}
void loop() {
if (digitalRead(btnPin) == HIGH) {
idle();
} else {
fire();
}
}
void idle() {
// blink if flash is detected while idle, useful for testing
if (digitalRead(ptxPin) == LOW) {
digitalWrite(ledPin, HIGH);
delay(500);
digitalWrite(ledPin, LOW);
}
}
void fire() {
Serial.println("firing...");
digitalWrite(halfShutterPin, HIGH);
delay(halfShutterMs);
digitalWrite(fullShutterPin, HIGH);
digitalWrite(ledPin, HIGH);
unsigned long fireTime = millis();
unsigned long flashTime = 0;
while (true) {
unsigned long now = millis();
if (now > fireTime + abortMs) {
break;
}
if (digitalRead(ptxPin) == LOW) {
flashTime = now;
break;
}
}
digitalWrite(fullShutterPin, LOW);
digitalWrite(halfShutterPin, LOW);
digitalWrite(ledPin, LOW);
if (flashTime == 0) {
Serial.println("aborted");
} else {
Serial.print("flash seen after ");
Serial.print(flashTime - fireTime);
Serial.println(" ms");
}
delay(resetMs);
}