Improve the fault tolerance, and recovery from network missing on bootup
[doorlock_v1.git] / door_wiegand.ino
CommitLineData
0d5d6475
B
1#include <TimeLib.h>
2#include <Time.h>
f1139a83
B
3#include <InputDebounce.h>
4#include <Wiegand.h>
5#include <ESP8266WiFi.h>
0d5d6475 6#include <WiFiUdp.h>
f1139a83
B
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"
0d5d6475 57#define LOG_FILE "/log.dat"
f1139a83
B
58
59// how long to hold the latch open in millis
60#define LATCH_HOLD 5000
61
0d5d6475
B
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
f1139a83
B
68/***************************
69 * code below
70 */
71WIEGAND wg;
0d5d6475
B
72ESP8266WebServer server(CONFIG_PORT);
73
74const unsigned int localPort = 2390;
75IPAddress ntpServerIP;
76
77const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
78
79byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
80
81WiFiUDP udp;
82
83
bb055419
B
84unsigned long ntp_lastset = 0;
85unsigned long ntp_lasttry = 0;
86
87
0d5d6475
B
88/* compose and send an NTP time request packet */
89void ntp_send()
90{
91 if (ntpServerIP == INADDR_NONE) {
bb055419
B
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 }
0d5d6475
B
104 }
105
bb055419 106 ntp_lasttry = millis();
0d5d6475
B
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 */
130time_t ntp_fetch()
131{
132 while (udp.parsePacket() > 0); // discard old udp packets
133 ntp_send();
f1139a83 134
0d5d6475 135 uint32_t beginWait = millis();
f1139a83 136
0d5d6475
B
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
bb055419 147 ntp_lastset = millis();
0d5d6475
B
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 */
f1139a83
B
158int 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
0d5d6475
B
169
170/* HTTP page request for / */
f1139a83
B
171void 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>\
0d5d6475
B
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";
f1139a83
B
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
0d5d6475
B
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>";
f1139a83 229 }
0d5d6475 230
f1139a83 231
0d5d6475
B
232 out += "</ul>";
233
234 if (SPIFFS.exists(LOG_FILE)) out += printLog(true, 10);
235
236 out += "</body>\
f1139a83
B
237</html>";
238
239 server.send( 200, "text/html", out);
240}
241
242void 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
0d5d6475
B
257void 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
266void 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
f1139a83
B
275void handleNotFound() {
276 String out = "File Not found\n\n";
277 server.send(404, "text/plain", out);
278}
279
280// User wants to reset config
281void 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
294void 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\" />\
298Select 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
304File uploadFile;
305
306String upload_error;
307int upload_code = 200;
308
309void 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
342void 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 }
0d5d6475 353 out += "</p><a href=\"/\">Back</a>";
f1139a83
B
354 server.send(upload_code, "text/plain", out);
355}
356
357
358void returnOK() {
359 server.send(200, "text/plain", "");
360}
361
0d5d6475
B
362String 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;
f1139a83
B
377}
378
0d5d6475 379String getDate(time_t when)
f1139a83 380{
0d5d6475
B
381 String ans;
382
383 ans += String(year(when)) + "-" + String(month(when)) + "-" + String(day(when));
384 return ans;
f1139a83
B
385}
386
0d5d6475 387
f1139a83
B
388int 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
417String 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
bb055419 459// add an entry to the log
0d5d6475
B
460void 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
bb055419 477// produce a copy of the log file
0d5d6475
B
478String 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
527static InputDebounce release_button;
528
529/********************************************
530 * Main setup routine
531 */
532void setup() {
533 // some serial, for debug
534 Serial.begin(115200);
535
bb055419 536 // The lock mechanism
0d5d6475
B
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);
f1139a83 551
0d5d6475
B
552 Serial.println("DoorLock. Testing WiFi config...");
553
554 // if we have no config, enter config mode
555 WiFiManager wfm;
bb055419
B
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...
0d5d6475
B
561 wfm.autoConnect(MANAGER_AP);
562
bb055419
B
563 Serial.println("Entering normal doorlock mode.");
564
0d5d6475
B
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
bb055419 588 Serial.println("Configuring Wiegand keyfob reader");
0d5d6475
B
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
bb055419 594 Serial.println("Requesting time from network");
0d5d6475
B
595 // listener port for replies from NTP
596 udp.begin(localPort);
597 setSyncProvider(ntp_fetch);
bb055419
B
598
599 Serial.println("Hackspace doorlock v1. READY");
0d5d6475
B
600}
601
602unsigned long locktime = 0;
603
604
605void 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
622void loop() {
f1139a83
B
623 // is the latch held open ?
624 if (locktime != 0) {
0d5d6475 625 if (locktime + LATCH_HOLD < millis()) {
f1139a83
B
626 locktime = 0;
627 digitalWrite(MOSFET, LOCK_CLOSE);
628 digitalWrite(DOORLED, DLED_RED);
629 }
630 }
631 // handle web requests
632 server.handleClient();
633
0d5d6475 634 unsigned int ertime = release_button.process(millis());
f1139a83
B
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();
0d5d6475 642 logEntry(now(), 0);
f1139a83
B
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();
0d5d6475 664 logEntry(now(), code);
f1139a83
B
665 }
666 }
667
bb055419
B
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 }
f1139a83 673}