LightRails – Dynamic External Exposure Control for Time-lapse

I’ve been sitting on this a little while, and I feel a bit remiss in not sharing it sooner.  I had intended to make it “perfect” before sharing, but feel I’ve reached a point where I’m not ready to spend all of my effort on this project, and instead wish to work more on OpenMoco (but we’ll talk more about that soon!).  So, I’ll share this as a more “rough” project.

Some time ago, I started out with the TSL230R chip from Taos with the intent of producing a (a) my own digital light meter and (b) a dynamic external control system for time-lapse photography.  Certainly, the act of creating my own digital light meter was a smashing success, also having it control my dSLR via a remote cable in bulb mode was also a great success.  Enough so that it should prove an invaluable tool to the DIY pinhole photographer.  (Man, how do you time that 2:35:20 exposure manually? =)

The problem I wanted to solve was a difficult, but common one: how do you effectively manage exposure changes across sunrise and sunset time-lapses with a dSLR?  It sounds very easy, just go into Av or Tv mode!  Of course, life isn’t that easy.  The standard dSLRs generally only meter and adjust exposure in the very rough terms of 1/6, 1/4, 1/3, or even 1/2 EV steps!  This means it has a sledge hammer where the problem calls for a gentle tapping of a finger.  I set out to create a system that would control the camera externally, metering and adjusting exposure in 1/100 EV steps…

Some more information about how this all got started, and some color commentary along the way can be found in these two forum postings over at timescapes.org:

Testing Dynamic External Exposure Control

Huge Dynamic Range Change

Sunset Time-lapses

Unfortunately, the system is still a bit rough for time-lapse photography in controlling exposure in all conditions, but it can be made to succeed in certain conditions, given practice and patience.  I’m not going to go into great detail on instructions, as it should be largely evident based on the information below, but you will need the following parts at a minimum to use this code:

  • (1x) Arduino board with Atmega168P chip (will probably work with 328p)
  • (1x) Taos TSL230R light sensor IC
  • (1x) 16×2 Parallel LCD screen
  • (1x) 4N28 opto-isolator (isolating camera trigger circuitry)
  • (5x) SPST mometary switches
  • (1x) 0.1uF Ceramic Capacitor

Effectively, the sensor should be placed remote from the control unit (but within specs of the TSL230R), and the control unit uses an LCD display to display information, and five momentary switches to control operation and input data.  The best configuration I’ve found for using this for time-lapse is to create a reflector, placing the sensor and the capacitor in a separate enclosure from the control, and place the sensor inside of the reflector.  For a reflector, I simply used three pieces of wood connected like a ‘C’, painted the inside flat white, and mounted the sensor housing inside of the ‘C’, facing downwards.  The reflector could then either be placed in complete shade, or pointed towards the scene.  Either way, the unit should be protected from over-head light changes – it should either meter the scene on average, or the amount of light read in shade.  Which is better depends on the scene in question — shooting sunny sky-scenes with dim foregrounds generally require scene metering, whereas equally-lit sky and foregrounds prefer shade-readings.  It is also capable of metering through your eye-piece, but resolution suffers greatly (set TTL F/stop in the UI to enable this mode).

The system is configured with ISO, f/stop, minimum and maximum exposure times, number of EV divisions per step (best left at 100), and shot interval.  The light reading is calculated once per second, and exposure time calculated from the settings and light reading.  EV adjust is set to achieve the proper exposure vs. the metered exposure, and the system is left to run.

Random fluctuations and massive jumps from physical interference (say a dog sniffing at your meter while you’re napping =) are prevented through the simple application of a ‘ev change ceiling’ setting, which prevents any change in calculated EV greater than the ceiling (in x/y EV steps — e.g.: no change greater than 3/100’s EV.) — this method has proven its self as both the most simple and effective means through empirical testing, beating out averaging, weighting, and all other sorts of complicated methods.

If you find this useful, come up with a novel application, or have any questions – please don’t hesitate to drop me a comment here.  I’d love to know what you do with it, or help you if you need it.

The following videos should give you some idea how to assemble and use it:

note: both videos show older versions of the UI and the device, where the sensor was integrated into the controller. This is not recommended.

And, here’s an example of its performance:

And, without further ado, here’s the code.  Each section should be pasted into a seperate tab in the Arduino UI, with the first section of code in the main tab.

lightrails.pde:


/*

 LightRails 1.0
   A dynamic external exposure control system with
   integrated intervalometer for time-lapse
   photography

 Copyright (c) 2008-2009 C. A. Church drone< a_t >dronecolony.com

 This program is free software: you can redistribute it
 and/or modify it under the terms of the GNU General
 Public License as published by the Free Software
 Foundation, either version 3 of the License, or (at
 your option) any later version.

 This program is distributed in the hope that it will be
 useful, but WITHOUT ANY WARRANTY; without even the
 implied warranty of MERCHANTABILITY or FITNESS FOR A
 PARTICULAR PURPOSE. See the GNU General Public License
 for more details. 

*/

#include <LiquidCrystal.h>
#include <avr/pgmspace.h>
#include <MsTimer2.h>

#define TSL_FREQ_PIN 2
#define TSL_S0         6
#define TSL_S1         5
#define TSL_S2         3
#define TSL_S3       4

#define CAMERA_PIN   13

 // buttons

#define B_LT   9
#define B_RT   10
#define B_UP   7
#define B_DN   8
#define B_CT   11

  // max number of setup steps available

#define MAX_SETUP_STEPS   12

  // read frequency for how many milliseconds
  // note: this code only works for 1000 any other value
  // and you need to adjust your frequency calculation

#define READ_TM      1000

  // low and high thresholds for adjusting sensitivity
  // of the TSL230R.  If freq is below LO thresh, it will
  // automatically increase sensitivity.  If it increases
  // beyond the HI thresh, it will decrease sensitivity

#define SENS_THRESH_LO 101
#define SENS_THRESH_HI 7999

  // LCD print buffer
  // set to LCD width + 1

#define SIZE_OF_LCD_BUF 17

 // threshold to accept a frequency change
 // the frequency read off the chip must change by
 // at least this many Hz to accept the change
 // this prevents fluttering.  However, it can cause
 // problems with low-light level changes.  You may need
 // to adjust according to types of shooting you
 // typically do (higher for more day shooting
 // and lower for more night shooting)

#define FREQ_CHG_THRESH  20

 // the minimum exposure gap is the minimum time
 // between triggering exposures (for the cases where
 // exposure_time exceeds interval_time).  At a bare
 // minimum, it should be the minimum amount of time
 // required by your camera to consistently register
 // the completion of one exposure and execution of
 // another.  this time is in milliseconds

