Introduction: Nikon Time Lapse Controller V2

About: Nothing special

EDIT: I wanted to make the code more human readable, so I changed a bunch of variables to be easier to understand what the code is doing.

This is version 2 of my Nikon Time Lapse Controller. Previously I had relied upon a separate application and used Bluetooth to communicate with the controller. I found the application unreliable, often not connecting or locking up for long periods. Also, the bluetooth module didn't work out and started giving me problems.

To correct this, I have revised the code and used a touchscreen display on the controller instead. So, let's get started!

Step 1: Parts

So, we're switching from the Arduino Nano to an Arduino Mega. Here's what you'll need to build this project:

  • Infrared LED Module - I like this module, as it's 5v and has good range
  • 3.5 Inch Touchscreen display
  • Arduino Mega Controller

I use a combined package of the controller and display:

That is what the code I have created is used with, you'll need to modify it slightly if you use a different display.

EDIT: I can no longer find the LCD & Mega combined package. Dor those looking at what display I use, it is this one:

Smraza 3.5 inch TFT LCD

Step 2: Assembly

This is pretty straight forward. The display I use mounts on the Mega. The IR LED mounts above the screen (I use pins 36 and 40).

Step 3: Programming the Mega Controller

Connect the controller to a computer and upload the following code (also attached as an INO file for Arduino IDE).

You will need to include Adafruit GFX library, MCUFRIEND library, TaskScheduler library and any board specific library (like the TouchScreen.h one I include). The first three libraries can be installed in the Arduino programming IDE under Sketch->Include Library->Manage Libraries... Usually the board specific ones override certain functions from Adafruit GFX and MCUFRIEND, so the code shouldn't have to be modified too much to work. Probably only just the parts in the Board Constants are of my code.

Edit to add rotate screen functionality.

