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