Home

Section  Top     Video    Drilling Template    Bearings  Left View   Right View   Rear View  Bottom View Top View  Quick Release Mount   Electronics  ULN2003 Driver  Schematic  Vero Board layout   Code

 

 

 

Time-Lapse Panorama Controller

 

DIY Panorama Controller for GoPro Cameras

The controller will rotate your GoPro over a set angle for a preset duration or will rotate you GoPro for a full rotation for a set duration

The project is based on hardware and code by Tyler Winegarner http://www.instructables.com/id/Timelapse-Panning-controller-for-GoPro-Cameras/

and

from Mark Wheeler http://markwheeler.com/wordpress/?p=381 or  http://www.instructables.com/id/DIY-Arduino-Time-Lapse-Panner-for-GoPro/

I have added code for a 4x20 I2C LCD display and switches rather than the SainSmart LCD Keypad Shield. I have also removed the Arduino UNO shield and replaced it with an Arduino Atmega 328

mounted on vero board. Full schematics and vero board layouts included.

The controller is based on an Arduino Uno built onto Vero board. The Arduino controls a Geared Stepper Motor 28BYJ-48 via a ULN2003 Stepper Motor Driver Board.

A 4x20 I2C LCD display and 5 micro switches form the interface to the Arduino.

The controller has a Manfrotto 200PL-14 quick release tripod mount for attachment to my tripod and other mounting hardware fitted with a Manfrotto 323 Quick Release Clamp Adapter.

 

Power is provided by a 50000mah USB Power Bank Battery Pack

Rechargeable Battery Pack

 

 

 

 

 

 

 

 

 

 

 

 

Hardware

The stepper motor and gear wheel are mounted between 2 Perspex sheets separated by 4 x M5 hex bolts and 4 x aluminium tubes to act as spacers.

Flanged bearings 4mm top and 3mm bottom are fitted into holes in the sheet.

Once the stepper motor and gears were fitted the control boards were then fitted in around these parts.

 

I drew a drilling template in Turbocad then marked up the Perspex sheet through the paper printout.

Top and lower Perspex sheet were then clamped together and mounting holes and preliminary gear wheel bearing holes were drilled out.

Final bearing holes were drilled out once the sheets were separated as the lower bearing is smaller.

Before drilling out the stepper motor mounting/pinion hole I mounted the 133 tooth gear wheel and bearings cut the pinion hole then test fitted the stepper motor to check alignment.

I slotted one of the stepper motor mounting holes to give a few millimetres of adjustment to enable correct gear meshing.

Above Top Perspex sheet drilling template for mounting bolts, stepper motor and gear wheel shown above

lower Perspex sheet is identical but does not require stepper motor holes. It will need additional holes for middle bearing plate mount.

 

Once the stepper motor and gear wheel had been mounted I cut a short piece of aluminium bar to mount the middle bearing.

To ensure the middle bearing was in alignment with the the upper and lower bearings I drilled a 4mm hole the middle of the aluminium bar and mounted it on the gear wheel drive shaft.

I then marked mounting holes on the lower Perspex sheet through small preliminary holes on the bearing mounting bar.

 

The 4x20 LCD display is mounted on 2 flat metal bars. These bars are bent to angle the display for viewing and also to keep the overall mounted height within the distance between the Perspex sheets.

The bars are fixed to the Perspex sheets with large washers to spread the load on the Perspex.

I have used one of the mounting holes to fix the spirit level in place. See picture views for details.

 

 

 

 

Motor, Gear Wheel and Bearings

 

Geared Stepper Motor 28BYJ-48

The Geared Stepper Motor 28BYJ-48 has a 19 tooth Meccano Pinion attached. The pinion is drilled out to 5mm to fit the stepper motor drive shaft.

The Meccano 133 toothed gear wheel is fitted to a 4mm shaft.

The shaft runs on 3 flanged bearings.

One on the top Perspex sheet (4mm), one on an aluminium bearing mount fixed 10mm off the bottom Perspex plate (4mm). The 3rd (3mm) on the lower Perspex sheet.

The lower bearing is 1mm smaller so the drive shaft cannot pass through but just sit on the bearing surface so it can be supported and still rotate.

 

Flanged bearing

 

 

 

 