#include <Adafruit_GFX.h>
#include <MCUFRIEND_kbv.h>
#include <TaskScheduler.h>
#include <TouchScreen.h> // LCD Touchscreen specific library
struct tsScreenRotation {
  uint16_t Left;
  uint16_t Right;
  uint16_t Top;
  uint16_t Bottom;
};
struct iPoint {
  float x;
  float y;
};
struct btnCorners {
  struct iPoint TopLeft;
  struct iPoint BottomLeft;
  struct iPoint BottomRight;
  struct iPoint TopRight;
};
/***************************************************************************************************/
/*** Board constants *******************************************************************************/
/***************************************************************************************************/
#define MINPRESSURE 400
#define MAXPRESSURE 800
#define TS_MINX 125
#define TS_MAXX 905
#define TS_MINY 125
#define TS_MAXY 965
const struct tsScreenRotation tsSR[4] = { // Left, Right, Top, Bottom
  {TS_MINX,TS_MAXX,TS_MINY,TS_MAXY}, // Normal
  {0,0,0,0}, // Rotated 90°
  {TS_MAXX,TS_MINX,TS_MAXY,TS_MINY}, // Rotated 180°
  {0,0,0,0}  // Rotated 270°
};
const uint8_t pinXM = A2; // must be an analog pin, use "An" notation!
const uint8_t pinXP = 8; // can be a digital pin
const uint8_t pinYM = 9; // can be a digital pin
const uint8_t pinYP = A3; // must be an analog pin, use "An" notation!
const uint8_t pinLEDIrNeg = 36; // IR Led -
const uint8_t pinLEDIrPos = 40; // IR Led +
/***************************************************************************************************/
/*** Assign human-readable names to some common 16-bit color values ********************************/
/***************************************************************************************************/
#define BLACK   0x0000
#define BLUE    0x001F
#define GREEN   0x07E0
#define CYAN    0x07FF
#define RED     0xF800
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF
/***************************************************************************************************/
/*** Callback method prototypes for Tasks **********************************************************/
/***************************************************************************************************/
void rotateBase();
void transmitPulses();
void updateScreen();
void tftLoop();
/***************************************************************************************************/
/*** Variables *************************************************************************************/
/***************************************************************************************************/
uint8_t iScreenRotation = 2;
bool bStatePics = true;
bool bStateRotateBaseBase = false;
bool bStateRotateBasePrev = true;
float btnSize;
float fCenterWidth;
float fColumnHeigth;
uint16_t iCount = 0;
int iCountPrev = -1;
struct iPoint ipPos;
uint16_t iTimer = 3;
const uint8_t iTimerMin = 1;
const uint8_t iTimerMinRotate = 5;
uint16_t iTimerPrev = 0;
uint16_t tftHeight;
uint16_t tftWidth;
struct btnCorners SubTime; // Subtract time button location (set during initializeTFT function)
struct btnCorners AddTime; // Add time button location (set during initializeTFT function)
struct btnCorners RotateBase; // Rotate base button location (set during initializeTFT function)
struct btnCorners RotateScreen; // Rotate screen button location (set during initializeTFT function)
struct btnCorners StartStop; // Start/Stop button location (set during initializeTFT function)
const String sColumnTitles[5] = {
  "CBass Photography",
  "Pictures Taken",
  "Time Between Pictures",
  "Rotate Base",
  "Start/Stop Timer"
};
/***************************************************************************************************/
/*** Class variables *******************************************************************************/
/***************************************************************************************************/
MCUFRIEND_kbv tft;
TouchScreen tsScreen = TouchScreen(pinXP, pinYP, pinXM, pinYM, 300); //300 is resistance for my touch screen
Scheduler sTasks;
Task tPictures(0, TASK_FOREVER, &transmitPulses, &sTasks, false);
Task tRotateBase(0, TASK_ONCE, &rotateBase, &sTasks, false);
Task tTFTScreen(0, TASK_FOREVER, &tftLoop, &sTasks, true);
Task tUpdateScreen(0, TASK_ONCE, &updateScreen, &sTasks, false);
/***************************************************************************************************/
/*** User functions ********************************************************************************/
/***************************************************************************************************/
void initializeTFT() {
  uint8_t iTxtSize = 2;
  tft.setRotation(iScreenRotation);
  fColumnHeigth = tft.height() / 5.0;
  fCenterWidth = tft.width() / 2.0;
  btnSize = fColumnHeigth * 0.4;

  // Define button corners
  RotateScreen.TopLeft.x = 0;
  RotateScreen.TopLeft.y = 0;
  RotateScreen.BottomLeft.x = RotateScreen.TopLeft.x;
  RotateScreen.BottomLeft.y = RotateScreen.TopLeft.y + btnSize;
  RotateScreen.BottomRight.x = RotateScreen.TopLeft.x + btnSize;
  RotateScreen.BottomRight.y = RotateScreen.BottomLeft.y;
  RotateScreen.TopRight.x = RotateScreen.BottomRight.x;
  RotateScreen.TopRight.y = RotateScreen.TopLeft.y;
  
  SubTime.TopLeft.x = fCenterWidth - (tft.width() / 4.0);
  SubTime.TopLeft.y = fColumnHeigth * 2.25;
  SubTime.BottomLeft.x = SubTime.TopLeft.x;
  SubTime.BottomLeft.y = SubTime.TopLeft.y + btnSize;
  SubTime.BottomRight.x = SubTime.TopLeft.x + btnSize;
  SubTime.BottomRight.y = SubTime.BottomLeft.y;
  SubTime.TopRight.x = SubTime.BottomRight.x;
  SubTime.TopRight.y = SubTime.TopLeft.y;

  AddTime.TopLeft.x = fCenterWidth + (tft.width() / 4.0) - btnSize;
  AddTime.TopLeft.y = fColumnHeigth * 2.25;
  AddTime.BottomLeft.x = AddTime.TopLeft.x;
  AddTime.BottomLeft.y = AddTime.TopLeft.y + btnSize;
  AddTime.BottomRight.x = AddTime.TopLeft.x + btnSize;
  AddTime.BottomRight.y = AddTime.BottomLeft.y;
  AddTime.TopRight.x = AddTime.BottomRight.x;
  AddTime.TopRight.y = AddTime.TopLeft.y;

  RotateBase.TopLeft.x = fCenterWidth - fColumnHeigth / 4.0;
  RotateBase.TopLeft.y = fColumnHeigth * 3.25;
  RotateBase.BottomLeft.x = RotateBase.TopLeft.x;
  RotateBase.BottomLeft.y = RotateBase.TopLeft.y + btnSize;
  RotateBase.BottomRight.x = RotateBase.TopLeft.x + btnSize;
  RotateBase.BottomRight.y = RotateBase.BottomLeft.y;
  RotateBase.TopRight.x = RotateBase.BottomRight.x;
  RotateBase.TopRight.y = RotateBase.TopLeft.y;

  StartStop.TopLeft.x = fCenterWidth - fColumnHeigth / 4.0;
  StartStop.TopLeft.y = fColumnHeigth * 4.25;
  StartStop.BottomLeft.x = StartStop.TopLeft.x;
  StartStop.BottomLeft.y = StartStop.TopLeft.y + btnSize;
  StartStop.BottomRight.x = StartStop.TopLeft.x + btnSize;
  StartStop.BottomRight.y = StartStop.BottomLeft.y;
  StartStop.TopRight.x = StartStop.BottomRight.x;
  StartStop.TopRight.y = StartStop.TopLeft.y;

  tft.fillScreen(BLACK);
  printCenter(8, iTxtSize, YELLOW, sColumnTitles[0]);
  printCenter(fColumnHeigth * 0.8, iTxtSize, MAGENTA, sColumnTitles[1]);
  printCenter(fColumnHeigth * 2.0, iTxtSize, WHITE, sColumnTitles[2]);
  printCenter(fColumnHeigth * 3.0, iTxtSize, CYAN, sColumnTitles[3]);
  printCenter(fColumnHeigth * 4.0, iTxtSize, GREEN, sColumnTitles[4]);
  tft.drawFastHLine(SubTime.TopLeft.x, SubTime.TopLeft.y + (btnSize / 2.0), btnSize, WHITE);
  tft.drawFastHLine(AddTime.TopLeft.x, AddTime.TopLeft.y + (btnSize / 2.0), btnSize, WHITE);
  tft.drawFastVLine(AddTime.TopLeft.x + (btnSize / 2.0), AddTime.TopLeft.y, btnSize, WHITE);
  tft.fillCircle(
    RotateScreen.TopLeft.x + (btnSize / 2.0),
    RotateScreen.TopLeft.y + (btnSize / 2.0),
    btnSize / 2.0,
    WHITE
  );
  tft.fillCircle(
    RotateScreen.TopLeft.x + (btnSize / 2.0),
    RotateScreen.TopLeft.y + (btnSize / 1.7),
    btnSize / 2.0,
    BLACK
  );
  tft.fillTriangle(
    RotateScreen.TopRight.x, RotateScreen.TopRight.y + (btnSize / 2.0),
    RotateScreen.TopRight.x - (btnSize * 0.2), RotateScreen.TopRight.y + (btnSize * 0.3),
    RotateScreen.TopRight.x + (btnSize * 0.2), RotateScreen.TopRight.y + (btnSize * 0.3),
    WHITE
  );
}
bool pointInRectangle(struct btnCorners bcButton, struct iPoint ipCheck) {
  if (((ipCheck.x >= bcButton.TopLeft.x) && (ipCheck.x <= bcButton.TopRight.x)) &&
     ((ipCheck.y >= bcButton.TopLeft.y) && (ipCheck.y <= bcButton.BottomLeft.y))) {
    return true;
  } else {
    return false;
  }
}
/***************************************************************************************************/
void printCenter(uint16_t yTop, uint8_t iSize, uint16_t iColor, String sLine) {
  /* Print text in the center of the display */
  tft.setTextSize(iSize); /* Text dimensions: 6px wide x 8x tall = 1 iSize */
  tft.setCursor(fCenterWidth - (sLine.length() / 2.0 * 6 * iSize), yTop);
  tft.setTextColor(iColor, BLACK);
  tft.print(sLine);
}
/***************************************************************************************************/
void rotateBase() {
  // Code to rotate a camera mount. Rotate to new position faster than iTimerMinRotate - 1.5 seconds
}
/***************************************************************************************************/
/***************************************************************************************************/
void rotateScreen() {
  iCountPrev = -1;
  iTimerPrev = 0;
  bStateRotateBasePrev = !bStateRotateBaseBase;
  bStatePics = !tPictures.isEnabled();
  if (iScreenRotation == 0) {
    iScreenRotation = 2;
  } else {
    iScreenRotation = 0;
  }
  initializeTFT();
}
/***************************************************************************************************/
void tftLoop() {
  /* Touchscreen loop */
  TSPoint tpPick = tsScreen.getPoint(); //tpPick.x, tpPick.y are ADC values
  pinMode(pinXM, OUTPUT); pinMode(pinYP, OUTPUT); pinMode(pinXP, OUTPUT); pinMode(pinYM, OUTPUT);
  if (tpPick.z > tsScreen.pressureThreshhold) {
    ipPos.x = map(tpPick.x, tsSR[iScreenRotation].Left, tsSR[iScreenRotation].Right, 0, tft.width());
    ipPos.y = map(tpPick.y, tsSR[iScreenRotation].Bottom, tsSR[iScreenRotation].Top, 0, tft.height());
    if (pointInRectangle (RotateScreen, ipPos)) {
      rotateScreen();
    } else if ((pointInRectangle (SubTime, ipPos)) && (iTimer > (bStateRotateBaseBase ? iTimerMinRotate : iTimerMin))) {
      iTimer = iTimer - 1;
      tPictures.setInterval(iTimer * 1000);
      tTFTScreen.restartDelayed(200);
    } else if (pointInRectangle (AddTime, ipPos)) {
      iTimer = iTimer + 1;
      tPictures.setInterval(iTimer * 1000);
      tTFTScreen.restartDelayed(200);
    } else if (pointInRectangle (RotateBase, ipPos)) {
      bStateRotateBaseBase = !bStateRotateBaseBase;
      if (iTimer < (bStateRotateBaseBase ? iTimerMinRotate : iTimerMin)) {
        iTimer = bStateRotateBaseBase ? iTimerMinRotate : iTimerMin;
        tPictures.setInterval(iTimer * 1000);
      }
      tTFTScreen.restartDelayed(1000);
    } else if (pointInRectangle (StartStop, ipPos)) {
      if (tPictures.isEnabled()) {
        tPictures.disable();
      } else {
        tPictures.restart();
      }
      tTFTScreen.restartDelayed(1250);
    }
    tUpdateScreen.restart();
  }
}
/***************************************************************************************************/
void transmitPulses() {
  /*
      3 rapid signals for Nikon camera remote customized to Arduino, which is why there is use
      of delay instead of delayMicroseconds. For some reason delay(2) == delayMicroseconds(800)
  */
  iCount = iCount + 1;
  for (uint8_t iNdx = 0; iNdx < 3; iNdx++) {
    tone(pinLEDIrPos, 38000); delay(2);
    noTone(pinLEDIrPos); delay(28);
    tone(pinLEDIrPos, 38000); delayMicroseconds(200);
    noTone(pinLEDIrPos); delayMicroseconds(1500);
    tone(pinLEDIrPos, 38000); delayMicroseconds(200);
    noTone(pinLEDIrPos); delayMicroseconds(3300);
    tone(pinLEDIrPos, 38000); delayMicroseconds(200);
    noTone(pinLEDIrPos); delayMicroseconds(100); delay(63);
  }
  tUpdateScreen.restart();
  tRotateBase.restartDelayed(1500);
}
/***************************************************************************************************/
void updateScreen() {
  /* Update display with any changed values */
  if (iCount != iCountPrev) {
    printCenter(fColumnHeigth, 4, BLACK, String(iCountPrev));
    printCenter(fColumnHeigth, 4, MAGENTA, String(iCount));
    iCountPrev = iCount;
  }

  if (iTimer != iTimerPrev) {
    printCenter(fColumnHeigth * 1.7, 3, BLACK, String(iTimerPrev) + " seconds");
    printCenter(fColumnHeigth * 1.7, 3, WHITE, String(iTimer) + " seconds");
    iTimerPrev = iTimer;
  }

  if (bStateRotateBaseBase != bStateRotateBasePrev) {
    if (bStateRotateBaseBase) {
      tft.drawLine(RotateBase.TopLeft.x, RotateBase.TopLeft.y, RotateBase.BottomRight.x, RotateBase.BottomRight.y, CYAN);
      tft.drawLine(RotateBase.BottomLeft.x, RotateBase.BottomLeft.y, RotateBase.TopRight.x, RotateBase.TopRight.y, CYAN);
    } else {
      tft.fillRect(RotateBase.TopLeft.x, RotateBase.TopLeft.y, btnSize, btnSize, BLACK);
      tft.drawRect(RotateBase.TopLeft.x, RotateBase.TopLeft.y, btnSize, btnSize, CYAN);
    }
    bStateRotateBasePrev = bStateRotateBaseBase;
  }

  if (tPictures.isEnabled() != bStatePics) {
    if (tPictures.isEnabled()) {
      tft.fillRect(StartStop.TopLeft.x, StartStop.TopLeft.y, btnSize, btnSize, RED);
    } else {
      tft.fillRect(StartStop.TopLeft.x, StartStop.TopLeft.y, btnSize, btnSize, BLACK);
      tft.fillTriangle(
        StartStop.TopLeft.x, StartStop.TopLeft.y,
        StartStop.BottomLeft.x, StartStop.BottomRight.y,
        StartStop.TopRight.x, StartStop.TopRight.y + (btnSize / 2.0),
        GREEN);
    }
    bStatePics = tPictures.isEnabled();
  }
}
/***************************************************************************************************/
/*** Default functions *****************************************************************************/
/***************************************************************************************************/
void setup() {
  pinMode(pinLEDIrNeg, OUTPUT); digitalWrite(pinLEDIrNeg, LOW);
  pinMode(pinLEDIrPos, OUTPUT);
  tPictures.setInterval(iTimer * 1000);
  tft.begin(tft.readID());
  initializeTFT();
  tUpdateScreen.restart();
}
/***************************************************************************************************/
void loop() {
  sTasks.execute();
}

Step 4: Running the Controller

You'll need a power supply and USB cord to run the Mega. Once running, you'll see it in all its glory. I hope you enjoy the project!