3 #include <InputDebounce.h>
5 #include <ESP8266WiFi.h>
8 #include <ESP8266WebServer.h>
9 #include <WiFiManager.h>
10 #include <ESP8266mDNS.h>
13 // MOSFET for door lock activation
14 // ON/HIGH == ground the output screw terminal
17 // Pin for LED / WS2812
20 // Wiegand keyfob reader pins
24 // Door lock sense pin
27 // emergency release switch
30 // Buzzer/LED on keyfob control
31 #define BUZZER 4 // Gnd to beep
32 #define DOORLED 5 // low = green, otherwise red
34 // orientation of some signals
35 #define DLED_GREEN LOW
37 #define LOCK_OPEN HIGH
38 #define LOCK_CLOSE LOW
44 /***********************
45 * Configuration parameters
47 // AP that it will apoear as for configuration
48 #define MANAGER_AP "DoorLock"
50 // Credentials required to reset or upload new info
51 #define www_username "admin"
52 #define www_password "wibble"
54 // files to store card/fob data in
55 #define CARD_TMPFILE "/cards.tmp"
56 #define CARD_FILE "/cards.dat"
57 #define LOG_FILE "/log.dat"
59 // how long to hold the latch open in millis
60 #define LATCH_HOLD 5000
62 // webserver for configuration portnumber
63 #define CONFIG_PORT 80
66 #define NTP_SERVER "1.uk.pool.ntp.org"
68 /***************************
72 ESP8266WebServer server(CONFIG_PORT);
74 const unsigned int localPort = 2390;
75 IPAddress ntpServerIP;
77 const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
79 byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
84 unsigned long ntp_lastset = 0;
85 unsigned long ntp_lasttry = 0;
88 /* compose and send an NTP time request packet */
91 if (ntpServerIP == INADDR_NONE) {
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;
98 Serial.print("Got NTP server " NTP_SERVER " address ");
99 Serial.println(ntpServerIP);
101 Serial.println("DNS lookup of " NTP_SERVER " failed.");
106 ntp_lasttry = millis();
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;
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);
126 Serial.println("Sending NTP request");
129 /* request a time update from NTP and parse the result */
132 while (udp.parsePacket() > 0); // discard old udp packets
135 uint32_t beginWait = millis();
137 while (millis() - beginWait < 2500) {
138 int size = udp.parsePacket();
139 if (size >= NTP_PACKET_SIZE) {
140 udp.read(packetBuffer, NTP_PACKET_SIZE);
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;
147 ntp_lastset = millis();
148 Serial.print("NTP update unixtime=");
149 Serial.println(unixtime);
153 Serial.println("No NTP response");
157 /* how big is a file */
158 int fileSize(const char *filename)
161 File file = SPIFFS.open(filename, "r");
170 /* HTTP page request for / */
174 int sec = millis() / 1000;
179 snprintf(mtime, 16, "%dd %02d:%02d:%02d", day, hr % 24, mi % 60, sec % 60);
181 String out = "<html>\
183 <title>Door Lock</title>\
185 body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; }\
190 <p>Uptime: " + (String)mtime + "</p>\n";
192 if (timeStatus() == timeSet) {
194 out += "<p>Time now: " + getDate(when) + " " + getTime(when) + "</p>\n";
199 if (SPIFFS.info(fs_info)) {
200 out += "<p>Onboard Flash disk: - Size:"+String(fs_info.totalBytes)+" Used:"+String(fs_info.usedBytes)+"</p>\n";
203 out += "<p>Lock is currently ";
204 if (digitalRead(SENSE) == HIGH) out += "LOCKED"; else out += "OPEN";
207 if (SPIFFS.exists(CARD_FILE)) {
209 out += "<p>Cardfile: " + String(CARD_FILE) + " is " + fileSize(CARD_FILE) + " bytes";
210 int count = sanityCheck(CARD_FILE);
212 out += ", in an invalid file";
214 out += ", contains " + String(count) + " keyfob IDs";
215 out += " - <a href=\"/download\">Download</a>";
222 <li><a href=\"/reset\">Reset Configuration</a>\
223 <li><a href=\"/upload\">Upload Cardlist</a>";
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>";
234 if (SPIFFS.exists(LOG_FILE)) out += printLog(true, 10);
239 server.send( 200, "text/html", out);
242 void handleDownload()
244 if (!server.authenticate(www_username, www_password))
245 return server.requestAuthentication();
247 if (!SPIFFS.exists(CARD_FILE)) {
248 server.send(404, "text/plain", "Card file not found");
252 File f = SPIFFS.open(CARD_FILE, "r");
253 server.streamFile(f, "text/csv");
259 if (!server.authenticate(www_username, www_password))
260 return server.requestAuthentication();
262 SPIFFS.remove(LOG_FILE);
263 server.send(200, "text/plain", "logfile deleted");
266 void handleDownloadLogfile()
268 if (!server.authenticate(www_username, www_password))
269 return server.requestAuthentication();
271 String result = printLog(false, 0);
272 server.send(200, "text/csv", result);
275 void handleNotFound() {
276 String out = "File Not found\n\n";
277 server.send(404, "text/plain", out);
280 // User wants to reset config
282 if (!server.authenticate(www_username, www_password))
283 return server.requestAuthentication();
285 server.send(200, "text/plain", "Rebooting to config manager...\n\n");
294 void 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\" />\
298 Select 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);
307 int upload_code = 200;
309 void handleFileUpload()
311 if (server.uri() != "/upload") return;
313 if (!server.authenticate(www_username, www_password))
314 return server.requestAuthentication();
316 HTTPUpload& upload = server.upload();
317 if (upload.status == UPLOAD_FILE_START) {
320 uploadFile = SPIFFS.open(CARD_TMPFILE, "w");
322 upload_error = "error opening file";
323 Serial.println("Opening tmpfile failed!");
327 if (upload.status == UPLOAD_FILE_WRITE) {
329 if (uploadFile.write(upload.buf, upload.currentSize) != upload.currentSize) {
330 upload_error = "write error";
335 if (upload.status == UPLOAD_FILE_END) {
342 void handleUploadComplete()
344 String out = "Upload finished.";
345 if (upload_code != 200) {
346 out += "Error: "+upload_error;
349 // upload with no errors, replace old one
350 SPIFFS.remove(CARD_FILE);
351 SPIFFS.rename(CARD_TMPFILE, CARD_FILE);
353 out += "</p><a href=\"/\">Back</a>";
354 server.send(upload_code, "text/plain", out);
359 server.send(200, "text/plain", "");
362 String getTime(time_t when)
366 int m = minute(when);
367 int s = second(when);
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";
379 String getDate(time_t when)
383 ans += String(year(when)) + "-" + String(month(when)) + "-" + String(day(when));
388 int sanityCheck(const char * filename)
392 File f = SPIFFS.open(filename, "r");
394 Serial.print("Sanity Check: Could not open ");
395 Serial.println(filename);
398 while (f.available()) {
400 // skip comment lines
406 String wcode = f.readStringUntil(',');
407 String wname = f.readStringUntil('\n');
408 unsigned int newcode = wcode.toInt();
410 if (newcode != 0) count++;
417 String findKeyfob(unsigned int code)
419 File f = SPIFFS.open(CARD_FILE, "r");
421 Serial.println("Error opening card file " CARD_FILE);
426 while (f.available()) {
428 // skip comment lines
434 String wcode = f.readStringUntil(',');
435 String wname = f.readStringUntil('\n');
437 unsigned int newcode = wcode.toInt();
440 Serial.print("Line: code='");
443 Serial.print(newcode);
444 Serial.print(") name='");
448 if (code == newcode) {
449 // Serial.println(" - FOUND IT");
459 // add an entry to the log
460 void logEntry(time_t when, uint32_t card)
462 unsigned char entry[8];
464 File f = SPIFFS.open(LOG_FILE, "a");
466 Serial.println("Error opening log file");
470 // compose the record to write
471 ((uint32_t *)entry)[0] = when;
472 ((uint32_t *)entry)[1] = card;
477 // produce a copy of the log file
478 String printLog(int html, int last)
481 File f = SPIFFS.open(LOG_FILE, "r");
482 if (!f) return String("Could not open log file");
484 unsigned char entry[8];
485 uint32_t * data = (uint32_t *)entry;
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 :-";
494 if (html) out += "<ul>";
496 while (f.available()) {
498 if (html) out += "<li> ";
499 out += getDate( data[0] );
501 out += getTime( data[0] );
502 if (html) out += " - "; else out += "," + String(data[1]) + ",";
505 if (html) out += "<i>";
506 out += "Emergency Release";
507 if (html) out += "</i>";
509 String whom = findKeyfob(data[1]);
511 if (html) out += "<i>by ";
512 out += "Unknown keyfob";
513 if (html) out += "</i>";
517 if (html) out += " (" + String(data[1]) + ")";
522 if (html) out += "</ul>";
527 static InputDebounce release_button;
529 /********************************************
533 // some serial, for debug
534 Serial.begin(115200);
536 // The lock mechanism
537 pinMode(MOSFET, OUTPUT);
538 digitalWrite(MOSFET, LOCK_CLOSE);
540 // lock sense microswitch
541 pinMode(SENSE, INPUT_PULLUP);
543 // emergency door release switch
544 pinMode(ERELEASE, INPUT);
546 // indicators on the keyfob reader
547 pinMode(BUZZER, OUTPUT);
548 pinMode(DOORLED, OUTPUT);
549 digitalWrite(BUZZER, BUZZ_OFF);
550 digitalWrite(DOORLED, DLED_RED);
552 Serial.println("DoorLock. Testing WiFi config...");
554 // if we have no config, enter config mode
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...
561 wfm.autoConnect(MANAGER_AP);
563 Serial.println("Entering normal doorlock mode.");
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 );
577 // advertise we exist via MDNS
578 if (!MDNS.begin("doorlock")) {
579 Serial.println("Error setting up MDNS responder.");
581 MDNS.addService("http", "tcp", 80);
584 // enable internal flash filesystem
587 // init wiegand keyfob reader
588 Serial.println("Configuring Wiegand keyfob reader");
589 wg.begin(WD0, WD0, WD1, WD1);
591 // setup button debounce for the release switch
592 release_button.setup(ERELEASE, 20, InputDebounce::PIM_EXT_PULL_DOWN_RES);
594 Serial.println("Requesting time from network");
595 // listener port for replies from NTP
596 udp.begin(localPort);
597 setSyncProvider(ntp_fetch);
599 Serial.println("Hackspace doorlock v1. READY");
602 unsigned long locktime = 0;
607 digitalWrite(DOORLED, DLED_GREEN);
608 digitalWrite(MOSFET, LOCK_OPEN);
610 digitalWrite(BUZZER, BUZZ_ON);
612 digitalWrite(BUZZER, BUZZ_OFF);
614 digitalWrite(BUZZER, BUZZ_ON);
616 digitalWrite(BUZZER, BUZZ_OFF);
623 // is the latch held open ?
625 if (locktime + LATCH_HOLD < millis()) {
627 digitalWrite(MOSFET, LOCK_CLOSE);
628 digitalWrite(DOORLED, DLED_RED);
631 // handle web requests
632 server.handleClient();
634 unsigned int ertime = release_button.process(millis());
635 unsigned int count = release_button.getStateOnCount();
636 static unsigned last_count = 0;
638 if (count != last_count) {
640 Serial.println("Door Release button triggered.");
644 // buttons is still pressed, do nothing
648 // handle card swipes
649 if (wg.available()) {
650 unsigned long code = wg.getCode();
652 Serial.print("wiegand HEX = ");
653 Serial.print(code,HEX);
654 Serial.print(", DECIMAL= ");
656 Serial.print(", TYPE W");
657 Serial.println(wg.getWiegandType());
659 String who = findKeyfob(code);
661 Serial.print("Unlocking door for ");
664 logEntry(now(), code);
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);