-
Notifications
You must be signed in to change notification settings - Fork 2
/
spartronics_clock.ino
768 lines (671 loc) · 20.8 KB
/
spartronics_clock.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
/**
* Spartronics sample code for Teensy LC based display via Adafruit NeoMatrix
*
* Parts/Libraries:
* - Teensy LC
* - Adafruit NeoMatrix
* - DS3231 RTC
* - AT24C32 for storage -- currently not used
*
* Libraries:
* - DebounceEvent button manager by Xose Perez
* https://github.com/xoseperez/debounceevent
* - IntervalTimer library for Teensy
* - DS1307RTC library (compatible w/ DS3231)
* https://www.pjrc.com/teensy/td_libs_DS1307RTC.html
*
* Usage:
* 3 buttons control the behavior of the display
* - button 1: specifies mode for reprogramming the display:
* prints scrolling message or coundown clock
* - button 2 & 3: unused -- button push will generate corresponding events
*
* Note: holding the DEC button down during boot will force setting
* the datetime via user input
*
* Note: Spartronics clock also hosts an AT24C32 which is a 32K EEPROM.
* It is currently not used.
* In the future, this can hold long messages or short novels.
*/
#include <Adafruit_GFX.h>
#include <Adafruit_NeoMatrix.h>
#include <DebounceEvent.h>
#include <DS1307RTC.h>
#include <Time.h>
#include <TimeLib.h>
#include <Wire.h>
#include <stdbool.h>
#define DEBUG_ON 0
#define NUM_ELEMENTS(x) (sizeof(x) / sizeof(x[0]))
// Spartronics colors! (From ATLaS, 2014)
#define SPARTRONICS_YELLOW Adafruit_NeoMatrix::Color(90, 90, 0)
#define SPARTRONICS_BLUE Adafruit_NeoMatrix::Color(0, 50, 170)
// Fun messages to print
static const char *_messages[] = {
"Spartronics 4915 - Woot!",
"Gracious Professionalism",
"Everybody is in Marketing!",
"Robots don't quit!",
"Clio was here...",
"Mentors Rock!",
"Water game confirmed!",
"Are we all having PHUN?",
"Did you do your survey?",
"ROCKIN' ROBOT!",
"ENABLING!",
"FIRST RISE: The Force is Building",
"ATLaS",
"GAEA",
"ARES",
"HELIOS",
"THEMIS",
"CHAOS",
"INFINITE RECHARGE!",
"Sleep? Who has time for that?",
"Safety Glasses!",
"More than Robots",
"How about a scissor lift?",
"I buid robots...",
"Do. Or do not. There is no try.",
};
/**
* Structure to hold time values in their human-readable components
*/
typedef struct {
unsigned year;
unsigned month;
unsigned day;
unsigned hour;
unsigned minute;
unsigned second;
const char *description;
} CalendarTime_t;
/**
* List of events to count-down to. They must be sorted in ascending order.
*/
static const CalendarTime_t _important_times[] = {
{ 2019, 10, 11, 15, 15, 0, "GirlsGen" }, // GirlsGen pack time
{ 2020, 1, 4, 7, 0, 0, "Kickoff" }, // Kickoff
{ 2020, 2, 18, 21, 0, 0, "Bag day" }, // Pseudo-bag-day
{ 2020, 2, 28, 12, 0, 0, "Glacier Peak" }, // Glacier Peak
{ 2020, 3, 20, 12, 0, 0, "Bellingham" }, // Bellingham
{ 2020, 4, 1, 9, 0, 0, "Districts" }, // District Champs
{ 2020, 4, 14, 12, 0, 0, "Worlds" }, // Worlds
};
static time_t _target_time;
static const char * _target_description;
/**
* Color names for various display modes
*/
typedef enum {
COLOR_RED,
COLOR_ORANGE,
COLOR_YELLOW,
COLOR_GREEN,
COLOR_BLUE,
COLOR_PURPLE,
// Insert new color names above this line
COLOR_MAX
} ColorName_t;
// colors used for the matrix display
static const uint16_t _colors[] = {
[COLOR_RED] = Adafruit_NeoMatrix::Color(128, 0, 0),
[COLOR_ORANGE] = Adafruit_NeoMatrix::Color(128, 128, 0),
[COLOR_YELLOW] = SPARTRONICS_YELLOW,
[COLOR_GREEN] = Adafruit_NeoMatrix::Color(0, 128, 0),
[COLOR_BLUE] = SPARTRONICS_BLUE,
[COLOR_PURPLE] = Adafruit_NeoMatrix::Color(0, 128, 128),
};
/**
* Setup the display state
* - countdown: displays the countdown clock -- updated by timer_ticks
* - days/hrs/mins/secs/: changes the value using inc/dec button presses
* - message: prints text messages
*/
typedef enum // at each button mode press, display mode changes accordingly
{
STATE_INIT = 0,
STATE_MESSAGE,
STATE_MESSAGE_ONCE,
STATE_MESSAGE_REPEAT,
STATE_COUNTDOWN,
STATE_DATE,
STATE_TIME,
STATE_SET_TIME,
// Add new states above this line
STATE_MAX
} State_t;
/**
* Setup the events for triggering state changes
* These are driven by the buttons
*/
typedef enum
{
EVENT_NULL = 0, // no event
EVENT_MODE,
EVENT_SET,
EVENT_INCREMENT,
EVENT_DECREMENT,
EVENT_TIMER,
EVENT_SCROLL,
// Add new events above this line
EVENT_MAX
} Event_t;
State_t _state; // tracks the state of the display
/**
* Variable to track the time interval remaining until the event being tracked
*/
static uint32_t _countdown_time;
/**
* Structure to hold the time interval value to be displayed on the clock
*/
typedef struct // parses the countdown_time to its components
{
uint8_t seconds;
uint8_t minutes;
uint8_t hours;
uint16_t days;
} TimeInterval_t;
/**
* Setup interrupts for scroll and timer events, callbacks,
* and variables to update controls
*/
IntervalTimer scroll_interval;
bool update_timer_event = false;
bool update_scroll_event = false;;
void scroll_callback()
{
update_scroll_event = true;
}
/** Good reference for display configuration:
* https://learn.adafruit.com/adafruit-neopixel-uberguide/neomatrix-library
*
* Spartronics display is setup as a 45x7 matrix.The first pixel is on top left,
* lines are arranged in rows as zig-zag order. The shield uses 800Khz (v2)
* pixels that expect GRB color data
*/
#define CONTROL_PIN 17
#define MATRIX_WIDTH 45
#define MATRIX_HEIGHT 7
Adafruit_NeoMatrix matrix = Adafruit_NeoMatrix(MATRIX_WIDTH, MATRIX_HEIGHT,
CONTROL_PIN, // pin number
NEO_MATRIX_TOP + NEO_MATRIX_LEFT + // matrix layout
NEO_MATRIX_ROWS + NEO_MATRIX_ZIGZAG, // flags describe our panel
NEO_GRB + NEO_KHZ800); // pixel type flags
/**
* For botton management using the DebounceEvent library
* https://github.com/xoseperez/debounceevent
*/
#define MODE_BUTTON_PIN 14 // D14 (D == digital port) -- sets display mode
#define INC_BUTTON_PIN 15 // D15 -- increments value
#define DEC_BUTTON_PIN 16 // D16 -- decrements value
#define CUSTOM_DEBOUNCE_DELAY 50
#define CUSTOM_REPEAT_DELAY 0 // disabled double clicks
DebounceEvent *mode_button;
DebounceEvent *inc_button;
DebounceEvent *dec_button;
void event_callback(uint8_t pin, uint8_t event, uint8_t count, uint16_t length) {
Event_t state_event = EVENT_NULL;
if (event != EVENT_RELEASED)
{
return;
}
switch (pin)
{
case MODE_BUTTON_PIN:
if (count == 1)
{
state_event = EVENT_MODE;
}
else if (count == 2)
{
state_event = EVENT_SET;
}
break;
case INC_BUTTON_PIN:
if (count == 1)
{
state_event = EVENT_INCREMENT;
}
break;
case DEC_BUTTON_PIN:
if (count == 1)
{
state_event = EVENT_DECREMENT;
}
break;
default:
return;
}
if (state_event != EVENT_NULL)
{
// update the state machine
_state = state_machine(_state, state_event);
}
}
/**
* setup the display, matrix, and button callbacks
**/
void setup()
{
// Setup the debug serial output
Serial.begin(115200);
// setup matrix display
matrix.begin();
matrix.setTextWrap(false);
matrix.setBrightness(100);
matrix.setTextColor(_colors[COLOR_RED]);
// setup the button event loops
mode_button = new DebounceEvent(MODE_BUTTON_PIN, event_callback,
BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH | BUTTON_SET_PULLUP);
inc_button = new DebounceEvent(INC_BUTTON_PIN, event_callback,
BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH | BUTTON_SET_PULLUP);
dec_button = new DebounceEvent(DEC_BUTTON_PIN, event_callback,
BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH | BUTTON_SET_PULLUP);
// Set the system clock from the RTC
get_time_from_rtc();
// initialize state machine
_state = STATE_INIT;
// IntervalTimer takes a callback function, and a number of microseconds
// scroll_interval is set to run the callback 10 times per second
scroll_interval.begin(scroll_callback, 100 /* ms */ * 1000 /* us per ms */);
}
/**
* continously loop through button callbacks to detect display state changes
* if no button presses, update the display accordingly
* - if in countdown mode, wait for timer_ticks to adjust seconds, etc.
* - if in sayings mode, print the 'sayings' on the matrix
* - in any other mode, wait for inc/dec button presses
*/
void loop()
{
Event_t event;
static time_t last_time = 0;
// listen for button callbacks and trigger state_machine change accordingly
mode_button->loop();
inc_button->loop();
dec_button->loop();
// TODO Fix this so it's the only timer source for seconds tick
if (now() != last_time)
{
last_time = now();
update_timer_event = true;
}
// listen for timer events & update state machine
if (update_timer_event)
{
update_timer_event = false;
event = EVENT_TIMER;
_state = state_machine(_state, event);
}
else if (update_scroll_event)
{
update_scroll_event = false;
event = EVENT_SCROLL;
_state = state_machine(_state, event);
}
// Check if a new countdown time is needed?
if (_countdown_time == 0)
{
// Go through list and find next time that is ahead of now
}
}
/**
* given a date info, makes time -- returns time in seconds since epoch
*/
time_t convert_time(const CalendarTime_t &time)
{
tmElements_t tmSet;
tmSet.Year = time.year - 1970;
tmSet.Month = time.month;
tmSet.Day = time.day;
tmSet.Hour = time.hour;
tmSet.Minute = time.minute;
tmSet.Second = time.second;
return makeTime(tmSet); //convert to time_t
}
time_t find_next_target(time_t now)
{
time_t target = 0;
for (unsigned idx=0; idx<NUM_ELEMENTS(_important_times); idx++)
{
time_t temp;
// Convert the time to time_t so we can compare to now
temp = convert_time(_important_times[idx]);
// Compare to 'now'. If temp is in the future, then it is our target
if (temp > now)
{
target = temp;
_target_time = target;
_target_description = _important_times[idx].description;
break;
}
}
return target;
}
/**
* Break down the provided time interval (in seconds) into clock-style
* components of days, hours, minutes and seconds.
* see: https://pastebin.com/sfEjA94n
*/
void compute_elapsedTime(TimeInterval_t &time_interval, uint32_t countdown_time)
{
time_interval.seconds = countdown_time % 60;
countdown_time /= 60; // now it is minutes
time_interval.minutes = countdown_time % 60;
countdown_time /= 60; // now it is hours
time_interval.hours = countdown_time % 24;
countdown_time /= 24; // now it is days
time_interval.days = countdown_time;
}
static State_t _handle_state_init(Event_t event, bool first_time)
{
State_t next_state = STATE_COUNTDOWN;
// We can do some initialization code here, if we need to
matrix.clear();
// Try to find a date to count down to in our list
if (find_next_target(now()) == 0)
{
// No target time found!
message_start("ERR: NO COUNTDOWN DATES!", COLOR_RED);
next_state = STATE_MESSAGE_ONCE;
}
return next_state;
}
static State_t _handle_state_countdown(Event_t event, bool first_time)
{
State_t next_state = _state;
TimeInterval_t time_interval;
time_t time_now = now();
uint32_t countdown_time = 0;
if (first_time)
{
// Refresh the software clock from the hardware
get_time_from_rtc();
}
switch (event)
{
case EVENT_TIMER:
if (_target_time >= time_now)
{
countdown_time = _target_time - time_now;
// Print the remaining time interval on the display
compute_elapsedTime(time_interval, countdown_time);
// display the time value
print_time_interval(time_interval);
#if DEBUG_ON
Serial.print("---_countdown_time: ");
Serial.println(countdown_time);
Serial.print("---display time: ");
Serial.print(time_interval.days);
Serial.print(":");
Serial.print(time_interval.hours);
Serial.print(":");
Serial.print(time_interval.minutes);
Serial.print(":");
Serial.println(time_interval.seconds);
#endif
if (time_in_state() > (15 /* minutes */ * 60 /* seconds */))
{
// Print a random message
message_start(_messages[random(NUM_ELEMENTS(_messages))],
(ColorName_t)random(NUM_ELEMENTS(_colors)));
next_state = STATE_MESSAGE_ONCE;
}
}
else
{
// No target found, so go to date/time display
next_state = STATE_DATE;
}
break;
case EVENT_MODE:
// Go to the message display
message_start(_messages[random(NUM_ELEMENTS(_messages))],
(ColorName_t)random(NUM_ELEMENTS(_colors)));
next_state = STATE_MESSAGE;
break;
case EVENT_INCREMENT:
// Go to the date display
next_state = STATE_DATE;
break;
case EVENT_DECREMENT:
// Display the description of this event
message_start(_target_description, COLOR_GREEN);
next_state = STATE_MESSAGE_ONCE;
break;
default:
// Ignore all other events
break;
}
return next_state;
}
/**
* Print the current date on the display
*/
static State_t _handle_state_date(Event_t event, bool first_time)
{
State_t next_state = _state;
CalendarTime_t calendar_time;
if (first_time)
{
tmElements_t tm;
// Refresh the software clock from the hardware
get_time_from_rtc();
// Get the calendar time
breakTime(now(), tm);
calendar_time.year = tmYearToCalendar(tm.Year);
calendar_time.month = tm.Month;
calendar_time.day = tm.Day;
calendar_time.hour = tm.Hour;
calendar_time.minute = tm.Minute;
calendar_time.second = tm.Second;
// Print the date
print_date(calendar_time);
}
switch (event)
{
case EVENT_TIMER:
if (time_in_state() > 10 /* seconds */)
{
next_state = STATE_TIME;
}
break;
case EVENT_MODE:
// Go to the message display
message_start(_messages[random(NUM_ELEMENTS(_messages))],
(ColorName_t)random(COLOR_MAX));
next_state = STATE_MESSAGE;
break;
case EVENT_DECREMENT:
// Go back to the countdown display
next_state = STATE_COUNTDOWN;
break;
case EVENT_INCREMENT:
// Go to the date display
next_state = STATE_TIME;
break;
default:
// Ignore all other events
break;
}
return next_state;
}
/**
* Print the current time on the display
*/
static State_t _handle_state_time(Event_t event, bool first_time)
{
State_t next_state = _state;
CalendarTime_t calendar_time;
tmElements_t tm;
if (first_time)
{
// Refresh the software clock from the hardware
get_time_from_rtc();
// Get the calendar time
breakTime(now(), tm);
calendar_time.year = tmYearToCalendar(tm.Year);
calendar_time.month = tm.Month;
calendar_time.day = tm.Day;
calendar_time.hour = tm.Hour;
calendar_time.minute = tm.Minute;
calendar_time.second = tm.Second;
// Print the time
print_time(calendar_time);
}
switch (event)
{
case EVENT_TIMER:
// TODO: Update display each second
breakTime(now(), tm);
calendar_time.year = tmYearToCalendar(tm.Year);
calendar_time.month = tm.Month;
calendar_time.day = tm.Day;
calendar_time.hour = tm.Hour;
calendar_time.minute = tm.Minute;
calendar_time.second = tm.Second;
// Print the time
print_time(calendar_time);
if (time_in_state() > 10 /* seconds */)
{
next_state = STATE_DATE;
}
break;
case EVENT_MODE:
// Go to the message display
message_start(_messages[random(NUM_ELEMENTS(_messages))],
(ColorName_t)random(COLOR_MAX));
next_state = STATE_MESSAGE;
break;
case EVENT_DECREMENT:
// Go back to the countdown display
next_state = STATE_COUNTDOWN;
break;
case EVENT_INCREMENT:
// Go to the date display
next_state = STATE_DATE;
break;
case EVENT_SET:
// Special event, only used in this state
// Set the time (over the USB port)
next_state = STATE_SET_TIME;
break;
default:
// Ignore all other events
break;
}
return next_state;
}
/**
* Set the system time
*/
static State_t _handle_state_set_time(Event_t event, bool first_time)
{
State_t next_state = STATE_TIME;
// Call the set_time() function to set the clock
set_time();
return next_state;
}
static State_t _handle_state_message(Event_t event, bool first_time)
{
State_t next_state = _state;
if (first_time)
{
// Reset message parameters
message_start(NULL, COLOR_RED);
}
switch (event)
{
case EVENT_SCROLL:
// Scroll a message on the display
message_scroll();
if (message_done())
{
switch (next_state)
{
case STATE_MESSAGE:
message_start(
_messages[random(NUM_ELEMENTS(_messages))],
(ColorName_t)random(COLOR_MAX));
break;
case STATE_MESSAGE_REPEAT:
message_start(NULL, COLOR_RED);
break;
case STATE_MESSAGE_ONCE:
default:
next_state = STATE_COUNTDOWN;
break;
}
}
break;
case EVENT_MODE:
case EVENT_INCREMENT:
case EVENT_DECREMENT:
// Advance to the next state
next_state = STATE_COUNTDOWN;
break;
default:
// Ignore all other events
break;
}
return next_state;
}
/**
* Allow a state handler to know how long the state has been running
*/
static time_t _state_entry_time = 0;
static uint32_t time_in_state(void)
{
uint32_t seconds = 0;
if (now() > _state_entry_time)
{
seconds = now() - _state_entry_time;
}
return seconds;
}
/**
* State machine to control the display
* Note: events triggered by the buttons and timer_ticks
*/
State_t state_machine(State_t current_state, Event_t event)
{
State_t next_state = current_state;
bool first_time = false;
static State_t last_state = STATE_INIT;
// Set a flag if this is the first time in this new state
if (current_state != last_state)
{
first_time = true;
_state_entry_time = now();
}
switch (current_state)
{
case STATE_INIT:
next_state = _handle_state_init(event, first_time);
break;
case STATE_COUNTDOWN:
next_state = _handle_state_countdown(event, first_time);
break;
case STATE_DATE:
next_state = _handle_state_date(event, first_time);
break;
case STATE_TIME:
next_state = _handle_state_time(event, first_time);
break;
case STATE_SET_TIME:
next_state = _handle_state_set_time(event, first_time);
break;
case STATE_MESSAGE:
case STATE_MESSAGE_ONCE:
case STATE_MESSAGE_REPEAT:
next_state = _handle_state_message(event, first_time);
break;
// Unhandled case. Don't know how we got here, just set the default
default:
next_state = STATE_INIT;
break;
}
last_state = current_state;
return next_state;
}