Nikon Programmable Remote

Alright, it's about time that I make a writeup about my latest project, my Nikon Intervalometer. Here goes... Entry-level Nikon DSLRs don't have many built in features that the higher-end cameras do, like an interval timer, for instance. It's an exceedingly useful feature. If you want to take star trails, time lapse, repetitive self-timer shooting, or whatever, an intervalometer is really important.

Another feature that Nikon seems to have forgotten about for us lower-class photographers, is a built in, physical, connected cable release. If that existed, this project wouldn't be that spectacular. What they do have, though, is an infrared sensor for taking photos with a remote. So, logically, I took advantage of that for the project.

...

Yep. That's them. The idea of this board here is to have seven different preset 'programs' to flip between to take a picture in an entirely different way. One could snap a picture every five seconds, the next could snap a picture every five minutes, and another could take a picture every time someone pressed the middle button. Using all of the program LEDs, IR LED, buttons, serial connections and status indicator, uses up every single digital GPIO pin on the Atmel Atmega168/328. Therefore, I decided to use that as the main microcontroller for the project. That, and it has been ported to run the wonderful Arduino bootloader, meaning it can be programmed over serial with an easy-to-use and understand language. I'm still trying to understand AVR-GCC.

So, it was great in theory. Using BigMike.it's oscilloscope observations, I wrote a little program to snap a picture. It worked occasionally, but hardware interrupts and my inacurate floating point delays made it almost unusable. Thankfully, I came across Cibo Mahto's Nikon Library. Woot. That was consistent. It was based directly off of clock cycles rather than the delay library as part of Arduino.

There's only one problem. Right after I sent the board out to be fabbed, I saved the final board revision on my desktop, and preceded to delete it after it was delivered to my house. I have and old version of the board, but I don't have the final one I sent out. I made a few modifications to make the old one close to mine as possible, but this particular board isn't tested, but mine worked fine, and the schematic was almost exactly the same, so I can't imagine that it wouldn't work. Whoa. That was a long sentence. Anyway, the hardware is simple. Here's the schematic:

Here are the Gerber Files for sending to a Fab house, and here are the Eagle files.

And, if anyone needs a Bill of Materials from Digikey, just tell me. All the components but the IR LED (I tore it out of an old remote.) were just found with a simple search, and the schematic should be self-explanatory. It's another thing I deleted from my desktop, and I would have to make a new one, but that's not hard.

EDIT: Here is the Bill of materials. Okay, well I found the little Digikey recipt, and OCR-ed it. If It's not entirely right, just search. I also put the Eagle BOM.ulp ouput on there.

Now, for the code. It's probably more complicated than it needs to be, but here 'tis:

You can download the following code here, as well.

// Ethan's Arduino Nikon Programmable Remote w/ Intervalometer (July 2009)

#include <NikonRemote.h>

// Flow control variables
boolean continueloop = true;
boolean beganserial = false;

// Pin assignments

// IR LED pin assignment
NikonRemote remote(12);

// Status LED
int statusPin = 13;

// Pin assignment for each program LED
int progOne = 2;
int progTwo = 3;
int progThree = 4;
int progFour = 5;
int progFive = 6;
int progSix = 7;
int progSeven = 8;

// Pin assignements for each button
int butLeft = 9;
int butGo = 10;
int butRight = 11;

//The amount of time that the LEDs are all off to eliminate ambient light.
int allOffMS = 300;

// Starting default program = manual trigger mode
int progNum = 6;

// Time delays for each interval program (1-5)
unsigned long milliOne = 5000;    // five seconds
unsigned long milliTwo = 30000;   // thirty seconds
unsigned long milliThree = 60000; // one minute
unsigned long milliFour = 300000; // five minutes
unsigned long milliFive = 840000; // fourteen minutes

// Runs when the microcontroller boots
void setup(){
	// Power all pins to default values
	pinMode(statusPin, OUTPUT);

	pinMode(progOne, OUTPUT);
	pinMode(progTwo, OUTPUT);
	pinMode(progThree, OUTPUT);
	pinMode(progFour, OUTPUT);
	pinMode(progFive, OUTPUT);
	pinMode(progSix, OUTPUT);
	pinMode(progSeven, OUTPUT);

	pinMode(butLeft, INPUT);
	pinMode(butGo, INPUT);
	pinMode(butRight, INPUT);
}