Bearing locations

A. 133 toothed gear wheel

B. Upper 4mm bearing

C. 133 toothed gear wheel shaft stop (see below)

D. 4mm 133 toothed gear wheel shaft

E. Middle 4mm bearing

F. Lower 3mm bearing

 

 

 

133 toothed gear wheel release

133 toothed gear wheel shaft stop (C) prevents the 133 toothed gear wheel and attached camera from falling out of the controller.

Free movement (J ) allows the 133 toothed gear wheel to be lifted free of the stepper motor pinion so the camera can be rotated to any start position by hand.

This is useful for initial camera positioning or a quick check of Panorama start and end points before setting on the controller.

 

 

 

 

 

 

 

 

 

 

 

Left Side

A. USB Socket for power

B Atmega 328

 

 

 

 

Right Side

A. Top 4mm Bearing

B Middle 4mm Bearing

C Bottom 3mm bearing

 

 

 

Back

 

 

 

 

 

Underside

A. Main Arduino Board

B. ULN2003 Stepper Motor Driver Board

C. I2C Board

D. Manfrotto Tripod Plate

E. LCD Display Board

F. Lower 3mm Bearing

 

 

 

 

 

 

Top

A. Meccano  Part 26 Pinion 19 Tooth

B. Meccano  Part 27b gear Wheel 33 Tooth

C. Main Arduino Board

D. ULN2003 Stepper Motor Driver Board

 

 

 

 

 

A Manfrotto PL200 quick release plate (A) is fitted to the bottom of the controller

using a number of Manfrotto 323 quick release adapters (B) I can quickly mount the controller to

any number of tri-pods or mounting devices

 

 

above controller fitted to a miniature tripod via a Manfrotto PL200 quick release adapter plate

and below a universal bar clamp

 

 

 

 

 

 

 

 

 

 

 

 

 

Electronics and Components

Geared Stepper Motor 28BYJ-48

 

 

 

 

 

 

 

 

 

ULN2003 Stepper Motor Driver Board

 

 

 

 

 

 

 

 

Schematic

 

 

 

 

 

 

 

 

 

Main Vero Board Layout Top and Switch Board Layout Bottom

Mouseover to see reverse

 

 

 

 

 

 

Code

This code is based on Tyler Winegarner/Mark Wheeler's code and has been modified to work on a 20x4 I2C LCD display without built in switches.

On normal boot/reset a modified version of Tyler Winegarner's set angled rotation Panorama is loaded.

If the Rotate Mode switch is held on reset then a modified version of Mark Wheeler's full rotation panorama is loaded.

Download Code

 

 

//  Time Lapse Panorama Controller for GoPro Cameras
//  by
//  Brett Oliver
//  http://www.brettoliver.org.uk
//  http://www.instructables.com/member/oliverb/
//
//  Based on hardware and code by Tyler Winegarner
//  http://www.instructables.com/member/twinegarner/
//  https://www.youtube.com/watch?v=cS93hhrdY6U
// 
//  and also
//  code by Mark Wheeler
//  http://markwheeler.com/wordpress/?p=381
//  http://www.instructables.com/member/mwheeler3/
//


#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <StepperAK.h>

#include <Time.h>
#define STEPS  2048   //Number of steps per revolution


// set the LCD address to 0x27 for a 20 chars 4 line display
// Set the pins on the I2C chip used for LCD connections:
//                    addr, en,rw,rs,d4,d5,d6,d7,bl,blpol
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);  // Set the LCD I2C address
//Stepper small_stepper(STEPS, 1, 3, 2, 12);
Stepper small_stepper(STEPS, 4, 3, 2, 6);
int lcd_key     = 0;
int adc_key_in  = 0;
int endStep = 1;
int startPos = 14336;
int endPos = 14336;
int startAngle=0; // to calc/display total angle
int endAngle=0; // to calc/display total angle
int panAngle=0; // to calc/display total angle
int curPos = 0;
int minutes = 1;
int endtime = 0;
int timeNow = 0;
int rotations = 1; // Fixed to 1 rotation
long runtime = 0;
int rightLock = 0;
//int leftLock = 13600;
int leftLock = 20700;
int span = 0;
int spanTotal = 0;
int stepDelay = 0;
int spanRemain = 0;
int motorPWR = 5;
int time = 0; // clock holder
int runlength = 0;
int StepJump = 100;
int PanorRotate = 11; // Off standard Panorama Timelapse  On Panorama Timelapse with Rotation
int degRemain = 0; // shows degree of camera


