Reduced hysteresis to 3 samples. Expanded valid pulse width range.
[the_perfect_clock.git] / the_perfect_clock.ino
1 // "The Perfect Clock" 
2
3 // Copyright (C) 2019-2020 by Art Cancro <ajc@citadel.org>
4
5 // My perfect clock has no buttons and cannot be set manually.  This version uses a WWVB receiver module
6 // attached to pin D9 of the Arduino, and sets the clock any time it receives a complete frame.  The clock
7 // is kept without an RTC, simply using the millis() timer.  When time is set, it is displayed on
8 // a 7-segment array connected using an HT16K33 decoder/driver (yes, an Adafruit backpack).  Our display
9 // can also display at 15 different brightness levels, so we dim it when the room is dark to avoid
10 // blasticating a dark room with super-bright LED display.
11
12 // The clock is hard coded to use US Eastern time with DST in effect whenever WWVB is announcing it.
13
14 // This software is made available to you conditionally upon you accepting the following terms and conditions:
15 // 1. You agree that it is called "open source", not "free software".
16 // 2. You agree that the Linux operating system is not called "GNU/Linux".
17 // 3. You agree that Corey Ehmke is a scumbag, as are all social justice warriors.
18 // 4. You promise never to vote democrat in any election.
19 // 5. Under no circumstances may you use this program and also maintain a Facebook account.
20 // Aside from these conditions, the program is made available to you under the terms of the GNU General Public License.
21
22 const uint8_t wwvb = 9;       // pin on which WWVB signal will be received
23 const uint8_t greenled = 2;   // An LED attached to this pin will illuminate if the time has been set within the last 24 hours
24 const uint8_t yellowled = 3;  // An LED attached to this pin will illuminate if we are currently receiving a clean frame
25 const uint8_t redled = 4;     // An LED attached to this pin will pulse for 1 ms every second
26 const uint8_t photocell = A0; // Attach a photocell with a 10K voltage divider to this pin
27 const uint8_t addr = 0x70;    // I2C address of HT16K33 (using Adafruit backpack with digits on 0,1,3,4; dots on 2)
28
29 long millis_per_minute = 60000; // Nominally 60000; adjust if your board runs fast or slow
30
31 // This is a simple BCD-to-7-segment font.  It includes 0x0A through 0x0F even though they're not needed for a time clock.
32 const uint8_t sevensegfont[] = { 63, 6, 91, 79, 102, 109, 125, 7, 127, 111, 119, 124, 57, 94, 121, 113 };
33 const uint8_t firstcolfont[] = { 0, 6, 91 };    // this version of the font is for the first position
34
35 #include <Wire.h>                               // I2C library to drive the HT16K33 display
36
37 int hour = 0;
38 int minute = 0;
39 unsigned long millisecond = 0;
40 unsigned long previous_millis = 0;
41 unsigned long last_sync = -86398000;
42 uint16_t displayBuffer[8];    // Digit buffer for HT16K33
43 int previous_minute = 61;     // What the minute was previously; we use this to detect whether an update is needed
44 int this_pulse = 0;           // Value of the current pulse received
45 int previous_pulse = 0;       // Value of the previous pulse received (two "mark" bits == new frame)
46 int start_of_pulse = 0;       // The value of the millis() timer when the current pulse began
47 uint8_t framebuf[60];         // We store the entire 60-bit frame here
48 uint8_t framesync = 0;        // Nonzero if we've received all good pulses since the start of the frame
49 int position_in_frame = 0;    // Where we are in the frame (1 bit per second)
50 int previous_signal = 0;      // "high" or "low" received on the previous cycle (so we can do edge detection)
51 int time_is_set = 0;          // nonzero when time has been set at least once
52
53 void setup()
54 {
55         int i;
56
57         pinMode(LED_BUILTIN, OUTPUT);   // The built-in LED will display the raw WWVB signal pulses
58         pinMode(greenled, OUTPUT);    // This LED will illuminate if the time has been set within the last 24 hours
59         pinMode(yellowled, OUTPUT);   // This LED will illuminate if we are currently receiving a clean frame
60         pinMode(redled, OUTPUT);      // This LED pulses for 1 ms every second
61         pinMode(wwvb, INPUT);                 // Input pin for WWVB receiver signal
62   pinMode(photocell, INPUT);    // Input pin for photocell
63
64         Wire.begin();                   // Initialize I2C
65
66         Wire.beginTransmission(addr);
67         Wire.write(0x21);               // turn on oscillator
68         Wire.endTransmission();
69
70         Wire.beginTransmission(addr);
71         Wire.write(0xE1);               // brightness (max is 15)
72         Wire.endTransmission();
73
74         Wire.beginTransmission(addr);
75         Wire.write(0x81);               // no blinking or blanking
76         Wire.endTransmission();
77
78         displayBuffer[0] = 0;
79         displayBuffer[1] = 0;
80         displayBuffer[2] = 16;
81         displayBuffer[3] = 0;
82         displayBuffer[4] = 0;
83         show();
84 }
85
86
87 // Note: only write to the display when the readout needs to be updated.
88 // Speaking I2C on every loop iteration jams the WWVB receiver.
89 void loop()
90 {
91         unsigned long m = millis();
92         digitalWrite(redled, (m%1000) ? LOW : HIGH);
93         if (m != previous_millis) {
94                 millisecond += (m - previous_millis);
95                 if (millisecond >= millis_per_minute) {
96                         millisecond -= millis_per_minute;
97                         ++minute;
98                         if (minute > 59) {
99                                 minute = 0;
100                                 ++hour;
101                                 if (hour > 23) {
102                                         hour = 0;
103                                 }
104                         }
105                 }
106         }
107         previous_millis = m;
108
109         int pulse_length;
110         int signal = digitalRead(wwvb);                                                         // is the input high or low right now?
111         digitalWrite(LED_BUILTIN, signal);                                              // use the onboard LED to show the signal
112
113         if (signal && (!previous_signal)) {                         // leading edge of pulse detected
114                 start_of_pulse = millis();
115         }
116         else if ((!signal) && (previous_signal)) {                  // trailing edge of pulse detected
117                 pulse_length = millis() - start_of_pulse;
118
119                 if (pulse_length > 150 && pulse_length < 250) {                 // "0" bit ~= 200 ms (represented as "0")
120                         this_pulse = 0;
121                 } else if (pulse_length > 450 && pulse_length < 550) {          // "1" bit ~= 500 ms (represented as "1")
122                         this_pulse = 1;
123                 } else if (pulse_length > 750 && pulse_length < 850) {          // marker bit ~= 800 ms (represented as "2")
124                         this_pulse = 2;
125                 } else {
126                         this_pulse = 15;                                        // bad pulse (represented as "15")
127                         framesync = 0;                                          // throw the whole frame away
128                 }
129
130                 // BEGIN -- THINGS TO DO AT THE END OF A PULSE
131
132                 if ((this_pulse == 2) && (previous_pulse == 2)) {               // start of a new frame!
133
134                         if (framesync == 1) {
135                                 set_the_time();                                                         // We have a whole good frame.  Set the clock!
136                         }
137       else if ((!framesync) && (time_is_set)) {
138         snap_to_zero();                                       // We don't have a whole frame, but we know it's :00 seconds now.
139       }
140
141                         framesync = 1;
142                         position_in_frame = 0;
143                   calibrate();                                            // calibrate the software timer
144                 }
145
146                 if (framesync) {                                                                        // yellow LED = we currently have frame sync
147                         analogWrite(yellowled, 10);                                             // (we run it at a low intensity)
148                 }
149                 else {
150                         digitalWrite(yellowled, LOW);
151                 }
152
153                 if ((framesync) && (position_in_frame < 60)) {
154                         framebuf[position_in_frame++] = this_pulse;
155                 }
156
157                 previous_pulse = this_pulse;
158
159                 // END -- THINGS TO DO AT THE END OF A PULSE
160         }
161
162         previous_signal = signal;
163
164         // Update the display only if it's a new minute.
165
166         if (time_is_set && (minute != previous_minute)) {
167                 previous_minute = minute;
168                 int h12 = (hour % 12) ;
169                 if (h12 == 0) h12 = 12;
170                 displayBuffer[0] = firstcolfont[h12 / 10];
171                 displayBuffer[1] = sevensegfont[h12 % 10];
172                 displayBuffer[2] = (hour<12) ? 0x06 : 0x0a;                     // AM or PM dot , colon always on
173                 displayBuffer[3] = sevensegfont[minute / 10];
174                 displayBuffer[4] = sevensegfont[minute % 10];
175                 show();
176         }
177
178         if ((m - last_sync) < 86400000) {                                               // green LED = got a good sync in the last 24 hours
179                 digitalWrite(greenled, HIGH);
180         }
181         else {
182                 digitalWrite(greenled, LOW);
183         }
184 }
185
186
187 // Write the display buffer to the display
188 void show()
189 {
190   // display the time
191         Wire.beginTransmission(addr);
192         Wire.write(0x00);       // start at address 0x0
193         for (int i = 0; i < 5; i++) {
194                 Wire.write(displayBuffer[i] & 0xFF);
195                 Wire.write(displayBuffer[i] >> 8);
196         }
197         Wire.endTransmission();
198  
199   // set the brightness
200   int light_level = analogRead(photocell) / 64;
201   if (light_level < 1) {
202     light_level = 1;
203   }
204   if (light_level > 15) {
205     light_level = 15;
206   }
207   Wire.beginTransmission(addr);
208   Wire.write(0xE0 + light_level);     // set the display brightness
209   Wire.endTransmission();
210 }
211
212
213 // Set the software clock to the WWVB time currently in the buffer
214 void set_the_time()
215 {
216         int i, newhour, newminute, dst;
217
218         // These six positions MUST contain marker bits.
219         // If any of them do not, we are looking at a corrupt frame.
220         int markers[] = { 0, 9, 19, 39, 49, 59 };
221         for (i=0; i<6; ++i) {
222                 if (framebuf[markers[i]] != 2) {
223                         return;
224                 }
225         }
226
227         newhour = (framebuf[12] ? 20 : 0);
228         newhour += (framebuf[13] ? 10 : 0);
229         newhour += (framebuf[15] ? 8 : 0);
230         newhour += (framebuf[16] ? 4 : 0);
231         newhour += (framebuf[17] ? 2 : 0);
232         newhour += (framebuf[18] ? 1 : 0);
233         if ((newhour < 0) || (newhour > 23)) {
234                 return;                         // reject impossible hours
235         }
236
237         newminute = (framebuf[1] ? 40 : 0);
238         newminute += (framebuf[2] ? 20 : 0);
239         newminute += (framebuf[3] ? 10 : 0);
240         newminute += (framebuf[5] ? 8 : 0);
241         newminute += (framebuf[6] ? 4 : 0);
242         newminute += (framebuf[7] ? 2 : 0);
243         newminute += (framebuf[8] ? 1 : 0);
244         if ((newminute < 0) || (newminute > 59)) {
245                 return;                         // reject impossible minutes
246         }
247
248         // advance 1 minute because WWVB gives the *previous* minute
249         newminute += 1;
250         if (newminute >= 60) {
251                 newminute = newminute % 60;
252                 newhour += 1;
253         }
254
255         // US Eastern time (yes it is hard coded)
256         newhour -= 5;
257
258         // DST (FIXME make this adjustable)
259         dst = (framebuf[57] ? 2 : 0);
260         dst += (framebuf[58] ? 1 : 0);
261         switch(dst) {
262                 case 0:                         // dst not in effect (make no adjustments)
263                         break;
264                 case 2:                         // dst begins today (adjust if local hour > 2)
265                         if (newhour >= 2) {
266                                 ++newhour;
267                         }
268                         break;
269                 case 3:                         // dst is in effect (always adjust)
270                         ++newhour;
271                         break;
272                 case 1:                         // dst ends today (adjust if local hour < 2)
273                         if (newhour < 2) {
274                                 ++newhour;
275                         }
276                         break;
277         }
278
279         // If we went back to the previous day, adjust so that hour > 0
280         if (newhour < 0) {
281                 newhour += 24;
282         }
283
284         // Set the software clock:
285         // * We have decoded the hour and minute from the signal
286         // * This function always gets called *after* the first pulse at :00, so we set the millisecond to 800
287         hour = newhour;
288         minute = newminute;
289         millisecond = 800;
290         time_is_set = 1;
291
292         // Let's remember the last time we synced the clock
293         last_sync = millis();
294 }
295
296
297 // Adjust the time to :00.8 seconds at the nearest minute.
298 void snap_to_zero()
299 {
300   if ((millisecond > 0) && (millisecond < 15000)) {   // If the second is from :00.0 to :15.0
301     millisecond = 800;                                // snap back to :00.8
302   }
303   else if (millisecond > 45000) {                     // If the second is :45.0 or above
304     millisecond = millis_per_minute + 800;      // snap forward to :00.8 (minute will advance automatically)
305   }
306 }
307
308
309 // By determining how many timer ticks elapsed between two minute markers, we can calibrate our software clock.
310 // Nominally it is 60000 milliseconds, but the software clock tends to drift.
311 void calibrate()
312 {
313   static unsigned long last_calib = -86398000;
314   unsigned long m = millis();
315   unsigned long mm = m - last_calib;
316   if ((mm > 50000) && (mm < 70000)) {
317     millis_per_minute = mm;
318   }
319   last_calib = m;
320 }