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