// Main Loop - Runs immediately after setup()
void loop(){
	// Reset status LED to OFF
	digitalWrite(statusPin, LOW);

	interface(); // button selection logic
	LEDstate(); // power all LEDs to selected values
	delay(100); // One tenth second button debounce
}

// Terminate a program, resume the main loop
void resumeLoop(){
	digitalWrite(statusPin, HIGH);
	continueloop = false;
	LEDstate();
}

// button selection logic
void interface(){
	if (digitalRead(butGo)==LOW){
		// Middle "GO" button to ground means it's pressed
		digitalWrite(statusPin, HIGH);
		continueloop = true;
		program(); // execute the selected program
		digitalWrite(statusPin, LOW);
	}else if (digitalRead(butLeft)==LOW){
		// "Left" button pressed, decrement selected program
		digitalWrite(statusPin, HIGH);
		progNum-=1;
		digitalWrite(statusPin, LOW);
	}else if (digitalRead(butRight)==LOW){
		// "Right" button pressed, increment selected program
		digitalWrite(statusPin, HIGH);
		progNum+=1;
		digitalWrite(statusPin, LOW);
	}

	// Keep program selection in range
	if (progNum > 7) {
		progNum = 1;
	} else if (progNum < 1) {
		progNum = 7;
	}

 /* uncomment for diagnostic code
	if (beganserial == false){
		digitalWrite(statusPin, HIGH);
		Serial.begin(9600);
		beganserial = true;
		delay (50);
		digitalWrite(statusPin, LOW);
	}
	Serial.println(progNum);
 */
}

// Run the selected program
void program(){
	delay(200);  //Debounce delay to make sure the button isn't still pressed.
	while (continueloop == true){
		switch (progNum){
		case 1: intervalsnap(milliOne); break;
		case 2: intervalsnap(milliTwo); break;
		case 3: intervalsnap(milliThree); break;
		case 4: intervalsnap(milliFour); break;
		case 5: intervalsnap(milliFive); break;
		case 6: manualTrigger(); break;
		case 7: serialTrigger(); break;
		}
	}
}

// Manually trigger the camera
void manualTrigger(){
	if (digitalRead(butGo)==LOW){
		// Trigger camera if GO is pressed
		snap();
		digitalWrite(statusPin, HIGH);
	} else if ((digitalRead(butLeft)==LOW) || (digitalRead(butRight)==LOW)){
		// If Left or Right are pressed, exit program back to main loop
		resumeLoop();
	}
}

// Trigger the camera via Laptop serial port, etc.
void serialTrigger(){
	// Initialize serial one time only
	if (beganserial == false){
		digitalWrite(statusPin, HIGH);
		Serial.begin(9600);
		beganserial = true;
		delay (50);
		digitalWrite(statusPin, LOW);
	}

	// Snap a photo upon upper-case "S"
	if (Serial.available() > 0) {
		int serialByte = Serial.read();
		if (serialByte == 'S') {
			digitalWrite(statusPin, HIGH);
			snap();
			Serial.println("Picture Taken.");
			digitalWrite(statusPin, LOW);
		}
	}
}

// Intervalometer - time delay determined by which program is selected
void intervalsnap(unsigned long timeMS){
	// If time to wait is longer than 8 seconds, the status blink will last 5 seconds
	// otherwise, the status blink lasts 2 seconds.
	unsigned long preStatusBlinkTime;
	int nBlinks;
	if (timeMS < 1000){
		nBlinks = 1;
		preStatusBlinkTime = 500;
	}else if (timeMS < 8000){
		nBlinks = 4;
		preStatusBlinkTime = timeMS - 2000;
	}else{ //anything longer
		nBlinks = 10;
		preStatusBlinkTime = timeMS - 5000;
	}
	//Take account of the time it takes to turn back on the LEDs during snap();
	preStatusBlinkTime = preStatusBlinkTime - (allOffMS + 63);

	//Delay for specified number of milliseconds, quitting if any button is pressed
	delaycheck(preStatusBlinkTime);

	// Blink the status lights at half-second intervals for the remainder of the time
	int i;
	for(i=0; i < nBlinks; i++){
		digitalWrite(statusPin, HIGH);
		delaycheck(400);
		digitalWrite(statusPin, LOW);
		delaycheck(100);
	}

	// Trigger the shutter
	snap();
}

