This forum uses cookies
This forum makes use of cookies to store your login information if you are registered, and your last visit if you are not. Cookies are small text documents stored on your computer; the cookies set by this forum can only be used on this website and pose no security risk. Cookies on this forum also track the specific topics you have read and when you last read them. Please confirm whether you accept or reject these cookies being set.

A cookie will be stored in your browser regardless of choice to prevent you being asked this question again. You will be able to change your cookie settings at any time using the link in the footer.

Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Race timer
#1
This project might someday become part of OpenPlotter, but the main reason for doing it was to create a timer for race officers to time race starts. It is currently based on an ESP8266. I chose the ESP rather than something like an Arduino Pro Mini because I was hoping, someday, to extend it to wireless operation. The code as it stands should be able to be moved to an Arduino with little or no modifications. I suppose that the wireless capability might someday provide input to OpenPlotter of some sort.

When mounted in a box, the unit has two buttons and a 4 digit, seven segment I2C display. Internally, it also has a tiny amplifier and a speaker to alert the deck crew of the time (with beeps) without needing to look at the display. The beep sequence is the same as used on Ronstan watches, Tack-Tic, and others. There is also an output to an external relay to activate a horn. We use 12V automotive horns rather than air horns for our starts, so the relay is plugged in so that it is in parallel with the horn button the Race Officer normally uses. He can still use his normal horn button - this is important as he might need to signal a boat OCS, a General Recall, or any number of other sound signals. It is also useful should the electronic timer fail for any reason.

The two buttons have these functions:
1 - start the sequence
2 - set the timing mode.

Currently, the only way to stop the sequence is the turn the unit off and on. I hope, when I get time, to add some code so that a long press of the start button will stop the sequence, but right now this is a simple solution that even Race Officers can understand!

All timing is based on a standard 5 minute starting sequence: 
Class Flag Up (short horn)  - 1 Minute - Prep Flag Up (short horn) - 3 minutes - Prep Flag Down (long horn) - Class Flag Down/Start (short horn). 

There are three timing modes available right now:

6' - Six minute sequence (this is used when the AP flag is dropped with 60 seconds to the Class flag up, etc.) The horn is sounded at the beginning, signalling the drop of AP.
5' - Five minute sequence, starting with Class Flag up
5r - Five minute repeating sequence, for when you have rolling starts (start for one class is the first warning of the sequence for the next class).

It is possible to change modes in mid-sequence - this is only meaningful if you might want to start with the 6 minute timer, then proceed with rolling starts, or perhaps to end a series of rolling starts.

In 5' and 6' modes, the timer will count up to 99 minutes after start. This is helpful if you have an OCS and you want to know when the 3 minute timer on alerting him has expired. It's also useful to determine time for first boat to weather mark, or even the entire race if it is short.

In the configuration with the ESP8266, I use a pair of 16850 batteries running through a buck converter to power everything. The batteries have to be removed from the case to be recharged, but they seem to last a long time!

That's about it! I'd take some photos, but right now the unit is in many pieces on my workbench and I don't think it would tell you much. Rolleyes

Here's the code:
Code:
/* Sailboat Race Timer for Arduino
  Activates a relay for an electric horn during a stardard ISAF 5 minute start sequence.
  Also providing audio and visual alerts to users regarding upcoming flag actions

  Will operate in 6 Min, 5 Min, or 5 Min Cycle mode.

  This code is in the Public Domain, and covered by the General Public License.

  Andy Barrow
*/

#include <Wire.h>
#include "Adafruit_LEDBackpack.h"
#include "Adafruit_GFX.h"
#include <Bounce2.h>


Adafruit_7segment matrix = Adafruit_7segment();
//Pin assignments for the NodeMCU ESP8266
/* //these first few appear to the assigned already
 static const uint8_t D0   = 16;
 static const uint8_t D1   = 5; //Display SCL
 static const uint8_t D2   = 4; //Display SDA
 static const uint8_t D3   = 0; //Tone Pin
 static const uint8_t D4   = 2; //Start Button
 static const uint8_t D5   = 14;//Mode Button
 static const uint8_t D6   = 12;
 static const uint8_t D7   = 13;
 static const uint8_t D8   = 15;//Relay*/
static const uint8_t RX   = 3;
static const uint8_t TX   = 1;

static const uint8_t TRUE = 1;
static const uint8_t FALSE = 0;

#define START_BUTTON 2
#define MODE_BUTTON 14

// Instantiate a Bounce objects
Bounce start_debounce = Bounce();

// Relay and Tone output
const short int RELAY = D8;          //Relay
const int TONE_PIN = D3;             // Tone Pin - this will go to an amp

// Mode Button
int buttonState;             // the current reading from the input pin
int lastButtonState = HIGH;   // the previous reading from the input pin

