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