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