OpenMarine

Full Version: Race timer
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
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;
 }

}
Hi Andy,

Your code look straight forward. What display do you use?
I tested a bit with the backlight LCD's but where poor readable in direct sunlight.
A Nokia lcd could be an option, but I would prefere something bigger.

From your code I expect you use a LED display, is that readable under all outdoor circumstances?
Do you have experience with Oled outdoor in bright sunlight?

E-Paper could be an option, but wondering if update speed is an issue for a clock displaying seconds.

Kr
Sjoerd

Hello Andy,

Thanks for sharing.
Probably the reason why I still did not complete my project because I want to much ~)
I am manly using vor ORC/IRC handicap racing, so not only the time between signals is important, also the absolute time for handicap calculation
Further I use a Gas Canon which has a delay of 1.7 - 2.5 seconds.

My wish list in order of importance:

- GPS synced signal (< 1 sec  Accuracy) on full minute, compensation for canon.
- 5 min repeating sequence
- Mute function (clock keeps counting down but no signal, eg for delay or General recall)
- Log of signals on webpage (including time), option to download / save
- (Next) Flags up | down on a webpage
- Solenoid to release a elastic powered flag (I only see a option for up only, down need to be a fixed line)


Where could OpenPlotter kick in:
ORC triple number requires to decide the handicap used based on average wind during the race.
To get that I want to record the average windspeed between starting signal and finish (could be a minute average)
So want to connect an anemometer to the (advanced) timer.

When we have a windset connected, It could be useful to have the average wind direction of last 30 min's to see a trend and to help you setting the course and pin-end.

During finish I want to log the exact time of each finish signal, preferable matching a sailnumber manual to it using a app or webpage.
So as you can see I have maybe to many wishes to complete in one go.

Should we build all this on a Rpi or an ESP8266/ESP32?

KR
Sjoerd
I tried several displays. I didn't try E-Paper, but I would think it would be the only thing that is really very readable in direct sunlight. The problem with E-Paper is that it has a slow update time, which I don't think would work well with a countdown timer.

I used an Adafruit LED display that was advertised as being very bright, It is, but still I find myself shielding the display from the sun sometimes. The countdown tones help, but for me, the ideal solution would be a monochrome display similar to what is used in most on-deck instruments, with a diagonal polarizer so that it is readable while wearing polarized sunglasses (I found out about the diagonal polarization when I replaced the polarizer one of my Raymarine instruments). I'd also like, some day, to have the alert sounds as an actual voice - while the beeps are good to indicate the countdown, nothing is better than a loud voice that says "One Minute to Class Flag down!" or something like that.

I've only tried the OLED in testing on the bench, and using sunlight through the window. It still washes out, but it doesn't seem to be as bad as LEDs. One thing I intend to try is inversing the display to black on white to see if that is better. One problem I'm having with the OLED and the current code is that the loop tries to update the display on every cycle. OLEDs take some time to update (as much as 40ms) and I'm thinking that might be contributing to exception errors I'm getting on the ESP32.

As far as what device to use, I'm discovering that the ESP32 may not be the right platform for this, at least not with the current code. For one thing, there is no TONE library for the ESP32 - I'm in the process of trying to do something with the internal CW generator, or perhaps with PWM - unfortunately there aren't really libraries for Arduino to work with it so more direct coding is required. The other things is, even though it is cheap, the ESP32 is a pretty big overkill for this little program. It's a very powerful device! So, I'd suggest either an Arduino, or an ESP8266. I'm still going to make it work on the ESP32 just as a learning exercise - one thing it does have that would be very helpful is a sleep mode that draws very little current - it might help in battery life.

As far as your wishlist:
- GPS synced signal (< 1 sec Accuracy) on full minute, compensation for canon.
I would think this would be pretty easy - since on most Openplotter-based boats GPS NMEA is available via Wifi. You really would only have to sync once in a session.
- 5 min repeating sequence
That's already there - mode 3 (5r)
- Mute function (clock keeps counting down but no signal, eg for delay or General recall)
That's a good idea. For the tone alerts I just turn off or turn down the amplifier, but it would be nice to prevent the horn from sounding as well. That might be as simple as a switch!
- Log of signals on webpage (including time), option to download / save
That would be cool!
- (Next) Flags up | down on a webpage
This also would be cool, and pretty easy to do.
- Solenoid to release a elastic powered flag (I only see a option for up only, down need to be a fixed line)
On the AC races in San Francisco, they used flags that got sucked into a pipe. I assume they were using a motor to make that happen, with perhaps some limit switches. Something like that wouldn't be hard, but I worry about too much automation! Still, as it becomes increasingly difficult to find volunteer deck crew for races, it would be nice to be able to run a committee boat with only 1 or 2 persons.

When we start talking about wind for ORC ratings and that sort of thing, we start needing storage, and then we get into something like a PI, or perhaps an SD card on an Arduino or ESP. That's great, and I would love to see it, but my experience with race officers, particularly the ones at the international level, is that they are pretty skeptical of any automation. Some, like my good friend Peter Von Muden, embrace it - he even brings a suitcase weather station to the events he works on. (My problem with Peter is convincing him that there are other operating systems beyond Windows!). Others generally still prefer their stopwatches, air horns and flags on poles! So whatever is made has to be simple, bullet proof, and easy to override.

Good discussion and suggestions. Let me know if you get it working and if you have any code suggestions. I'm not a professional programmer, and I'm a little ashamed to show my code to others!