-
Notifications
You must be signed in to change notification settings - Fork 1
/
RaceTimerAndController.ino
3039 lines (2780 loc) · 117 KB
/
RaceTimerAndController.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Race Timer and Controller
// Version 2.x
// include enums from list file
// enums are kept in a seperate file to force them to compile early so they can be used in functions
#include "enum_lists.h"
// Enable if using RTTL type song/melody data for playing sounds
//-------------------
// library for playing RTTTL song types
#include <PlayRtttl.h>
// file of RTTTL song definition strings.
// Because these strings are stored in PROGMEM we must also include 'avr/pgmspace.h' to access them.
#include "RTTTL_songs.h"
//-------------------
// Enable if using Note-Lengths Array method of playing Arduino sounds
// //-------------------
// // Defines the note constants that make up a melodie's Notes[] array.
// #include <pitches.h>
// // File of songs/melodies defined using Note & Lengths array.
// // Because these arrays are stored in PROGMEM we must also include 'avr/pgmspace.h' to access them.
// #include "melodies_prog.h"
// //-------------------
// Library to support storing/accessing constant variables in PROGMEM
#include <avr/pgmspace.h>
// The 'Wire' library is for I2C, and is included in the Arduino installation.
// Specific implementation is determined by the board selected in Arduino IDE.
#include <Wire.h>
// LCD driver libraries
#include <hd44780.h> // main hd44780 header file
#include <hd44780ioClass/hd44780_I2Cexp.h> // i/o class header for i2c expander backpack
// Custom character layout Byte's for LCD
#include "CustomChars.h"
// library for 7-seg LED Bars
#include <LedControl.h>
// Library to support 4 x 4 keypad
#include <Keypad.h>
// load default and local settings that define menu text and default race controller attributes.
#include "defaultSettings.h"
// --------- I2C ADAFRUIT LED BARGRAPH GLOBAL CODE --------------------------
// Adafruit Bar LED libraries
// code taken from built in Adafruit LEDBackpack library 'bargraph24)
#include <Adafruit_GFX.h>
#include "Adafruit_LEDBackpack.h"
// declaring object representing Adafruit LED bar, called 'bar'.
Adafruit_24bargraph bar = Adafruit_24bargraph();
// create a global variable to track the 'on' state of LED bar
// We'll use this flage to make if faster to check status during a race
bool clearStartLight = false;
// This function sets specified block of LEDs on bargraph to the input color.
// 'color' can be 'LED_RED', 'LED_YELLOW', 'LED_GREEN', or 'LED_OFF'
// 'start' is the index reference of 1st LED to change, index 0 is LED 1
// 'end' is index of last LED to change, index 23 is LED 24
// by default, leaving 'start' and 'end' out of function call, will set entire bar
void setBargraph(byte color, byte end = 23, byte start = 0) {
// cyle through each LED to change, set new color, and update display
for (uint8_t i=start; i<=end; i++) {
bar.setBar(i, color);
bar.writeDisplay();
}
}
// ------------- END OF BARGRAPH GLOBALS ------------------------
// The # of physical lanes that will have a sensor and lap timer/counter display.
const byte laneCount = LANE_COUNT;
// ********* AUDIO HARDWARE *********
const byte buzzPin1 = BUZZPIN;
// indicator LED, disable buzzer to use
// const byte ledPIN = 13;
// int ledState = HIGH;
//***** Setting Up Lap Triggers and Pause-Stop button *********
// debounceTime (ms), time within which not to accept additional signal input
const int debounceTime = DEBOUNCE;
// LANES DEFININTION
// The following array constant, lanes[], defines the hardware/software
// relationship between the physical arduino lane gates, their interrupts,
// and the associated 'Racer #'.
// The configuration to follow, below, is for the default lane wiring;
// Where, pinA0 is wired to lane1, pinA1-lane2, pinA2-lane3, & pinA3-lane4
// The first term of each row pair, making up lanes[],
// is the hardware pin used by the associated racer/lane# index.
// ex: lanes[1][0] = PIN_A0;
// tells controller that PIN_A0 is wired to lane used by racer #1
// The second term of each row pair, making up lanes[],
// is a byte mask, that indicates the bit, on the PCINT1_vect byte,
// that represents an interrupt trigger for that pin.
// ex: lanes[1][1] = 0b00000001;
// tells controller that 1st bit of interrupt byte (PCINT1_vec) represents PIN_A0
// Each given pin# and associated byte mask value, must stay together.
// However, pin-mask pairs can be assigned to any racer/lane# index,
// according to the physical wiring.
// The zero row, lanes[0] = {255, 255} is reserved, but not currently used.
// Otherwise, the settings for racerX are held in the array at index lanes[X]
// ---- Default lane configuration
// ------------------------------------
// The following is the default lane wiring, pinA0-lane1, pinA1-lane2, pinA2-lane3, pinA3-lane4
// const byte lanes[5][2] = {
// {255, 255},
// {PIN_A0, 0b00000001},
// {PIN_A1, 0b00000010},
// {PIN_A2, 0b00000100},
// {PIN_A3, 0b00001000}
// };
// See the 'defaultSettings.h' file or 'localSettings.h' file,
// for the actual pin-mask byte values of the 'LANE#' tokens below.
// The default values should match those commented out above.
// Lanes greater than 'laneCount' will go unused, but 'lanes[]' is sized for system's max of 4.
const byte lanes[5][2] = {
{255, 255},
LANE1,
LANE2,
LANE3,
LANE4
};
// ---- Alternate Configurations
// -----------------------------
// The following example could be used for alternative wiring,
// where pinA3 is connected to lane1, pinA0-lane2, pinA1-lane3, and pinA2-lane4
// const byte lanes[5][2] = {
// {255, 255},
// {PIN_A3, 0b00001000},
// {PIN_A0, 0b00000001},
// {PIN_A1, 0b00000010},
// {PIN_A2, 0b00000100}
// };
// ----------------------------
// This is the deafult configuration If using Arduino Mega2560
// Use Port K, analog pins A8-A11
// const byte lanes[5][2] = {
// {255, 255},
// {PIN_A8, 0b00000001},
// {PIN_A9, 0b00000010},
// {PIN_A10, 0b00000100},
// {PIN_A11, 0b00001000}
// };
// -------------------------------
// A6 and A7 are analog input only pins and thus, must be read as analog.
// An external pullup resistor must be added to button wiring.
// This input is expected to be HIGH and go LOW when pressed.
const byte pauseStopPin = PAUSEPIN;
const byte startButtonPin = STARTPIN;
// timestamp marking new press of pause button, used to set start of debounce period.
unsigned long buttonDebounceMillis = 0;
//***** Variables for LCD 4x20 Display **********
// This display communicates using I2C via the SCL and SDA pins,
// which are dedicated by the hardware and cannot be changed by software.
// If using Arduino Nano, pin A4 is used for SDA, pin A5 is used for SCL.
// If using Arduino Mega2560, pin D20 can be used for SDA, & pin D21 for SCL.
// Make sure the LCD is wired accordingly.
// Declare 'lcd' object representing display using class 'hd44780_I2Cexp'
// because we are using the i2c i/o expander backpack (PCF8574 or MCP23008)
hd44780_I2Cexp lcd;
// When more than 2 MAX7219s are chained, additional LED bars
// may need direct power supply to avoid intermittent error.
// # of attached max7219 controlled LED bars
const byte LED_BAR_COUNT = LANE_COUNT + 1;
// const byte LED_BAR_COUNT = 4;
// # of digits on each LED bar
const byte LED_DIGITS = 8;
// LedControl parameters (DataIn, CLK, CS/LOAD, Number of Max chips (ie 8-digit bars))
LedControl lc = LedControl(PIN_TO_LED_DIN, PIN_TO_LED_CLK, PIN_TO_LED_CS, LED_BAR_COUNT);
//***** KeyPad Variables *****
// set keypad size
const byte KP_COLS = 4;
const byte KP_ROWS = 4;
// Layout KeyMap
char keys[KP_ROWS][KP_COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
// Establish the row pinouts, {Row1,Row2,Row3,Row4} => Arduino pins 5,6,7,8
byte pin_rows[KP_ROWS] = {5,6,7,8};
// Establish the column pinouts, {Col1,Col2,Col3,Col4} => Arduino pins 9,10,11,12
byte pin_column[KP_COLS] = {9,10,11,12};
// Creating keypad object
Keypad keypad = Keypad( makeKeymap(keys), pin_rows, pin_column, KP_ROWS, KP_COLS );
//*** RACE PROPERTIESS ******
races raceType = Standard;
// variable to hold the 'race to' x lap total setting
int raceLaps = DEFAULT_LAPS;
// variable indicating final lap of race, most often will equal 'raceLaps' except for drag race.
int endLap = DEFAULT_LAPS;
// Race time for a Timed race type, most laps before time runs out wins.
// Create a clock time array to hold converted values for easy display update.
// raceSetTime[1] holds Min, raceSetTime[0] holds Sec, settable from menu.
// const byte raceSetMin = DEFAULT_SET_MIN;
// const byte raceSetSec = DEFAULT_SET_SEC;
// byte raceSetTime[2] = {raceSetSec, raceSetMin};
byte raceSetTime[2] = {DEFAULT_SET_SEC, DEFAULT_SET_MIN};
// raceSetTime in milliseconds, will be initialized in setup().
unsigned long raceSetTimeMs;
// pre-race countdown length in seconds, settable from menu
byte preStartCountDown = DEFAULT_COUNTDOWN;
byte preStartTimerDrag = 3; //default to 3 sec, but will be randomized when used.
// Flag to tell race timer if race time is going up or going down as in a time limit race.
// Since the default race type is standard the default countingDown is false.
bool countingDown = false;
// flag to discern a new start from a re-start from pause.
bool newRace = true;
// Used to hold the millis() timestamp of the start of current program loop.
// This time is used by all processes in the loop so that everything happens
// psuedo concurrently.
unsigned long curMillis;
// Interval in milliseconds that the clock displays are updated.
// this value does not affect lap time precision, it only sets min display update rate.
// This is done to be more efficient and give's us some control over refresh rate.
int displayTick = DEFAULT_REFRESH_TICKS;
// timing between start light updates during PreStart
int preStartTick = 1000;
int nextStageCountdownTime = 0;
// A millis() timestamp marking last tick completion.
unsigned long lastTickMillis;
// flag indicating if race state is in preStart countdown or active race
// volatile bool preStart = false;
// ***** RACE DATA *********
// Running count of the current lap a racer is on.
// Note that this is 1 greater than the number of completed laps.
// idx of lapCount relates to corresponding lane#, idx = 0 is reserved.
volatile int lapCount[laneCount + 1] = {};
// array to hold the running total time (in ms) of each racer to complete their current lapCount
unsigned long racersTotalTime[ laneCount + 1 ] = {};
// For each lane/racer a list of the fastest X laps will be recorded.
// This will be stored in 2, 2D arrays.
// One array to hold the time (long), and one to hold the corresponding lap# (int).
// The row index indicates the associated lane/racer #.
// Row idx = 0 is used to hold the Top Overall Laps.
// const byte fastestQSize = DEFAULT_MAX_STORED_LAPS;
unsigned int fastestLaps[laneCount + 1][DEFAULT_MAX_STORED_LAPS] = {};
unsigned long fastestTimes[laneCount + 1][DEFAULT_MAX_STORED_LAPS] = {};
// Another pair of lap and time arrays holds the fastest overall laps
// unsigned int topFastestLaps[ DEFAULT_MAX_STORED_LAPS ] = {};
// unsigned long topFastestTimes[ DEFAULT_MAX_STORED_LAPS ] = {};
// With the overall lap times we need to track the associated lane/racer in a seperate array.
byte topFastestRacers[ DEFAULT_MAX_STORED_LAPS ] = {};
// During the actual race, due to memory limits, we only track the
// last few completed lap millis() timestamps.
// These timestamps are used to calculate lap times, which are stored in another array,
// so these timestamps can be discarded once a lap time is logged.
// The modulus of the lap count, by the lapMillisQSize, will set the looping index.
// (lapCount % lapMillisQSize) = idx, will always be 0 <= idx < lapMillisQSize
// DO NOT SET lapMillisQSize < 3
const byte lapMillisQSize = 5;
// The row index indicates the lane/racer associated with it's row array timestamps.
volatile unsigned long lastXMillis[laneCount + 1] [ lapMillisQSize ] = {};
// To keep a running time of the race and each current lap,
// we record a start, millis() timestamp and log elapsed time in ms.
// idx0 used to log overall race time.
// idx > 0, log the current lap time of corresponding lane #.
volatile unsigned long startMillis[laneCount + 1];
volatile unsigned long currentTime[laneCount + 1] = {};
// Flag to alert results menu whether race data has been collected.
// If it tries to print empty tables it will print garbage to the screen.
bool raceDataExists = false;
// Variable to record state of lane seneor pins.
volatile byte triggeredPins = 0;
// ******* LANE/RACER VARIABLES ******************
// In all arrays relating to this data the array index will equal the associate lane/racer#.
// Index 0 will be reserved for race level times and data or may not be used at present.
// Logs current lane state (see enum for details)
volatile byte laneEnableStatus[ laneCount + 1 ] = {};
// These help keep code easier to read instead of calculating them repeatedly from status array.
byte enabledLaneCount = 0;
byte finishedCount = 0;
// Holds index of Racers[] array that indicates the name of racer associated with lane.
// set racer name to '0' when status is Off.
// Other than 0, the same Racer[idx] cannot be used for more than 1 racer.
// During use, the code will prevent this, but it will not correct duplicates made here.
byte laneRacer[ laneCount + 1 ] = {};
// After a racer finishes a lap the logged time will be 'flashed' up
// to that racer's LED. The period that this lap time stays displayed
// before the display returns to logging current lap time is the 'flash' period.
// The lane Lap Flash variables indicate the state of this flash period.
// 0 = update display with racer's current lap and running time.
// 1 = process data for last finished lap, and write result to racer's LED.
// 2 = hold the last finished lap info on display until flash time is up.
// lap flash status idx corresponds to status of lane #, idx0 reserved.
// The flash display period does not affect active timing, or any other functions.
volatile byte flashStatus[ laneCount + 1 ] = {};
const int flashDisplayTime = DEFAULT_FLASH_PERIOD_LENGTH;
// logs millis() timestamp at start of current flash period for each racer.
unsigned long flashStartMillis[ laneCount + 1 ];
// Index of lap time to display in first row of results window.
// Keep this an int so it can be signed. A negative is the trigger to loop index
int resultsRowIdx = 0;
// Tracks which racer's results to show in Results Menu, 0 = show top results overall.
byte resultsMenuIdx = 0;
// Variable to hold the last digit displayed to LEDs during last 3 sec
// of the pre-start countdown, so it does not rewrite every program loop.
byte ledCountdownTemp = 0;
// flag to trigger when 1st racer crosses finish.
bool winner = false;
// Create variables to hold current game state and menu state.
volatile states state;
volatile states prevState;
Menus currentMenu;
// This flag is used to indicate first entry into a state or menu so
// that one time only setup can be performed.
volatile bool entryFlag;
void ChangeStateTo(volatile states newState){
prevState = state;
state = newState;
entryFlag = true;
}
// // enum to use names with context instead of raw numbers when coding audio state
// enum audioModes {
// AllOn,
// GameOnly,
// Mute
// };
// Setup default settings and flags
// audioModes audioState = AllOn;
audioModes audioState = DEFAULT_AUDIO_MODE;
bool gameAudioOn = true;
bool musicAudioOn = true;
// Racer Names list.
// Because this is an array of different length character arrays
// we use an array of pointers.
// The size of 'Racers[]' and 'victorySongs[]' should match if SONGS_BY_PLACE is 'false'.
// byte const racerListSize = RACER_LIST_SIZE;
// Keep in mind that the 7-seg racer lap displays, cannot write W's, M's, X's, K's, or V's
// const char* Racers[racerListSize] = RACER_NAMES_LIST;
const char* Racers[] = RACER_NAMES_LIST;
// Racer's victory song, matched by index of racer.
// const char* victorySong[racerListSize] = RACER_SONGS_LIST;
const char* victorySong[] = RACER_SONGS_LIST;
// sets screen cursor position for the names on the racer select menu
byte nameEndPos = 19;
// *** STRING PROGMEM *************
// in this section we define our menu string constants to use program memory
// this frees up significant RAM. In this case, using progmem to replace
// these few const char* arrays, reduced RAM used by globals, by 10%.
// The character buffer needs to be as large as largest string to be used.
// Since our longest row of characters is on the LCD we use its column count.
char buffer[LCD_COLS];
// const char Main0[] PROGMEM = "A| Select Racers";
// const char Main1[] PROGMEM = "B| Change Settings";
// const char Main2[] PROGMEM = "C| Start a Race";
// const char Main3[] PROGMEM = "D| See Results";
const char Main0[] PROGMEM = A_SELECT_RACER;
const char Main1[] PROGMEM = B_CHANGE_SETTINGS;
const char Main2[] PROGMEM = C_START_RACE;
const char Main3[] PROGMEM = D_SEE_RESULTS;
const char* const MainText[4] PROGMEM = {
Main0,
Main1,
Main2,
Main3
};
// const char* MainText[4] = {
// "A| Select Racers",
// "B| Change Settings",
// "C| Select a Race",
// "D| See Results"
// };
// const char Settings0[] PROGMEM = "Settings mm:ss";
// const char Settings0[] PROGMEM = " A |Audio";
// const char Settings1[] PROGMEM = " B |Time :";
// const char Settings2[] PROGMEM = " C |Laps";
// const char Settings3[] PROGMEM = "0-4|Lanes";
const char Settings0[] PROGMEM = A_SETTING_AUDIO;
const char Settings1[] PROGMEM = B_SETTING_TIME;
const char Settings2[] PROGMEM = C_SETTING_LAPS;
const char Settings3[] PROGMEM = D_SETTING_LANES;
const char* const SettingsText[4] PROGMEM = {
Settings0,
Settings1,
Settings2,
Settings3
};
// Main Menu's sub-Menus
// const char* SettingsText[4] = {
// " A|Lap B|Min C|Sec",
// " D|Lanes:",
// "Num Laps:",
// "Racetime: :"
// };
// const char SelectRacers0[] PROGMEM = "A|Racer1";
// const char SelectRacers1[] PROGMEM = "B|Racer2";
// const char SelectRacers2[] PROGMEM = "C|Racer3";
// const char SelectRacers3[] PROGMEM = "D|Racer4";
const char SelectRacers0[] PROGMEM = A_RACER1;
const char SelectRacers1[] PROGMEM = B_RACER2;
const char SelectRacers2[] PROGMEM = C_RACER3;
const char SelectRacers3[] PROGMEM = D_RACER4;
const char* const SelectRacersText[4] PROGMEM = {
SelectRacers0,
SelectRacers1,
SelectRacers2,
SelectRacers3
};
// const char* SelectRacersText[4] = {
// "<--A B-->",
// "Racer1:",
// "<--C D-->",
// "Racer2:"
// };
// const char SelectRace0[] PROGMEM = "A|First to Laps";
// const char SelectRace1[] PROGMEM = "B|Most Laps in :";
// const char SelectRace2[] PROGMEM = "";
// const char SelectRace3[] PROGMEM = "D|Countdown: Sec";
const char SelectRace0[] PROGMEM = A_START_RACE_STANDARD;
const char SelectRace1[] PROGMEM = B_START_RACE_IMED;
const char SelectRace2[] PROGMEM = START_RACE_3RD_ROW;
const char SelectRace3[] PROGMEM = D_START_RACE_COUNTDOWN;
const char* const StartRaceText[4] PROGMEM = {
SelectRace0,
SelectRace1,
SelectRace2,
SelectRace3
};
// const char* StartRaceText[4] = {
// "A|First to Laps",
// "B|Most Laps in :",
// "",
// "D|Countdown: Sec"
// };
// flag to identify state of UI text that has an A and B part flashed intermittently.
bool titleA = true;
// additional text strings used in multiple places
const char* Start = {TEXT_START};
// const char DidNotFinish[] PROGMEM = "DNF";
// const char FirstPlace[] PROGMEM = "1st";
// const char SecondPlace[] PROGMEM = "2nd";
// const char ThirdPlace[] PROGMEM = "3rd";
// const char FourthPlace[] PROGMEM = "4th";
const char DidNotFinish[] PROGMEM = FINISH_DNF;
const char FirstPlace[] PROGMEM = FINISH_1ST;
const char SecondPlace[] PROGMEM = FINISH_2ND;
const char ThirdPlace[] PROGMEM = FINISH_3RD;
const char FourthPlace[] PROGMEM = FINISH_4TH;
const char* const FinishPlaceText[5] PROGMEM = {
DidNotFinish,
FirstPlace,
SecondPlace,
ThirdPlace,
FourthPlace
};
// Reviews lane status array and updates the settings menu.
// Run after a settings change.
void PrintLaneSettings(){
for(int i = 1; i <= laneCount; i++){
lcd.setCursor(10+ 2 * i, 3);
// Something about the lcd.print() function doesn't work to use a ternery to assess this.
if (laneEnableStatus[i] == 0){
// write a skull
lcd.write(3);
} else {
lcd.print(i);
}
}
}
// This function accepts in a millisecond value (msIN) and converts it into clock time.
// Because we have so many variables and want to save memory,
// we pass the clock time variables in by reference (using &).
// This means this function will be updating those variables from the higher scope directly.
void SplitTime(unsigned long msIN, unsigned long &ulHour, unsigned long &ulMin, unsigned long &ulSec, unsigned long &ulDec, unsigned long &ulCent, unsigned long &ulMill) {
// Calculate HH:MM:SS from millisecond count
// HH:MM:SS.000 --> ulHour:ulMin:ulSec.(0 = ulDec, 00 = ulCent, 000 = ulMill)
ulMill = msIN % 1000;
ulCent = msIN % 1000 / 10;
ulDec = msIN % 1000 / 100;
// total number of seconds
ulSec = msIN / 1000;
// total minutes
ulMin = ulSec / 60;
// total hours
ulHour = ulMin / 60;
// subtract out the minutes used by whole hours to get actual minutes digits
ulMin -= ulHour * 60;
// subtract seconds used by hour and minutes to get actual seconds digits
ulSec = ulSec - ulMin * 60 - ulHour * 3600;
// Debug printout
// Serial.print("SplitTime ulSec: ");
// Serial.println(ulSec);
// Serial.println(msIN);
// Serial.println("ulMill");
// Serial.println(ulMill);
}
// Converts clock time into milliseconds
unsigned long ClockToMillis(const byte ulHour, const byte ulMin, const byte ulSec) {
// Sets the number of milliseconds from midnight to current time
// NOTE: we must us the 'UL' designation or otherwise cast to unsigned long.
// If we don't, then the multiplication will not be the exptected value.
// This is because the default type of the number is a signed int which
// in this case, results in 60 * 1000, which is out of range for int.
// Even though the variable it is going into is a long, the right side
// remains an int until after the evaluation so we would get an overflow.
return (ulHour * 60UL * 60UL * 1000UL) +
(ulMin * 60UL * 1000UL) +
(ulSec * 1000UL);
}
// tone(pin with buzzer, freq in Hz, duration in ms)
void Beep() {
if (gameAudioOn) tone(buzzPin1, BEEP_FREQ, BEEP_DUR);
}
void Boop() {
if (gameAudioOn) tone(buzzPin1, BOOP_FREQ, BOOP_DUR);
}
void Bleep() {
if (gameAudioOn) tone(buzzPin1, BLEEP_FREQ, BLEEP_DUR);
}
// Used to set fastest lap array to high numbers that will be replaced on comparison.
// A lap number of 0, and laptimes of 999999, marks these as dummy laps.
void InitializeRacerArrays(){
for (byte i = 0; i <= laneCount; i++) {
for (byte j = 0; j < DEFAULT_MAX_STORED_LAPS; j++) {
fastestLaps[i][j] = 0;
fastestTimes[i][j] = 999999;
}
for (byte j = 0; j < lapMillisQSize; j++) {
lastXMillis[i][j] = 0;
}
racersTotalTime[i] = 0;
}
// for (byte i = 0; i <= laneCount; i++) {
// for (byte j = 0; j < lapMillisQSize; j++) {
// lastXMillis[i][j] = 0;
// }
// }
}
// Used to initialize results, top fastest, lap array to high numbers.
// A lap number of 0, and racer id of 255, marks these as dummy laps.
void InitializeTopFastest(){
for (byte i = 0; i < DEFAULT_MAX_STORED_LAPS; i++) {
// topFastestLaps[i] = 0;
topFastestRacers[i] = 255;
// topFastestTimes[i] = 999999;
}
}
// Update menu on main LCD with static base text for given menu screen
void UpdateLCDMenu(const char *curMenu[]){
// For each string in the menu array, print it to the screen
// Clear screen first, in case new text doesn't cover all old text
lcd.clear();
// Code for using menus in form of global variable, const char* arrays.
// This is left as a comparison in syntax with the PROGMEM approach used
// for (int i=0; i<4; i++){
// lcd.setCursor(0,i);
// lcd.print(curMenu[i]);
// }
// Code for using menus built from strings saved in PROGMEM.
for (int i=0; i<4; i++){
strcpy_P(buffer, (char*)pgm_read_word(&(curMenu[i])));
lcd.setCursor(0,i);
lcd.print(buffer);
}
}
// This function provides cycling of racer name selection without,
// allowing two racer's to choose the same name.
// laneID is the laneNumber whose racer name should be indexed.
// The value in laneRacer[laneID] is the index of the racer name array.
byte IndexRacer(byte laneID) {
byte newRacerNameIndex = laneRacer[laneID];
bool notUnique = true;
// The modulus (%) of a numerator smaller than its denominator is equal to itself,
// while the modulus of an integer with itself is 0. Add 1 to restart at idx 1, not 0.
byte racerListSize = (sizeof(Racers)/sizeof(char*));
while(notUnique){
newRacerNameIndex = ((newRacerNameIndex + 1)%racerListSize == 0) ? 1 : (newRacerNameIndex + 1)%racerListSize;
notUnique = false;
for(byte i = 1; i <= laneCount; i++){
if((i != laneID) && (laneRacer[i] == newRacerNameIndex)){
notUnique = true;
}
}
}
return newRacerNameIndex;
}
// // Provides, looping, up or down indexing of items in list
// // This function assumes the start of the list is at zero index
// byte IndexList(byte curIdx, byte listLength, bool cycleUp, byte otherRacer) {
// byte newIndex = curIdx;
// if (cycleUp) {
// // Cycling up we need to reset to zero if we reach end of list.
// // The modulus (%) of a numerator smaller than its denominator is equal to itself,
// // while the modulus of an integer with itself is 0.
// newIndex = (newIndex + 1) % (listLength);
// } else {
// // if cycling down list we need to reset to end of list if we reach the beginning
// if (curIdx == 0){
// newIndex = listLength - 1;
// } else {
// newIndex--;
// }
// }
// // We can't allow two racers to have the same id,
// // so if the result is the other selected racer then index again.
// if (newIndex == otherRacer) {
// newIndex = IndexList(newIndex, listLength, cycleUp, otherRacer);
// }
// return newIndex;
// }
// // Returns the number of digits in an integer.
// int calcDigits(int number){
// int digitCount = 0;
// while (number != 0) {
// // integer division will drop decimal
// number = number/10;
// digitCount++;
// }
// return digitCount;
// }
// Used to write same character repeatedly from start to end position, inclusive.
// This is primarily used to clear lines and space before writing an update to display.
void PrintSpanOfChars(displays disp, byte lineNumber = 0, int posStart = 0, int posEnd = LCD_COLS - 1, char printChar = ' ') {
switch(disp){
case lcdDisp:{
// Serial.println(printChar);
// Serial.print("StartPos: ");
// Serial.println(posStart);
// Serial.print("EndPos: ");
// Serial.println(posEnd);
lcd.setCursor(posStart, lineNumber);
for(int n = posStart; n <= posEnd; n++){
lcd.print(printChar);
}
}
break;
// Assumes that Racer LED bars are the only other display.
default: {
for(int i = posStart; i <= posEnd; i++){
lc.setChar(disp-1, (LED_DIGITS - 1) - i, printChar, false);
}
}
break;
}
}
// function to write pre-start final countdown digits to LEDs
void ledWriteDigits(byte digit) {
// Use laneCount instead of LED_BAR_COUNT because this is for racer LED only, not start light.
for (int i = 1; i <= laneCount; i++){
if(laneEnableStatus[i] > 0) PrintSpanOfChars( displays(i), 0, 0, 7, char(digit) );
}
}
// numberIn --Any positive number to be printed to display
// width --The number of places/digits to be printed
// endPosIdx --The index of the character position that contains the final digit
// display --The display enum that determines where to print number
// leadingZs --Flag, if = true, if number actual width is less than 'width' fill with '0'
// line --For display's that have more than 1 line, line on which to print number
// endWithDecimal --Flag, if true print decimal at end of number
// NOTES: this function doesn't error check, if the number is larger than the width
// the digits outside of the width (the largest place digits) will not be printed.
void PrintNumbers(const unsigned long numberIN, const byte width, const byte endPosIdx, displays display, bool leadingZs = true, const byte line = 0, bool endWithDecimal = false){
// we take 1 away from width because the endposition is inclusive in the width count
byte cursorStartPos = endPosIdx - (width - 1);
byte cursorEndPos = endPosIdx;
byte digitValue;
// variable to track present digit by its place value
byte placeValue = width - 1;
switch (display) {
// for printing to the main LCD
case lcdDisp: {
// written to screen from max place value to min defined by width and end position
for (byte displayIdx = cursorStartPos; displayIdx <= cursorEndPos; displayIdx++) {
lcd.setCursor(displayIdx, line);
// print current place value's digit
digitValue = numberIN / ipow(10, placeValue) % 10;
if (!leadingZs && digitValue == 0){
lcd.print(" ");
}
else {
lcd.print(digitValue);
// once we get non-zero digit then we want all zeros
leadingZs = true;
}
placeValue--;
}
if (endWithDecimal) lcd.print(".");
}
break;
// for printing to LED bar displays
default:{
for (byte displayIdx = cursorStartPos; displayIdx <= cursorEndPos; displayIdx++) {
// print current place value's digit
digitValue = numberIN / ipow(10, placeValue) % 10;
if(!leadingZs && digitValue == 0){
// skip this digit
}
else {
lc.setDigit(
display - 1,
(LED_DIGITS - 1) - displayIdx,
digitValue,
endWithDecimal && displayIdx == cursorEndPos);
// once we get non-zero digit then we want all zeros
leadingZs = true;
}
placeValue--;
}
}
break;
}
}
// This function prints the input text, to the indicated display, at the indicated position.
void PrintText(const char textToWrite[LCD_COLS], const displays display, const byte writeSpaceEndPos, const byte width = LED_DIGITS, bool rightJust = false, const byte line = 0, bool clear = true) {
// Even if text is right justified, available space is filled starting with 1st character.
// Need to track the character index seperately from display digit position.
byte const textLength = strlen(textToWrite);
// We take 1 away from width because the endposition is inclusive in the width count.
byte cursorStartPos = writeSpaceEndPos - (width - 1);
byte cursorEndPos = writeSpaceEndPos;
// Adjust start and end positions based on text length and available space.
if (textLength < width && rightJust) cursorStartPos = cursorEndPos - (textLength - 1);
if (textLength < width && !rightJust) cursorEndPos = cursorEndPos - (width - textLength);
// Because the writing position index may not match the text char index
// we nust track them seperately.
byte textIndex = 0;
switch (display) {
// For writing to LCD display
case lcdDisp: {
// For clearing we want to clear the whole space so we don't use adjusted positions.
if (clear) PrintSpanOfChars(display, line, (writeSpaceEndPos-width+1), writeSpaceEndPos);
for (byte i = cursorStartPos; i <= cursorEndPos; i++){
lcd.setCursor(i, line);
lcd.print(textToWrite[textIndex]);
textIndex++;
}
} // END LCD case
break;
// For writing to LED display
// Note that 7-seg cannot display 'K', 'W', 'M', 'X's, 'W'x, or 'V's
default:{
// If 'clear' = true then clear the original width from the end position -1.
if (clear) PrintSpanOfChars(display, line, (writeSpaceEndPos-width+1), writeSpaceEndPos);
// Serial.print("Print TExt - writeSpacedEnd Pos: ");
// Serial.println(writeSpaceEndPos);
// Serial.print("beginning position: ");
// Serial.println(writeSpaceEndPos-width+1);
for (byte j = cursorStartPos; j <= cursorEndPos; j++){
// The digit position of the LED bars is right to left so flip the requested index.
lc.setChar(display - 1, (LED_DIGITS-1) - j, textToWrite[textIndex], false);
textIndex++;
}
} // END LED cases
break;
} // END display switch
}
// timeMillis
// - Time in ms to be printed as a clock time to given display.
// clockEndPos
// - The index of display row, where the last digit of the clock should go.
// This index is always considered left to right, and starting with 0.
// The function will flip index internally if needed for a right to left indexed display.
// For the MAX7219 LED bars, natively, the digit idx 0 is the far right digit.
// However, when using this function with a MAX7219 LED bar,
// to have clock end at far right, input clockEndPos 7, not 0.
// printWidth
// - The number of character spaces available to print time, including ':' and '.'
// precision
// - The number of decimal places desired. (allowed: 1 = 0.0, 2 = 0.00, or 3 = 0.000)
// display
// - display enum indicating display to write clock time to.
// line
// - The line on the display to write to, if display only has 1 line, use any#.
// leadingZs
// - Indicates if leading time block should have a leading zero.
// If true, 3,903,000 = 1hr 5min 3sec = 01:05:03.precision
//
// Precision will automatically be dropped to make room until a whole digit
// is dropped, at which point the time exceeds the width available,
// and an 'E' will be written to notify view width has been exceeded.
// NOTE: An 'E' does not affect that actual timing which can go on for about 49 days.
void PrintClock(ulong timeMillis, byte clockEndPos, byte printWidth, byte precision, displays display, byte line = 0, bool leadingZs = false) {
clockWidth nextTimeBlock = H;
int maxPrecision = 0;
byte baseTimeWidth = 0;
// Based on the time value, and width of display area (printWidth),
// calculate how many decimal places can be accomodated.
// Because the LCD requires a character space for ':' and '.',
// we must adjust the required width accordingly.
// 'baseTimeWidth' is min width needed for the whole units HH:MM:SS and colons.
//
// if t < 10sec
// 0.0
if(timeMillis < 10000){
nextTimeBlock = S;
maxPrecision = (display == lcdDisp ? printWidth-2 : printWidth-1);
baseTimeWidth = 1;
} else
// if 10sec <= t < 1min
// 00
if (timeMillis < 60000){
nextTimeBlock = S;
maxPrecision = (display == lcdDisp ? printWidth-3 : printWidth-2);
baseTimeWidth = 2;
} else
// if 1min <= t < 10min
// 0:00
if (timeMillis < 600000){
nextTimeBlock = M;
maxPrecision = (display == lcdDisp ? printWidth-5 : printWidth-3);
baseTimeWidth = (display == lcdDisp ? 4 : 3);
} else
// if 10min <= t < 1hr
// 00:00
if (timeMillis < 3600000){
nextTimeBlock = M;
maxPrecision = (display == lcdDisp ? printWidth-6 : printWidth-4);
baseTimeWidth = (display == lcdDisp ? 5 : 4);
} else
// if 1hr <= t < 10hr
// 0:00:00
if (timeMillis < 30600000){
nextTimeBlock = H;
maxPrecision = (display == lcdDisp ? printWidth-8 : printWidth-5);
baseTimeWidth = (display == lcdDisp ? 7 : 5);
} else
// if 10hr <= t < 24hr
// 00:00:00
if (timeMillis < 86,400,000){
nextTimeBlock = H;
maxPrecision = (display == lcdDisp ? printWidth-9 : printWidth-6);
baseTimeWidth = (display == lcdDisp ? 8 : 6);
} else {
// if 24hr <= t
// over max clock limit of 24 hrs.
if (timeMillis >= 86,400,000)
maxPrecision = -1;
}
// A negative maxPrecision means we don't have space to print the time.
if (maxPrecision < 0){
// Print an 'E' to alert current time is over display width limit.
PrintText("E", display, clockEndPos, printWidth, true, line, display == lcdDisp ? true : false);
} else {
unsigned long ulHour;
unsigned long ulMin;
unsigned long ulSec;
unsigned long ulDec;
unsigned long ulCent;
unsigned long ulMill;
// Convert millisecond time into its individual time blocks.
SplitTime(timeMillis, ulHour, ulMin, ulSec, ulDec, ulCent, ulMill);
unsigned long decimalSec;
byte hourEndPos;
// if requested precision is higher than available max, use max.
precision = maxPrecision < precision ? maxPrecision : precision;
// LCD must add character space for colons and decimal place if it has precision.
hourEndPos = (display == lcdDisp ?
clockEndPos - (6 + precision + (precision > 0 ? 1 : 0)) :
clockEndPos - (4 + precision)
);
// If leading zeros are requested but there is no space turn them off.
// If LCD, with precision, subtract additional 1 for the decimal position.
if( (printWidth - precision - baseTimeWidth - ((display==lcdDisp && precision>0)?1:0)) <= 0 ){
leadingZs = false;
}
// calculate total width needed to print time string
// Must account for end position being included in the printWidth total.
byte totalWidth = baseTimeWidth + (display == lcdDisp ? precision+1: precision);
// Clear spaces in print width that occur before clock time's first display digit
PrintSpanOfChars(display, line, clockEndPos - printWidth +1, clockEndPos - totalWidth);
// Set which precision time block to use based on available precision.
switch(precision){
case 1:
decimalSec = ulDec;
break;
case 2:
decimalSec = ulCent;
break;
case 3:
decimalSec = ulMill;
break;
default:
decimalSec = NULL;
break;
}
if (nextTimeBlock == H) {
// H 00:00:00
switch (display){
case lcdDisp: {
PrintNumbers(ulHour, 2, hourEndPos, display, leadingZs, line);
lcd.print(":");
}
break;
default:{
PrintNumbers(ulHour, 2, hourEndPos, display, leadingZs, 0, true);
}
break;
} // END of display switch
// All non-leading parts of the clock time should have leading zeros
leadingZs = true;
// Move on to print Minutes.
nextTimeBlock = M;
}
if (nextTimeBlock == M) {
// M 00:00
switch (display){
case lcdDisp: {