#define MIN_EXP_GAP 1000

 // some info about exposure data
 // need iso and f/stop to calculate
 // exposure time.  These are just
 // defaults - they are adjusted via the UI

int   iso_rating = 100;
float f_stop     = 8.0;

 // Maximum Diff % in EV Value calculated from reading to reading
byte ev_diff_ceiling  = 0; 

 // for through-a-lens metering - f-stop of the lens
 // (light read must be calculated)

float ttl_stop = 0.0;

 // default times
unsigned int camera_delay       = 5;
unsigned int min_exp_tm         = 10;
byte         max_exp_tm         = 20;

 // actual delay time in mS

unsigned long real_camera_delay = camera_delay * 1000;

  // running variables

float         uwcm2          = 0; // uw/cm2 calculated 
float         lux            = 0; // Lux caclulated
float         ev             = 0; // EV value calculated
float         exp_tm         = 0; // camera exposure time calculated in 1/x value
float         pre_exp_tm     = 0; // for latching, need previous reading
unsigned long frequency      = 0; // frequency read
unsigned long cur_exp_tm     = 0; // current exposure time in mS
unsigned int  shots_fired    = 0; // how many exposures taken since interval started

bool camera_engaged          = false; // camera currently exposing?
bool light_type              = false; // which spd graph to use - d65 [0] or ilA [1]

 // this is used by the pulse counter.

volatile unsigned long pulse_cnt = 0;

 // for calculating time differences

unsigned long pre_tm         = millis();

unsigned int  freq_tm_diff   = 0; // how much time has passed since last frequency reading
                                  // -- note the int size limit
unsigned long camera_tm_diff = 0; // how much time has passed since last exposure

 // this is used by the sensitivity adjustment
 // to record the amount to divide the freq by
 // to get the uW/cm2 value at 420nm

unsigned int  calc_sensitivity   = 10;

 // this is the frequency calculation multiplier
 // based on what scaling factor is chosen. 
 // set_scaling() will adjust it for you.

byte freq_mult = 1;

 // our previous frequency count
 // -- we set it to fifty so it doesn't swing
 // auto-sensitivity adjustment either way on the first
 // reading

unsigned long last_cnt  = 50;

  // some EV tracking variables

float prev_ev = 0;
float set_ev  = 0;

  // our wavelengths (nm) we're willing to calculate illuminance for (lambda)
int wavelengths[18] = { 380, 400, 420, 440, 460, 480, 500, 520, 540, 560, 580, 600, 620, 640, 660, 680, 700, 720 };
  // the CIE V(l) for photopic vision - CIE Vm(l) 1978 - mapping to the same (l) above
float v_lambda[18]  = { 0.0002, 0.0028, 0.0175, 0.0379, 0.06, 0.13902, 0.323, 0.71, 0.954, 0.995, 0.87, 0.631, 0.381, 0.175, 0.061, 0.017, 0.004102, 0.001047 };

  // CIE SPD graphs for D65 and Illuminant A light sources, again mapping to same lambda as included in wavelengths
float spd_graphs[2][18] = {
  { 49.975500, 82.754900, 93.431800, 104.865000, 117.812000, 115.923000, 109.354000, 104.790000, 104.405000, 100.000000, 95.788000, 90.006200, 87.698700, 83.699200, 80.214600, 78.284200, 71.609100, 61.604000 },
  { 9.795100, 14.708000, 20.995000, 28.702700, 37.812100, 48.242300, 59.861100, 72.495900, 85.947000, 100.000000, 114.436000, 129.043000, 143.618000, 157.979000, 171.963000, 185.429000, 198.261000, 210.365000 }
};

/*

  Let's setup all of our display strings for the LCD

  These are stored as PROGMEM so they don't take up valuable
  SRAM

*/
  // main display strings

// setup strings

const prog_char setup_str1[] PROGMEM  = "ISO";
const prog_char setup_str2[] PROGMEM  = "Target F/Stop";
const prog_char setup_str3[] PROGMEM  = "EV Adjust";
const prog_char setup_str4[] PROGMEM  = "EV Steps";
const prog_char setup_str5[] PROGMEM  = "Max Exp. - Sec";
const prog_char setup_str6[] PROGMEM  = "Exposure Cycle";
const prog_char setup_str7[] PROGMEM  = "Latch";
const prog_char setup_str8[] PROGMEM  = "Min Exp. - mSec";
const prog_char setup_str9[] PROGMEM  = "Scale Read";
const prog_char setup_str10[] PROGMEM = "TTL F/Stop";
const prog_char setup_str11[] PROGMEM = "Light Source";
const prog_char setup_str12[] PROGMEM = "EV Change Max";

PGM_P setup_strings[MAX_SETUP_STEPS] PROGMEM =
 {
   setup_str1,
   setup_str2, 
   setup_str3,
   setup_str4, 
   setup_str6,
   setup_str7,
   setup_str8,
   setup_str5,
   setup_str9,
   setup_str10,
   setup_str11,
   setup_str12
 };

 // buffer for above setup strings
char lcd_print_buffer[SIZE_OF_LCD_BUF];

 // ev adjust can be negative
 //
 // how many EV steps to add or subtract
 // to each EV calculation - UI moves in 1/4EV
 // increments.

float ev_adjust     = 0.0;

 // how many steps per EV to deal with -
 // calculations round to the nearest step
 // and adjustments happen in these steps,
 // e.g.: 10 steps = .1 EV per step,
 // 1 step = 1 EV per step

byte ev_steps     = 100;

 // status flags:
 // B0 = intervalometer enable
 // B1 = latch up
 // B2 = latch down
 // B3 = in setup
 // B4 = latched (exp tm not moved due to latch)
 // B5 = latched to min/max exp tm setting
 // B6 = ev adjust enabled
 // B7 = update setup display

byte status = B00000000;

 // current setup step

byte setup_step  = 0;

 // display type flags
 // 0 = 1/x shutter speed/time (calculated)
 // 1 = ms shutter speed (calculated)
 // 2 = frequency
 // 3 = EV (calculated)
 // 4 = W/m2 (calculated)
 // 5 = sensitivity (10, 100, 1000)
 // 6 = lux (calculated)
 // 7 = scale

byte disp_enable = B10000000;

 // button hit flags (plus a few others)
 // B0 = up
 // B1 = dn
 // B2 = lt
 // B3 = rt
 // B4 = ct
 // B5
 // B6 =
 // B7 = camera exposing (use this to save a var elsewhere)

byte buttons = B00000000;

 // time last button was hit
unsigned long button_hit = 0;

 // init lcd display

LiquidCrystal lcd(14, 14, 15, 16, 17, 18, 19);