// Delay for specified number of tenths of a second, quitting if any button is pressed
void delaycheck(unsigned long delayMs){
	int i;
	int hundredms = delayMs / 100;
	for(i=0; i < hundredms; i++){
		delay(100);
		if ((digitalRead(butLeft)==LOW) || (digitalRead(butRight)==LOW) || (digitalRead(butGo)==LOW)){
			resumeLoop();
		}
	}
}

// Trigger the shutter
void snap(){
	remote.Snap();
	delay(63);
	remote.Snap();
	allOff();
	delay(allOffMS);
	LEDstate();
}

// Called by main loop to power all LEDs to selected values
void LEDstate(){
	digitalWrite(statusPin, LOW);
	if (progNum == 1) {
		digitalWrite(progOne, HIGH);
		digitalWrite(progTwo, LOW);
		digitalWrite(progThree, LOW);
		digitalWrite(progFour, LOW);
		digitalWrite(progFive, LOW);
		digitalWrite(progSix, LOW);
		digitalWrite(progSeven, LOW);
	}
	if (progNum == 2) {
		digitalWrite(progOne, LOW);
		digitalWrite(progTwo, HIGH);
		digitalWrite(progThree, LOW);
		digitalWrite(progFour, LOW);
		digitalWrite(progFive, LOW);
		digitalWrite(progSix, LOW);
		digitalWrite(progSeven, LOW);
	}
	if (progNum == 3) {
		digitalWrite(progOne, LOW);
		digitalWrite(progTwo, LOW);
		digitalWrite(progThree, HIGH);
		digitalWrite(progFour, LOW);
		digitalWrite(progFive, LOW);
		digitalWrite(progSix, LOW);
		digitalWrite(progSeven, LOW);
	}
	if (progNum == 4) {
		digitalWrite(progOne, LOW);
		digitalWrite(progTwo, LOW);
		digitalWrite(progThree, LOW);
		digitalWrite(progFour, HIGH);
		digitalWrite(progFive, LOW);
		digitalWrite(progSix, LOW);
		digitalWrite(progSeven, LOW);
	}
	if (progNum == 5) {
		digitalWrite(progOne, LOW);
		digitalWrite(progTwo, LOW);
		digitalWrite(progThree, LOW);
		digitalWrite(progFour, LOW);
		digitalWrite(progFive, HIGH);
		digitalWrite(progSix, LOW);
		digitalWrite(progSeven, LOW);
	}
	if (progNum == 6) {
		digitalWrite(progOne, LOW);
		digitalWrite(progTwo, LOW);
		digitalWrite(progThree, LOW);
		digitalWrite(progFour, LOW);
		digitalWrite(progFive, LOW);
		digitalWrite(progSix, HIGH);
		digitalWrite(progSeven, LOW);
	}
	if (progNum == 7) {
		digitalWrite(progOne, LOW);
		digitalWrite(progTwo, LOW);
		digitalWrite(progThree, LOW);
		digitalWrite(progFour, LOW);
		digitalWrite(progFive, LOW);
		digitalWrite(progSix, LOW);
		digitalWrite(progSeven, HIGH);
	}
	
	/*  
	//simple, overrated approach to the same thing as above.
	int pin;
	for (pin = 2; pin <= 8; pin++) {
		if (pin == progNum+1) {
			digitalWrite(pin, HIGH);
		} else {
			digitalWrite(pin, LOW);
		}
	}
	digitalWrite(statusPin, LOW);
*/
}

//Turn off all LEDs
void allOff(){
	digitalWrite(progTwo, LOW);
	digitalWrite(progThree, LOW);
	digitalWrite(progFour, LOW);
	digitalWrite(progFive, LOW);
	digitalWrite(progSix, LOW);
	digitalWrite(progSeven, LOW);

	digitalWrite(statusPin, LOW);
}

Okay, if you got to the bottom of the page here, congratulations. I made a list of what I would do differently if I were to make a second one.
  1. Incorporate the switch into the board.
  2. Take out the power LED and resistor.
  3. Use some sort of jumper or something to turn off the main IR LED and only use the external one.
  4. I would use a PC mount battery holder, and incorporate that into the board design so I wouldn't have to use a zip-tie

Here's an untested version two of the board.

And a double-A with boost version of the board: here.

Along with newer code: here.

Woot! That's it!