unsigned long lastDebounceTime = 0;  // the last time the output pin was toggled
unsigned long debounceDelay = 50;    // the debounce time; increase if the output flickers

// This is the state we are in during the sequence
// State is one of:
// 0: Ready (Holding, no action)
// 1: One Minute to Class Up
// 2: Class Flag Up
// 3: Prep Signal Up
// 4: One Minute to Start (Prep Down)
// 5: Start (Class Down)

int state = 0;

// other flag related variables
unsigned long remainingFlagTime = 0;   //Used for countdown beeps and flashes
unsigned long remainingGoTime = 0;     //For future counter
unsigned long startTime = 0;           // time we started counting down
unsigned long classUpTime = 0;         // time to class up
unsigned long prepUpTime = 0;          // time to prep up
unsigned long prepDownTime = 0;        // time to prep down
unsigned long goTime = 0;              // time to start
unsigned int remainingFlagDisp = 0;    // time to display
short int clockRunning = FALSE;        // is the timer running?

// delay before a cancel is allowed (ms)
unsigned long resetDelay = 3000;        // don't allow immediate cancellation, wait a few seconds

// matrix to hold beep times and a variable to hold the index
const unsigned long beepMatrix[2][19] = {
 { 50, 40, 30, 20, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 }, //Seconds in the countdown
 {  4,  4,  4,  4,  1,  1,  1,  1,  1,  2, 2, 2, 2, 2, 3, 3, 3, 3, 3 }  //Number of beeps (4 = one long beep)
};
int beepIndex = -1;                 // the matrix index

// Countdown Mode
short int countMode = 1;

// Functions

// Print what state we are in (for debugging)
void StatePrint(int state)
{
 Serial.print("Now in state: ");
 Serial.println(state);
}

// Short and Long Horn
void shorthorn()
{
 digitalWrite(RELAY, HIGH);
 delay(600);
 digitalWrite(RELAY, LOW);
}
void longhorn()
{
 digitalWrite(RELAY, HIGH);
 delay(1200);
 digitalWrite(RELAY, LOW);
}

//stuff to print on the display
//print rEdy
void printready()
{
 matrix.writeDigitRaw(0, B01010000);
 matrix.writeDigitRaw(1, B01111001);
 matrix.drawColon(false);
 matrix.writeDigitRaw(3, B01011110);
 matrix.writeDigitRaw(4, B01101110);
 matrix.writeDisplay();
}

//print P in the first position
void print_P()
{
 matrix.writeDigitRaw(0, B01110011);
 matrix.writeDisplay();
}
//print C in the first position
void print_C()
{
 matrix.writeDigitRaw(0, B00111001);
 matrix.writeDisplay();
}
//print U in the second position
void print_u()
{
 matrix.writeDigitRaw(1, B00111110);
 matrix.writeDisplay();
}
//print u in the second position
void print_d()
{
 matrix.writeDigitRaw(1, B01011110);
 matrix.writeDisplay();
}
//print 6'
void print_6min()
{
 matrix.clear();
 matrix.drawColon(false);
 matrix.writeDigitNum(0, 6);
 matrix.writeDigitRaw(1, B00100000);
 matrix.writeDisplay();
}
//print 5'
void print_5min()
{
 matrix.clear();
 matrix.drawColon(false);
 matrix.writeDigitNum(0, 5);
 matrix.writeDigitRaw(1, B00100000);
 matrix.writeDisplay();
}
//print 5'r
void print_5minrep()
{
 matrix.clear();
 matrix.drawColon(false);
 matrix.writeDigitNum(0, 5);
 matrix.writeDigitRaw(1, B00100000);
 matrix.writeDigitRaw(3, B01010000);
 matrix.writeDisplay();
}
void printcount(int timecount)
{
 int x = 0;
 remainingFlagDisp = timecount / 1000;
 int s = remainingFlagDisp % 60;
 remainingFlagDisp = (remainingFlagDisp - s) / 60;
 int m = remainingFlagDisp % 60;
 x = s + (m * 100);
 if (x > 99)
 {
   matrix.writeDigitNum(1, (x / 100) % 10, false);
 }
 matrix.drawColon(true);
 matrix.writeDigitNum(3, (x / 10) % 10, false);
 matrix.writeDigitNum(4, x % 10, false);
 matrix.writeDisplay();
}