void setup() {

 lcd.clear();
 lcd.print("LightRails/1.0"); 

  // attach interrupt to pin8, send output pin of TSL230R to arduino 8
  // call handler on each rising pulse

 attachInterrupt(0, add_pulse, RISING);

 pinMode(TSL_FREQ_PIN, INPUT);

 pinMode(TSL_S0, OUTPUT);
 pinMode(TSL_S1, OUTPUT);
 pinMode(TSL_S2, OUTPUT);
 pinMode(TSL_S3, OUTPUT);

 pinMode(CAMERA_PIN, OUTPUT);

   // set input button pins

 pinMode(B_LT, INPUT);
 pinMode(B_RT, INPUT);
 pinMode(B_UP, INPUT);
 pinMode(B_DN, INPUT);
 pinMode(B_CT, INPUT);

   // enable pullup resistors
 digitalWrite(B_LT, HIGH);
 digitalWrite(B_RT, HIGH);
 digitalWrite(B_UP, HIGH);
 digitalWrite(B_DN, HIGH);
 digitalWrite(B_CT, HIGH);

  // write TSL sensitivity 1x

 digitalWrite(TSL_S0, HIGH);
 digitalWrite(TSL_S1, LOW);

  // set frequency scaling

 tsl_set_scaling(2); 

}

void loop() {

  // operate camera
  // handle user input
  // update light sensor data

    // calculate how much time has passed
    // both for camera firing and light reading

    // this code requires arduino 012 or any version with
    // compatible millis() overflow and behavior

 freq_tm_diff   += millis() - pre_tm;
 camera_tm_diff += millis() - pre_tm;

 pre_tm          = millis();

  // if intervalometer enabled, enough time has passed,
  // and the camera isn't already engaged
  // - fire camera (camera delay is in seconds)

 if( status & B10000000 && camera_tm_diff >= real_camera_delay &&  camera_engaged == false ) {

       // re-set camera difference timer
     camera_tm_diff = 0;

       // trigger camera remote
     fire_camera();

      // convert sec to msec for next exposure check
      // always try and use the configured value first
      // (see next operation)

     real_camera_delay = camera_delay * 1000;

       // assure MIN_EXP_GAP threshold is enforced
       // between exposure triggers
       //
       // if the exposure time leaves less than the
       // minimum gap between exposures (MIN_EXP_GAP
       // must be <= real_camera_delay), add the minimum gap
       // to the current exposure time, and make that the
       // interval time before the next shot

     if( cur_exp_tm >= ( real_camera_delay - MIN_EXP_GAP ) )
       real_camera_delay = cur_exp_tm + MIN_EXP_GAP;

       // update count of shots fired
     shots_fired++;

 } // end if( status...   

   // see if the user has pressed any buttons, and handle
   // them if need be

 check_input();

   // if a second has passed, we need to
   // update our calculations

 if( freq_tm_diff >= READ_TM ) {

     // reset time counter

   freq_tm_diff = 0;

    // get current frequency

   frequency = tsl_get_freq(); 

    // ignore changes in frequency below FREQ_CHG_THRESH

   if( last_cnt > 0 && abs( (long) ( frequency - last_cnt ) ) < FREQ_CHG_THRESH )
     frequency = last_cnt;

     // set other needed values

   if ( frequency > 0 ) {  

       // chip has given us a positive reading

       // calculate power   
     uwcm2     = tsl_calc_uwatt_cm2(frequency);   

     if( uwcm2 <= 0 ) {
         // handle no actual power reading available
       uwcm2 = 0;
       lux   = 0;
     }
      else {

        // there was a positive power reading...

        // calculate lx value using gaussian formula

       lux = calc_lux_gauss(uwcm2);
      }
   }
   else {
       // 0 frequency read from chip - set all values to zero
     uwcm2 = 0;
     lux   = 0;
   }

   if ( lux <= 0 ) {
       // don't try calculating nonsense - we cant calculate
       // an EV with no light.
     lux = 0;
     ev = -6.0;
   }
   else {
       // we have positive lux reading
       // calculate EV from lux reading
     ev = calc_ev(lux);
   }

      // calculate exposure time value

   exp_tm     = calc_exp_tm( ev, f_stop );

      // determine if we need to latch on to a particular exposure
      // as lowest or highest allowed

   if( status & B10000000 && status & B01100000) {
     // latch high or low enabled - and intervalometer on     

     if( status & B01000000 ) {
         // latch low is enabled (don't increase exposure time)
         // remember that exp_tm is a divisor - so a higher number == faster exposure
       if( exp_tm > pre_exp_tm) {
         exp_tm = pre_exp_tm;
           // indicate that exposure is latched
         status |= B00001000;
       }
       else {
           // reset latch indicator
         status &= B11110111;
           // set new exposure value to keep exp from
           // getting longer than current exp. (new
           // ceiling)
         pre_exp_tm = exp_tm;
       }
     }
     else if( status & B00100000 ) {
         // latch high enabled (don't decrease exposure time)
       if( exp_tm < pre_exp_tm ) {
          exp_tm = pre_exp_tm;
          status |= B00001000;
       }
       else {
         status &= B11110111;
           // new floor
         pre_exp_tm = exp_tm;
       }
     }
   } // end if latch high or low enabled

     // calculate exposure ms
     // we can only go as short as 1ms, so don't
     // try and calculate below that.  Check for
     // 1/1000 ceiling

   if( exp_tm >= 1000 ) {
    cur_exp_tm = 1;
   }
   else {
     cur_exp_tm = calc_exp_ms( exp_tm );
   }

    // check for minimum/max exposure time

   if( cur_exp_tm < min_exp_tm ) {
       // bring time up to min. exposure time
     cur_exp_tm = min_exp_tm;
       // set exposure time ceiling/floor engaged
     status |= B00000100;
   }
     // max exposure time is in seconds
   else if( max_exp_tm > 0 && cur_exp_tm > ( max_exp_tm * 1000 ) ) {
     cur_exp_tm = max_exp_tm * 1000;
       // set exposure time ceiling/floor engaged
     status |= B00000100;
   }
   else {
       // at neither min nor max exp. time
       // reset exposure time ceiling/floor flag
     status &= B11111011;
   }

         // determine if we need to change sensitivity --
         // two readings in a row must pass our thresholds

   if( frequency < SENS_THRESH_LO && last_cnt < SENS_THRESH_LO && calc_sensitivity < 1000 ) {
      tsl_sensitivity( HIGH );
   }
   else if( frequency > SENS_THRESH_HI && last_cnt > SENS_THRESH_HI && calc_sensitivity > 10) {
      tsl_sensitivity( LOW );
   }

     // save off current reading so we can see if we need to adjust sensitivity
     // on the next pass

   last_cnt = frequency;

     // set display to update

   status |= B00000001;   

 } // end if(freq_tm_diff > READ_TM)

    //
    // update user interface as the last step in the main
    // loop...
    // 

 if(  status & B00010000 && status & B00000001 ) {
     // in setup and screen needs updating

       // clear update flag
     status &= B11111110;

       // display setup info

     print_setup_display();
 }
  else if( status & B00000001 ) {
    // on main screen and need to update display

       // clear update flag
     status &= B11111110;

     print_info();

  }

} // end main loop

 // interrupt handler to function as a pulse-counter

