Update wiegand door code, fix log download, add OTA updates
[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>
bf22b081 9#include <ArduinoOTA.h>
f1139a83
B
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
bf22b081
B
52// should contain #def's for www_username and www_password
53#include "config.h"
f1139a83
B
54
55// files to store card/fob data in
56#define CARD_TMPFILE "/cards.tmp"
57#define CARD_FILE "/cards.dat"
0d5d6475 58#define LOG_FILE "/log.dat"
f1139a83
B
59
60// how long to hold the latch open in millis
61#define LATCH_HOLD 5000
62
0d5d6475
B
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
bf22b081
B
69// NTP retry time (mS)
70#define NTP_RETRY 30000
71
f1139a83
B
72/***************************
73 * code below
74 */
75WIEGAND wg;
0d5d6475
B
76ESP8266WebServer server(CONFIG_PORT);
77
78const unsigned int localPort = 2390;
79IPAddress ntpServerIP;
80
81const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
82
83byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
84
85WiFiUDP udp;
86
87
bb055419
B
88unsigned long ntp_lastset = 0;
89unsigned long ntp_lasttry = 0;
90
91
0d5d6475
B
92/* compose and send an NTP time request packet */
93void ntp_send()
94{
95 if (ntpServerIP == INADDR_NONE) {
bb055419
B
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 }
0d5d6475
B
108 }
109
bb055419 110 ntp_lasttry = millis();
0d5d6475
B
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 */
134time_t ntp_fetch()
135{
136 while (udp.parsePacket() > 0); // discard old udp packets
137 ntp_send();
f1139a83 138
0d5d6475 139 uint32_t beginWait = millis();
f1139a83 140
0d5d6475
B
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
bb055419 151 ntp_lastset = millis();
0d5d6475
B
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 */
f1139a83
B
162int 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
0d5d6475
B
173
174/* HTTP page request for / */
f1139a83
B
175void 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>\
0d5d6475
B
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";
f1139a83
B
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
0d5d6475
B
229 if (SPIFFS.exists(LOG_FILE)) {
230 out += "<li><a href=\"/wipelog\">Wipe log file</a>";
bf22b081 231 out += "<li><a href=\"/viewlog?count=30\">View entry log</a>";
0d5d6475 232 out += "<li><a href=\"/download_logfile\">Download full logfile</a>";
f1139a83 233 }
0d5d6475 234
0d5d6475 235 out += "</ul>";
0d5d6475 236 out += "</body>\
f1139a83
B
237</html>";
238
239 server.send( 200, "text/html", out);
240}
241
bf22b081
B
242void 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
f1139a83
B
260void 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
0d5d6475
B
275void 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
bf22b081
B
284// need to do this chunked.
285// https://github.com/luc-github/ESP3D/blob/master/esp3d/webinterface.cpp#L214-L394
0d5d6475
B
286void handleDownloadLogfile()
287{
288 if (!server.authenticate(www_username, www_password))
289 return server.requestAuthentication();
290
bf22b081
B
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("");
0d5d6475
B
328}
329
f1139a83
B
330void handleNotFound() {
331 String out = "File Not found\n\n";
332 server.send(404, "text/plain", out);
333}
334
335// User wants to reset config
336void 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
349void 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\" />\
353Select 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
359File uploadFile;
360
361String upload_error;
362int upload_code = 200;
363
364void 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
397void 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 }
0d5d6475 408 out += "</p><a href=\"/\">Back</a>";
f1139a83
B
409 server.send(upload_code, "text/plain", out);
410}
411
412
413void returnOK() {
414 server.send(200, "text/plain", "");
415}
416
0d5d6475
B
417String 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;
f1139a83
B
432}
433
0d5d6475 434String getDate(time_t when)
f1139a83 435{
0d5d6475
B
436 String ans;
437
438 ans += String(year(when)) + "-" + String(month(when)) + "-" + String(day(when));
439 return ans;
f1139a83
B
440}
441
0d5d6475 442
f1139a83
B
443int 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
472String 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
bb055419 514// add an entry to the log
0d5d6475
B
515void 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
bb055419 532// produce a copy of the log file
bf22b081 533String printLog(int last)
0d5d6475
B
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);
bf22b081 547 out += "Last " + String(last) + " log entries :-";
0d5d6475 548 }
bf22b081 549 out += "<ul>";
0d5d6475
B
550
551 while (f.available()) {
552 f.read(entry, 8);
bf22b081 553 out += "<li> ";
0d5d6475
B
554 out += getDate( data[0] );
555 out += " ";
556 out += getTime( data[0] );
bf22b081 557 out += " - ";
0d5d6475
B
558
559 if (data[1] == 0) {
bf22b081 560 out += "<i>";
0d5d6475 561 out += "Emergency Release";
bf22b081 562 out += "</i>";
0d5d6475
B
563 } else {
564 String whom = findKeyfob(data[1]);
565 if (whom == "") {
bf22b081 566 out += "<i>by ";
0d5d6475 567 out += "Unknown keyfob";
bf22b081 568 out += "</i>";
0d5d6475
B
569 } else {
570 out += whom;
571 }
bf22b081 572 out += " (" + String(data[1]) + ")";
0d5d6475
B
573 }
574 out += "\n";
575 }
576 f.close();
bf22b081 577 out += "</ul>";
0d5d6475
B
578 return out;
579}
580
581
582static InputDebounce release_button;
583
584/********************************************
585 * Main setup routine
586 */
587void setup() {
588 // some serial, for debug
589 Serial.begin(115200);
590
bb055419 591 // The lock mechanism
0d5d6475
B
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);
f1139a83 606
0d5d6475
B
607 Serial.println("DoorLock. Testing WiFi config...");
608
609 // if we have no config, enter config mode
610 WiFiManager wfm;
bb055419
B
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...
0d5d6475
B
616 wfm.autoConnect(MANAGER_AP);
617
bf22b081
B
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();
bb055419 638 Serial.println("Entering normal doorlock mode.");
bf22b081 639
0d5d6475
B
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 );
bf22b081 645 server.on( "/viewlog", handleViewLog );
0d5d6475
B
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
bb055419 664 Serial.println("Configuring Wiegand keyfob reader");
0d5d6475
B
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
bb055419 670 Serial.println("Requesting time from network");
0d5d6475
B
671 // listener port for replies from NTP
672 udp.begin(localPort);
673 setSyncProvider(ntp_fetch);
bb055419
B
674
675 Serial.println("Hackspace doorlock v1. READY");
0d5d6475
B
676}
677
678unsigned long locktime = 0;
679
680
681void 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
698void loop() {
f1139a83
B
699 // is the latch held open ?
700 if (locktime != 0) {
0d5d6475 701 if (locktime + LATCH_HOLD < millis()) {
f1139a83
B
702 locktime = 0;
703 digitalWrite(MOSFET, LOCK_CLOSE);
704 digitalWrite(DOORLED, DLED_RED);
705 }
706 }
707 // handle web requests
708 server.handleClient();
bf22b081 709 ArduinoOTA.handle();
f1139a83 710
0d5d6475 711 unsigned int ertime = release_button.process(millis());
f1139a83
B
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();
0d5d6475 719 logEntry(now(), 0);
f1139a83
B
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();
0d5d6475 741 logEntry(now(), code);
f1139a83
B
742 }
743 }
744
bb055419 745 // has ntp failed, do we need to try again?
bf22b081 746 if (ntp_lastset == 0 && ntp_lasttry + NTP_RETRY < millis()) {
bb055419
B
747 Serial.println("Ask Time service to try again");
748 setSyncProvider(ntp_fetch);
749 }
f1139a83 750}