// This function checks the countdown to see if we need to alert to a flag change
int minuteCountDown(unsigned long remainingTime, int beepMatrixIndex)
{
 unsigned long beepSeconds = beepMatrix[0][beepMatrixIndex]; //Get the countdown seconds we are looking for
 unsigned long beepCounter = beepMatrix[1][beepMatrixIndex]; //Get the number of beeps

 beepSeconds = beepSeconds * 1000ul;   //convert to milliseconds
 if (remainingTime >= beepSeconds)     //not there yet
   return beepMatrixIndex;             //return index given when called
 else {                                //beep
   beep(beepCounter);
   beepMatrixIndex++;                  //increment index
   return beepMatrixIndex;             //return incremented index
 }
}

// This function provides sounds during the last 50 seconds to any flag
void beep(int beepCount)
{
 const int toneFreq = 1000;
 const int shortToneLength = 100;
 const int longToneLength = 700;
 const int toneDelay = 200;

 switch (beepCount) {                  //number of beeps
   case 1:
     Serial.println("1 Beep");
     tone(TONE_PIN, toneFreq, shortToneLength);
     delay(shortToneLength);
     break;
   case 2:
     Serial.println("2 Beeps");
     tone(TONE_PIN, toneFreq, shortToneLength);
     delay(toneDelay);
     tone(TONE_PIN, toneFreq, shortToneLength);
     break;
   case 3:
     Serial.println("3 Beeps");
     tone(TONE_PIN, toneFreq, shortToneLength);
     delay(toneDelay);
     tone(TONE_PIN, toneFreq, shortToneLength);
     delay(toneDelay);
     tone(TONE_PIN, toneFreq, shortToneLength);
     break;
   case 4:
     Serial.println("long beep");            // Index 4 is a long beep, not 4 beeps
     tone(TONE_PIN, toneFreq, longToneLength);
     delay(longToneLength);
     break;
   default:
     return;
 }
}
void resetCounter()
{
 state = 0;
 remainingFlagTime = 0;
 remainingGoTime = 0;
 startTime = 0;
 classUpTime = 0;
 prepUpTime = 0;
 prepDownTime = 0;
 goTime = 0;
 remainingFlagDisp = 0;
 return;
}

void modeBeeps(int modeBeepCount) {
 switch (modeBeepCount) {
   case 1: {
       Serial.println("Countmode = 1");        // Go around back to mode 1
       beep(1);
       print_6min();                           // print "6'"
       break;
     }
   case 2: {
       Serial.println("Countmode = 2");        // switch to mode 2
       beep(2);
       print_5min();                           // print "5'"
       break;
     }
   case 3: {
       Serial.println("Countmode = 3");        // switch to mode 3
       beep(3);
       print_5minrep();                        // print "5'r
       break;
     }
   default:
     break;
 }
}

// Set everything up

void setup() {
 // Set up serial so I can see whats happening for debug
 Serial.begin(115200);
 while (!Serial) {
   ;
 }
 Serial.println("Serial Port Ready");

 // Setup start button with an internal pull-up :
 pinMode(START_BUTTON, INPUT_PULLUP);
 // After setting up the button, setup the Bounce instance :
 start_debounce.attach(START_BUTTON);
 start_debounce.interval(100);          // debounce interval in ms

 // Set GPIOs to correct states
 pinMode(RELAY, OUTPUT);               // Relay
 digitalWrite(RELAY, LOW);             // Make sure it starts out in a low state
 pinMode(START_BUTTON, INPUT);         // Start Button
 matrix.begin(0x70);                   // Prepare the display
 printready();                         // Print "rEdy" on display
 StatePrint(state);

 pinMode(MODE_BUTTON, INPUT_PULLUP);
}