void add_pulse() {

  // increase pulse count
 pulse_cnt++;
 return;
}

float float_abs_diff( float f1, float f2 ) {

  float diff = f1 > f2 ? f1 - f2 : f2 - f1;

  if( diff < 0 )
    diff *= 1;

  return(diff);
}

&#91;/sourcecode&#93;

<em>tsl230r_functions.pde:</em>



/*

 LightRails - TSL230R Control and Conversion Functions

   Copyright (c) 2008-2009 C. A. Church drone< a_t >dronecolony.com

 unsigned long tsl_get_freq()
 float tsl_calc_uwatt_cm2(unsigned long freq)
 void tsl_sensitivity( bool dir )
 void tsl_set_scaling( int what )

 This program is free software: you can redistribute it
 and/or modify it under the terms of the GNU General
 Public License as published by the Free Software
 Foundation, either version 3 of the License, or (at
 your option) any later version.

 This program is distributed in the hope that it will be
 useful, but WITHOUT ANY WARRANTY; without even the
 implied warranty of MERCHANTABILITY or FITNESS FOR A
 PARTICULAR PURPOSE. See the GNU General Public License
 for more details. 

*/

unsigned long tsl_get_freq() {

    // we have to scale out the frequency - the only
    // 1:1 frequency pulse we get is 100x scale.  Smaller
    // scaling on the TSL230R requires us to multiply by a factor
    // to get actual frequency

  unsigned long freq = pulse_cnt * freq_mult;
  pulse_cnt = 0;
  return(freq);
}

float tsl_calc_uwatt_cm2(unsigned long freq) {

  // get uW observed - assume 640nm wavelength
  // calc_sensitivity is our divide-by to map to a given signal strength
  // for a given sensitivity (each level of greater sensitivity reduces the signal
  // (uW) by a factor of 10)
  //
  // note: this function does not take sensor size into account
  //       other functions handle this

  float uw_cm2 = (float) freq / (float) calc_sensitivity;

  return(uw_cm2);

}

void tsl_sensitivity( bool dir ) {

  // adjust sensitivity of TSL230R in 3 steps of 10x either direction

  int pin_0 = false;
  int pin_1 = false;

  if( dir == true ) {

      // increasing sensitivity

      // -- already as high as we can get
    if( calc_sensitivity == 1000 )
      return;

    if( calc_sensitivity == 100 ) {
        // move up to max sensitivity
      pin_0 = true;
      pin_1 = true;
    }
    else {
        // move up to med. sesitivity
      pin_1 = true;
    }

      // increase sensitivity divider
    calc_sensitivity *= 10;
  }
  else {
      // reducing sensitivity

      // already at lowest setting

    if( calc_sensitivity == 10 )
      return;

    if( calc_sensitivity == 100 ) {
        // move to lowest setting
      pin_0 = true;
    }
    else {
        // move to medium sensitivity
      pin_1 = true;
    }

      // reduce sensitivity divider
    calc_sensitivity = calc_sensitivity / 10;
  }

    // make any necessary changes to pin states

 digitalWrite(TSL_S0, pin_0);
 digitalWrite(TSL_S1, pin_1);

 return;
}

void tsl_set_scaling ( int what ) {

  // set output frequency scaling for TSL230R
  // when increasing the scaling for divide-by-output, you reduce the
  // freq multiplier by a factor of ten. 
  // e.g.:
  // scale = 2 == freq_mult = 100
  // scale = 10 == freq_mult = 10
  // scale = 100 == freq_mult = 1

  byte pin_2 = HIGH;
  byte pin_3 = HIGH;

  switch( what ) {
    case 2:
      pin_3     = LOW;
      freq_mult = 2;
      break;
    case 10:
      pin_2     = LOW;
      freq_mult = 10;
      break;
    case 100:
      freq_mult = 100;
      break;
    default:
      return;
  }

  digitalWrite(TSL_S2, pin_2);
  digitalWrite(TSL_S3, pin_3);

  return;
}

camera_controls.pde:


/*

   -- Camera Control Functions

 LightRails 1.0
   A dynamic external exposure control system with
   integrated intervalometer for time-lapse
   photography

 Copyright (c) 2008-2009 C. A. Church drone< a_t >dronecolony.com

 This program is free software: you can redistribute it
 and/or modify it under the terms of the GNU General
 Public License as published by the Free Software
 Foundation, either version 3 of the License, or (at
 your option) any later version.

 This program is distributed in the hope that it will be
 useful, but WITHOUT ANY WARRANTY; without even the
 implied warranty of MERCHANTABILITY or FITNESS FOR A
 PARTICULAR PURPOSE. See the GNU General Public License
 for more details. 

*/

void fire_camera() {

  digitalWrite(CAMERA_PIN, HIGH);
    // start timer to stop camera exposure
  MsTimer2::set(cur_exp_tm, stop_camera);
  MsTimer2::start();

    // update camera currently enaged
  camera_engaged = true;

  return;
}

void stop_camera() {

  digitalWrite(CAMERA_PIN, LOW);
    // turn off timer
  MsTimer2::stop();

    // update camera currently enaged
  camera_engaged = false;
}

lcd_display.pde:


/*

  -- LCD Display Functions

 LightRails 1.0
   A dynamic external exposure control system with
   integrated intervalometer for time-lapse
   photography

 Copyright (c) 2008-2009 C. A. Church drone< a_t >dronecolony.com

 This program is free software: you can redistribute it
 and/or modify it under the terms of the GNU General
 Public License as published by the Free Software
 Foundation, either version 3 of the License, or (at
 your option) any later version.

 This program is distributed in the hope that it will be
 useful, but WITHOUT ANY WARRANTY; without even the
 implied warranty of MERCHANTABILITY or FITNESS FOR A
 PARTICULAR PURPOSE. See the GNU General Public License
 for more details. 

*/

