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.



No comments:

Post a Comment