Monday, December 9, 2013

40. Second Version of the Yacht Race Timer - YRTb

7000 page views!!
I just made a couple of refinements to the previous design - the main one being the reduction from 3 buttons to 2.  I decided that the as Button 1 wasn't being used until after time zero, T0, and Button 3 (red button) was only used before  T0, that I could remove Button 3 and give its functionality to Button 1.  This involved just a very small adjustment to the code, and a slight re-organisation of the breadboard.  The RTC board now sits more comfortably on the breadboard than it did before.

And then just to confuse everything, I put in a third button (the red button's back!) but this time, it's an Arduino RESET button.  This is effectively a breadboard button in parallel with the Arduino's RESET button, and when pressed, it grounds the Arduino's RESET connection, causing a reset.  I thought this would be necessary if the Arduino was to be buried behind the other hardware if it ever gets boxed.

The project now looks like this:



You can also see that I added a little refinement to the software so that when Button 2 is pressed to scroll through earlier finishing times, instead of displaying the previously used " *", it shows "*Readback" to make it very clear what's being displayed.  Additionally, if the read-back finishing time is more than 30 minutes after the first finishing time, the message is " *!>30min".

The red button on the left can now be used to reset the whole thing (as can the Arduino's button, just to the right of the red LED).  The first (left-hand) brown button is now used to set the number of countdown minutes when the prompt "Set c/d" shows just after reset or powering up for the first time.  The second brown button (on the right) still has the scrollback functionality.

I was thinking about having the unit boxed and even re-produced.  In this case you probably wouldn't want to include a whole Arduino Uno in the box.  Instead you should be able to get away with just the ATMEGA328P-PU chip and some other components, so here is an imaginary breadboard setup:


Please note that I have NOT tried building this circuit!! The first thing you will notice is that the LCD is not a 20 x 4 like the one I am using, but that's the only one I could find on Fritzing.  The connections are the same.  The buttons I referred to are shown as S2, S3 and S4.  There is also an Arduino RESET button on the left called S1.  The circuit includes a voltage regulator, so the recommended 7V to 12V input can be used safely.  It would be possible to just include the components of the RTC, but the pre-fabricated RTC circuit board (in the example above, from Adafruit) is hard to beat!

Just a note on Fritzing - it's brilliant!  This is free and can be downloaded from http://fritzing.org/download/.  As well as "building" a circuit (with or without a breadboard) as above, you can then ask the software to draw the schematic, "autoroute" it, and best of all, draw a double-sided PCB.  More than this - you can create an etchable PCB, and for about 32.20, Fritzing will actually build your PCB!  It can also point you to places on the web for buying your parts.  Amazing!

Here is an example of a schematic for the circuit above:


I'm showing this purely an an illustration of what can be done.  There are actually errors in the above schematic.

And for completeness, here is a PCB version:



Again a warning - this hasn't been shown to work!  I just wanted to show you what Fritzing can do.  The yellow and orange copper traces are on different sides of the board, so that's why they are allowed to cross each other.  There are places above where a yellow trace becomes an orange one, because the connection goes from one side of the board to the other using the black dot surrounded by green, known as a "Via".  The grey rectangle at the bottom represents the  LCD board.  The board shown above would probably be about 10 cm square.

Just a note on the RTC accuracy - after a couple of weeks, the RTC time of day is 3 minutes behind my PC's time.  It might be wise to periodically return the setup to the PC and run the SetTime sketch, or else use the high accuracy temperature compensated DS3231 RTC which I previously referred to.  Don't forget that the finishing times are using the Arduino's millis() function, modified by my fiddle factor, so in theory, the accuracy of this part of the timing can be adjusted to give very high accuracy.


Thursday, November 28, 2013

39. YRTa - Version 'a' of the Yacht Race Timer

6000 page views!!
So I finally got my real time clock (RTC) - all the way from China, at £2.78 including battery and P&P!  And it works!  Reading around these on the web I got the impression that some of them don't work, but mine works a treat (I haven't tested how good the time-keeping is, but I'll need a few weeks to do that).  I did have some trouble getting the software libraries going, but when I deleted all previous attempts and re-downloaded them from scratch, this seemed to do the trick.  You have to solder on a header for the breadboard, but that's no problem.  Here's mine:



The dimensions are approximately 1 inch by 1 inch.  The other side looks like this:


There's a 3.6V rechargeable LIR2032 lithium battery inserted underneath for which a full charge lasts for about a year, although it trickle-charges from the Arduino power supply.  You can see where I have soldered a 4-pin header (this can be seen on the left hand side of the upper image, and along the bottom of the lower image) for insertion into the breadboard.  

Apart from VCC (5V) and GND, only the serial data line (SDA) and serial clock (SCL) connections of the I²C system are necessary.  These connections are repeated along with some other connections, on the right hand side of the upper image.  When you first connect this up to the Arduino, you need to run the SetTime example sketch to set the time to the PC's time, and running the ReadTest sketch verifies that the RTC's date and time has been set.

Here is the latest configuration of the Yacht Race Timer, named YRTa:


You can see that I have re-arranged the layout on the LCD display.  The photo above shows, on the first line, that an "A plus B" - ie a 1-fleet sequence with all boats starting together, has already been started.  On the top left, the first symbol indicates that a flag - the AP (postponement) flag - is flying and that the time of day is 09:05.  The second line displays the time in hours, minutes and seconds, of the sequence to or from the start.

In the middle of the display is a large numeral showing the count-down seconds to the next critical point. The 3rd line shows the current date.  The bottom line shows the next flag to move - the AP flag will come down in 4 seconds.

Here is a snapshot of another situation:


This shows that there has already been an "A then B" ie a 2-fleet, sequence start.  The 8 symbols on the top left show that the flags have been raised and lowered for the first fleet, then for the second fleet.  The second line shows that the race has been started (for the second fleet) more than 3 hours.  (The first fleet will have started exactly 5 minutes before this). The 3rd line shows the finishing time for the first boat and the 4th line shows the finishing time of, in this case, the 4th boat followed by a warning that this finishing time is more than 30 minutes after the first finishing time.

As before, the first button when pressed, records the finishing time of each boat passing through the finish line.  The second button allows you to step back, at any time, through finishing times as far as the 2nd boat, and the 3rd (red) button is for setting the starting sequence time in minutes.  If not pressed within a few seconds of an Arduino reset (using the "RESET" button on the Arduino, or simply powering up the Arduino), the start sequence defaults to an "A plus B" (ie one-fleet) sequence, at a few seconds before 6 minutes.

Extra attributes which have been added to this version include:
  • Current date and time (from the RTC) displayed
  • Numerals 9 to 0 for the large-character count-down warning (previously 5 to 0)
  • Indication of the next flag change to be made after the 9-second countdown.  eg for the "A then B" sequence:
    • "Next flag = AP DOWN "              
      Postponement over (only used if the start is delayed)  T0-11
    • "Next flag = CLASS UP"                                          
                                                                          A-fleet flag  T0-10
    • "Next flag = PREP UP "                                     
                                                          4-minute preparatory  T0-9
    • "Next flag=PREP DOWN "                                
                                                          4-minute preparatory  T0-6
    • "Next = CLASS DN + UP"        &      
                                                                    A-fleet starts       T0-5  
    • "Next flag = PREP UP "                                     
                                                          4-minute preparatory  T0-4
    • "Next flag=PREP DOWN "                                
                                                         4-minute preparatory   T0-1
    • "Next flag=CLASS DOWN"     
                                                                       B-fleet starts  
      T0   
There are other flags flying, but above are the ones involved in the starting sequence at our Club.  When there is a 1-fleet start, the A-fleet flag and the B-fleet flag are raised and lowered together at T0-5 mins and Trespectively.

The YRTa contains all the attributes needed to control the flags and horn sounds for not only one, but 2 different starting sequences, and carries out an automatic count-down/count-up sequence allowing a single button to be pressed to record the finishing time of each boat passing through the finish line.  It even alerts the Race Officer if any finishing time is outside the time limit - currently set at 30 minutes after the first boat.  It not only prompts the Race Officer to get ready to lower or raise the next appropriate flag, but it tells him which flag it is!  

After the race, it allows all the finishing times to be inspected for calculation using a different system - ie writing down on a sheet for later PC entry.  Fancy downloading to a PC is possible, but the finishing times have to be written down anyway, so downloading is un-necessary.

Because the display space is limited, only essential information is displayed at any one time, so after use, information is removed or replaced, keeping the display uncluttered, but indicating exactly at what stage the proceedings are.

All this while displaying the time of day and the date - very useful!  

Note that the timing part still makes use of the millis() function of the Arduino (modified by a tweaking factor to maximise accuracy) and not the RTC - it is used only for the date and time display.  Maybe it would be worth trying to use the seconds from the RTC's tmElements_t.Second function from the Time library in case more accuracy can be obtained.  Further accuracy could be got from a slightly more expensive RTC: The ChronoDot (ref http://www.amazon.co.uk/ChronoDot-Ultra-precise-Real-Time-Clock/dp/B007XEV79O/ref=wl_it_dp_o_pC_nS_nC?ie=UTF8&colid=2CD3HRH1RXYAP&coliid=I35FS3VXMIP2A6#productDescription) based on the DS3231 temperature compensated RTC is said to be ultra-accurate because it has built-in temperature compensation.

Because the Arduino can always be brought back to the PC for programming, things can be changed (eg the 30 minute warning, or the time accuracy adjustment factor).

Here is the complete circuit diagram:



Here is the full code listing:
1:  /*  
2:   KCTiming  
3:   Arduino Yacht Race Timer using the LiquidCrystal Library  
4:   Demonstrates the use of a 20 column x 4 row LCD display. The LiquidCrystal library works with all   
5:   LCD displays that are compatible with the Hitachi HD44780 driver.   
6:   This sketch prints count-down / count-up times to the LCD - and more!.  
7:   The circuit:  
8:   * LCD RS pin to digital pin 12  
9:   * LCD Enable pin to digital pin 11  
10:   * LCD D4 pin to digital pin 5  
11:   * LCD D5 pin to digital pin 4  
12:   * LCD D6 pin to digital pin 3  
13:   * LCD D7 pin to digital pin 2  
14:   * LCD R/W pin to ground  
15:   * Variable (eg 10K) resistor:  
16:     - ends to +5V and ground   
17:     - wiper to LCD VO pin (LCD pin 3)  
18:   Library originally added 18 Apr 2008 by David A. Mellis. Library modified 5 Jul 2009 by Limor Fried  
19:   Example added 9 Jul 2009 by Tom Igoe. Modified 22 Nov 2010 by Tom Igoe. Adopted and adapted by KC Oct 2013  
20:  */  
21:  // Include the library code:  
22:  #include <LiquidCrystal.h>  
23:  // Include libraries for the Real Time Clock  
24:  #include <DS1307RTC.h>  
25:  #include <Time.h>  
26:  #include <Wire.h>  
27:  // Make a new LiquidCrystal object and call it lcd with the numbers of the interface pins  
28:  LiquidCrystal lcd(12, 11, 5, 4, 3, 2);  
29:  // Array of bits defining pixels for 4 custom characters  
30:  // ...pixel=1 : on and pixel=0 : off  
31:  // See Custom Character Generator at http://omerk.github.io/lcdchargen/  
32:   byte oneflagdown[8] =   
33:    {0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01100, 0b01100};  // 1 flag down  
34:   byte twoflagsup[8]  =    
35:    {0b11011, 0b11011, 0b01010, 0b01010, 0b01010, 0b01010, 0b01010, 0b01010};  // 2 flags up  
36:   byte oneflagup[8]  =   
37:    {0b01100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100};  // 1 flag up  
38:   byte twoflagsdown[8] =   
39:    {0b01010, 0b01010, 0b01010, 0b01010, 0b01010, 0b01010, 0b11011, 0b11011};  // 2 flags down  
40:  // see Arduino Cookbook 2nd ed by Michael Margolis (only 4 glyphs are needed)  
41:   byte glyphs[4][8] = {  
42:    {0b11111, 0b11111, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000},  
43:    {0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b11111, 0b11111},  
44:    {0b11111, 0b11111, 0b00000, 0b00000, 0b00000, 0b00000, 0b11111, 0b11111},  
45:    {0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111}};  
46:   const int digitWidth = 3;              // the width in characters of a big digit  
47:                                          // (excludes the space between characters)  
48:   // Here are arrays to index into custom characters that will make up the big numbers:  
49:   // ...(ASCII code 32 represents the space, or null (blank) character):  
50:   // Digits 0 - 4  (top halves)      0   1     2   3    4  
51:   const char bigDigitsTop[10][digitWidth]={3,0,3, 0,3,32,  2,2,3, 0,2,3,  3,1,3,  
52:   // Digits 5 - 9  (top halves)      5   6     7   8    9  
53:                        3,2,2, 3,2,2,  0,0,3, 3,2,3,  3,2,3};  
54:   // Digits 0 - 4  (bottom halves)     0   1     2   3    4  
55:   const char bigDigitsBot[10][digitWidth]={3,1,3, 1,3,1,  3,1,1, 1,1,3, 32,32,3,  
56:   // Digits 5 - 9  (bottom halves)     5   6     7   8    9  
57:                        1,1,3, 3,1,3, 32,32,3, 3,1,3,  1,1,3};  
58:   int button1 = 6;  
59:   int button2 = 7;  
60:   int button3 = 8;  
61:   int outPin = 13;                        // the number of the output pin (LED)  
62:   int reading1, reading2, reading3;  
63:   long debounce = 200;                    // debounce delay in milliseconds  
64:   int i = 2;                              // which LCD row to print resul  
65:   int sec00 = 0;  
66:   int j;  
67:   int timeset = 371;  // default countdown seconds (a few secs longer than are displayed)  
68:   int countdownMinutes = 0;  
69:   int loopCounter = 0;  
70:   const int BOAT_COUNT = 99;              // maximum number of boats accommodated  
71:   int finish_time[BOAT_COUNT];            // array of finish times  
72:  void setup() {  
73:   // Set up the LCD's number of columns and rows:   
74:   lcd.begin(20, 4);                       // 20 columns & 4 rows  
75:   lcd.createChar(4, oneflagdown);         // create first custom character  
76:   lcd.createChar(5, twoflagsup);          // create second custom character  
77:   lcd.createChar(6, oneflagup);           // create third custom character  
78:   lcd.createChar(7, twoflagsdown);        // create fourth custom character  
79:   for(int i=0; i < 4; i++)  
80:    lcd.createChar(i, glyphs[i]);  
81:   lcd.clear();   
82:   // Print the header on the LCD  
83:   lcd.setCursor(10,0);  
84:   lcd.print("YRTa");                      // for "Yacht Race Timer - version a"  
85:   pinMode(button1, INPUT);  
86:   pinMode(button2, INPUT);  
87:   pinMode(button3, INPUT);  
88:   //Set up for the RTC  
89:   //Serial.begin(9600);  
90:   //while (!Serial); //wait for serial  
91:   //delay(200);  
92:   lcd.setCursor(0,1);  
93:   lcd.print("Set c/d");  
94:  }  
95:  void loop() {  
96:   tmElements_t tm;  
97:   if (RTC.read(tm)) {  
98:    lcd.setCursor(15,0);  
99:    print2digits(tm.Hour);lcd.print(":");  
100:    print2digits(tm.Minute);  
101:    lcd.setCursor(12,2);  
102:    print2digits(tm.Day);lcd.print("-");  
103:    print2digits(tm.Month);lcd.print("-");  
104:    print2digits(tmYearToCalendar(tm.Year));    
105:   }  
106:   /* else {  
107:   if(RTC.chipPresent()){  
108:    Serial.println("The DS1307 has stopped. Please run the SetTime example");  
109:    Serial.println("sketch to initialise the time and begin running.");  
110:    Serial.println();  
111:   }  
112:   else{  
113:    Serial.println("DS1307 read error! Please check the circuitry.");  
114:    Serial.println();  
115:    }  
116:   }*/  
117:   loopCounter++;  
118:   if (loopCounter == 1)  
119:   {   
120:    for(int l=0; l<60; l++)  
121:    { delay(100);  
122:     reading3 = digitalRead(button3);  
123:     if (reading3 == HIGH)  
124:     { delay(debounce);                          // in case the button bounces  
125:      countdownMinutes++;  
126:      timeset = countdownMinutes*60;  
127:      lcd.setCursor(9,1);  
128:      lcd.print(countdownMinutes); } } }  
129:   int time = -timeset;                          // countdown seconds before time zero  
130:   long sec0 = millis()*1.0000/998.750991;       // correction for milli-seconds slow/fast  
131:   //long sec0 = millis()/1000;                  // alternative with no correction  
132:   long secs = sec0 + time;  
133:  // Call the routine for the preferred sequence  
134:   if (timeset < 660)  
135:    {twoFleetsTogether(secs);  
136:   }  
137:   else  
138:    {A_then_B(secs);  
139:    lcd.setCursor(11,1);  
140:    lcd.print(" A then B");}   
141:   clockDisplay(0, 1, secs);                    // display the running time before/after time zero   
142:   reading1 = digitalRead(button1);             // listen for button press  
143:   if (reading1 == HIGH)                        // if the button is pressed  
144:   { delay(debounce);                           // in case the button bounces  
145:    button1pressed(secs); i++;                  // move on to next line for next time  
146:    j = i-1; }                                  // new counter for button2pressed()  
147:   reading2 = digitalRead(button2);             // listen for button press  
148:   if (reading2 == HIGH)                        // if the button is pressed  
149:   {delay(debounce);                            // in case the button bounces  
150:    button2pressed(j);  
151:    j--;}                                       // scroll back through finish times  
152:  }  
153:  void twoFleetsTogether (long s){              // single start (bothe fleets together)  
154:   if (s < -360){lcd.setCursor(0, 0);lcd.write(6);lcd.setCursor(0,3);lcd.print("Next flag = AP DOWN ");}   // postponement flag up  
155:   if (s == -369 || s == -309 || s == -249 || s == -69 || s == -9) showDigit(9, 5);                        // big number countdown  
156:   if (s == -368 || s == -308 || s == -248 || s == -68 || s == -8) showDigit(8, 5);  
157:   if (s == -367 || s == -307 || s == -247 || s == -67 || s == -7) showDigit(7, 5);  
158:   if (s == -366 || s == -306 || s == -246 || s == -66 || s == -6) showDigit(6, 5);  
159:   if (s == -365 || s == -305 || s == -245 || s == -65 || s == -5) showDigit(5, 5);                        // big number countdown  
160:   if (s == -364 || s == -304 || s == -244 || s == -64 || s == -4) showDigit(4, 5);  
161:   if (s == -363 || s == -303 || s == -243 || s == -63 || s == -3) showDigit(3, 5);  
162:   if (s == -362 || s == -302 || s == -242 || s == -62 || s == -2) showDigit(2, 5);  
163:   if (s == -361 || s == -301 || s == -241 || s == -61 || s == -1) showDigit(1, 5);  
164:   if (s == -360 || s == -300 || s == -240 || s == -60 || s == 0) showDigit(0, 5);  
165:   if (s == -359 || s == -299 || s == -239 || s == -59 || s == +1)  
166:    {lcd.setCursor(8, 1);lcd.print("  "); lcd.setCursor(8, 2);lcd.print("  ");}                            // clear big digit  
167:   if (s == -360){lcd.setCursor(0, 0);lcd.write(4);lcd.setCursor(0,3);lcd.print("Next flag = CLASS UP");}  // display first character at 6 minutes (1 flag down)  
168:   if (s == -300){lcd.setCursor(0, 0);lcd.write(5);lcd.setCursor(0,3);lcd.print("Next flag = PREP UP ");}  // display second character at 5 minutes (2 flags up)  
169:   if (s == -240){lcd.setCursor(1, 0);lcd.write(6);lcd.setCursor(0,3);lcd.print("Next flag=PREP DOWN ");}  // display third character (1 flag up)  
170:   if (s ==  -60){lcd.setCursor(2, 0);lcd.write(4);lcd.setCursor(0,3);lcd.print("Next flag=CLASS DOWN");}   // display first character (1 flag down)  
171:   if (s ==    0){lcd.setCursor(3, 0);lcd.write(7);lcd.setCursor(0,3);lcd.print("          ");}  // display fourth character (2 flags down)   
172:  }  
173:  void A_then_B (long s){                                           // two separate fleet starts  
174:   if (s < -660){lcd.setCursor(0, 0);lcd.write(6);lcd.setCursor(0,3);lcd.print("Next flag = AP DOWN ");}   // postponement flag up  
175:   if (s == -669 || s == -609 || s == -549 || s == -369 || s == -309) showDigit(9, 5);                     // big number countdown  
176:   if (s == -668 || s == -608 || s == -548 || s == -368 || s == -308) showDigit(8, 5);  
177:   if (s == -667 || s == -607 || s == -547 || s == -367 || s == -307) showDigit(7, 5);  
178:   if (s == -666 || s == -606 || s == -546 || s == -366 || s == -306) showDigit(6, 5);   
179:   if (s == -665 || s == -605 || s == -545 || s == -365 || s == -305) showDigit(5, 5);                     // big number countdown  
180:   if (s == -664 || s == -604 || s == -544 || s == -364 || s == -304) showDigit(4, 5);  
181:   if (s == -663 || s == -603 || s == -543 || s == -363 || s == -303) showDigit(3, 5);  
182:   if (s == -662 || s == -602 || s == -542 || s == -362 || s == -302) showDigit(2, 5);  
183:   if (s == -661 || s == -601 || s == -541 || s == -361 || s == -301) showDigit(1, 5);  
184:   if (s == -660 || s == -600 || s == -540 || s == -360 || s == 300) showDigit(0, 5);  
185:   if (s == -659 || s == -599 || s == -539 || s == -359 || s == +301)  
186:    {lcd.setCursor(8, 1);lcd.print("  "); lcd.setCursor(8, 2);lcd.print("  ");}                            // clear big digit  
187:   if (s == -369 || s == -309 || s == -249 || s == -69 || s ==  -9) showDigit(9, 5);                       // big number countdown  
188:   if (s == -368 || s == -308 || s == -248 || s == -68 || s ==  -8) showDigit(8, 5);  
189:   if (s == -367 || s == -307 || s == -247 || s == -67 || s ==  -7) showDigit(7, 5);  
190:   if (s == -366 || s == -306 || s == -246 || s == -66 || s ==  -6) showDigit(6, 5);    
191:   if (s == -365 || s == -305 || s == -245 || s == -65 || s ==  -5) showDigit(5, 5);                       // big number countdown  
192:   if (s == -364 || s == -304 || s == -244 || s == -64 || s ==  -4) showDigit(4, 5);  
193:   if (s == -363 || s == -303 || s == -243 || s == -63 || s ==  -3) showDigit(3, 5);  
194:   if (s == -362 || s == -302 || s == -242 || s == -62 || s ==  -2) showDigit(2, 5);  
195:   if (s == -361 || s == -301 || s == -241 || s == -61 || s ==  -1) showDigit(1, 5);  
196:   if (s == -360 || s == -300 || s == -240 || s == -60 || s ==  0) showDigit(0, 5);  
197:   if (s == -359 || s == -299 || s == -239 || s == -59 || s ==  +1)  
198:    {lcd.setCursor(8, 1);lcd.print("  "); lcd.setCursor(8, 2);lcd.print("  ");}                            // clear big digit  
199:   if (s == -660){lcd.setCursor(0, 0);lcd.write(4);lcd.setCursor(0,3);lcd.print("Next flag = CLASS UP");}  // display first character at 11 minutes (1 flag down)  
200:   if (s == -600){lcd.setCursor(0, 0);lcd.write(5);lcd.setCursor(0,3);lcd.print("Next flag = PREP UP ");}  // display second character at 10 minutes (2 flags up)  
201:   if (s == -540){lcd.setCursor(1, 0);lcd.write(6);lcd.setCursor(0,3);lcd.print("Next flag=PREP DOWN ");}  // display third character (1 flag up)  
202:   if (s == -360){lcd.setCursor(2, 0);lcd.write(4);lcd.setCursor(0,3);lcd.print("Next = CLASS DN + UP");}  // display first character (1 flag down)  
203:   if (s == -300){lcd.setCursor(3, 0);lcd.write(7);}                             // display fourth character (A-Fleet flag down)   
204:   if (s == -300){lcd.setCursor(5, 0);lcd.write(5);lcd.setCursor(0,3);lcd.print("Next flag = PREP UP ");}  // display second character (B-Fleet flag up)  
205:   if (s == -240){lcd.setCursor(6, 0);lcd.write(6);lcd.setCursor(0,3);lcd.print("Next flag=PREP DOWN ");}  // display third character (1 flag up)  
206:   if (s ==  -60){lcd.setCursor(7, 0);lcd.write(4);lcd.setCursor(0,3);lcd.print("Next flag=CLASS DOWN");}   // display first character (1 flag down)  
207:   if (s ==    0){lcd.setCursor(8, 0);lcd.write(7);lcd.setCursor(0,3);lcd.print("          ");}              // display fourth character (2 flags down)   
208:  }  
209:  void clockDisplay(int x, int y, long s){  
210:   // Display hours, mins, secs from time zero, at required position  
211:   int mins = s/60;  
212:   int hrs = mins/60;  
213:   long secs = s - (mins*60);  
214:   mins = mins - (hrs*60);  
215:   lcd.setCursor(x,y);  
216:   if (s > 0)  
217:    lcd.print("+");  
218:   else if (s < 0)  
219:    lcd.print ("-");  
220:   else  
221:    lcd.print (" ");                            // .. THEY'RE OFF!!  
222:   lcd.setCursor(x+1, y);lcd.print(abs(hrs));lcd.print (":"); // trailing colon  
223:   lcd.setCursor(x+3, y);  
224:   if (abs(mins)<10)                            // include a leading zero if less than 10  
225:    lcd.print("0" + String(abs(mins)));  
226:   else   
227:    lcd.print(abs(mins));  
228:   lcd.print (":");                             // trailing colon  
229:   lcd.setCursor(x+6, y);  
230:   if (abs(secs)<10)                            // include a leading zero if less than 10  
231:    lcd.print("0" + String(abs(secs)));  
232:   else   
233:    lcd.print(abs(secs));  
234:   //lcd.print(" ");  
235:  }  
236:  void button1pressed(long s){                  // press button to capture finishing times  
237:   lcd.setCursor(0, i);  
238:   if ((i-1) < 10)  
239:    lcd.write(" ");                             // tidy up spacing if number less than 10  
240:   lcd.print(i-1);  
241:   clockDisplay(3, i, s);  
242:   finish_time[i-2] = s;                        // put first, 2nd, etc finish times into the array  
243:   if (i == 2)  
244:    sec00 = s;                                  // remember first boat's time  
245:   else if ((s-sec00) > (30*60))                // compare this one with first boat's time  
246:    lcd.write(" >30 min!");                     // and give a warning if it's out of time  
247:   else  
248:    lcd.write("     ");                         // otherwise clear this part of the display  
249:  }  
250:  void button2pressed(int ii){                  // scroll back through finish times  
251:   if((ii-2)>1)  
252:   {lcd.setCursor(0, 3);  
253:    if ((ii-2) < 10)  
254:     lcd.write(" ");                            // tidy up spacing if number less than 10  
255:    lcd.print(ii-2);  
256:    clockDisplay(3, 4, finish_time[ii-3]);  
257:    lcd.write(" *    ");}                       // mark with an asterisk to indicate an old finish time  
258:  }  
259:  void showDigit(int digit, int posn){          // display the big digits at desired position  
260:  // lcd.setCursor(posn * (digitWidth + 1), 0);  
261:   lcd.setCursor(8, 1);  
262:   for(int i=0; i < digitWidth; i++)  
263:    lcd.write(bigDigitsTop[digit][i]);          // top half of big digit  
264:  // lcd.setCursor(posn * (digitWidth + 1), 1);  
265:   lcd.setCursor(8, 2);  
266:   for(int i=0; i < digitWidth; i++)  
267:    lcd.write(bigDigitsBot[digit][i]);          // bottom half of big digit  
268:  }  
269:  void print2digits(int number){  
270:   if (number>=0 && number <10){  
271:    lcd.write("0");  
272:   }  
273:   if (number>99){  
274:    number = number-2000;                      // years after the year 2000 (ie a 2-digit year)  
275:   }  
276:   lcd.print(number);  
277:  }  





Tuesday, November 5, 2013

38. More on the Yacht Race Timer

5000 page views!!
I have added a few things to the timer (see the last post at http://smokespark.blogspot.co.uk/2013/10/37-yacht-race-timer-in-making.html), hoping to keep it as simple as possible, but also as useful as possible.  It now looks like this:
The image shows two extra push buttons - the middle one is for stepping back through the finishing times.  For example, if there are 4 finishing times recorded by pressing the first button on the left (4 times), then pressing the middle button once will display the third finishing time and indicates with an asterisk, the fact that it is not the latest recorded time.  Pressing the middle button again would display the previous (ie the second) finishing time.  When the first and second times are displayed, further presses of the middle button would have no further effect.  The image above shows the first finishing time and the 3rd time (with an asterisk to indicate that it's not the last one recorded).

Up to 99 boat times can be recorded by pressing the first button, these times being captured in an array, and this could be increased in size considerably if required - see line 66 in the code listing below.  This is way more than necessary as fleets that size would almost certainly not arise, but a Round the Island Race of more than a thousand entries may well be possible - I haven't tried this!

Also shown is the comment "A plus B" which indicates a one-fleet race.  (Often two fleets are started at the same time - and this would constitute a one-fleet start).  The alternative would be "A then B" where the countdown time is 10 minutes or more, and critical times would be:
 T0-10,   T0-9,   T0-6,   T0-5,   T0-4,   T0-1 minutes and T0. The first 3 times relate to the first fleet which would start 5 minutes ahead of the second fleet.  At T0-5 the first fleet would start and simultaneously, the second fleet would have its 5-minute warning signal.  Finishing times for the first fleet would need 5 minutes added. However, this is not a problem.

The recent additions include:
  • 2-fleet starting
  • Large character 5-second count-down warning when approaching critical times
  • Visual indication that the finishing time is more than 30 minutes later than the first time
  • A second button for stepping back through previously recorded finishing times
  • A third button for setting the count-down time
  • Automatic selection of one- or two-fleet operation depending on the count-down time
  • Calibration of the timer to improve the accuracy of recorded finishing times
A further item which I am waiting for is a Real Time Clock (RTC) which has its own power source and so can keep the time of day even when the Arduino system has been switched off.

The code is currently as follows:
1:  /*  
2:   KCTiming  
3:   Arduino Yacht Race Timer using the LiquidCrystal Library  
4:   Demonstrates the use a 20 column x 4 row LCD display. The LiquidCrystal library works with all   
5:   LCD displays that are compatible with the Hitachi HD44780 driver.   
6:   This sketch prints count-down / count-up times to the LCD - and more!.  
7:   The circuit:  
8:   * LCD RS pin to digital pin 12  
9:   * LCD Enable pin to digital pin 11  
10:   * LCD D4 pin to digital pin 5  
11:   * LCD D5 pin to digital pin 4  
12:   * LCD D6 pin to digital pin 3  
13:   * LCD D7 pin to digital pin 2  
14:   * LCD R/W pin to ground  
15:   * Variable (eg 10K) resistor:  
16:     - ends to +5V and ground   
17:     - wiper to LCD VO pin (LCD pin 3)  
18:   Library originally added 18 Apr 2008 by David A. Mellis. Library modified 5 Jul 2009 by Limor Fried  
19:   Example added 9 Jul 2009 by Tom Igoe. Modified 22 Nov 2010 by Tom Igoe. Adopted and adapted by KC Oct 2013  
20:  */  
21:  // Include the library code:  
22:  #include <LiquidCrystal.h>  
23:  // Make a new LiquidCrystal object and call it lcd with the numbers of the interface pins  
24:  LiquidCrystal lcd(12, 11, 5, 4, 3, 2);  
25:  // Array of bits defining pixels for 4 custom characters  
26:  // ...pixel=1 : on and pixel=0 : off  
27:  // See Custom Character Generator at http://omerk.github.io/lcdchargen/  
28:   byte oneflagdown[8] =   
29:    {0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01100, 0b01100};  // 1 flag down  
30:   byte twoflagsup[8]  =    
31:    {0b11011, 0b11011, 0b01010, 0b01010, 0b01010, 0b01010, 0b01010, 0b01010};  // 2 flags up  
32:   byte oneflagup[8]  =   
33:    {0b01100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100};  // 1 flag up  
34:   byte twoflagsdown[8] =   
35:    {0b01010, 0b01010, 0b01010, 0b01010, 0b01010, 0b01010, 0b11011, 0b11011};  // 2 flags down  
36:  // see Arduino Cookbook 2nd ed by Michael Margolis (only 4 glyphs are needed)  
37:   byte glyphs[4][8] = {  
38:    {0b11111, 0b11111, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000},  
39:    {0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b11111, 0b11111},  
40:    {0b11111, 0b11111, 0b00000, 0b00000, 0b00000, 0b00000, 0b11111, 0b11111},  
41:    {0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111}};  
42:   const int digitWidth = 3;              // the width in characters of a big digit  
43:                              // (excludes the space between characters)  
44:   // Here are arrays to index into custom characters that will make up the big numbers:  
45:   // ...(ASCII code 32 represents the space, or null (blank) character):  
46:   // Digits 0 - 4  (top halves)      0   1     2   3    4  
47:   const char bigDigitsTop[10][digitWidth]={3,0,3, 0,3,32,  2,2,3, 0,2,3,  3,1,3,  
48:   // Digits 5 - 9  (top halves)      5   6     7   8    9  
49:                        3,2,2, 3,2,2,  0,0,3, 3,2,3,  3,2,3};  
50:   // Digits 0 - 4  (bottom halves)     0   1     2   3    4  
51:   const char bigDigitsBot[10][digitWidth]={3,1,3, 1,3,1,  3,1,1, 1,1,3, 32,32,3,  
52:   // Digits 5 - 9  (bottom halves)     5   6     7   8    9  
53:                        1,1,3, 3,1,3, 32,32,3, 3,1,3,  1,1,3};  
54:   int button1 = 6;  
55:   int button2 = 7;  
56:   int button3 = 8;  
57:   int outPin = 13;                                    // the number of the output pin (LED)  
58:   int reading1, reading2, reading3;  
59:   long debounce = 200;                                // debounce delay in milliseconds  
60:   int i = 2;                                          // which LCD row to print resul  
61:   int sec00 = 0;  
62:   int j;  
63:   int timeset = 371;  // default countdown seconds (a few secs longer than are displayed)  
64:   int countdownMinutes = 0;  
65:   int loopCounter = 0;  
66:   const int BOAT_COUNT = 99;                          // maximum number of boats accommodated  
67:   int finish_time[BOAT_COUNT];                        // array of finish times  
68:  void setup() {  
69:   // Set up the LCD's number of columns and rows:   
70:   lcd.begin(20, 4);                                   // 20 columns & 4 rows  
71:   lcd.createChar(4, oneflagdown);                     // create first custom character  
72:   lcd.createChar(5, twoflagsup);                      // create second custom character  
73:   lcd.createChar(6, oneflagup);                       // create third custom character  
74:   lcd.createChar(7, twoflagsdown);                    // create fourth custom character  
75:   for(int i=0; i < 4; i++)  
76:    lcd.createChar(i, glyphs[i]);  
77:   lcd.clear();   
78:   // Print the header on the LCD  
79:   lcd.print("     YRT h m s");                        // for "Yacht Race Timer"  
80:   pinMode(button1, INPUT);  
81:   pinMode(button2, INPUT);  
82:   pinMode(button3, INPUT);  
83:  }  
84:  void loop() {  
85:   loopCounter++;  
86:   if (loopCounter == 1)  
87:   { lcd.setCursor(0,1);  
88:    lcd.print("Set c/d mins");  
89:    for(int l=0; l<60; l++)  
90:    { delay(100);  
91:     reading3 = digitalRead(button3);  
92:     if (reading3 == HIGH)  
93:     { delay(debounce);                                // in case the button bounces  
94:      countdownMinutes++;  
95:      timeset = countdownMinutes*60;  
96:      lcd.setCursor(13,1);  
97:      lcd.print(countdownMinutes); } } }  
98:   lcd.setCursor(0,1);  
99:   lcd.print("      ");  
100:   int time = -timeset;                               // countdown seconds before time zero  
101:   long sec0 = millis()*1.0000/998.750991;            // correction for milli-seconds slow/fast  
102:   //long sec0 = millis()/1000;                       // alternative with no correction  
103:   int secs = sec0 + time;  
104:  // Call the routine for the preferred sequence  
105:   if (timeset < 660)  
106:    {twoFleetsTogether(secs);  
107:    lcd.setCursor(12,2);  
108:    lcd.print("A plus B");}  
109:   else  
110:    {A_then_B(secs);  
111:    lcd.setCursor(12,2);  
112:    lcd.print("A then B");}   
113:   clockDisplay(12, 1, secs);                         // display the running time from time zero   
114:   reading1 = digitalRead(button1);                   // listen for button press  
115:   if (reading1 == HIGH)                              // if the button is pressed  
116:   { delay(debounce);                                 // in case the button bounces  
117:    button1pressed(secs); i++;                        // move on to next line for next time  
118:    j = i-1; }                                        // new counter for button2pressed()  
119:   reading2 = digitalRead(button2);                   // listen for button press  
120:   if (reading2 == HIGH)                              // if the button is pressed  
121:   {delay(debounce);                                  // in case the button bounces  
122:    button2pressed(j);  
123:    j--;}                                             // scroll back through finish times  
124:  }  
125:  void twoFleetsTogether (int s){                     // single start (both fleets together)  
126:   if (s < -360){lcd.setCursor(0, 0);lcd.write(6);}   // postponement flag up  
127:   if (s == -365 || s == -305 || s == -245 || s == -65 || s == -5) showDigit(5, 5);  // big number countdown  
128:   if (s == -364 || s == -304 || s == -244 || s == -64 || s == -4) showDigit(4, 5);  
129:   if (s == -363 || s == -303 || s == -243 || s == -63 || s == -3) showDigit(3, 5);  
130:   if (s == -362 || s == -302 || s == -242 || s == -62 || s == -2) showDigit(2, 5);  
131:   if (s == -361 || s == -301 || s == -241 || s == -61 || s == -1) showDigit(1, 5);  
132:   if (s == -360 || s == -300 || s == -240 || s == -60 || s == 0) showDigit(0, 5);  
133:   if (s == -359 || s == -299 || s == -239 || s == -59 || s == +1)  
134:    {lcd.setCursor(0, 2);lcd.print("  "); lcd.setCursor(0, 3);lcd.print("  ");}      // clear big digit  
135:   if (s == -360){lcd.setCursor(0, 0);lcd.write(4);}  // display first character at 6 minutes (1 flag down)  
136:   if (s == -300){lcd.setCursor(0, 0);lcd.write(5);}  // display second character at 5 minutes (2 flags up)  
137:   if (s == -240){lcd.setCursor(1, 0);lcd.write(6);}  // display third character (1 flag up)  
138:   if (s == -60){lcd.setCursor(2, 0);lcd.write(4);}   // display first character (1 flag down)  
139:   if (s ==  0){lcd.setCursor(3, 0);lcd.write(7);}    // display fourth character (2 flags down)   
140:  }  
141:  void A_then_B (int s){                 // two separate fleet starts  
142:   if (s < -660){lcd.setCursor(0, 0);lcd.write(6);}   // postponement flag up  
143:   if (s == -665 || s == -605 || s == -545 || s == -365 || s == -305) showDigit(5, 5); // big number countdown  
144:   if (s == -664 || s == -604 || s == -544 || s == -364 || s == -304) showDigit(4, 5);  
145:   if (s == -663 || s == -603 || s == -543 || s == -363 || s == -303) showDigit(3, 5);  
146:   if (s == -662 || s == -602 || s == -542 || s == -362 || s == -302) showDigit(2, 5);  
147:   if (s == -661 || s == -601 || s == -541 || s == -361 || s == -301) showDigit(1, 5);  
148:   if (s == -660 || s == -600 || s == -540 || s == -360 || s == 300) showDigit(0, 5);  
149:   if (s == -659 || s == -599 || s == -539 || s == -359 || s == +301)  
150:    {lcd.setCursor(0, 2);lcd.print("  "); lcd.setCursor(0, 3);lcd.print("  ");}       // clear big digit  
151:   if (s == -365 || s == -305 || s == -245 || s == -65 || s == -5)  showDigit(5, 5);  // big number countdown  
152:   if (s == -364 || s == -304 || s == -244 || s == -64 || s == -4)  showDigit(4, 5);  
153:   if (s == -363 || s == -303 || s == -243 || s == -63 || s == -3)  showDigit(3, 5);  
154:   if (s == -362 || s == -302 || s == -242 || s == -62 || s == -2)  showDigit(2, 5);  
155:   if (s == -361 || s == -301 || s == -241 || s == -61 || s == -1)  showDigit(1, 5);  
156:   if (s == -360 || s == -300 || s == -240 || s == -60 || s == 0)   showDigit(0, 5);  
157:   if (s == -359 || s == -299 || s == -239 || s == -59 || s == +1)  
158:    {lcd.setCursor(0, 2);lcd.print("  "); lcd.setCursor(0, 3);lcd.print("  ");}  
159:   if (s == -660){lcd.setCursor(0, 0);lcd.write(4);}  // display first character at 11 minutes (1 flag down)  
160:   if (s == -600){lcd.setCursor(0, 0);lcd.write(5);}  // display second character at 10 minutes (2 flags up)  
161:   if (s == -540){lcd.setCursor(1, 0);lcd.write(6);}  // display third character (1 flag up)  
162:   if (s == -360){lcd.setCursor(2, 0);lcd.write(4);}  // display first character (1 flag down)  
163:   if (s == -300){lcd.setCursor(3, 0);lcd.write(7);}  // display fourth character (A-Fleet flag down)   
164:   if (s == -300){lcd.setCursor(5, 0);lcd.write(5);}  // display second character (B-Fleet flag up)  
165:   if (s == -240){lcd.setCursor(6, 0);lcd.write(6);}  // display third character (1 flag up)  
166:   if (s == -60){lcd.setCursor(7, 0);lcd.write(4);}   // display first character (1 flag down)  
167:   if (s ==  0){lcd.setCursor(8, 0);lcd.write(7);}    // display fourth character (2 flags down)   
168:  }  
169:  void clockDisplay(int x, int y, int s){  
170:   // Display hours, mins, secs from time zero, at required position  
171:   int mins = s/60;  
172:   int hrs = mins/60;  
173:   int secs = s - (mins*60);  
174:   mins = mins - (hrs*60);  
175:   lcd.setCursor(x,y);  
176:   if (s > 0)  
177:    lcd.print("+");  
178:   else if (s < 0)  
179:    lcd.print ("-");  
180:   else  
181:    lcd.print (" ");                                  // .. THEY'RE OFF!!  
182:   lcd.setCursor(x+1, y);lcd.print(abs(hrs));lcd.print (":"); // trailing colon  
183:   lcd.setCursor(x+3, y);  
184:   if (abs(mins)<10)                                  // include a leading zero if less than 10  
185:    lcd.print("0" + String(abs(mins)));  
186:   else   
187:    lcd.print(abs(mins));  
188:   lcd.print (":");                                   // trailing colon  
189:   lcd.setCursor(x+6, y);  
190:   if (abs(secs)<10)                                  // include a leading zero if less than 10  
191:    lcd.print("0" + String(abs(secs)));  
192:   else   
193:    lcd.print(abs(secs));  
194:  }  
195:  void button1pressed(int s){                         // press button to capture finishing times  
196:   lcd.setCursor(0, i);  
197:   if ((i-1) < 10)  
198:    lcd.write(" ");                                   // tidy up spacing if number less than 10  
199:   lcd.print(i-1);  
200:   clockDisplay(3, i, s);  
201:   finish_time[i-2] = s;                              // put first, 2nd, etc finish times into the array  
202:   if (i == 2)  
203:    sec00 = s;                                        // remember first boat's time  
204:   else if ((s-sec00) > (30*60))                      // compare this one with first boat's time  
205:    lcd.write(" >30 min!");                           // and give a warning if it's out of time  
206:   else  
207:    lcd.write("     ");                               // otherwise clear this part of the display  
208:  }  
209:  void button2pressed(int ii){                        // scroll back through finish times  
210:   if((ii-2)>1)  
211:   {lcd.setCursor(0, 3);  
212:    if ((ii-2) < 10)  
213:     lcd.write(" ");                                  // tidy up spacing if number less than 10  
214:    lcd.print(ii-2);  
215:    clockDisplay(3, 4, finish_time[ii-3]);  
216:    lcd.write(" *    ");}                             // mark with an asterisk to indicate an old finish time  
217:  }  
218:  void showDigit(int digit, int posn){                // display the big digits at desired position  
219:   lcd.setCursor(posn * (digitWidth + 1), 0);  
220:   for(int i=0; i < digitWidth; i++)  
221:    lcd.write(bigDigitsTop[digit][i]);                // top half of big digit  
222:   lcd.setCursor(posn * (digitWidth + 1), 1);  
223:   for(int i=0; i < digitWidth; i++)  
224:    lcd.write(bigDigitsBot[digit][i]);                // bottom half of big digit  
225:  }  

Working down through the above code, lines 37 to 41 define "glyphs" - a 4-element array of 8 bytes each, representing patterns which will later be used for the large characters.  For example, glyph 0 (the first one below) is represented by bytes representing the pixel rows
0b11111, 0b11111, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000  :

and here is how the glyphs are combined to make the large characters:
The above ideas came from "Arduino Cookbook" 2nd Ed by Michael Margolis.

I only use numerals "0" to "5" as the 5-second warning before critical times, but "6" to "9" can also be formed from the glyphs.

You can see that the large characters now occupy 2 rows and 3 columns of the LCD array.  The lines 44 to 53 actually form the large characters ("0" through "9") from the glyphs.  The null glyph seen in numerals "1" (top right) and "4" (bottom left and centre) is represented by the ASCII code "32" which is the space, or the null or blank character.

I have added some functions, 
void showDigit() to display the big digits, 
void twoFleetsTogether() to handle the 1-fleet sequence,
void A_then_B() to handle the 2-fleet sequence,
void clockDisplay() to display the running time,
void button1pressed() to handle the routine when the first button is pressed, and 
void button2pressed() to handle the routine when the middle button is pressed.

Reading the 3rd button (the red one on the right) is handled directly in the main void loop() function.

Although I suggested in the last post that the accuracy of the timer is acceptable (a few seconds per hour), this in fact wouldn't do because obviously yacht skippers would be watching their own times and checking that the recorded time was right, so I aimed for an accuracy within one second per race.  Races normally wouldn't last more than about an hour, but could conceivably be longer than that, so I replaced line 102 with line 101 and tinkered with the divisor to hone in to an acceptable reading after some hours.  

With the value used above in the code, I am currently getting an accuracy of within one second (according to my sailing watch) after eight and a half hours and it's still running as I type.  The accuracy of course is only as good as the ability of the human operator to press the correct button at the correct time, but I did the calibration for as long a running time as practicable, to minimise the effect of the operator's reaction time.

When my RTC is delivered and I get it incorporated, I think that will finalise the design of the Yacht Race Timer (famous last words!).  All I will need then is a container and a means of powering the beast.  If there are any 3-D printing enthusiasts out there, I would be grateful for some advice on this.

Now - sit back and enjoy the following videos (speeded up by a factor of three to hold your attention!):



The videos start with me pressing the "Reset" button on the Arduino:

In the first one, the red button is NOT pressed, so after a few seconds, the count-down time automatically defaults to 6 minutes.  This tells the system to do a single-fleet sequence and display the comment "A plus B". 

In the second video, the red button is used to set a count-down time of 12 minutes.  This tells the system to start a two-fleet sequence, with the comment "A then B".


Note that the flag symbols for each fleet (the entire count-down sequence) are displayed at the top left of the LCD screen.

If the set count-down time is more than 11 minutes, the 2-fleet sequence will run. 

Otherwise, the 1-fleet sequence will be executed.

Neither of these videos runs long enough to show the comment ">30 min!" meaning that any finishing times subsequent to the first one, and more than 30 minutes after the first one, are flagged.  This is because some sailing instructions may state that any boats finishing later than this, may be discounted. 

Here is the message:

Note that the finish time of more than 8 hours is way beyond what would be encountered in club racing.  Even at this time, the difference between my sailing watch reading and the YRT time is within one second.  Note that when the time reaches +09:06:07, it goes negative and starts to count down.  This apparently non-sensical behaviour is the result of the positive range of an integer being 32767.  32767 seconds is 9 h 6 m 7 s.  

I tried to address this by using a long data type for sec0 in line 101, but I need to do this more consistently throughout the program - for example, at line 103, the variable secs is cast as an integer again.  I'm just going to live with this because 9 hours is way beyond the range that would be measured in reality, and as this is close to the point where the time would be one second out, this suits me fine.



Wednesday, October 16, 2013

37. A Yacht Race Timer in the Making

4000 page views!!
Yacht races generally use flag signals and have a set routine for the starting sequence:
  • Postponement signal - only used if the start is running late.  The postponement flag, if used, is removed at T0 minus 6 minutes
  • Class warning flag displayed - at T minus 5 minutes. Often 2 classes start together, so two flags are raised
  • Preparatory signal flag displayed - at T minus 4 minutes
  • Preparatory signal removed - at Tminus 1 minute
  • Start - class flag is removed at time T0
  • Change seamlessly from count-down mode to count-up mode

It is also necessary to record the finishing time of each yacht.  The times required are times relative to time T0, but it is also necessary for the Race Officer to know the time of day (for obvious reasons).  There are of course, lots and lots of other signals which can be involved, but the above sequence alone would be great.

Many attempts have been made by others to automate such a system, and if the resulting devices are easy to use, and reliable, they would be a great help to Race Officers.  However, they can be expensive, and races tend to be run using wrist-watches, and writing down the finish times (usually on soggy paper).  

Unless there are two or three people involved, it's impossible to raise and lower flags to the nearest second, sound a horn and record boat identities and finishing times, all at the same time.  A smart octopus would be useful.

In the absence of an intelligent octopus, any automated gadget needs to be easy to use, and cheap (don't forget - it could conceivably be dropped overboard!).  Remember all this is happening on board the Committee Boat (a motor boat which has a 12 V DC supply available).

So here's yet another attempt to make a gadget for this task - while trying to keep it simple - the YRT (yacht race timer).  The Arduino can be a good timing device - there are no other programs running to cause interrupt problems - and it lends itself to driving other devices including lights, relays, horns etc.  I did a quick check on the Arduino's timing accuracy, and found it to be 0.13 per cent fast.  This is acceptable for the purpose of recording times of up to a couple of hours (just a few seconds out), as the yacht racing times are all relative to one another.

I got myself an LCD screen (£5.89, delivered, from Amazon), which is the 20 column x 4 row version of the Hitachi HD44780 compatible system, and which I thought would be more useful for this task than the 16 column x 2 row version.  The characters are represented by 5 pixel x 8 pixel images.  There are different sizes of these displays available.

Here's the circuit (this one shows the 16 x 2 character display, but the connections to the 20 x 4 display are the same).  Note that not all 16 pins of the LCD display are required:
Arduino Pin 6 is pulled down to GND with a 10 kΩ resistor, and sent "High" when the breadboard button is pressed.  The 10 kΩ trimmer is used to adjust the brightness of the LCD screen's backlight, which requires its own power supply.

Here's a video of my work in progress, with ideas as usual pinched from lots of other sources which I have tried to reference here.  At the beginning of the video I use the Arduino's reset button to start the procedure from the beginning, and later the time-capturing button on the breadboard.  Please note that the timing sequence has been speeded up by a factor of 100 to prevent boredom setting in, so you have to concentrate to see what's going on:



In line 69 of the code below, the number of count-down (negative value) seconds is set, and if that time exceeds 6 minutes, the first signal it gives is the 6 minute, followed by the 5 minute, 4 minute, one minute and then the T0 signal. 

I thought it would be nice (ie useful) to have a visual indication of the state of the major flags, so I made some custom characters to represent:
1 flag down:2 flags up:1 flag up:and 2 flags down:.

These characters appear on the top left of the LCD display at the appropriate time.  

Making custom characters was enabled using the extremely useful page at http://omerk.github.io/lcdchargen/ which generates the code for any symbols you can dream up within 5 x 8 pixels.  Buzzers or relays, LEDs and lights can easily be added, but haven't yet been included in this version.

The time in the video is running so fast, 100 times normal,  that it's not easy to see what's going on, but the postponement flag coming down is the first.  If there is no postponement, then the first signal would be the 5-minute one.  

The symbols eventually line up as  but without the first one, as the postponement flag down symbol is intentionally written over by the next symbol (2 class flags up).  This allows the Race Officer to visually check that the critical flags he has been flying, are down after the start of the race.

The main timer on the right of the LCD screen shows hours, minutes and seconds from T0.  This is given as a negative time if it's before T0.   As the time passes through the critical 6, 5, 4, 1 and 0 minutes, the above characters are displayed to remind the Race Officer that he should have already carried out those operations.  These symbols could of course be displayed in advance with an audible warning to give him prior notice to raise or lower the flags.

Then comes the push button (on the breadboard).  When the button is pressed, as the first boat crosses the start/finish line, the time (from T0) at which it was pressed is displayed on the 3rd row (row 2) starting at position zero (first column).  This indicates the boat's finish time.  The next finish time is recorded on the fourth row, below.  Subsequent presses of the button over-write all previous ones on the fourth row, leaving the first finishing time on the 3rd row still visible.  

This gives the Race Officer time to write down the times (or they could easily be stored in an array in the Atmega 328 chip's memory).  The first finishing time is a useful one to have continuously displayed, as other rules of sailing come into play if any of the other boats finish out of time (eg more than 30 minutes after the first finisher).  The Racing Rules of Sailing are more complicated than Arduino programming!

Here is the code as it currently stands:
1:  /*  
2:   LiquidCrystal Arduino Library - Timing  
3:     
4:   Demonstrates the use a 20 column x 4 row LCD display. The LiquidCrystal  
5:   library works with all LCD displays that are compatible with the   
6:   Hitachi HD44780 driver.   
7:     
8:   This sketch prints count-down / count-up times to the LCD - and more!  
9:     
10:   The circuit:  
11:   * LCD RS pin to digital pin 12  
12:   * LCD Enable pin to digital pin 11  
13:   * LCD D4 pin to digital pin 5  
14:   * LCD D5 pin to digital pin 4  
15:   * LCD D6 pin to digital pin 3  
16:   * LCD D7 pin to digital pin 2  
17:   * LCD R/W pin to ground  
18:   * Variable (eg 10K) resistor:  
19:   * ends to +5V and ground  
20:   * wiper to LCD VO pin (pin 3)  
21:     
22:   Library originally added 18 Apr 2008 by David A. Mellis  
23:   library modified 5 Jul 2009 by Limor Fried (http://www.ladyada.net)  
24:   example added 9 Jul 2009 by Tom Igoe  
25:   modified 22 Nov 2010 by Tom Igoe  
26:   modified October 2013 by KC  
27:    
28:   http://www.arduino.cc/en/Tutorial/LiquidCrystal  
29:  */  
30:  // include the library code:  
31:  #include <LiquidCrystal.h>  
32:    
33:  // initialize the library with the numbers of the interface pins  
34:  LiquidCrystal lcd(12, 11, 5, 4, 3, 2);  
35:    
36:  // array of bits defining pixels for 8 custom characters  
37:  // pixel=1 : on and pixel=0 : off  
38:  // see Custom Character Generator at http://omerk.github.io/lcdchargen/  
39:   byte oneflagdown[8] =   
40:    {0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01100, 0b01100};  // 1 flag down  
41:   byte twoflagsup[8] =    
42:    {0b11011, 0b11011, 0b01010, 0b01010, 0b01010, 0b01010, 0b01010, 0b01010};  // 2 flags up  
43:   byte oneflagup[8] =   
44:    {0b01100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100};  // 1 flag up  
45:   byte twoflagsdown[8] =   
46:    {0b01010, 0b01010, 0b01010, 0b01010, 0b01010, 0b01010, 0b11011, 0b11011};  // 2 flags down  
47:  // also see Arduino Cookbook 2nd ed by Michael Margolis  
48:   int inPin = 6;  
49:   int outPin = 13;   // the number of the output pin (LED)  
50:   int reading;  
51:   long debounce = 200; // debounce delay in milliseconds  
52:   int i = 2;      // for lots more on button bounce, see http://arduino.cc/en/Tutorial/Debounce  
53:      
54:  void setup() {  
55:   // set up the LCD's number of columns and rows:   
56:   lcd.begin(20, 4);         // 20 columns & 4 rows  
57:   lcd.createChar(1, oneflagdown);  // create first custom character  
58:   lcd.createChar(2, twoflagsup);  // create second custom character  
59:   lcd.createChar(3, oneflagup);   // create third custom character  
60:   lcd.createChar(4, twoflagsdown); // create fourth custom character  
61:   lcd.clear();   
62:   // Print the header on the LCD  
63:   lcd.print("   YRT   h: m: s");// for "Yacht Race Timer"  
64:   pinMode(inPin, INPUT);  
65:  }  
66:    
67:  void loop() {  
68:   int seconds;  
69:   int time = -405; // countdown seconds before time zero  
70:   // print the time since reset (at 'int time' seconds):  
71:   int sec0 = millis()/1000;  
72:   int secs = sec0 + time;  
73:   lcd.setCursor(0, 0);   
74:   if (secs == -360)   
75:    lcd.write(1);      // display first character at 6 minutes (1 flag down)  
76:   if (secs == -300)  
77:    lcd.write(2);      // display second character at 5 minutes (2 flags up)  
78:    lcd.setCursor(1, 0);     
79:   if (secs == -240)  
80:    lcd.write(3);      // display third character (1 flag up)  
81:    lcd.setCursor(2, 0);    
82:   if (secs == -60)   
83:    lcd.write(1);      // display first character (1 flag down)  
84:    lcd.setCursor(3, 0);     
85:   if (secs == 0)   
86:    lcd.write(4);      // display fourth character (2 flags down)  
87:   clockDisplay(12, 1, secs);// display the running time from time zero  
88:    
89:   reading = digitalRead(inPin);  
90:   if (reading == HIGH)   // if the button is pressed  
91:    {  
92:    delay(debounce);    // in case the button bounces  
93:    clockDisplay(0, i, secs);  
94:    i++;          // move on to next line for next time  
95:    }  
96:  }  
97:    
98:  void clockDisplay(int x, int y, int s){  
99:   // display hours, mins, secs from time zero, at required position  
100:   // x can be 0 to 12, (positions across LCD screen), y can be 0 to 3 (lines down screen)  
101:   int mins = s/60;  
102:   int hrs = mins/60;  
103:   int secs = s - (mins*60);  
104:   mins = mins - (hrs*60);  
105:   lcd.setCursor(x,y);  
106:   if (s > 0)  
107:    lcd.print("+");  
108:   else if (s < 0)  
109:    lcd.print ("-");  
110:   else  
111:    lcd.print (" "); // could sound a buzzer or flash a light .. THEY'RE OFF!!  
112:   lcd.setCursor(x+1, y);  
113:   lcd.print(abs(hrs));  
114:   lcd.print (":");  // trailing colon  
115:   lcd.setCursor(x+1, y);  
116:   lcd.print(abs(hrs));  
117:   lcd.print (":");  // trailing colon  
118:   lcd.setCursor(x+3, y);  
119:   if (abs(mins)<10) // include a leading zero  
120:    lcd.print("0" + String(abs(mins)));  
121:   else   
122:    lcd.print(abs(mins));  
123:   lcd.print (":");  // trailing colon  
124:   lcd.setCursor(x+6, y);  
125:   if (abs(secs)<10) // include a leading zero  
126:    lcd.print("0" + String(abs(secs)));  
127:   else   
128:    lcd.print(abs(secs));  
129:  }  
130:    

I have mentioned some suggestions above which I will probably continue to develop.  

Things to further think about include:
  • the power supply - direct from the Committee Boat's 12V supply, or connected in parallel with a set of re-chargeables which can re-charge while powering the YRT.  The recommended input voltage to the Arduino is 7 V - 12 V, with input voltage limits of 6 V to 20 V, so to be safe, voltage regulation would be advisable.
  • encasing the unit if it ever reaches fruition (including waterproofing).
  • further button control to enter the count-down time, re-display (and even download) the array of finishing times.
  • add a real-time clock (RTC) with its own coin battery, so that the device can actually display the correct time of day, even if it has been switched off and on again since the last time it was used.  The RTC may provide a more accurate source of time (a possible factor of 10 in the accuracy) than the Arduino's millis() function (line 71 of the code, above).
The temptation at this stage is to start incorporating lots of bells and whistles which could, if not curtailed, rather over-egg the pudding!!