void print_setup_display() {

   // clear out lcd print buffer

 memset(lcd_print_buffer, 0, SIZE_OF_LCD_BUF);

   // get setup string from hash

 strcpy_P(lcd_print_buffer, (char*)pgm_read_word( &(setup_strings[setup_step])) );

 lcd.clear();
 lcd.setCursor(0, 0);

 lcd.print(lcd_print_buffer);

   // go to second line
 lcd.setCursor(0, 1);

   // determine which setup step to display

 switch(setup_step) {
     case 0:
       lcd.print(iso_rating, DEC);
       break;

     case 1:
             // handle rounding for higher f-stops

       if( f_stop > 7.1 ) {
         print_float((float) int(f_stop));
       }
       else {
         print_float(f_stop);
       }

       break;

     case 2:
       print_float(ev_adjust);
       break;

     case 3:
       lcd.print(ev_steps, DEC);
       break;

     case 4:
       lcd.print(camera_delay, DEC);
       break;

     case 5:
       if( status & B01000000 ) {
         lcd.print('-');
       }
       else if( status & B00100000 ) {
         lcd.print('+');
       }
       else {
         lcd.print('X');
       }
       break;

     case 6:

      lcd.print(min_exp_tm, DEC);
      break;

     case 7:

      lcd.print(max_exp_tm, DEC);
      break;

     case 8:

      lcd.print(freq_mult, DEC);
      break;

     case 9:

      print_float(ttl_stop);
      break;

     case 10:

      lcd.print(light_type, DEC);
      break;

     case 11:

      lcd.print(ev_diff_ceiling, DEC);
      break;

     default:
       break;
   }

return;
}

void print_info() {

   lcd.clear();

   lcd.setCursor(0, 0);

    // display set iso and f-stop  
   lcd.print(iso_rating);
   lcd.print(' ');

   lcd.print('f');

             // handle rounding for higher f-stops
   if( f_stop > 7.1 ) {
     print_float((float) int(f_stop));
   }
   else {
     print_float(f_stop);
   }

     // if intervalometer on, show 'I' indicator
     // and count of shots fired

   if( status & B10000000 ) {
     lcd.setCursor(15,0);
     lcd.print('I');

     if( shots_fired > 999 ) {
       lcd.setCursor(11, 0);
     }
     else if( shots_fired > 99 ) {
       lcd.setCursor(12,0);
     }
     else if( shots_fired > 9 ) {
       lcd.setCursor(13,0);
     }
     else {
       lcd.setCursor(14,0);
     }

     lcd.print(shots_fired, DEC);

   }

    // move to second line 
   lcd.setCursor(0,1);   

   if( disp_enable & B10000000 ) {

         // show exposure time in 1/x notation

        if( exp_tm >= 2 ) {
         float disp_v = 0;

         if( exp_tm >= (float) int(exp_tm) + (float) 0.5 ) {
           disp_v = int(exp_tm) + 1;
         }
         else {
           disp_v = int(exp_tm);
         }

         lcd.print("1/");
         lcd.print(exp_tm, DEC);
        }
        else if( exp_tm >= 1 ) {
           // deal with times larger than 1/2 second

         float disp_v = 1 / exp_tm;
           // get first significant digit
         disp_v       = int( disp_v * 10 );     

         lcd.print('.');
         lcd.print(disp_v, DEC);
         lcd.print('"');    
        }
        else {
         int disp_v = int( (float) 1 / exp_tm);

         lcd.print(disp_v, DEC);
         lcd.print('"');

      }
    }

   else if( disp_enable & B01000000 ) {
         // display actual exposure mS
       print_ul(cur_exp_tm);
       lcd.print("mS");
   }

   else if( disp_enable & B00100000 ) {
         // display frequency reading
     lcd.print("Fq ");
     lcd.print(frequency, DEC);

   }

   else if( disp_enable & B00010000 ) {

         // display EV
       lcd.print("EV ");
       print_float(ev);

       if( ev_adjust > 0 || ev_adjust < 0 ) {
           // show any adjustment made
         lcd.print('&#91;');
         print_float(ev_adjust);
         lcd.print('&#93;');
       }

   } 

   else if( disp_enable & B00001000 ) {
       // uW/m2
     print_float(uwcm2);
     lcd.print("uW");

   }
   else if( disp_enable & B00000100 ) {
       // sensitivity level
     lcd.print('S');
     lcd.print(calc_sensitivity, DEC);     

   }

   else if( disp_enable & B00000010 ) {
         // display lux
       lcd.print("Lx ");
       print_float(lux);

   }

   else if( disp_enable & B00000001 ) {
       // divide-by-factor display
     lcd.print('R');
     lcd.print(freq_mult, DEC);
   }

  lcd.setCursor(14,1);

          // minimum/maximum exposure time enabled?
  if( status & B00000100 )
       lcd.print('M');

           // exposure was latched?        
 if( status & B00001000 )
        lcd.print('L');

}

void print_float( float val ){

  if ( val < 0 ) {
    lcd.print('-');
    val *= -1;
  }    

  print_ul( (unsigned long int) int(val) ); 
  lcd.print('.');

    // add 1 digit for hundreths precision
  if( (val * 100) - (int(val) * 100)  < 10 )
    lcd.print('0');

  print_ul( (val * 100) - (int(val) * 100) );

}   

void print_ul( unsigned long val ) {

    // print unsigned long to lcd

   // clear out lcd print buffer

 memset(lcd_print_buffer, 0, SIZE_OF_LCD_BUF);

 ultoa(val, lcd_print_buffer, 10); 
 lcd.print(lcd_print_buffer);

 return;
}
&#91;/sourcecode&#93;

<em>photo_calculations.pde:</em>



/*

 LightRails 1.0
   A dynamic external exposure control system with
   integrated intervalometer for time-lapse
   photography

 Copyright (c) 2008-2009 C. A. Church drone< a_t >dronecolony.com

 This program is free software: you can redistribute it
 and/or modify it under the terms of the GNU General
 Public License as published by the Free Software
 Foundation, either version 3 of the License, or (at
 your option) any later version.

 This program is distributed in the hope that it will be
 useful, but WITHOUT ANY WARRANTY; without even the
 implied warranty of MERCHANTABILITY or FITNESS FOR A
 PARTICULAR PURPOSE. See the GNU General Public License
 for more details. 

*/

/* 

 Functions to calculate photographic values
 from light readings, the following functions are
 found here:

 calc_lux_single*
 calc_lux_gauss
 calc_ev
 calc_exp_tm
 calc_exp_ms

 Last Modified: 2/29/2009, c.a. church
 Original:      10/15/2008, c.a. church
*/ 

/* 

  We always use multiple wavelengths.  This function is
  if you happen to be erm, you know, filming something illuminated
  by a single wavelength light source.  Given the rarity of this
  situation, you'll have to provide the luminous efficiency function
  value yourself.

float calc_lux_single(float uw_cm2, float efficiency) {

    // calculate lux (lm/m^2) for single wavelength, using standard formula:
    // Xv = Xl * V(l) * Km
    // Xl is W/m^2 (calculate actual receied uW/cm^2, extrapolate from sensor size (0.0136cm^2)
    // to whole cm size, then convert uW to W)
    // V(l) = efficiency function (provided via argument)
    // Km = constant, L/W @ 555nm = 683 (555nm has efficiency function of nearly 1.0)
    //
    // Only a single wavelength is calculated - you'd better make sure that your
    // source is of a single wavelength...  Otherwise, you should be using
    // calc_lux_gauss() for multiple wavelengths

  return( ( ( uw_cm2 * ( (float) 1 / (float) 0.0136) ) / (float) 1000000 ) * (float) 100   * efficiency * (float) 683 );
}

*/

