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








Sunday 12 June 2011

Useful sites

http://www.bitsbox.co.uk/sensors.html
http://techcobweb.wordpress.com/2009/09/02/slot-car-challenge/
http://blog.thiseldo.co.uk/?p=383
http://dandr.org/2010/02/arduino-scalextric-lap-counter/

Sunday 15 May 2011

The Kitchen-Table Industrialists

Nice piece about makers in the NYT, talks about OmniCorp, Adafruit and littlebits:

New York Times article

Omnicorp: working on Arduino Lasertag badges

Sunday 27 March 2011

TI Series 2000 transponder reader

I just got the transponder reader working on a Sparkfun breadboard. The microreader takes about 100mA, and with an LCD shield the USB supply is struggling a bit. With an external PSU it's fine, though.

The microreader is wired in permanent read mode, which means the built-in serial ports will not be able to accept programming commands from the PC. To combat this, the microreader data is input on Pin 3 of the Arduino using NewSoftSerial.

The LCD shield is from eBay, from eKitsZone in Hong Kong. The shield was £12.50, as was the Duemilanove from the same source.

Here's the breadboarded circuit from the underside:

















The code below works fine. The bit that identifies the transponders is crude, due to the limited selection of tags I have. But this is only a proof of concept, so there's no real problem. PCB shield to follow.

TI page on MicroReader: http://www.ti.com/rfid/shtml/prod-readers-RI-STU-MRD1.shtml

Here's the code. Blogger has removed all the indents, but Arduino's auto format function will repair that.

/*

Serial test program 3 - works with TI Series 2000 reader

This version uses the newsoftserial library as the native serial port is disrupted by
the reader's constant output.

*/
#include // upgraded serial library
#include // library for LCD

int inByte = 0; // incoming serial byte
int iMsgPtr = 0; // position in reader message
boolean bMsgOn = false; // true when message being processed
boolean bNextID = false; // indicates next byte IDs the transponder (utter bodge!)
NewSoftSerial tiReader(3,2); // define pin 3 as RX, 2 as TX
LCD4Bit_mod lcd = LCD4Bit_mod(2); // define LCD, 2 line display

void setup()
{
// start serial port for input from TI reader at 9600 bps:
tiReader.begin(9600);
// start serial port fordebug output to PC serial monitor at 9600 bps:
Serial.begin(9600);
// initialise the display, clear it and put up a message
lcd.init();
lcd.clear();
lcd.printIn("Transponder ID");
}

void loop()
{
if (tiReader.available() > 0)
{
// get incoming byte from serial port:
inByte = tiReader.read();
// if start of message byte detected declare new message
if ((inByte==1) && (bMsgOn == false))
{
bMsgOn = true; // message being processed
iMsgPtr = -1; // initialise message pointer
}
else
// if processing a message...
if (bMsgOn == true)
{
// if this is the message length byte
if (iMsgPtr == -1)
{
iMsgPtr = inByte + 1; // add a byte to accommodate the error check
bNextID = true; // next btye is start of transponder ID
}
else
{
// decrement message pointer, check for end of message
iMsgPtr--;
if (iMsgPtr == 0)
{
// end of message
bMsgOn = false;
}
// This is the bodged bit. All the tags we have use different first bytes, allowing them to
// be ID'd straight away. The code should really build up an 8-byte ID, checking when complete.
if (bNextID = true)
{
bNextID = false;
switch (inByte){
case 0x61: outputData("Keyring 1");
break;
case 0x63: outputData("Trainer tag");
break;
case 0xCD: outputData("Stick");
break;
case 0xE4: outputData("Keyring 2");
break;
case 0xF5: outputData("Disc");
break;
}
}
}
}
}
}

void outputData(char value[])
{
// output data to LCD, copying to the PC serial for debugging
Serial.println(value);
lcd.cursorTo(2, 0); //line=2, x=0
lcd.printIn(" ");
lcd.cursorTo(2, 0); //line=2, x=0
lcd.printIn(value);
}

Sunday 20 March 2011

Starting out with Serial

It looks like NewSoftSerial is the way to go: http://arduiniana.org/libraries/newsoftserial/

For programming Atmegas without the bootloader, here's an article on using PL-2303 instead of FTDI: http://www.uchobby.com/index.php/2009/10/04/diy-usb-to-serial-cable-for-3/

The Arduino will happily stuff data out to the serial port, next I need to get it receive data.

Arduino print info: http://www.arduino.cc/en/Serial/Print

Good code example and comments on DIYDrones: http://diydrones.com/forum/topics/razor-9dof-gps-arduino