#define btnRIGHT  12
//#define btnUP     7
#define btnDOWN   8
#define btnLEFT   9
#define btnSELECT 10
#define btnNONE   7


int read_LCD_buttons()
{
 adc_key_in = digitalRead(12);
 if (adc_key_in == HIGH) return btnRIGHT;      // read the value from the switch
//}

//{
// adc_key_in = digitalRead(7);
// if (adc_key_in == HIGH) return btnUP;      // read the value from the switch
//}

//{
 adc_key_in = digitalRead(8);
if (adc_key_in == HIGH) return btnDOWN;      // read the value from the switch 
//}

//{
 adc_key_in = digitalRead(9);
if (adc_key_in == HIGH) return btnLEFT;      // read the value from the switch
//}
 
//{
 adc_key_in = digitalRead(10);
if (adc_key_in == HIGH) return btnSELECT;      // read the value from the switch
//}

return btnNONE;  // when all others fail, return this... 
} 
 



void setup(){

lcd.begin(20, 4);  // initialize the lcd for 20 chars 4 lines, turn on backlight

pinMode(motorPWR, OUTPUT);
pinMode(4, OUTPUT);
pinMode(btnRIGHT, INPUT);
//pinMode(btnUP, INPUT);
pinMode(11, INPUT);
pinMode(btnDOWN, INPUT);
pinMode(btnLEFT, INPUT);
pinMode(btnSELECT, INPUT);
}

 
 

