Updated wiegand doorlock system
[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
84/* compose and send an NTP time request packet */
85void 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 */
117time_t ntp_fetch()
118{
119 while (udp.parsePacket() > 0); // discard old udp packets
120 ntp_send();
f1139a83 121
0d5d6475 122 uint32_t beginWait = millis();
f1139a83 123
0d5d6475
B
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 */
f1139a83
B
144int 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
0d5d6475
B
155
156/* HTTP page request for / */
f1139a83
B
157void 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>\
0d5d6475
B
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";
f1139a83
B
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
0d5d6475
B
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>";
f1139a83 215 }
0d5d6475 216
f1139a83 217
0d5d6475
B
218 out += "</ul>";
219
220 if (SPIFFS.exists(LOG_FILE)) out += printLog(true, 10);
221
222 out += "</body>\
f1139a83
B
223</html>";
224
225 server.send( 200, "text/html", out);
226}
227
228void 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
0d5d6475
B
243void 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
252void 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
f1139a83
B
261void handleNotFound() {
262 String out = "File Not found\n\n";
263 server.send(404, "text/plain", out);
264}
265
266// User wants to reset config
267void 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
280void 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\" />\
284Select 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
290File uploadFile;
291
292String upload_error;
293int upload_code = 200;
294
295void 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
328void 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 }
0d5d6475 339 out += "</p><a href=\"/\">Back</a>";
f1139a83
B
340 server.send(upload_code, "text/plain", out);
341}
342
343
344void returnOK() {
345 server.send(200, "text/plain", "");
346}
347
0d5d6475
B
348String 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;
f1139a83
B
363}
364
0d5d6475 365String getDate(time_t when)
f1139a83 366{
0d5d6475
B
367 String ans;
368
369 ans += String(year(when)) + "-" + String(month(when)) + "-" + String(day(when));
370 return ans;
f1139a83
B
371}
372
0d5d6475 373
f1139a83
B
374int 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
403String 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
0d5d6475
B
445void 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
462String 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
511static InputDebounce release_button;
512
513/********************************************
514 * Main setup routine
515 */
516void 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);
f1139a83 535
0d5d6475
B
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
576unsigned long locktime = 0;
577
578
579void 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
596void loop() {
f1139a83
B
597 // is the latch held open ?
598 if (locktime != 0) {
0d5d6475 599 if (locktime + LATCH_HOLD < millis()) {
f1139a83
B
600 locktime = 0;
601 digitalWrite(MOSFET, LOCK_CLOSE);
602 digitalWrite(DOORLED, DLED_RED);
603 }
604 }
605 // handle web requests
606 server.handleClient();
607
0d5d6475 608 unsigned int ertime = release_button.process(millis());
f1139a83
B
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();
0d5d6475 616 logEntry(now(), 0);
f1139a83
B
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();
0d5d6475 638 logEntry(now(), code);
f1139a83
B
639 }
640 }
641
642}