Improve the fault tolerance, and recovery from network missing on bootup
[doorlock_v1.git] / door_wiegand.ino
1 #include <TimeLib.h>
2 #include <Time.h>
3 #include <InputDebounce.h>
4 #include <Wiegand.h>
5 #include <ESP8266WiFi.h>
6 #include <WiFiUdp.h>
7 #include <DNSServer.h>
8 #include <ESP8266WebServer.h>
9 #include <WiFiManager.h>
10 #include <ESP8266mDNS.h>
11 #include <FS.h>
12
13 // MOSFET for door lock activation
14 // ON/HIGH == ground the output screw terminal
15 #define MOSFET 16
16
17 // Pin for LED / WS2812
18 #define STATUSLED 2
19
20 // Wiegand keyfob reader pins
21 #define WD0 12
22 #define WD1 13
23
24 // Door lock sense pin
25 #define SENSE 14
26
27 // emergency release switch
28 #define ERELEASE 15
29
30 // Buzzer/LED on keyfob control
31 #define BUZZER 4   // Gnd to beep
32 #define DOORLED 5  // low = green, otherwise red
33
34 // orientation of some signals
35 #define DLED_GREEN LOW
36 #define DLED_RED HIGH
37 #define LOCK_OPEN HIGH
38 #define LOCK_CLOSE LOW
39 #define BUZZ_ON LOW
40 #define BUZZ_OFF HIGH
41
42
43
44 /***********************
45  * Configuration parameters
46  */
47 // AP that it will apoear as for configuration
48 #define MANAGER_AP "DoorLock"
49
50 // Credentials required to reset or upload new info
51 #define www_username "admin"
52 #define www_password "wibble"
53
54 // files to store card/fob data in
55 #define CARD_TMPFILE "/cards.tmp"
56 #define CARD_FILE "/cards.dat"
57 #define LOG_FILE "/log.dat"
58
59 // how long to hold the latch open in millis
60 #define LATCH_HOLD 5000
61
62 // webserver for configuration portnumber
63 #define CONFIG_PORT 80
64
65 // ntp server to use
66 #define NTP_SERVER "1.uk.pool.ntp.org"
67
68 /***************************
69  * code below
70  */
71 WIEGAND wg;
72 ESP8266WebServer server(CONFIG_PORT);
73
74 const unsigned int localPort = 2390;
75 IPAddress ntpServerIP;
76
77 const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
78
79 byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
80
81 WiFiUDP udp;
82
83
84 unsigned long ntp_lastset = 0;
85 unsigned long ntp_lasttry = 0;
86
87
88 /* compose and send an NTP time request packet */
89 void ntp_send()
90 {
91   if (ntpServerIP == INADDR_NONE) {
92     if (WiFi.hostByName(NTP_SERVER, ntpServerIP) == 1) {
93       if (ntpServerIP == IPAddress(1,0,0,0)) {
94         Serial.println("DNS lookup failed for " NTP_SERVER " try again later.");
95         ntpServerIP = INADDR_NONE;
96         return;
97       }
98       Serial.print("Got NTP server " NTP_SERVER " address ");
99       Serial.println(ntpServerIP);
100     } else {
101       Serial.println("DNS lookup of " NTP_SERVER " failed.");
102       return;
103     }
104   }
105   
106   ntp_lasttry = millis();
107   memset(packetBuffer, 0, NTP_PACKET_SIZE);
108   // Initialize values needed to form NTP request
109   // (see URL above for details on the packets)
110   packetBuffer[0] = 0b11100011;   // LI, Version, Mode
111   packetBuffer[1] = 0;     // Stratum, or type of clock
112   packetBuffer[2] = 6;     // Polling Interval
113   packetBuffer[3] = 0xEC;  // Peer Clock Precision
114   // 8 bytes of zero for Root Delay & Root Dispersion
115   packetBuffer[12]  = 49;
116   packetBuffer[13]  = 0x4E;
117   packetBuffer[14]  = 49;
118   packetBuffer[15]  = 52;
119
120   // all NTP fields have been given values, now
121   // you can send a packet requesting a timestamp:
122   udp.beginPacket(ntpServerIP, 123); //NTP requests are to port 123
123   udp.write(packetBuffer, NTP_PACKET_SIZE);
124   udp.endPacket(); 
125
126   Serial.println("Sending NTP request");
127 }
128
129 /* request a time update from NTP and parse the result */
130 time_t ntp_fetch()
131 {
132   while (udp.parsePacket() > 0); // discard old udp packets
133   ntp_send();
134
135   uint32_t beginWait = millis();
136
137   while (millis() - beginWait < 2500) {
138     int size = udp.parsePacket();
139     if (size >= NTP_PACKET_SIZE) {
140       udp.read(packetBuffer, NTP_PACKET_SIZE);
141   
142       // this is NTP time (seconds since Jan 1 1900):
143       unsigned long secsSince1900 = packetBuffer[40] << 24 | packetBuffer[41] << 16 | packetBuffer[42] << 8 | packetBuffer[43];
144       const unsigned long seventyYears = 2208988800UL;
145       time_t unixtime = secsSince1900 - seventyYears;
146
147       ntp_lastset = millis();
148       Serial.print("NTP update unixtime=");
149       Serial.println(unixtime);
150       return unixtime;
151     }
152   }
153   Serial.println("No NTP response");
154   return 0;
155 }
156
157 /* how big is a file */
158 int fileSize(const char *filename)
159 {
160   int ret = -1;
161   File file = SPIFFS.open(filename, "r");
162   if (file) {
163     ret = file.size();
164     file.close();
165   }
166   return ret;
167 }
168
169
170 /* HTTP page request for /  */
171 void handleRoot()
172 {
173   char mtime[16];
174   int sec = millis() / 1000;
175   int mi = sec / 60;
176   int hr = mi / 60;
177   int day = hr / 24;
178   
179   snprintf(mtime, 16, "%dd %02d:%02d:%02d", day, hr % 24, mi % 60, sec % 60);
180     
181   String out = "<html>\
182   <head>\
183     <title>Door Lock</title>\
184     <style>\
185       body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; }\
186     </style>\
187   </head>\
188   <body>\
189     <h1>Door Lock!</h1>\
190     <p>Uptime: " + (String)mtime + "</p>\n";
191
192   if (timeStatus() == timeSet) {
193     time_t when = now();
194     out += "<p>Time now: " + getDate(when) + " " + getTime(when) + "</p>\n";
195   }
196
197
198   FSInfo fs_info;
199   if (SPIFFS.info(fs_info)) {
200     out += "<p>Onboard Flash disk: - Size:"+String(fs_info.totalBytes)+" Used:"+String(fs_info.usedBytes)+"</p>\n";
201   }
202
203   out += "<p>Lock is currently ";
204   if (digitalRead(SENSE) == HIGH) out += "LOCKED"; else out += "OPEN";
205   out += "</p>\n";
206
207   if (SPIFFS.exists(CARD_FILE)) {
208     
209      out += "<p>Cardfile: " + String(CARD_FILE) + " is " + fileSize(CARD_FILE) + " bytes";
210      int count = sanityCheck(CARD_FILE);
211      if (count <= 0) {
212       out += ", in an invalid file";
213      } else {
214       out += ", contains " + String(count) + " keyfob IDs";
215       out += " - <a href=\"/download\">Download</a>";
216      }
217      
218      out += ".</p>";
219   }
220
221   out += "<ul>\
222       <li><a href=\"/reset\">Reset Configuration</a>\
223       <li><a href=\"/upload\">Upload Cardlist</a>";
224
225   
226   if (SPIFFS.exists(LOG_FILE)) {
227     out += "<li><a href=\"/wipelog\">Wipe log file</a>";
228     out += "<li><a href=\"/download_logfile\">Download full logfile</a>";
229   }
230   
231       
232   out += "</ul>";
233
234   if (SPIFFS.exists(LOG_FILE)) out += printLog(true, 10);
235
236   out += "</body>\
237 </html>";
238
239   server.send( 200, "text/html", out);
240 }
241
242 void handleDownload()
243 {
244   if (!server.authenticate(www_username, www_password))
245     return server.requestAuthentication();
246
247   if (!SPIFFS.exists(CARD_FILE)) {
248     server.send(404, "text/plain", "Card file not found");
249     return;
250   }
251
252   File f = SPIFFS.open(CARD_FILE, "r");
253   server.streamFile(f, "text/csv");
254   f.close();
255 }
256
257 void handleWipelog()
258 {
259   if (!server.authenticate(www_username, www_password))
260     return server.requestAuthentication();
261
262   SPIFFS.remove(LOG_FILE);
263   server.send(200, "text/plain", "logfile deleted");
264 }
265
266 void handleDownloadLogfile()
267 {
268   if (!server.authenticate(www_username, www_password))
269     return server.requestAuthentication();
270
271   String result = printLog(false, 0);
272   server.send(200, "text/csv", result);
273 }
274
275 void handleNotFound() {
276   String out = "File Not found\n\n";
277   server.send(404, "text/plain", out);
278 }
279
280 // User wants to reset config
281 void handleReset() {
282   if (!server.authenticate(www_username, www_password))
283     return server.requestAuthentication();
284
285   server.send(200, "text/plain", "Rebooting to config manager...\n\n");
286
287   WiFiManager wfm;
288   wfm.resetSettings();
289   WiFi.disconnect();
290   ESP.reset();
291   delay(5000);
292 }
293
294 void handleUploadRequest() {
295   String out = "<html><head><title>Upload Keyfob list</title></head><body>\
296 <form enctype=\"multipart/form-data\" action=\"/upload\" method=\"POST\">\
297 <input type=\"hidden\" name=\"MAX_FILE_SIZE\" value=\"32000\" />\
298 Select file to upload: <input name=\"file\" type=\"file\" />\
299 <input type=\"submit\" value=\"Upload file\" />\
300 </form></body></html>";
301   server.send(200, "text/html", out);
302 }
303
304 File uploadFile;
305
306 String upload_error;
307 int upload_code = 200;
308
309 void handleFileUpload()
310 {
311   if (server.uri() != "/upload") return;
312
313   if (!server.authenticate(www_username, www_password))
314     return server.requestAuthentication();
315
316   HTTPUpload& upload = server.upload();
317   if (upload.status == UPLOAD_FILE_START) {
318     upload_error = "";
319     upload_code = 200;
320     uploadFile = SPIFFS.open(CARD_TMPFILE, "w");
321     if (!uploadFile) {
322       upload_error = "error opening file";
323       Serial.println("Opening tmpfile failed!");
324       upload_code = 403;
325     }
326   }else
327   if (upload.status == UPLOAD_FILE_WRITE) {
328     if (uploadFile) {
329       if (uploadFile.write(upload.buf, upload.currentSize) != upload.currentSize) {
330         upload_error = "write error";
331         upload_code = 409;
332       }
333     }
334   }else
335   if (upload.status == UPLOAD_FILE_END) {
336     if (uploadFile) {
337       uploadFile.close();
338     }
339   }
340 }
341
342 void handleUploadComplete()
343 {
344   String out = "Upload finished.";
345   if (upload_code != 200) {
346     out += "Error: "+upload_error;
347   } else {
348     out += " Success";
349     // upload with no errors, replace old one
350     SPIFFS.remove(CARD_FILE);
351     SPIFFS.rename(CARD_TMPFILE, CARD_FILE);
352   }
353   out += "</p><a href=\"/\">Back</a>";
354   server.send(upload_code, "text/plain", out);
355 }
356
357
358 void returnOK() {
359   server.send(200, "text/plain", "");
360 }
361
362 String getTime(time_t when)
363 {
364   String ans;
365   int h = hour(when);
366   int m = minute(when);
367   int s = second(when);
368
369   if (h<10) ans += "0";
370   ans += String(h) + ":";
371   if (m<10) ans += "0";
372   ans += String(m) + ":";
373   if (s<10) ans += "0";
374   ans += String(s);
375
376   return ans;
377 }
378
379 String getDate(time_t when)
380 {
381   String ans;
382
383   ans += String(year(when)) + "-" + String(month(when)) + "-" + String(day(when));
384   return ans;
385 }
386
387
388 int sanityCheck(const char * filename)
389 {
390   int count = 0;
391   
392   File f = SPIFFS.open(filename, "r");
393   if (!f) {
394     Serial.print("Sanity Check: Could not open ");
395     Serial.println(filename);
396     return -1;
397   }
398   while (f.available()) {
399     char c = f.peek();
400     // skip comment lines
401     if (c == '#') {
402       f.find("\n");
403       continue;
404     }
405
406     String wcode = f.readStringUntil(',');
407     String wname = f.readStringUntil('\n');
408     unsigned int newcode = wcode.toInt();
409
410     if (newcode != 0) count++;
411   }
412   f.close();
413
414   return count; 
415 }
416
417 String findKeyfob(unsigned int code)
418 {
419   File f = SPIFFS.open(CARD_FILE, "r");
420   if (!f) {
421     Serial.println("Error opening card file " CARD_FILE);
422     return "";
423   }
424
425   String answer = "";
426   while (f.available()) {
427     char c = f.peek();
428     // skip comment lines
429     if (c == '#') {
430       f.find("\n");
431       continue;
432     }
433
434     String wcode = f.readStringUntil(',');
435     String wname = f.readStringUntil('\n');
436
437     unsigned int newcode = wcode.toInt();
438
439 /* debug
440     Serial.print("Line: code='");
441     Serial.print(wcode);
442     Serial.print("' (");
443     Serial.print(newcode);
444     Serial.print(") name='");
445     Serial.print(wname);
446     Serial.print("'");
447 */
448     if (code == newcode) {
449    //   Serial.println(" - FOUND IT");
450       answer = wname;
451       break;
452     }
453     //Serial.println();
454   }
455   f.close();
456   return answer;
457 }
458
459 // add an entry to the log
460 void logEntry(time_t when, uint32_t card)
461 {
462   unsigned char entry[8];
463   
464   File f = SPIFFS.open(LOG_FILE, "a");
465   if (!f) {
466     Serial.println("Error opening log file");
467     return;
468   }
469
470   // compose the record to write
471   ((uint32_t *)entry)[0] = when;
472   ((uint32_t *)entry)[1] = card;
473   f.write(entry, 8);
474   f.close();
475 }
476
477 // produce a copy of the log file
478 String printLog(int html, int last)
479 {
480   String out;
481   File f = SPIFFS.open(LOG_FILE, "r");
482   if (!f) return String("Could not open log file");
483
484   unsigned char entry[8];
485   uint32_t * data = (uint32_t *)entry;
486
487   if (last != 0) {
488     // print only the last N items
489     int pos = f.size() / 8;
490     if (pos > last) pos -= last; else pos = 0;
491     f.seek( pos * 8, SeekSet);
492     if (html) out += "Last " + String(last) + " log entries :-";
493   }
494   if (html) out += "<ul>";
495   
496   while (f.available()) {
497     f.read(entry, 8);
498     if (html) out += "<li> ";
499     out += getDate( data[0] );
500     out += " ";
501     out += getTime( data[0] );
502     if (html) out += " - "; else out += "," + String(data[1]) + ",";
503     
504     if (data[1] == 0) {
505       if (html) out += "<i>";
506       out += "Emergency Release";
507       if (html) out += "</i>";
508     } else {
509       String whom = findKeyfob(data[1]);
510       if (whom == "") {
511         if (html) out += "<i>by ";
512         out += "Unknown keyfob";
513         if (html) out += "</i>";
514       } else {
515         out += whom;
516       }
517       if (html) out += " (" + String(data[1]) + ")";
518     }
519     out += "\n";
520   }
521   f.close();
522   if (html) out += "</ul>";
523   return out;
524 }
525
526
527 static InputDebounce release_button;
528
529 /********************************************
530  * Main setup routine
531  */
532 void setup() {
533   // some serial, for debug
534   Serial.begin(115200);
535
536   // The lock mechanism
537   pinMode(MOSFET, OUTPUT);
538   digitalWrite(MOSFET, LOCK_CLOSE);
539
540   // lock sense microswitch
541   pinMode(SENSE, INPUT_PULLUP);
542   
543   // emergency door release switch
544   pinMode(ERELEASE, INPUT);
545
546   // indicators on the keyfob reader
547   pinMode(BUZZER, OUTPUT);
548   pinMode(DOORLED, OUTPUT);
549   digitalWrite(BUZZER, BUZZ_OFF);
550   digitalWrite(DOORLED, DLED_RED);
551
552   Serial.println("DoorLock. Testing WiFi config...");
553
554   // if we have no config, enter config mode
555   WiFiManager wfm;
556   // Only wait in config mode for 3 minutes max
557   wfm.setConfigPortalTimeout(180);
558   // Try to connect to the old Ap for this long
559   wfm.setConnectTimeout(60);
560   // okay, lets try and connect...
561   wfm.autoConnect(MANAGER_AP);
562
563   Serial.println("Entering normal doorlock mode.");
564
565   // we have config, enable web server
566   server.on( "/", handleRoot );
567   server.on( "/reset", handleReset );
568   server.on( "/download", handleDownload );
569   server.on( "/wipelog", handleWipelog );
570   server.on( "/download_logfile", handleDownloadLogfile );
571   server.onFileUpload( handleFileUpload);
572   server.on( "/upload", HTTP_GET, handleUploadRequest);
573   server.on( "/upload", HTTP_POST, handleUploadComplete);
574   server.onNotFound( handleNotFound );
575   server.begin();
576
577   // advertise we exist via MDNS
578   if (!MDNS.begin("doorlock")) {
579     Serial.println("Error setting up MDNS responder.");
580   } else {
581     MDNS.addService("http", "tcp", 80);
582   }
583
584   // enable internal flash filesystem
585   SPIFFS.begin();
586
587   // init wiegand keyfob reader
588   Serial.println("Configuring Wiegand keyfob reader");
589   wg.begin(WD0, WD0, WD1, WD1);
590
591   // setup button debounce for the release switch
592   release_button.setup(ERELEASE, 20, InputDebounce::PIM_EXT_PULL_DOWN_RES);
593
594   Serial.println("Requesting time from network");
595   // listener port for replies from NTP
596   udp.begin(localPort);
597   setSyncProvider(ntp_fetch);
598
599   Serial.println("Hackspace doorlock v1. READY");
600 }
601
602 unsigned long  locktime = 0;
603
604
605 void unlock_door()
606 {
607   digitalWrite(DOORLED, DLED_GREEN);
608   digitalWrite(MOSFET, LOCK_OPEN);
609   if (locktime == 0) {
610     digitalWrite(BUZZER, BUZZ_ON);
611     delay(100);
612     digitalWrite(BUZZER, BUZZ_OFF);
613     delay(50);
614     digitalWrite(BUZZER, BUZZ_ON);
615     delay(100);
616     digitalWrite(BUZZER, BUZZ_OFF);
617   }
618   locktime = millis();
619 }
620
621
622 void loop() {
623   // is the latch held open ?
624   if (locktime != 0) {
625     if (locktime + LATCH_HOLD < millis()) {
626       locktime = 0;
627       digitalWrite(MOSFET, LOCK_CLOSE);
628       digitalWrite(DOORLED, DLED_RED);
629     }
630   }
631   // handle web requests
632   server.handleClient();
633
634   unsigned int ertime = release_button.process(millis());
635   unsigned int count = release_button.getStateOnCount();
636   static unsigned last_count = 0;
637   if (ertime > 0) {
638     if (count != last_count) {
639       last_count = count;
640       Serial.println("Door Release button triggered.");
641       unlock_door();
642       logEntry(now(), 0);
643     } else {
644       // buttons is still pressed, do nothing
645     }
646   }
647
648   // handle card swipes
649   if (wg.available()) {
650     unsigned long code = wg.getCode();
651     
652     Serial.print("wiegand HEX = ");
653     Serial.print(code,HEX);
654     Serial.print(", DECIMAL= ");
655     Serial.print(code);
656     Serial.print(", TYPE W");
657     Serial.println(wg.getWiegandType());
658
659     String who = findKeyfob(code);
660     if (who != NULL) {
661       Serial.print("Unlocking door for ");
662       Serial.println(who);
663       unlock_door();
664       logEntry(now(), code);
665     }
666   }
667
668   // has ntp failed, do we need to try again?
669   if (ntp_lastset == 0 && ntp_lasttry + 300000 < millis()) {
670     Serial.println("Ask Time service to try again");
671     setSyncProvider(ntp_fetch);
672   }
673 }