Monday, 25 February 2013

Swimming Timer

With yet another swimming gala to time, an Arduino-powered timing system was called for. I wanted to keep things simple, so came up with the following spec: 


  • One button to start
  • Six separate lane buttons, one person timing each lane
  • LC display showing finish times with up/down buttons
  • Output to PC in man-readable format
  • As foolproof as possible
I started off with a quick Google search which produced a Pinewood Derby timer from Miscjunk (http://www.miscjunk.org/mj/pg_pdt.html). I made many changes to the program, but I must acknowledge the great start given me by David Gadberry's code.

After a bit of hacking, I came up with the following hardware design. The key components are an Arduino-compatible Seeduino from Seeed Studios, a 16x2 LCD, ten switches and three LEDs.


I wanted the lane signal buttons to be quite heavy-duty and reliable, so I bought some arcade buttons with decent microswitches. I used another of these for the start button. Being a tightwad - and in the spirit of Scouting - I nipped off to the local scrap store and found some great components to house the buttons. There were loads of fabric conditioner bottle tops which were ideal to mount the buttons on, and some salt container lids fitted the base of the bottle tops nicely. I bought some inexpensive 3.5mm jack extension cables from eBay and soldered them onto the microswitches and my action buttons were ready.



I mounted the LCD and some small pushbuttons into a small plastic case, along with the Arduino and a Ciseco ProtoX prototype shield. The shield wasn't expensive, but makes connecting up much simpler. It's not my tidiest job, but I decided as long as it was secure, i was happy. Since I'd used a normal LCD, I was running short of inputs on the Arduino, so I rigged a couple of resistors to share two buttons through one analog input. To give a remote start facility, I also put a spare 3.5mm jack socket across the start button. An R/C car connector was used to connect the remote switches.

Finally, to provide a nice heavy-duty cable to plug the switches into, I got some 7-core trailer cable, and spliced out the conductors to jack sockets at appropriate intervals. I used shrink wrap to keep the joint secure. The jacks were the other ends of the extension cables I used for the buttons.



So, does it work?

Here's the system when ready to go. The white LED indicates the unit is reset and ready to start. The red button is the reset.



When running, the LED goes green. The run is started using the green button, or by closing contacts on the jack socket above.



The times start to arrive on the LCD as the buttons are pushed. Position and lane number are displayed.



When all buttons have been pushed, the red LED is illuminated, and a summary is printed to the serial port. At this point, you can use the black up/down buttons to review the times. Using up/down during the run will end the run. Incidentally, there is a constant in the code which allows you to reduce the number of lanes.


Finally, the times are output to the serial port. They are output in lane order and finishing order, for maximum versatility. 



And that's it. We ran a huge Cub, Scout and Explorer Scout gala in record time, and have now got a multi-lane timer that is both rugged and versatile.

Finally, here's the Arduino code:

/*================================================================================*
  
 Pins moved to use analog for up/down buttons
 up/down button processing added
   
   
   
   Pinewood Derby Timer                                Version 1.10 - 23 Jan 2012

   Flexible and affordable Pinewood Derby timer that interfaces with the 
   following software:
     - PD Test/Tune/Track Utility
     - Grand Prix Race Manager software

   Refer to the "PDT_MANUAL.PDF" file for setup and usage instructions.
   Website: www.miscjunk.org/mj/pg_pdt.html


   Copyright (C) 2011 David Gadberry

   This work is licensed under the Creative Commons Attribution-NonCommercial-
   ShareAlike 3.0 Unported License. To view a copy of this license, visit 
   http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter to 
   Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 
   94041, USA.
 *================================================================================*/
// include the library code:
#include <LiquidCrystal.h>

#define PDT_VERSION  "1.10"            // software version

/*-----------------------------------------*
  - static definitions -
 *-----------------------------------------*/
#define NUM_LANES    5                 // number of lanes

#define MAX_LANES    6                 // maximum number of lanes (Uno)

#define mREADY       0                 // program modes
#define mRACING      1
#define mFINISH      2

#define START_TRIP   LOW               // start switch trip condition

#define NULL_TIME    9.999             // null (non-finish) time
#define NUM_DIGIT    3                 // timer resolution

#define char2int(c) (c - '0') 

/*-----------------------------------------*
  - pin assignments -
 *-----------------------------------------*/
byte RESET_SWITCH  = A0;               // reset switch
byte START_GATE    = A1;               // start gate switch
byte RACING_LED    = A2;               // racing LED
byte READY_LED     = A3;               // ready LED
byte FINISHED_LED  = A4;               // finished LED
byte UPDOWN_BUTTON = A5;               // analog input to give 2 input buttons
//
//                    Lane #  1   2   3   4   5   6
//
byte LANE_DET [MAX_LANES] = { 2,  3,  4,  5,  6,  7};    // finish detection pins

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(13, 12, 11, 10, 9, 8);

/*-----------------------------------------*
  - global variables -
 *-----------------------------------------*/
boolean       fDebug = false;          // debug flag
boolean       ready_first;             // first pass in ready mode flag
boolean       racing_first;            // first pass in racing mode flag

unsigned long start_time;              // race start time (milliseconds)
unsigned long lane_timer [MAX_LANES];  // lane timing data (milliseconds)
unsigned      lane_posn  [MAX_LANES];  // finishing position of the lane
int           lane_order [MAX_LANES];  // order in which lanes finish
float         lane_time;               // calculated lane time (seconds)

int           lane;                    // lane number
int           finish_count;            // finish count
int           finish_order;            // finish order
unsigned long last_finish_time;        // previous finish time
byte          mode;                    // current program mode
int           buttonVal;               // used with up/down buttons
int           prevButtonVal = 1023;    // used with up/down buttons
unsigned long pushTime;                // time at which up/down pushed
int           modShow;                 // tracks last time manually shown
long          debounceDelay = 50;      // switch debounce time 
boolean       buttonDone = true;       // has button been processed?

int           run_number = 0;          // incrementing run ID

/*================================================================================*
  SETUP
 *================================================================================*/
void setup()
{  
/*-----------------------------------------*
  - hardware setup -
 *-----------------------------------------*/
  pinMode(READY_LED,    OUTPUT);
  pinMode(RACING_LED,   OUTPUT);
  pinMode(FINISHED_LED, OUTPUT);

  pinMode(RESET_SWITCH, INPUT);
  pinMode(START_GATE,   INPUT);
  
  digitalWrite(RESET_SWITCH, HIGH);    // enable pull-up resistor
  digitalWrite(START_GATE,   HIGH);    // enable pull-up resistor

  for (int t=0; t<NUM_LANES; t++)
  {
    pinMode(LANE_DET[t], INPUT);
    digitalWrite(LANE_DET[t], HIGH);   // enable pull-up resistor
  }
  
  // set up the LCD's number of columns and rows: 
  lcd.begin(16, 2);
  // Print a message to the LCD.
  lcd.print("Swim Timer V1.1a");
  delay(800);
  
/*-----------------------------------------*
  - software setup -
 *-----------------------------------------*/
  Serial.begin(9600);
  Serial.println("Swim Timer V1.1a");
  Serial.println();
  initialize();
}


/*================================================================================*
  MAIN LOOP
 *================================================================================*/
void loop()
{
/*-----------------------------------------*
  - CHECK IF MANUAL RESET CALLED -
 *-----------------------------------------*/

  if (digitalRead(RESET_SWITCH) == LOW)    // timer reset
  {
      initialize();
  } 

/*-----------------------------------------*
  - READY -
 *-----------------------------------------*/
  if (mode == mREADY)
  {
    if (ready_first)
    {
      digitalWrite(READY_LED, HIGH);
      ready_first = false;
    }
   
    if (digitalRead(START_GATE) == START_TRIP)    // timer start depressed
    {
      start_time = millis();

      digitalWrite(READY_LED, LOW);

      delay(100); 

      mode = mRACING; 
      lcd.clear();
      lcd.print("Timer running");
      lcd.setCursor(0,1);    
      lcd.print("Run: ");
      lcd.setCursor(5,1);    
      lcd.print(run_number+1);   
    }
  }
  
/*-----------------------------------------*
  - RACING -
 *-----------------------------------------*/
  else if (mode == mRACING)
  {
    boolean count = false;

    if (racing_first)
    {
      digitalWrite(RACING_LED, HIGH);
      racing_first = false;
    }

    for (int t=0; t<NUM_LANES; t++)
    {
      if (lane_timer[t] == 0 && digitalRead(LANE_DET[t]) == LOW)    // cross finish line
      {
        finish_count++;
        modShow = finish_count;
        lane_timer[t] = millis() - start_time;
        lane_posn[t] = finish_count;  // 0 to NUM_LANES-1
        lane_order[finish_count-1] = t;
        
        lcd.setCursor(0,0);
        if (finish_count > 1)
        {                         
          lane_time = (float)(lane_timer[lane_order[finish_count-2]] / 1000.0);
          lcd.print("P");
          lcd.print(finish_count-1);
          lcd.print(" L");
          lcd.print(lane_order[finish_count-2]+1);
          lcd.print(" ");          
          lcd.print(lane_time);
         
          lcd.setCursor(0,1);
          lane_time = (float)(lane_timer[t] / 1000.0);
          lcd.print("P");
          lcd.print(finish_count);
          lcd.print(" L");
          lcd.print(lane_order[finish_count-1]+1);
          lcd.print(" ");          
          lcd.print(lane_time);
        
        }
        else
        {
          lcd.clear();
          lcd.setCursor(0,1);
          lane_time = (float)(lane_timer[t] / 1000.0);
          lcd.print("P");
          lcd.print(finish_count);
          lcd.print(" L");
          lcd.print(lane_order[finish_count-1]+1);
          lcd.print(" ");          
          lcd.print(lane_time);
          lcd.print("     ");          
         }
  
        if (lane_timer[t] > last_finish_time)
        {
          finish_order++;
          last_finish_time = lane_timer[t];
        }
      }
    }
    
    count = true;
    for (int t=0; t<NUM_LANES; t++)    // check if all finished
    {
      if (lane_timer[t] == 0)    // not finished or masked
      {
        count = false;
        break;
      }
    }
          
    if (count)    // all lanes finished
    {
      endRace();
    }
  }

/*-----------------------------------------*
  - FINISHED -
 *-----------------------------------------*/
  else if (mode == mFINISH)
  {
    digitalWrite(FINISHED_LED, HIGH);
  } 
  
/*-----------------------------------------*
  - CHECK STATUS OF UP/DOWN BUTTONS -
 *-----------------------------------------*/
  
  buttonVal = analogRead(UPDOWN_BUTTON);
  
  if ((buttonVal < 1000) && (mode != mFINISH))
  {
    // using the up/down will STOP timing
    endRace();
    //Serial.print ("buttonVal in stop = ");
    //Serial.println (buttonVal);
  }
  
  if ((mode == mFINISH) && (buttonVal != prevButtonVal))
  {
    // smooth out borderline values
    if (abs(buttonVal - prevButtonVal) > 10)
    {
      pushTime = millis();              // get the time at which button pushed
      prevButtonVal = buttonVal;
      buttonDone = false;
      //Serial.print ("buttonVal changed to ");
      //Serial.println (buttonVal);
    }
    
  }
  
  if ((mode == mFINISH) && (buttonDone == false))
  {
    //pushTime = millis();
    buttonDone = true;
    
    //Serial.print ("pushTime = ");
    //Serial.println (pushTime);
    //Serial.print ("buttonVal = ");
    //Serial.println (buttonVal);
    
    // whatever the reading is at, it's been there for longer
    // than the debounce delay, so take it as the actual current state
    
    if (buttonVal < 10)
    {
      // UP button pushed
      // show previous times
      lcd.clear();
      lcd.setCursor(0,1);
    
      //Serial.print ("modShow was: ");
      //Serial.println (modShow);
      
      if (modShow > 2){
        modShow--; 
        
      //Serial.print ("modShow now: ");
      //Serial.println (modShow);
      //Serial.println ();
       
     }
     showResult(modShow);
     delay(100);
      
    }
    else if ((buttonVal < 520) && (buttonVal > 500))
    {
      // DOWN button pushed
      // show later times
      lcd.clear();
      lcd.setCursor(0,1);
      
      //Serial.print ("modShow was: ");
      //Serial.println (modShow);

      if (modShow < finish_count){
        modShow++; 
      //Serial.print ("modShow now: ");
      //Serial.println (modShow);
      //Serial.println ();
     }
     showResult(modShow);
     delay(100);
    }
    
  }

}


/*================================================================================*
  INITIALIZE
 *================================================================================*/
void initialize()
{  
  for (int t=0; t<NUM_LANES; t++)
  {  
    lane_timer[t] = 0;
  }

  start_time = 0;
  finish_count = 0;
  finish_order = 0;
  last_finish_time = 0;
  
  digitalWrite(READY_LED,    LOW);
  digitalWrite(RACING_LED,   LOW);
  digitalWrite(FINISHED_LED, LOW);

  delay(100);
  Serial.flush();
  
  for (int t=0; t<NUM_LANES; t++)
  {
    lane_timer[t] = 0;
    lane_posn[t] = 0;
    lane_order[t] = 0;
  }

  ready_first  = true;
  racing_first = true;


  lcd.clear();
  lcd.print("Ready for start");
  
  mode = mREADY;
  return;
}

void endRace()
{
  digitalWrite(RACING_LED, LOW);
  
  run_number++;
  Serial.print ("Run number: ");
  Serial.println (run_number);
  Serial.println ();

  Serial.println ("Lane Times:");
  Serial.println ("-----------");
  for (int t=0; t<NUM_LANES; t++)    // send times
  {
    lane_time = (float)(lane_timer[t] / 1000.0);

    Serial.print(t+1);
    Serial.print(" - ");

    if (lane_timer[t] > 0)           // finished
    {
      Serial.print(lane_time, 3);
      Serial.print("  P");
      Serial.println(lane_posn[t]);
      
    }
    else                             // did not finish
    {
      Serial.print("NO TIME ");
      Serial.println(lane_timer[t]);
    }
    
    
  }
  mode = mFINISH;


  Serial.println (" ");
  Serial.println ("Results:");
  Serial.println ("--------");
  for (int t=0; t<NUM_LANES; t++)    // send times
  {
    lane_time = (float)(lane_timer[lane_order[t]] / 1000.0);

    Serial.print("P");
    Serial.print(t+1);
    Serial.print(": L");
    if (lane_time > 0)     
    {
      Serial.print(lane_order[t]+1);
    }
    Serial.print("  ");

    if (lane_time > 0)           // finished
    {
      Serial.println(lane_time, 3);
    }
    else                             // did not finish
    {
      Serial.println("NO TIME ");
    }
  }
  
  // DEBUG PRINT
 /*      
   Serial.println("INDEX lane_timer lane_posn lane_order");
   for (int t=0; t<NUM_LANES; t++)    // send times
  {
    Serial.print(t);
    Serial.print("     ");
    Serial.print(lane_timer[t]);
    Serial.print("       ");
    Serial.print(lane_posn[t]);
    Serial.print("         ");
    Serial.println(lane_order[t]);
  }
 */
  Serial.println();
  Serial.println ("======================");
  Serial.println();
  
}

/*================================================================================*
  SHOW RESULT ON LCD
 *================================================================================*/
void showResult(int lowNumber)
{
  lcd.clear();
  lcd.setCursor(0,0);
  lane_time = (float)(lane_timer[lane_order[lowNumber-2]] / 1000.0);
  lcd.print("P");
  lcd.print(lowNumber-1);
  lcd.print(" L");
  lcd.print(lane_order[lowNumber-2]+1);
  lcd.print(" ");          
  lcd.print(lane_time);
  lcd.setCursor(0,1);
  lane_time = (float)(lane_timer[lane_order[lowNumber-1]] / 1000.0);
  lcd.print("P");
  lcd.print(lowNumber);
  lcd.print(" L");
  lcd.print(lane_order[lowNumber-1]+1);
  lcd.print(" ");          
  lcd.print(lane_time);
}








4 comments:

  1. Nice job. Trying to make a slalom skateboard timer. Any chance of some help?

    ReplyDelete
  2. Well Cedric, you can see my design and code so it should be 90% there. What else do you need?

    ReplyDelete
  3. Hi and thanks for answering!

    I'm no programmer, but learning arduino, but very slowly!

    As for the timer, it would be 2 lane, but independent start and stop. I.e 2 start buttons (tapeswitches) and 2 stop buttons (tapeswitches)

    So 2 timers would be running at the same time.
    Would not need the up/down buttons as there would only be to final times to show on the LCD (lane 1 and lane 2)

    Do you have an email address I could contact you on?

    Again Thank you .

    ReplyDelete
  4. Hi Can You Make a video in youtube or something to see how this is work. Š¢hank you in advance!

    ReplyDelete