d437e392bfbd2ec2182770119718611d84251a2f
[the_perfect_clock.git] / the_perfect_clock.ino
1 // "The Perfect Clock" 
2
3 // Copyright (C) 2019-2023 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 // On my clock, there is a green LED on 2, a yellow LED on 3, and a red LED on 4.
23
24 const uint8_t wwvb = 9;         // pin on which WWVB signal will be received
25 const uint8_t last24led = 2;    // An LED attached to this pin will illuminate if the time has been set within the last 24 hours
26 const uint8_t cleantimecodeled = LED_BUILTIN;   // An LED attached to this pin will illuminate if we are currently receiving a clean frame
27 const uint8_t timecodeled = 3;
28 const uint8_t photocell = A0;   // Attach a photocell with a 10K voltage divider to this pin
29 const uint8_t addr = 0x70;      // I2C address of HT16K33 (using Adafruit backpack with digits on 0,1,3,4; dots on 2)
30
31 long millis_per_minute = 60000; // Nominally 60000; adjust if your board runs fast or slow
32
33 // This is a simple BCD-to-7-segment font.  It includes 0x0A through 0x0F even though they're not needed for a time clock.
34 const uint8_t sevensegfont[] = { 63, 6, 91, 79, 102, 109, 125, 7, 127, 111, 119, 124, 57, 94, 121, 113 };
35 const uint8_t firstcolfont[] = { 0, 6, 91 };    // this version of the font is for the first position
36
37 #include <Wire.h>               // I2C library to drive the HT16K33 display
38
39 int hour = 0;
40 int minute = 0;
41 unsigned long millisecond = 0;
42 unsigned long previous_millis = 0;
43 unsigned long last_sync = -86398000;
44 uint16_t displayBuffer[8];      // Digit buffer for HT16K33
45 int previous_minute = 61;       // What the minute was previously; we use this to detect whether an update is needed
46 int this_pulse = 0;             // Value of the current pulse received
47 int previous_pulse = 0;         // Value of the previous pulse received (two "mark" bits == new frame)
48 int start_of_pulse = 0;         // The value of the millis() timer when the current pulse began
49 uint8_t framebuf[60];           // We store the entire 60-bit frame here
50 uint8_t framesync = 0;          // Nonzero if we've received all good pulses since the start of the frame
51 int position_in_frame = 0;      // Where we are in the frame (1 bit per second)
52 int previous_signal = 0;        // "high" or "low" received on the previous cycle (so we can do edge detection)
53 int time_is_set = 0;            // nonzero when time has been set at least once
54
55 void setup() {
56         int i;
57
58         pinMode(timecodeled, OUTPUT);   // The built-in LED will display the raw WWVB signal pulses
59         pinMode(last24led, OUTPUT);     // This LED will illuminate if the time has been set within the last 24 hours
60         pinMode(cleantimecodeled, OUTPUT);      // This LED will illuminate if we are currently receiving a clean frame
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   int signal;
91   static int total_samples = 0;
92   static int high_samples = 0;
93
94   // read from the WWVB receiver, with some hysteresis
95   if (total_samples >= 100) {
96     if (high_samples > 20) {
97       signal = HIGH;
98     }
99     else {
100       signal = LOW;
101     }
102     total_samples = 0;
103     high_samples = 0;
104   }
105   else {
106     total_samples += 1;
107     if (digitalRead(wwvb) == HIGH) {
108       ++high_samples;
109     }
110   }
111
112   // has the timer ticked?
113         unsigned long m = millis();
114         if (m != previous_millis) {
115                 millisecond += (m - previous_millis);
116                 if (millisecond >= millis_per_minute) {
117                         millisecond -= millis_per_minute;
118                         ++minute;
119                         if (minute > 59) {
120                                 minute = 0;
121                                 ++hour;
122                                 if (hour > 23) {
123                                         hour = 0;
124                                 }
125                         }
126                 }
127   }
128         previous_millis = m;
129
130         int pulse_length;
131
132   if (signal) {
133     analogWrite(timecodeled, 5);       // it's too bright on my board so we dim it; change to digitalWrite() if not needed
134   }
135   else {
136     digitalWrite(timecodeled, LOW);
137   }
138
139         if (signal && (!previous_signal)) {                     // leading edge of pulse detected
140                 start_of_pulse = millis();
141         }
142         else if ((!signal) && (previous_signal)) {              // trailing edge of pulse detected
143                 pulse_length = millis() - start_of_pulse;
144
145                 if (pulse_length > 175 && pulse_length < 225) { // "0" bit ~= 200 ms (represented as "0")
146                         this_pulse = 0;
147                 }
148                 else if (pulse_length > 475 && pulse_length < 525) {    // "1" bit ~= 500 ms (represented as "1")
149                         this_pulse = 1;
150                 }
151                 else if (pulse_length > 775 && pulse_length < 825) {    // marker bit ~= 800 ms (represented as "2")
152                         this_pulse = 2;
153                 }
154                 else {
155                         this_pulse = 15;        // bad pulse (represented as "15")
156                         framesync = 0;  // throw the whole frame away
157                 }
158
159                 // BEGIN -- THINGS TO DO AT THE END OF A PULSE
160
161                 if ((this_pulse == 2) && (previous_pulse == 2)) {       // start of a new frame!
162
163                         if (framesync == 1) {
164                                 set_the_time(); // We have a whole good frame.  Set the clock!
165                         }
166                         else if ((!framesync) && (time_is_set)) {
167                                 snap_to_zero(); // We don't have a whole frame, but we know it's :00 seconds now.
168                         }
169
170                         framesync = 1;
171                         position_in_frame = 0;
172                         calibrate();    // calibrate the software timer
173                 }
174
175                 if (framesync) {        // yellow LED = we currently have frame sync
176                         digitalWrite(cleantimecodeled, HIGH);   // (we run it at a low intensity)
177                 }
178                 else {
179                         digitalWrite(cleantimecodeled, LOW);
180                 }
181
182                 if ((framesync) && (position_in_frame < 60)) {
183                         framebuf[position_in_frame++] = this_pulse;
184                 }
185
186                 previous_pulse = this_pulse;
187
188                 // END -- THINGS TO DO AT THE END OF A PULSE
189         }
190
191         previous_signal = signal;
192
193         // Update the display only if it's a new minute.
194
195         if (time_is_set && (minute != previous_minute)) {
196                 previous_minute = minute;
197                 int h12 = (hour % 12);
198                 if (h12 == 0)
199                         h12 = 12;
200                 displayBuffer[0] = firstcolfont[h12 / 10];
201                 displayBuffer[1] = sevensegfont[h12 % 10];
202                 displayBuffer[2] = (hour < 12) ? 0x06 : 0x0a;   // AM or PM dot , colon always on
203                 displayBuffer[3] = sevensegfont[minute / 10];
204                 displayBuffer[4] = sevensegfont[minute % 10];
205                 show();
206         }
207
208         if ((m - last_sync) < 86400000) {       // green LED = got a good sync in the last 24 hours
209                 digitalWrite(last24led, HIGH);
210         }
211         else {
212                 digitalWrite(last24led, LOW);
213         }
214 }
215
216
217 // Write the display buffer to the display
218 void show() {
219         // display the time
220         Wire.beginTransmission(addr);
221         Wire.write(0x00);       // start at address 0x0
222         for (int i = 0; i < 5; i++) {
223                 Wire.write(displayBuffer[i] & 0xFF);
224                 Wire.write(displayBuffer[i] >> 8);
225         }
226         Wire.endTransmission();
227
228         // set the brightness
229         int light_level = analogRead(photocell) / 64;
230         if (light_level < 1) {
231                 light_level = 1;
232         }
233         if (light_level > 15) {
234                 light_level = 15;
235         }
236         Wire.beginTransmission(addr);
237         Wire.write(0xE0 + light_level); // set the display brightness
238         Wire.endTransmission();
239 }
240
241
242 // Set the software clock to the WWVB time currently in the buffer
243 void set_the_time() {
244         int i, newhour, newminute, dst;
245
246         // These six positions MUST contain marker bits.
247         // If any of them do not, we are looking at a corrupt frame.
248         int markers[] = { 0, 9, 19, 39, 49, 59 };
249         for (i = 0; i < 6; ++i) {
250                 if (framebuf[markers[i]] != 2) {
251                         return;
252                 }
253         }
254
255         newhour = (framebuf[12] ? 20 : 0);
256         newhour += (framebuf[13] ? 10 : 0);
257         newhour += (framebuf[15] ? 8 : 0);
258         newhour += (framebuf[16] ? 4 : 0);
259         newhour += (framebuf[17] ? 2 : 0);
260         newhour += (framebuf[18] ? 1 : 0);
261         if ((newhour < 0) || (newhour > 23)) {
262                 return;         // reject impossible hours
263         }
264
265         newminute = (framebuf[1] ? 40 : 0);
266         newminute += (framebuf[2] ? 20 : 0);
267         newminute += (framebuf[3] ? 10 : 0);
268         newminute += (framebuf[5] ? 8 : 0);
269         newminute += (framebuf[6] ? 4 : 0);
270         newminute += (framebuf[7] ? 2 : 0);
271         newminute += (framebuf[8] ? 1 : 0);
272         if ((newminute < 0) || (newminute > 59)) {
273                 return;         // reject impossible minutes
274         }
275
276         // advance 1 minute because WWVB gives the *previous* minute
277         newminute += 1;
278         if (newminute >= 60) {
279                 newminute = newminute % 60;
280                 newhour += 1;
281         }
282
283         // US Eastern time (yes it is hard coded)
284         newhour -= 5;
285
286         // DST (FIXME make this adjustable)
287         dst = (framebuf[57] ? 2 : 0);
288         dst += (framebuf[58] ? 1 : 0);
289         switch (dst) {
290         case 0:         // dst not in effect (make no adjustments)
291                 break;
292         case 2:         // dst begins today (adjust if local hour > 2)
293                 if (newhour >= 2) {
294                         ++newhour;
295                 }
296                 break;
297         case 3:         // dst is in effect (always adjust)
298                 ++newhour;
299                 break;
300         case 1:         // dst ends today (adjust if local hour < 2)
301                 if (newhour < 2) {
302                         ++newhour;
303                 }
304                 break;
305         }
306
307         // If we went back to the previous day, adjust so that hour > 0
308         if (newhour < 0) {
309                 newhour += 24;
310         }
311
312         // Set the software clock:
313         // * We have decoded the hour and minute from the signal
314         // * This function always gets called *after* the first pulse at :00, so we set the millisecond to 800
315         hour = newhour;
316         minute = newminute;
317         millisecond = 800;
318         time_is_set = 1;
319
320         // Let's remember the last time we synced the clock
321         last_sync = millis();
322 }
323
324
325 // Adjust the time to :00.8 seconds at the nearest minute.
326 void snap_to_zero() {
327         if ((millisecond > 0) && (millisecond < 15000)) {       // If the second is from :00.0 to :15.0
328                 millisecond = 800;      // snap back to :00.8
329         }
330         else if (millisecond > 45000) { // If the second is :45.0 or above
331                 millisecond = millis_per_minute + 800;  // snap forward to :00.8 (minute will advance automatically)
332         }
333 }
334
335
336 // By determining how many timer ticks elapsed between two minute markers, we can calibrate our software clock.
337 // Nominally it is 60000 milliseconds, but the software clock tends to drift.
338 // So we start with an array of all 60000 ms, and we keep ten calibrations and average them.
339 void calibrate() {
340
341         static unsigned long mpm_array[10] = { 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000 };
342         static int mpm = 0;                             // next one to update
343
344         static unsigned long last_calib = -86398000;
345         unsigned long m = millis();
346         unsigned long mm = m - last_calib;
347         if ((mm > 50000) && (mm < 70000)) {
348                 mpm_array[mpm++] = mm;
349                 if (mpm >= 10) {
350                         mpm = 0;
351                 }
352                 millis_per_minute = (mpm_array[0] + mpm_array[1] + mpm_array[2] + mpm_array[3]
353                                 + mpm_array[4] + mpm_array[5] + mpm_array[6] + mpm_array[7]
354                                 + mpm_array[8] + mpm_array[9]) / 10;
355         }
356         last_calib = m;
357 }