void loop(){
  
  if (digitalRead(11) == 0) // If selection switch is off then Standard Panorama Timelapse is run
  {
 
  digitalWrite(motorPWR, HIGH);
  lcd.setCursor(0,0);
    lcd.print(" TimeLapse Panorama ");
    lcd.setCursor(0,1);
    lcd.print("        v9.8        ");
    delay(2000);
    lcd.setCursor(0,0);
    lcd.print("                    ");
    lcd.setCursor(0,1);
    lcd.print("                    ");
  lcd.setCursor(0,0);
  lcd.print("Enter Start Angle   ");
  lcd.setCursor(0,2);
  lcd.print("Use Left/Right Keys ");
  lcd.setCursor(0,3);
  lcd.print("   Sel to confirm   ");
  small_stepper.setSpeed(9);
  

  while(endStep){
    
    lcd_key = read_LCD_buttons();  // read the buttons
     switch (lcd_key)               // depending on which button was pushed, we perform an action
     {
       case btnRIGHT:
       {
         if (startPos > rightLock){
           small_stepper.step(-StepJump);
           startPos = startPos - StepJump;
         }
         lcd.setCursor(0,1);
         lcd.print("pos:            ");
         lcd.setCursor(4,1);
         lcd.print(startPos);
         
         lcd.setCursor(10,1);
         lcd.print("Angle:");
         lcd.setCursor(16,1);
         lcd.print("    ");
         lcd.setCursor(16,1);
         lcd.print((14336-startPos)/40); // show position in degrees
         startAngle= (14336-startPos)/40;
         
         
         delay(250);
         break;
       }
     case btnLEFT:
       {
         if (startPos < leftLock){
           small_stepper.step(+StepJump);
           startPos = startPos + StepJump;
         }
         lcd.setCursor(0,1);
         lcd.print("pos:            ");
         lcd.setCursor(4,1);
         lcd.print(startPos);
         
         lcd.setCursor(10,1);
         lcd.print("Angle:");
         lcd.setCursor(16,1);
         lcd.print("    ");
         lcd.setCursor(16,1);
         lcd.print((14336-startPos)/40); // show position in degrees
         startAngle= (14336-startPos)/40;
         delay(250);
         break;
       }
       case btnSELECT:
       {
         endStep = 0;
         delay(250);
         break;
       }
         case btnNONE:
       {
         break;
       }
     }
  }
  endStep = 1;
  lcd.setCursor(0,0);
  lcd.print("Enter End Angle     ");
  lcd.setCursor(0,3);
  lcd.print("   Sel to Confirm   ");

  endPos = startPos;
  
  while(endStep){
     lcd_key = read_LCD_buttons();  // read the buttons
     switch (lcd_key)               // depending on which button was pushed, we perform an action
     {
       case btnRIGHT:
       {
         if (endPos > rightLock){
           small_stepper.step(-StepJump);
           endPos = endPos - StepJump;
         }
         lcd.setCursor(0,1);
         lcd.print("pos:            ");
         lcd.setCursor(4,1);
         lcd.print(startPos);
         
         lcd.setCursor(10,1);
         lcd.print("Angle:");
         lcd.setCursor(16,1);
         lcd.print("    ");
         lcd.setCursor(16,1);
         
         lcd.print((14336-endPos)/40); // show position in degrees
         endAngle= (14336-endPos)/40;
         delay(250);
         break;
       }
     case btnLEFT:
       {
         if (endPos < leftLock){
           small_stepper.step(+StepJump);
           endPos = endPos + StepJump;
         }
         lcd.setCursor(0,1);
         lcd.print("pos:            ");
         lcd.setCursor(4,1);
         lcd.print(startPos);
         
         lcd.setCursor(10,1);
         lcd.print("Angle:");
         lcd.setCursor(16,1);
         lcd.print("    ");
         lcd.setCursor(16,1);
         
         lcd.print((14336-endPos)/40); // show position in degrees
         endAngle= (14336-endPos)/40;
         delay(250);
         break;
       }
       case btnSELECT:
       {
         endStep = 0;
         delay(250);
         break;
       }
         case btnNONE:
       {
         break;
       }
     }
  }
  endStep = 1;
  lcd.setCursor(0,0);
  lcd.print("Enter Run Time:     ");
  lcd.setCursor(0,1);
  lcd.print("                    ");
  while(endStep){
    lcd_key = read_LCD_buttons();  // read the buttons
     lcd.setCursor(0,1);
     lcd.print("    minutes");
     lcd.setCursor(0,1);
     lcd.print(minutes);
     delay(50);
     switch (lcd_key)               // depending on which button was pushed, we perform an action
     {
       case btnRIGHT:
       {
         if (minutes < 30) {
         minutes = minutes + 1;
         }
         else if (minutes < 120) {
         minutes = minutes + 15;
         }
         else if (minutes < 480) {
         minutes = minutes + 30;
         }
         delay(250);
         break;
       }
     case btnLEFT:
       {
         if (minutes > 0 && minutes < 35) {
         minutes = minutes - 1;
         }
         else if (minutes > 0 && minutes < 150) {
         minutes = minutes - 15;
         }
         else if (minutes > 0 && minutes < 485) {
         minutes = minutes - 30;
         }
         delay(250);
         break;
       }
       case btnSELECT:
       {
         endStep = 0;
         delay(250);
         break;
       }
         case btnNONE:
       {
         break;
       }
     }
  }
  lcd.setCursor(0,2);
     lcd.print("                    ");
  lcd.setCursor(0,3);
     lcd.print("                    ");
  runtime = (minutes * 60000);
  lcd.setCursor(0,0);
  lcd.print("  Panorama Running  ");
  lcd.setCursor(0,1);
  lcd.print("    Minutes Left");
  
  if (startAngle > 0 && endAngle > 0)
  {
    panAngle = startAngle - endAngle;
    abs(panAngle);
  }
  
  else if (startAngle > 0 && endAngle < 0 || startAngle > 0 && endAngle == 0)
  {
  panAngle = startAngle + abs(endAngle);
  }
  
  else if (startAngle < 0 && endAngle > 0 || startAngle == 0 && endAngle > 0)
  {
  panAngle = abs(startAngle) + abs(endAngle);
  }
  
  else if ((startAngle < 0 && endAngle < 0 || startAngle < 0 && endAngle == 0) && startAngle > endAngle)
  {
  panAngle = startAngle + abs(endAngle);
    abs(panAngle);
  }
  
  else if ((startAngle < 0 && endAngle < 0 || startAngle < 0 && endAngle == 0) && startAngle < endAngle)
  {
  panAngle = abs(startAngle) + endAngle;
    
  }
  
  else 
  {
  panAngle = 0;
  
  }
  
  
  if (startPos > endPos){ // checks to see if span is +ve or -ve
    span = startPos - endPos;
    spanTotal = startPos - endPos;
    spanRemain = span;
    stepDelay = (runtime / span);
    small_stepper.step(span);
    while (spanRemain) {
      small_stepper.step(-1);
      runtime = (runtime - stepDelay);
      lcd.setCursor(0,1);
      lcd.print("   ");
      lcd.setCursor(0,1);
      lcd.print((runtime / 60000) + 1);
      spanRemain--;
      delay(stepDelay);
        //**********************
  lcd.setCursor(0,2);
  lcd.print("Step Time m/s ");
 
    lcd.setCursor(14,2);
    lcd.print(stepDelay);
    
    
    
    
    lcd.setCursor(0,3);    
  lcd.print("Panorama Angle ");
  lcd.print(panAngle);
  //lcd.print(span/7,0);
  //lcd.setCursor(13,3);
  //lcd.print("Deg");
  
  //**********************
    }
      
  } else { //if span is -ve do this
    span = endPos - startPos;
    spanTotal = endPos - startPos;
    spanRemain = span;
    stepDelay = (runtime / span);
    small_stepper.step(span * -1);
    while (spanRemain) {
      small_stepper.step(1);
      runtime = (runtime - stepDelay);
      lcd.setCursor(0,1);
      lcd.print("   ");
      lcd.setCursor(0,1);
      lcd.print((runtime / 60000) + 1);
      spanRemain--;
      delay(stepDelay);
        //**********************
  lcd.setCursor(0,2);
  lcd.print("Step Time m/s ");

    lcd.setCursor(14,2);
    lcd.print(stepDelay);
    
  
    
    lcd.setCursor(0,3);
  lcd.print("Panorama Angle ");
  lcd.print(panAngle);
  //lcd.print(span/7,0);
  //lcd.setCursor(13,3);
  //lcd.print("Deg");
  
  //**********************
    }
    
   
  }
  endStep = 0;
  digitalWrite(4, LOW); // sets all motor coils off
  while(1){
   
    lcd.setCursor(0,0);
    lcd.print("                    ");
    lcd.setCursor(0,2);
    lcd.print("                    ");
    
    lcd.setCursor(0,1);
    lcd.print(" Panorama  Complete ");
    lcd.setCursor(0,3);
    lcd.print("                v9.8");
    delay(2000);
    lcd.setCursor(0,1);
    lcd.print("                    ");
    lcd.setCursor(0,3);
    lcd.print("                    ");
    lcd.setCursor(0,1);
    lcd.print("    Press Reset     ");
    lcd.setCursor(0,2);
    lcd.print("  To Start Again    ");
    
    delay(3000);
   
  }
  }
 else // If selection switch is on then Timelapse Panorama with rotation is run
 {
  //############################################################################
  //############################ ROTATION Panorama #############################
  //###########################Now fixed at 1 Rotation##########################
  
  digitalWrite(motorPWR, HIGH);
    while(endStep){
      // I do this for no reason.
    lcd.setCursor(0,0);
    lcd.print(" Rotation Panorama");
    lcd.setCursor(0,1);
    lcd.print("        v9.8        ");
    delay(2000);
    lcd.setCursor(0,0);
    lcd.print("                    ");
    lcd.setCursor(0,1);
    lcd.print("                    ");
    time_t timeNow = now(); // What time is it right now?
     endStep = 0;
  
  }
  // If you require more than 1 rotation un comment between /* and */ below
  /*
  endStep = 1; // Ask for Rotations
  lcd.setCursor(0,0);
  lcd.print("Rotations:      ");
  lcd.setCursor(0,1);
  lcd.print("(L/R) then SEL");

  

  while(endStep){
     lcd_key = read_LCD_buttons();  // read the buttons
     switch (lcd_key)               // depending on which button was pushed, we perform an action
     {
       case btnRIGHT:
       {
         
          rotations = rotations + 1;           
   
         lcd.setCursor(0,1);
         lcd.print("Rotations:     ");
         lcd.setCursor(11,1);
         lcd.print(rotations);
         delay(150);
         break;
       }
     case btnLEFT:
       {
    
          rotations = rotations - 1;           
         

         lcd.setCursor(0,1);
         lcd.print("Rotations:     ");
         lcd.setCursor(11,1);
         lcd.print(rotations);
         delay(150);
         break;
       }
       case btnSELECT:
       {
         endStep = 0;
         delay(150);
         break;
       }
         case btnNONE:
       {
         break;
       }
     }
  }
  */
  endStep = 1;
  // Ask for Duration
  lcd.setCursor(0,0);
  lcd.print("Enter Run Time:     ");
  lcd.setCursor(0,1);
  lcd.print("                    ");
  lcd.setCursor(0,2);
  lcd.print("Use Min Down/Up Keys");
  
  lcd.setCursor(0,3);
  lcd.print("   Sel to Confirm   ");

  while(endStep){
     lcd_key = read_LCD_buttons();  // read the buttons
     switch (lcd_key)               // depending on which button was pushed, we perform an action
     {
       case btnRIGHT:
       {
          minutes = minutes + 1;
         
         lcd.setCursor(0,1);
         lcd.print("Minutes:      ");
         lcd.setCursor(11,1);
         lcd.print(minutes);
         delay(150);
         break;
       }
     case btnLEFT:
       {
           minutes = minutes - 1;
       
         lcd.setCursor(0,1);
         lcd.print("Minutes:      ");
         lcd.setCursor(11,1);
         lcd.print(minutes);
         delay(150);
         break;
       }
       case btnSELECT:
       {
    lcd.setCursor(0,2);
    lcd.print("                    "); // blanks display
    lcd.setCursor(0,3);
    lcd.print("                    "); // blanks display
         time_t timeNow = now(); // What time is it right now?
         endtime = second(timeNow) + (minutes*60); // Add duration seconds to get endtime
          endStep = 0;
         delay(150);
         break;
       }
         case btnNONE:
       {
         break;
       }
     }
  }


  

  runtime = (minutes * 60000);
  lcd.setCursor(0,0);
  lcd.print("  Panorama Running  ");
  lcd.setCursor(0,1);
  lcd.print("        Seconds Left");

  if (rotations > 0) {
   
    span = (endPos - curPos);
    spanRemain = span;
    stepDelay = (runtime / span);
    // small_stepper.step(span * -1);
    while (spanRemain) {
      // small_stepper.step(1); // Counterclockwise
      small_stepper.step(-1); // Clockwise

      runtime = (runtime - stepDelay);
      // test for seconds left length
      runlength = ((runtime / 1000) + 1);
      
      
     // add number blanking for seconds display
     if (runlength >= 1000)
     {
      lcd.setCursor(0,1);
     } 
     else if (runlength >= 100 and runlength <= 999)
     {
       lcd.setCursor(0,1);
       lcd.print(" ");
       lcd.setCursor(1,1);
     } 
     else if (runlength >= 10 and runlength <= 99)
     {
     lcd.setCursor(0,1);
      lcd.print("   ");
       lcd.setCursor(2,1);
     }
     
     else {
     lcd.setCursor(0,1);
      lcd.print("    ");
       lcd.setCursor(3,1);
     }
      lcd.print((runtime / 1000) + 1  );
    // end add number blanking for seconds display  
      
     
    degRemain = (endPos/(spanRemain * 360));
    
    
      
     
      spanRemain--;
      // curPos = curPos + 1;
      delay(stepDelay);
    }
  rotations = rotations - 1;
 }
 
  endStep = 0;
  digitalWrite(4, LOW); // sets all motor coils off
  while(1){
    lcd.setCursor(0,0);
    lcd.print("                    ");
    lcd.setCursor(0,2);
    lcd.print("                    ");
    
    lcd.setCursor(0,1);
    lcd.print(" Panorama  Complete ");
    lcd.setCursor(0,3);
    lcd.print("                v9.8");
    delay(2000);
    lcd.setCursor(0,1);
    lcd.print("                    ");
    lcd.setCursor(0,3);
    lcd.print("                    ");
    lcd.setCursor(0,1);
    lcd.print("    Press Reset     ");
    lcd.setCursor(0,2);
    lcd.print("  To Start Again    ");
    
    delay(3000);
  }
  
  //==================================================================== 
 }
}

//######################################################################################################