Hardware components | ||||||
![]() |
| × | 1 | |||
![]() |
| × | 2 | |||
![]() |
| × | 1 | |||
![]() |
| × | 2 | |||
![]() |
| × | 1 | |||
![]() |
| × | 1 | |||
| × | 1 | ||||
Software apps and online services | ||||||
![]() |
| |||||
Hand tools and fabrication machines | ||||||
![]() |
| |||||
![]() |
|
I am a distance runner and have always been fascinated by the timing equipment used to score races. Athletes wear a UHF RFID chip, and antennas read the chip when the athlete crosses the finish line. The "chip" usually has a bib number or some piece of information to identify the runner. I wanted to host my own race and use this type of system to time it. However, professional kits cost over $10,000, and I didn't want to spend that much money. In addition, many of these big proprietary systems such as IPICO, MyLaps, and ChronoTrack require the use of their proprietary chips and equipment. As cool and feature-packed as they are, I didn't need them because I wasn't timing races with hundreds of runners.
So, I bought the SparkFun Simultaneous RFID reader and hookup accessories, and built my own timing system! Yes, I know you might not want to spend the $200+ for the reader and equipment, but trust me, it's the best option. When I was researching to build this project, I looked at almost every reader and antenna under the sun, and this was the best option. It makes managing the RFID easy, and it is fairly well documented and easy to program for.
With this system, I can program athletes' names or bib numbers into chips, and when they cross the finish line, their names and times are printed! It is a very reliable system and makes timing small races easy.
Project Tutorial coming soon! (I'm very busy so I don't really have time to sit down and make a tutorial... I'm just working on it in my spare time)
Here is a video of how it works:
I had this SKB i-series camera case lying around, so I repurposed it to hold my timing system more securely and be more robust. It also just looks cooler and more like systems from MyLaps and Chronotrack. It also makes the system more robust and easy to manage.
Leave a comment below and I'll reply as soon as I can!
#include <SoftwareSerial.h>
SoftwareSerial softSerial(2, 3);
#include "SparkFun_UHF_RFID_Reader.h"
RFID nano;
#define buzzer 9
#define buzzergnd 10
//place variables - data placement
int place = 1;
float seconds;
float personTime;
unsigned long currentMillis = 0;
unsigned long prevMillis = millis;
unsigned long minutes = 0;
int heat = 1;
//switches and such
const int buttonPin = 6;
int buttonPushCounter = 1;
int buttonState = 0;
int lastButtonState = 0;
int onbtn = 9;
int offbtn = 8;
// create structure to filter
struct DubFilter {
uint8_t EPC[12];
//unsigned long LastTime; // optionally you could add data to capture
};
// maximum number of unique EPC
#define MaxEPC 15
struct DubFilter dub[MaxEPC];
void setup() {
//RFID
pinMode(onbtn, INPUT);
pinMode(offbtn, INPUT);
pinMode(12, OUTPUT);
pinMode(7, OUTPUT);
pinMode(buzzer, OUTPUT);
pinMode(buzzergnd, OUTPUT);
digitalWrite(buzzergnd, LOW);
Serial.begin(115200);
while(! Serial);
if(setupNano(38400) == false){
Serial.println("Module failed to respond");
while (1);
}
nano.setRegion(REGION_NORTHAMERICA);
nano.setReadPower(1500);
// init de-dub
for (uint8_t i=0 ; i < MaxEPC ; i++)
dub[i].EPC[0] = 0;
Serial.println("Ready");
Serial.println("Turn on switch to begin race");
/*while(!Serial.available());
Serial.read();
nano.startReading();
digitalWrite(12, HIGH);
Serial.println("RFID Reader active. Ready to begin race");
*/
//timing
pinMode(buttonPin, INPUT);
}
void startRace(){
Serial.print("Race ");
Serial.print(heat);
Serial.println(" started");
heat++;
flash();
}
//where the magic happens
void runTimer(){
currentMillis = millis();
seconds = currentMillis - prevMillis;
float personTime = seconds/1000;
if(seconds < 100){
startRace();
}
//data - THIS IS WHERE IT ALL GOES DOWN
if(nano.check() == true){
byte responseType = nano.parseResponse();
if(responseType == RESPONSE_IS_TAGFOUND){
// it is already in the list
if ( DeDub() ) return ;
Serial.print(place);
Serial.print(" ");
byte tagEPCBytes = nano.getTagEPCBytes();
for(byte x = 0 ; x< tagEPCBytes ; x++){
if(nano.msg[31 + x] < 0x10) Serial.print("0");
Serial.print(char(nano.msg[31 + x]));
Serial.print((""));
}
unsigned minutes = (personTime + .0005) /60;
personTime -= minutes * 60;
Serial.print(" ");
Serial.print(minutes);
Serial.print(':');
if ((personTime + 0.0005) < 10)
Serial.print('0');
Serial.println(personTime, 2);
place++;
// flash();
// delay(10);
}
}
}
/**
* This routine will check against the table
* If EPC is already in the table true will be returned.
* if NOT EPC will be added to the list and false is turned
*/
bool DeDub() {
uint8_t i,x;
bool MissMatch = false;
// get the num EPC bytes
uint8_t tagEPCBytes = 12;
// check each entry in the table
for (i = 0 ; i < MaxEPC; i++){
// if empty entry
if (dub[i].EPC[0] == 0) break;
MissMatch = false;
for(x = 0 ; x< tagEPCBytes ; x++){
// check for miss match
if(nano.msg[31 + x] != dub[i].EPC[x] ){
MissMatch = true;
break;
}
}
// A this point we check for MisMatch (false means we have a Match)
if (! MissMatch) return true;
}
// EPC was not in the list already
if (i == MaxEPC) {
Serial.println("Table is full\nCan NOT add more");
}
else {
// add to the list
for(x = 0 ; x< tagEPCBytes ; x++){
// Add to the list
dub[i].EPC[x] = nano.msg[31 + x];
}
}
return(false);
}
void flash(){
tone(buzzer, 2349);
delay(150);
noTone(buzzer);
}
void stopTimer(){
seconds = 0;
place = 1;
minutes = 0;
prevMillis = millis();
}
void loop() {
// put your main code here, to run repeatedly:
buttonState = digitalRead(buttonPin);
if(buttonState == HIGH){
runTimer();
digitalWrite(7, HIGH);
}
else{
stopTimer();
digitalWrite(7, LOW);
}
int onbtnstate = digitalRead(onbtn);
int offbtnstate = digitalRead(offbtn);
if (onbtnstate == HIGH){
nano.startReading();
digitalWrite(12, HIGH);
Serial.println("RFID Reader Active");
delay(100);
}
if (offbtnstate == HIGH){
nano.stopReading();
digitalWrite(12, LOW);
Serial.println("RFID Reader deactivated");
delay(100);
}
}
boolean setupNano(long baudRate)
{
nano.begin(softSerial); //Tell the library to communicate over software serial port
//Test to see if we are already connected to a module
//This would be the case if the Arduino has been reprogrammed and the module has stayed powered
softSerial.begin(baudRate); //For this test, assume module is already at our desired baud rate
while (softSerial.isListening() == false); //Wait for port to open
//About 200ms from power on the module will send its firmware version at 115200. We need to ignore this.
while (softSerial.available()) softSerial.read();
nano.getVersion();
if (nano.msg[0] == ERROR_WRONG_OPCODE_RESPONSE)
{
//This happens if the baud rate is correct but the module is doing a ccontinuous read
nano.stopReading();
Serial.println(F("Module continuously reading. Asking it to stop..."));
delay(1500);
}
else
{
//The module did not respond so assume it's just been powered on and communicating at 115200bps
softSerial.begin(115200); //Start software serial at 115200
nano.setBaud(baudRate); //Tell the module to go to the chosen baud rate. Ignore the response msg
softSerial.begin(baudRate); //Start the software serial port, this time at user's chosen baud rate
delay(250);
}
//Test the connection
nano.getVersion();
if (nano.msg[0] != ALL_GOOD) return (false); //Something is not right
//The M6E has these settings no matter what
nano.setTagProtocol(); //Set protocol to GEN2
nano.setAntennaPort(); //Set TX/RX antenna ports to 1
return (true); //We are ready to rock
}
/*
Welcome!
Ensure all cables are properly connected.
First, press on the Right Arrow in the upper left hand corner. When
you hover on it, it will say "Upload". Wait for the message
"Done Uploading" to appear in the bottom of this screen.
Then click on the magnifying glass in the upper right corner.
UNABLE TO UPLOAD ERROR? - Make sure your board is connected
properly. If you receive thhis error, reference the troubleshooting
section of the manual.
System is ready to begin when "Ready" message appears.
TO START A RACE
1. Activate starter arm switch (red toggle)
2. Starter button is now ready to start, push it to start a race OR
if starter gun is connected, pull the trigger to start a race
3. Deactivate starter arm switch (red toggle)
MID-RACE OPERATION
If split times are desired, press the RFID activation button (silver
button) and wait for all athletes to cross the finish line. To start
the next lap, press the start button.
TO END A RACE
When athletes are about to finish the race, activate the RFID reader
with the silver activate button. Once all athetes are across,
turn the RFID reader off by pressing the button again. To end the
timer, activate the starter arm switch and press the start button.
Questions? Comments? Concerns?
Contact John McKellar at (208) 201-6279
DO NOT TYPE IN THIS PROGRAM, ESPECIALLY THE SENSITIVE CODE BELOW.
TYPING INTO IT WILL CAUSE THE SYSTEM TO STOP WORKING.
*/
#include <SoftwareSerial.h>
SoftwareSerial softSerial(2, 3);
#include "SparkFun_UHF_RFID_Reader.h"
RFID nano;
//place variables - data placement
int place = 1;
float seconds;
float personTime;
unsigned long currentMillis = 0;
unsigned long prevMillis = millis;
unsigned long minutes = 0;
int heat = 1;
bool gunReady = false;
bool panelReady = false;
int rfidcounter = 1;
#define piezo 10
bool data = true;
int note = 2000;
#define buzzgnd 9
//switches and such
const int buttonPin = 6;
int buttonPushCounter = 1;
int buttonState = 0;
int lastButtonState = 0;
int rfidbtn = 8;
int gun = 13;
int gunCounter = 1;
int gunState = 0;
int lastGunState = 0;
int lapno = 1;
// create structure to filter
struct DubFilter {
uint8_t EPC[12];
//unsigned long LastTime; // optionally you could add data to capture
};
// maximum number of unique EPC
#define MaxEPC 40
struct DubFilter dub[MaxEPC];
void setup() {
//RFID
pinMode(gun, INPUT);
pinMode(rfidbtn, INPUT);
pinMode(12, OUTPUT);
digitalWrite(buzzgnd, LOW);
Serial.begin(115200);
while(! Serial);
if(setupNano(38400) == false){
Serial.println("Module failed to respond");
while (1);
}
nano.setRegion(REGION_NORTHAMERICA);
nano.setReadPower(2700);
// init de-dub
for (uint8_t i=0 ; i < MaxEPC ; i++)
dub[i].EPC[0] = 0;
Serial.println("Ready");
pinMode(buttonPin, INPUT);
pinMode(piezo, OUTPUT);
pinMode(buzzgnd, OUTPUT);
}
void startRace(){
lapno = 1;
Serial.print("Race ");
Serial.print(heat);
Serial.println(" started");
heat++;
lap();
}
void lap(){
Serial.print("Lap ");
Serial.println(lapno);
place = 1;
for (uint8_t i=0 ; i < MaxEPC ; i++)
dub[i].EPC[0] = 0;
lapno++;
}
//where the magic happens
void runTimer(){
currentMillis = millis();
seconds = currentMillis - prevMillis;
float personTime = seconds/1000;
unsigned minutes = (personTime + .0005) /60;
personTime -= minutes * 60;
//data - THIS IS WHERE IT ALL GOES DOWN
if(nano.check() == true){
byte responseType = nano.parseResponse();
if(responseType == RESPONSE_IS_TAGFOUND){
// it is already in the list
if ( DeDub() ) return ;
Serial.print(place);
Serial.print(" ");
byte tagEPCBytes = nano.getTagEPCBytes();
for(byte x = 0 ; x< tagEPCBytes ; x++){
if(nano.msg[31 + x] < 0x10) Serial.print("0");
Serial.print(char(nano.msg[31 + x]));
Serial.print((""));
}
Serial.print(" ");
Serial.print(minutes);
Serial.print(':');
if ((personTime + 0.0005) < 10)
Serial.print('0');
Serial.println(personTime, 2);
place++;
tone(piezo, note, 1000);
tone(12, note, 1000);
}
}
}
void playTone(){
tone(piezo, note, 1000);
tone(12, note, 1000);
}
/**
* This routine will check against the table
* If EPC is already in the table true will be returned.
* if NOT EPC will be added to the list and false is turned
*/
bool DeDub() {
uint8_t i,x;
bool MissMatch = false;
// get the num EPC bytes
uint8_t tagEPCBytes = 12;
// check each entry in the table
for (i = 0 ; i < MaxEPC; i++){
// if empty entry
if (dub[i].EPC[0] == 0) break;
MissMatch = false;
for(x = 0 ; x< tagEPCBytes ; x++){
// check for miss match
if(nano.msg[31 + x] != dub[i].EPC[x] ){
MissMatch = true;
break;
}
}
// A this point we check for MisMatch (false means we have a Match)
if (! MissMatch) return true;
}
// EPC was not in the list already
if (i == MaxEPC) {
Serial.println("Table is full\nCan NOT add more");
}
else {
// add to the list
for(x = 0 ; x< tagEPCBytes ; x++){
// Add to the list
dub[i].EPC[x] = nano.msg[31 + x];
}
}
return(false);
}
void flash(){
}
void stopTimer(){
seconds = 0;
place = 1;
minutes = 0;
prevMillis = millis();
}
void loop() {
// put your main code here, to run repeatedly:
gunState = digitalRead(gun);
buttonState = digitalRead(buttonPin);
if(buttonState == HIGH){
if(gunState == HIGH){
if(gunCounter %2 != 0){
startRace();
gunCounter++;
tone(piezo, note, 1000);
delay(150);
}
else if(gunCounter %2 == 0){
gunCounter++;
Serial.print("Race ");
Serial.print(heat - 1);
Serial.println(" Ended");
tone(piezo, note, 1000);
delay(150);
}
}
}
else if(buttonState == LOW){
if(gunState == HIGH){
lap();
tone(piezo, note, 500);
delay(175);
}
}
if(gunCounter %2 == 0){
runTimer();
}
if(gunCounter %2 != 0){
stopTimer();
for (uint8_t i = 0 ; i < MaxEPC ; i++)
dub[i].EPC[0] = 0;
}
int rfidbtnstate = digitalRead(rfidbtn);
if (rfidbtnstate == HIGH){
rfidcounter++;
if(rfidcounter %2 == 0){
nano.startReading();
digitalWrite(7, HIGH);
digitalWrite(5, LOW);
//Serial.println("RFID reader activated");
}
else if(rfidcounter %2 != 0){
nano.stopReading();
digitalWrite(7, LOW);
digitalWrite(5, HIGH);
//Serial.println("RFID Reader deactivated");
}
delay(200);
}
if(Serial.available() > 0){
char code = Serial.read();
if(code == '1'){
nano.startReading();
digitalWrite(7, HIGH);
digitalWrite(5, LOW);
Serial.println("RFID ON");
}
if(code == '2'){
nano.stopReading();
digitalWrite(7, LOW);
digitalWrite(5, HIGH);
Serial.println("RFID OFF");
}
}
//end loop
}
boolean setupNano(long baudRate)
{
nano.begin(softSerial); //Tell the library to communicate over software serial port
//Test to see if we are already connected to a module
//This would be the case if the Arduino has been reprogrammed and the module has stayed powered
softSerial.begin(baudRate); //For this test, assume module is already at our desired baud rate
while (softSerial.isListening() == false); //Wait for port to open
//About 200ms from power on the module will send its firmware version at 115200. We need to ignore this.
while (softSerial.available()) softSerial.read();
nano.getVersion();
if (nano.msg[0] == ERROR_WRONG_OPCODE_RESPONSE)
{
//This happens if the baud rate is correct but the module is doing a ccontinuous read
nano.stopReading();
Serial.println(F("Module continuously reading. Asking it to stop..."));
delay(1500);
}
else
{
//The module did not respond so assume it's just been powered on and communicating at 115200bps
softSerial.begin(115200); //Start software serial at 115200
nano.setBaud(baudRate); //Tell the module to go to the chosen baud rate. Ignore the response msg
softSerial.begin(baudRate); //Start the software serial port, this time at user's chosen baud rate
delay(250);
}
//Test the connection
nano.getVersion();
if (nano.msg[0] != ALL_GOOD) return (false); //Something is not right
//The M6E has these settings no matter what
nano.setTagProtocol(); //Set protocol to GEN2
nano.setAntennaPort(); //Set TX/RX antenna ports to 1
return (true); //We are ready to rock
}
Comments
Please log in or sign up to comment.