float calc_lux_gauss( float uw_cm2 ) {

    // # of wavelengths mapped to V(l) values - better have
    // enough V(l) values!

  int nm_cnt = sizeof(wavelengths) / sizeof(int);

    // watts/m2

  float w_m2 =  ( uw_cm2 * ( (float) 1 / (float) 0.0136 ) / (float) 1000000 ) * (float) 100;

  float result = 0;

    // integrate XlV(l) dl
    // Xl = uW-m2-nm caclulation weighted by the CIE lookup for the given light
    //   temp
    // V(l) = standard luminous efficiency function

  for( int i = 0; i < nm_cnt; i++) {

    if( i > 0) {
      result +=  ( spd_graphs[light_type][i] / (float) 1000000)  * (wavelengths[i] - wavelengths[i - 1]) * w_m2  * v_lambda[i];
    }
    else {
      result += ( spd_graphs[light_type][i] / (float) 1000000) * wavelengths[i] * w_m2 * v_lambda[i];
    }

  }

    // multiply by constant Km and return

  return(result * (float) 683);
}

float calc_ev( float lux ) {

    // calculate EV using APEX method:
    // Ev = Av + Tv = Bv + Sv
    // Bv = log2( B/NK )
    // Sv = log2( NSx )
    // K  = Meter Calibration Constant
    //      14 = Pentax standard
    // N  = constant relationship between Sx and Sv

  float ev = ( log( (float)  0.3 * (float) iso_rating ) / log(2) ) + ( log( lux / ( (float) 0.3 * (float) 14.0 ) ) / log(2) );

   // round down if EV step
   // value is set to whole steps

  if( ev_steps == 1 )
    ev = (float) int(ev);

    // convert to positive value temporarily, if needed
  bool neg_ev = false;

  if( ev < 0 ) {
     neg_ev = true;
     ev *= -1;
   }

   if( ev > int(ev) ) {

      // if ev has a decimal value, determine nearest
      // fraction of EV to round to, based on ev step
      // setting

         // handle rounding to nearest step

       int rem = ( ( ev - int(ev) ) * 100 );    
       int step = 100 / ev_steps;

       for (int i = ev_steps; i > 0; i-- ) {

         if( rem >= step * i) {
           rem = step * i;
           break;
         }
       }

       ev = (float) int(ev) + ((float) rem / 100);

   }

    // reset back to negative EV value
  if( neg_ev == true )
    ev *= -1.0;

    // is there a ceiling set for maximum EV change?
    // if so, handle only moving a maximum amount of <ceiling>
    // steps between readings

  if( ev_diff_ceiling > 0 ) {

    float diff = float_abs_diff( ev, prev_ev );
    float ceiling = (float) ev_diff_ceiling / (float) ev_steps;

    if( diff > ceiling ) {

      if( ev < prev_ev ) {
        ev = prev_ev - ceiling;
      }
      else {
        ev = prev_ev + ceiling;
      }

    }

  }

  prev_ev = ev; 

    // if ev adjust enabled - apply it now 

   ev += ev_adjust;

    // deal with TTL compensation
    // each full aperture step results in one less
    // EV read in, so adjust output
    // up by one EV per stop    

  if( ttl_stop > 0 )
    ev += log( ttl_stop ) / log(sqrt(2));

  return(ev);

}

float calc_exp_tm ( float ev, float aperture  ) {

    // Ev = Av + Tv = Bv + Sv
    // need to determine Tv value, so Ev - Av = Tv
    // Av = log2(Aperture^2)
    // Tv = log2( 1/T ) = log2(T) = 2 ^^ (Ev - Av)

  float exp_tm = ev - ( log( pow(aperture, 2) ) / log(2) );

  float exp_log = pow(2, exp_tm); 

  return( exp_log  );
}

unsigned long calc_exp_ms( float exp_tm ) {

  return( (unsigned long) ( 1000 / exp_tm ) );

    // if you wish to round actual mS for exposure to nearest
    // exposure step (the actual exposure step displayed in 1/x
    // format), un-comment the following code and comment the
    // line above out.
    //
    // Mind you, that doing so will make the device
    // no more accurate than your standard camera's meter.

/*   
      // deal with times less than or equal to half a second
   if( exp_tm >= 2 ) {

     if( exp_tm >= (float) int(exp_tm) + (float) 0.5 ) {
       exp_tm = int(exp_tm) + 1;
     }
     else {
       exp_tm = int(exp_tm);
     }

     return(1000 / exp_tm);

   }
   else if( exp_tm >= 1 ) {
     // deal with times larger than 1/2 second

     float disp_v = 1 / exp_tm;
       // get first significant digit
     disp_v       = int( disp_v * 10 );    
     return( ( 1000 * disp_v ) / 10 );

   }
    else {
      // times larger
     int disp_v = int( (float) 1 / exp_tm);
     return((unsigned long) 1000 * (unsigned long) disp_v);

    }
*/

}

user_input.pde:


/*

 -- user input functions

 LightRails 1.0
   A dynamic external exposure control system with
   integrated intervalometer for time-lapse
   photography

 Copyright (c) 2008-2009 C. A. Church drone< a_t >dronecolony.com

 This program is free software: you can redistribute it
 and/or modify it under the terms of the GNU General
 Public License as published by the Free Software
 Foundation, either version 3 of the License, or (at
 your option) any later version.

 This program is distributed in the hope that it will be
 useful, but WITHOUT ANY WARRANTY; without even the
 implied warranty of MERCHANTABILITY or FITNESS FOR A
 PARTICULAR PURPOSE. See the GNU General Public License
 for more details. 

*/

