- 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);
}