void loop() {
 // Update the Bounce instance for the start button
 start_debounce.update();

 // Get the start button
 int startButton = start_debounce.read();

 // Start the sequence
 if ((startButton == LOW) && (state == 0)) {
   Serial.println("Start button press");
   Serial.print("countMode = ");
   Serial.println(countMode);
   startTime = 0;
   switch (countMode) {
     case 1:
       state = 1;                        //Start in 6 minute mode (1 min delay to Class up)
       clockRunning = TRUE;
       break;
     case 2:
       state = 2;;                       //Start in 5 minute mode (immediate Class up)
       clockRunning = TRUE;
       break;
     case 3:
       state = 2;                        //Start in 5 minute cycle mode (immediate Class up)
       clockRunning = TRUE;
       break;
     default:
       state = 0;
       clockRunning = FALSE;
       return;
   }
 }
 // Set the mode
 // Not sure why, but couldn't get a second instance of BOUNCE to work on this button. So
 // we do it manually
 int reading = digitalRead(MODE_BUTTON);

 // If the switch changed, due to noise or pressing:
 if (reading != lastButtonState) {
   // reset the debouncing timer
   lastDebounceTime = millis();
 }

 if ((millis() - lastDebounceTime) > debounceDelay) {
   // whatever the reading is at, it's been there for longer
   // than the debounce delay, so take it as the actual current state:

   // if the button state has changed:
   if (reading != buttonState) {
     buttonState = reading;

     // only toggle the LED if the new button state is HIGH
     if (buttonState == LOW) {
       countMode++;
       Serial.println(countMode);
       if (countMode == 4) {
         countMode = 1;
       }
       modeBeeps(countMode);
     }
   }
 }


 // save the reading.  Next time through the loop,
 // it'll be the lastButtonState:
 lastButtonState = reading;


 switch (state) {
   case 0:
     break;
   case 1:                                     // Button was pushed - One minute to start class up
     {
       if (startTime == 0) {                   // just started counting down, initialize
         startTime = millis();
         // calculate times
         classUpTime = startTime + 60000ul;     // class flag up
         prepUpTime = startTime + 120000ul;     // prep flag up
         prepDownTime = startTime + 300000ul;   // prep flag down
         goTime = startTime + 360000ul;         // start signal (go)
         beepIndex = 0;                         // array index for the minute countdown
         StatePrint(state);
         shorthorn();                            // this horn indicates AP coming down
       } else if (classUpTime >= millis()) {     //waiting for the flag
         remainingFlagTime = classUpTime - millis();
         remainingGoTime = goTime - millis();
         printcount(remainingFlagTime);
         print_C();
         print_u();
         if (beepIndex < 20)
           beepIndex = minuteCountDown(remainingFlagTime, beepIndex);
       } else {
         state = 2;
         shorthorn();
         StatePrint(state);
         beepIndex = 0;
       }
       break;
     }
   case 2:                                       // We are in the first minute
     {
       if (startTime == 0) {                     // just started counting down, initialize
         startTime = millis();                   // We're doing this again because we might be beginning timing from this point for modes 2 and 3
         // calculate times
         classUpTime = startTime;                // class flag up (this is actually unnecessary)
         prepUpTime = startTime + 60000ul;       // prep flag up
         prepDownTime = startTime + 240000ul;    // prep flag down
         goTime = startTime + 300000ul;          // start signal (go)
         beepIndex = 0;                          // array index for the minute countdown
         StatePrint(state);
         shorthorn();
       } else if (prepUpTime >= millis()) {       // waiting for the flag
         remainingFlagTime = prepUpTime - millis();
         printcount(remainingFlagTime);
         print_P();
         print_u();
         remainingGoTime = goTime - millis();
         if (beepIndex < 20)
           beepIndex = minuteCountDown(remainingFlagTime, beepIndex);
       } else {
         // Prep Flag Up
         state = 3;
         shorthorn();
         StatePrint(state);
         beepIndex = 0;
         remainingFlagTime = 60000;
       }
       break;
     }
   case 3:                                       //We are in prep
     {
       if (prepDownTime >= millis()) {           //waiting for the flag
         remainingFlagTime = prepDownTime - millis();
         remainingGoTime = goTime - millis();
         printcount(remainingFlagTime);
         print_P();
         //this prevents the minutes character being overwritten before the last minute
         if (remainingFlagTime <= 60000)
         {
           print_d();
         }
         if (beepIndex < 20)
           beepIndex = minuteCountDown(remainingFlagTime, beepIndex);
       } else {
         // Prep Flag Down - enter final minute
         state = 4;
         longhorn();
         StatePrint(state);
         beepIndex = 0;
         remainingFlagTime = 60000;                //60 seconds to start
       }
       break;
     }
   case 4:                                         //We are in last minute
     {
       if (goTime >= millis()) {                   //waiting for the flag
         remainingFlagTime = goTime - millis();
         remainingGoTime = goTime - millis();
         printcount(remainingFlagTime);
         print_C();
         print_d();
         if (beepIndex < 20)
           beepIndex = minuteCountDown(remainingFlagTime, beepIndex);
       } else {
         // Class Flag Down (start)
         if (countMode != 3)                      //we don't want to beep the horn twice when we start the next sequence
           shorthorn();
         state = 5;
         StatePrint(state);
         beepIndex = 0;
       }
       break;
     }
   case 5:                                       //We have started
     {
       unsigned long currentMillis = millis();
       // If we are in Mode 3, reset to state 2, set startTime to 0, then recycle
       if (countMode == 3) {
         state = 2;
         startTime = 0;
         return;
       } else {
         matrix.clear();
         printcount(currentMillis - goTime);
       }
       break;
     }
   default:
     return;
 }

}
Reply


Messages In This Thread
Race timer - by abarrow - 2017-12-30, 03:58 PM
RE: Race timer - by Sjoerd02 - 2017-12-31, 02:32 PM
RE: Race timer - by abarrow - 2017-12-31, 04:31 PM

Forum Jump:


Users browsing this thread: 1 Guest(s)