void check_input() {

   // this function determines if any button has been hit,
   // and what to do about it

   // up and down do not require a HIGH (unpressed) reading
   // so that you can hold them down and quickly cycle through
   // values (about 3 changes per second)

 if( digitalRead(B_UP) == LOW && millis() - button_hit > 300) {

      button_hit = millis();
      take_button_action(0);
      return;
 }

 if( digitalRead(B_DN) == LOW && millis() - button_hit > 300 ) {
      //pre_dn = true;
      take_button_action(1);
      button_hit = millis();
      return;
 }

 if( digitalRead(B_LT) == LOW) {
  if(buttons & B00100000 && millis() - button_hit > 300) {  
      buttons &= B11011111;

      take_button_action(2);
      button_hit = millis();
      return;
   }

   buttons &= B11011111;

 }
  else {
    buttons  |= B00100000;
  }

 if( digitalRead(B_RT) == LOW ) {
   if( buttons & B00010000 && millis() - button_hit > 300) {
      buttons &= B11101111;
      take_button_action(3);
      button_hit = millis();
      return;
   }
  buttons &= B11101111;
 }
 else {
   buttons |= B00010000;
 }

 if( digitalRead(B_CT) == LOW ) {
   if( buttons & B00001000 && millis() - button_hit > 300) {
      buttons &= B11110111;
      take_button_action(4);
      button_hit = millis();
      return;
   }
   buttons &= B11110111;
 }
 else {
   buttons |= B00001000;
 }

}

void take_button_action( byte button ) {

  // this function operates whatever action is desired when
  // a button is pressed.  called by check_input()

  switch(button) {
    case 0:
      // up hit

      if( status & B00010000 ) {

            // we're in a setup screen...

            // increasing value at current cursor position

          change_setup_value(setup_step, 1);

            // update flag to show change in display value
          status |= B00000001;

          return;
      }
      else {      
        // on main screen - up turns intervalometer on

            // if intervalometer on already, do nothing

          if( status & B10000000 )
            return;

            // turn on intervalometer
          status |= B10000000;
            // save current exposure reading for latch
          pre_exp_tm = exp_tm; 

        }

      break;

    case 1:
      // down hit

      if( status & B00010000 ) {
            // we're in a setup screen     
            // decreasing value at current cursor position

          change_setup_value(setup_step, -1);
          status |= B00000001;

          return;
      }
      else {

           // main menu - turn off intervalometer
           // re-set latch matched (if set)
         status &= B01110111;
         shots_fired = 0;
       }

      break;

   case 2:
     // left button was hit

      // do nothing if in setup menu          
     if( status & B00010000 )
       return;

           // main display, cycle through display values

     if( disp_enable & B10000000 ) {
           // we were already left, move to the furthest right
           // value
         disp_enable = B00000001;
           // set screen to update
         status |= B00000001;
     }
     else {
             // shift current display value left
           disp_enable = disp_enable << 1;
           status |= B00000001;
     }  

     break;

   case 3:
     // right button was hit

      // do nothing if in setup menu    
     if( status & B00010000 )
         return;       

           // main display, cycle through display values

       if( disp_enable & B00000001 ) {
           // we were already at furthest right value,
           // need to move to furthest left value
         disp_enable = B10000000;

           // set screen to update
         status |= B00000001;
       }
       else {
             // shift current display right
           disp_enable = disp_enable >> 1;
             // set screen to update
           status |= B00000001;
       }

     break;

   case 4:

      // center button hit

      if( status & B00010000 ) {
          // in setup mode - we're going to change which
          // setup value we're displaying

          // set display to update   
        status |= B00000001;

        if( setup_step == MAX_SETUP_STEPS - 1 ) {
            // no more steps to go, exit menu 
          setup_step = 0;
            // set that we're no longer in setup
          status &= B11101111;
          return;
        }

        setup_step++;
        return;

      }
      else {
          // not in setup or input -
          // go into setup mode, tell display
          // to refresh
        status |= B00010001;
        return;
      }

      break;
  }

}

void change_setup_value( byte which, int what ) {

  switch( which ) {
    case 0:

      {

        // change iso value, convert to logarithmic
        // scale first, then add or subtract one step
        // and convert back to arithmetic scale 

       float foo = log(iso_rating) / log(10);

       int din = ( 10 * foo ) + 1;

       din += what;

       if( din < 1 )
          din = 1; 

       float bar = pow(10, (float) ( (float) din - 1) / (float) 10 );

       if( bar > int(bar) ) {
          iso_rating = int(bar) + 1;
       }
        else {
          iso_rating = int(bar);
        }

      }

      break;

    case 1:
      {
          // move f_stop value - move one third step
          // either direction

        float steps = log( f_stop ) / log( sqrt(2) );

        if( what > 0 ) {
          steps += 0.3333;
        }
        else {
          steps -= 0.3333;
        }

        if( steps < 0 )
          steps = 0;

        f_stop = pow( sqrt(2), steps );

      }
      break; 

    case 2:
        // ev adjust moes in 1/4 EV increments

      ev_adjust += (float) what * 0.25;
      break;

    case 3:
        // ev steps determines how many divisions of an EV
        // there are when calculating automatic exposure
        // values.  more steps = better granularity in exposure
        // timing.

      ev_steps += what;
      if( ev_steps < 0 )
        ev_steps = 0;

      break;

    case 4:

      camera_delay += what;

      if( camera_delay < 1 )
        camera_delay = 1;

        // ms delay time
      real_camera_delay = camera_delay * 1000;

      break;    

    case 5: 

        // cycle between latch settings

      if( status & B01000000 ) {
        status &= B10111111;
        status |= B00100000;
      }
      else if( status & B00100000 ) {
        status &= B10011111;
      }
      else {
        status |= B01000000;
      }

      break;

    case 6:

      min_exp_tm += what;

        // don't allow min time to be less than 0
      if( min_exp_tm <= 0 )
        min_exp_tm = 0;

      break;

    case 7:

      max_exp_tm += what;

        // don't allow max time to be less than 0
      if( max_exp_tm <= 0 )
        max_exp_tm = 0;

      break;

    case 8:

        // change scaling:
        // scaling value can be 2, 10, or 100
        // allow wrap around from highest to lowest
        // and vice-versa
      freq_mult += what;

        // we only get to 11 by going up from 10 and
        // we only 1 by going down from 2 - next step
        // either way is 100
      if( freq_mult == 11 || freq_mult == 1 ) {
        tsl_set_scaling(100);
      }
      else if( freq_mult == 9 || freq_mult == 101 ) {
          // if we're moving down from 10, or up from 100
          // move to lowest setting
        tsl_set_scaling(2);
      }
      else {
          // if neither of the above cases is true, then our only
          // destination is 10
        tsl_set_scaling(10);
      }

      break;

    case 9:
      {
          // move ttl_stop value - move one third step
          // either direction

        float steps = log( ttl_stop ) / log(sqrt(2));

        if( steps < 0 )
          steps = 0;

        if( what > 0 ) {
          steps += 0.3333;
        }
        else {
          steps -= 0.3333;
        }

          // for ttl stop, we want to get to zero, if possible
          // for non-ttl readings.  We go ahead and skip every stop below
          // 1.0 for the heck of it. (Very few lenses go below 1.0)

        if( steps < 0 ) {
          ttl_stop = 0.0;
        }
         else {
           ttl_stop = pow( sqrt(2), steps );

            // handle rounding for higher f-stops
           if( ttl_stop > 7.1 )
             ttl_stop = int(ttl_stop);
        }

      }
      break; 

    case 10:

      light_type = light_type == 1 ? 0 : 1;
      break;

    case 11:

      ev_diff_ceiling += what;
      break;

    default:
      break;
  }

 return;
} 

~ by c.a. church on March 25, 2009.

13 Responses to “LightRails – Dynamic External Exposure Control for Time-lapse”

  1. I’m looking to start this for my 11×14 pinhole, from what I gather this is designed to take photos at intervals, this should easily be removed to take one photo and adjust the exposure time throughout the shot, right?
    Also do you have any software/hardware refinements since this was posted?

  2. Hi,
    great thing.
    I am new to this one.It will be useful for my DIY time lapse projects, but it´s interesting to know how to wire all this switches and the LCD to arduino. If it´s possible to post some kind of wiring diagram or something similar? I look whit interest your project for time lapse, but can´t figure out how all of this is connected in one machine to work fine, like I see in your videos. If are any other links to your project or similar to see all in one piece?
    Regards
    Deyan

    • Deyan,

      See this tutorial on how to hook up the LCD:

      http://www.arduino.cc/en/Tutorial/LiquidCrystal

      Line 279 in the main PDE (in this post) shows how I had my LCD wired up (I just used different pins). The buttons and camera pin assignments are defined on lines 34-42, and the TSL230R pins are right above that. The first tutorial (Arduino and the TSL230R) talks about how to wire up the TSL230R.

      Note that all of the buttons connect to GND when pressed.

      !c

  3. Thanks for the post!

    Other things to know if it´s possible. Do you use 2 different arduino,one for the LightRails and one for the engine whit the motors?
    As I understand, one is controlling the metering and triggering the camera and the other one the movements of the rig. They are programed separately to perform there tasks.I live in Spain and if I can´t find the TSL230R in local store, is possible to replace it with other model or this will cause different light metering? I sou one wiring diagram for stepper motors in other post, and if I understand right, this one is for the engine that dives the rig and needs the isye drivers or other ones hooked up to the motors to perform the movements.
    It will be interesting to program all this from PDA in the future. So will be much is ye to move the rig from one location to other, whiteout need of laptop or programing it at home.

    Thank´s one more time!

  4. I love the project but I think the premise of 1/100EV may be a little over the top. DSLRs do only meter in 1/2EV increments but they can also only expose in 1/2EV aperture/shutter speed increments. Also the general rule is accuracy greater than 1/2EV isn’t necessary. That doesn’t mean that two images 1/4EV apart are going to look identical. What it means is that given two camera RAW files that are 1/4EV apart you should be able to post-process them to look pretty much exactly identical. Of course this rule came from the film world where a negative has almost twice the dynamic range of digital sensors..

    • Hi Eric!

      In practice, the actual change is coarser than 1/100 EV as the reality is that the light levels do not follow a nice, linear curve as day goes to night and vice-versa. The underlying issue is that most people are shooting JPG’s for timelapse, and want to avoid the flicker that the 1/4 or 1/2EV changes result in. This is a real problem in timelapse video that people are facing each day. If it weren’t a problem, I would’ve never taken on the challenge to try and solve it *grin*

      Of course, we could all shoot raw, and then use some software to provide the “right” change in exposure levels after leaving our camera on auto-mode. But it doesn’t seem to exist (not without a lot of manual intervention). The goal here was to provide a light meter that one can get more granular (auto) exposure adjustments out of than 1/4-1/2EV – which do fail to produce the desired results.

      In practice, my testing revealed that most of the time you’re going to see realistic changes on the order of 1/10th to 1/20th EV. Mostly because of the weighting and EV change limits one applies to make it survivable in real-world situations. (One issue I had was brief changes in scene luminance would have disappeared between the metering and actual shooting, bringing flicker back.)

      So yeah, 1/100EV is unnecessary in most cases, but *shrug* it’s there! And, by shooting in bulb, we do our own timing, so the only limit is the upper shutter speed (due to the fact that the analog inputs must be de-bounced, and cameras try to avoid responding to noisy remotes, etc.)

      !c

  5. […] intervalometer has been built and based on code kindly provided “the roaming drone” and then modified . The main function of the intervalometer system is to allow automated […]

  6. Hi, great work inventing this awesome device!
    Perhaps my question is a bit impudent, but:
    Could you maybe build me another one and if yes: how much would it cost? 🙂
    Best regards,
    Daniel

  7. In the process of building my own system based on your work and on the work of PhotosbyKev… Thanks for posting such an informative project description! It’s really given my experimentation a head-start…

    I’m such a visual guy that I’m having a hard time envisioning the reflector you built for your light sensor and was wondering if you’d be able to post a picture to help me out?

    Best Wishes,

    ~J

    • Hi Jeff,

      I don’t have any pictures of the mount its self, and I’ve since recycled the lumber, but it was really quite simple: just an 8″ high “C” made of three pieces of 1×6″ wood, painted white on the inside. Then the sensor just mounted to the back piece (inside of the “C”) and then pointed towards the bottom.

      !c

  8. Your work inspired me to get an arduino, go to my local electronics shop to buy all the required components, and use a soldering iron for the first time! Thanks so much for this. Having now built bulb-ramping intervalometer, I have a few questions. I have done a little experimentation and I am not yet pleased with my results. I hope with your experience you are able to give me a few pointers.

    I have had a little bit of a problem with flicker. I presume that it is because of the way I have mounted the TSL230 chip and will be trying various alternatives.

    I am using a Canon camera, and with shutter speeds below 1/2 seconds, it seems that exposure is not very accurate. Have you had any similar experience?

    My last question is about the battery life of this arduino project. After running it for an hour, my backlight on the LCD no longer lights up (it slowly becomes dim over the course of an hour) and I can no longer read it. I really seem to be going through batteries. I am using a 9v battery like this: http://www.propersonaldefense.com/store1/36-77-thickbox/eveready-energizer-9-volt.jpg. Do you think this is normal?

    I plan to write my experiences up and put them on my website. I’ll be sure to let you know and link through to your excellent work when I do.

    Thanks in advance.
    John

    • Hi John,

      Sorry for the slow response! A 9V battery won’t be providing power for very long, I’d suggest a handful of AA batteries, or some C/D batteries. I usually use 12V SLA batteries (8+ Ah rating) in the field for all day power for my moco rigs.

      The LightRails is definitely not perfected, and in fact, there are a few errors in the code (see the comments on the first two tutorials). I spent many weeks getting this far, and largely got tired of working on the same thing every day =)

      If you see PhotoKev’s implementation, it is improved and likely performs much better.

      !c

Leave a comment