3 #include <InputDebounce.h>
5 #include <ESP8266WiFi.h>
8 #include <ESP8266WebServer.h>
9 #include <ArduinoOTA.h>
10 #include <WiFiManager.h>
11 #include <ESP8266mDNS.h>
14 // MOSFET for door lock activation
15 // ON/HIGH == ground the output screw terminal
18 // Pin for LED / WS2812
21 // Wiegand keyfob reader pins
25 // Door lock sense pin
28 // emergency release switch
31 // Buzzer/LED on keyfob control
32 #define BUZZER 4 // Gnd to beep
33 #define DOORLED 5 // low = green, otherwise red
35 // orientation of some signals
36 #define DLED_GREEN LOW
38 #define LOCK_OPEN HIGH
39 #define LOCK_CLOSE LOW
45 /***********************
46 * Configuration parameters
48 // AP that it will apoear as for configuration
49 #define MANAGER_AP "DoorLock"
51 // Credentials required to reset or upload new info
52 // should contain #def's for www_username and www_password
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"
60 // how long to hold the latch open in millis
61 #define LATCH_HOLD 5000
63 // webserver for configuration portnumber
64 #define CONFIG_PORT 80
67 #define NTP_SERVER "1.uk.pool.ntp.org"
69 // NTP retry time (mS)
70 #define NTP_RETRY 30000
72 /***************************
76 ESP8266WebServer server(CONFIG_PORT);
78 const unsigned int localPort = 2390;
79 IPAddress ntpServerIP;
81 const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
83 byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
88 unsigned long ntp_lastset = 0;
89 unsigned long ntp_lasttry = 0;
91 bool ota_enabled = false;
97 Serial.println("Enabling OTA Mode.");
104 /* compose and send an NTP time request packet */
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;
114 Serial.print("Got NTP server " NTP_SERVER " address ");
115 Serial.println(ntpServerIP);
117 Serial.println("DNS lookup of " NTP_SERVER " failed.");
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;
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);
142 Serial.println("Sending NTP request");
145 /* request a time update from NTP and parse the result */
148 while (udp.parsePacket() > 0); // discard old udp packets
151 uint32_t beginWait = millis();
153 while (millis() - beginWait < 2500) {
154 int size = udp.parsePacket();
155 if (size >= NTP_PACKET_SIZE) {
156 udp.read(packetBuffer, NTP_PACKET_SIZE);
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;
163 ntp_lastset = millis();
164 Serial.print("NTP update unixtime=");
165 Serial.println(unixtime);
169 Serial.println("No NTP response");
173 /* how big is a file */
174 int fileSize(const char *filename)
177 File file = SPIFFS.open(filename, "r");
186 /* HTTP page request for / */
190 int sec = millis() / 1000;
195 snprintf(mtime, 16, "%dd %02d:%02d:%02d", day, hr % 24, mi % 60, sec % 60);
197 String out = "<html>\
199 <title>Door Lock</title>\
201 body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; }\
206 <p>Uptime: " + (String)mtime + "</p>\n";
208 if (timeStatus() == timeSet) {
210 out += "<p>Time now: " + getDate(when) + " " + getTime(when) + "</p>\n";
215 if (SPIFFS.info(fs_info)) {
216 out += "<p>Onboard Flash disk: - Size:"+String(fs_info.totalBytes)+" Used:"+String(fs_info.usedBytes)+"</p>\n";
219 out += "<p>Lock is currently ";
220 if (digitalRead(SENSE) == HIGH) out += "LOCKED"; else out += "OPEN";
223 if (SPIFFS.exists(CARD_FILE)) {
225 out += "<p>Cardfile: " + String(CARD_FILE) + " is " + fileSize(CARD_FILE) + " bytes";
226 int count = sanityCheck(CARD_FILE);
228 out += ", in an invalid file";
230 out += ", contains " + String(count) + " keyfob IDs";
231 out += " - <a href=\"/download\">Download</a>";
238 <li><a href=\"/reset\">Reset Configuration</a>\
239 <li><a href=\"/upload\">Upload Cardlist</a>";
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>";
248 out += "<li>OTA Updates ENABLED.";
250 out += "<li><a href=\"/enable_ota\">Enable OTA Updates</a>";
257 server.send( 200, "text/html", out);
262 String out = "<html>";
264 out += "<title>Card entry log</title>";
269 if (server.hasArg("count")) {
270 count = server.arg("count").toInt();
272 out += printLog(count);
274 out += "</body></html>";
275 server.send(200, "text/html", out);
278 void handleDownload()
280 if (!server.authenticate(www_username, www_password))
281 return server.requestAuthentication();
283 if (!SPIFFS.exists(CARD_FILE)) {
284 server.send(404, "text/plain", "Card file not found");
288 File f = SPIFFS.open(CARD_FILE, "r");
289 server.streamFile(f, "text/csv");
295 if (!server.authenticate(www_username, www_password))
296 return server.requestAuthentication();
298 SPIFFS.remove(LOG_FILE);
299 server.send(200, "text/plain", "logfile deleted");
302 // need to do this chunked.
303 // https://github.com/luc-github/ESP3D/blob/master/esp3d/webinterface.cpp#L214-L394
304 void handleDownloadLogfile()
306 if (!server.authenticate(www_username, www_password))
307 return server.requestAuthentication();
309 File f = SPIFFS.open(LOG_FILE, "r");
311 server.send(404, "text/plain", "logfile not found");
315 server.setContentLength(CONTENT_LENGTH_UNKNOWN);
316 server.sendHeader("Content-Type", "text/csv", true);
317 server.sendHeader("Cache-Control", "no-cache");
320 unsigned char entry[8];
321 uint32_t * data = (uint32_t *)entry;
323 while (f.available()) {
326 out += getDate( data[0] );
328 out += getTime( data[0] );
329 out += "," + String(data[1]) + ",";
332 out += "Emergency Release";
334 String whom = findKeyfob(data[1]);
336 out += "Unknown keyfob";
342 server.sendContent(out);
345 server.sendContent("");
348 void handleNotFound() {
349 String out = "File Not found\n\n";
350 server.send(404, "text/plain", out);
353 // User wants to reset config
355 if (!server.authenticate(www_username, www_password))
356 return server.requestAuthentication();
358 server.send(200, "text/plain", "Rebooting to config manager...\n\n");
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);
380 int upload_code = 200;
382 void handleFileUpload()
384 if (server.uri() != "/upload") return;
386 if (!server.authenticate(www_username, www_password))
387 return server.requestAuthentication();
389 HTTPUpload& upload = server.upload();
390 if (upload.status == UPLOAD_FILE_START) {
393 uploadFile = SPIFFS.open(CARD_TMPFILE, "w");
395 upload_error = "error opening file";
396 Serial.println("Opening tmpfile failed!");
400 if (upload.status == UPLOAD_FILE_WRITE) {
402 if (uploadFile.write(upload.buf, upload.currentSize) != upload.currentSize) {
403 upload_error = "write error";
408 if (upload.status == UPLOAD_FILE_END) {
415 void handleUploadComplete()
417 String out = "Upload finished.";
418 if (upload_code != 200) {
419 out += "Error: "+upload_error;
422 // upload with no errors, replace old one
423 SPIFFS.remove(CARD_FILE);
424 SPIFFS.rename(CARD_TMPFILE, CARD_FILE);
426 out += "</p><a href=\"/\">Back</a>";
427 server.send(upload_code, "text/plain", out);
432 server.send(200, "text/plain", "");
435 String getTime(time_t when)
439 int m = minute(when);
440 int s = second(when);
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";
452 String getDate(time_t when)
456 ans += String(year(when)) + "-" + String(month(when)) + "-" + String(day(when));
461 int sanityCheck(const char * filename)
465 File f = SPIFFS.open(filename, "r");
467 Serial.print("Sanity Check: Could not open ");
468 Serial.println(filename);
471 while (f.available()) {
473 // skip comment lines
479 String wcode = f.readStringUntil(',');
480 String wname = f.readStringUntil('\n');
481 unsigned int newcode = wcode.toInt();
483 if (newcode != 0) count++;
490 String findKeyfob(unsigned int code)
492 File f = SPIFFS.open(CARD_FILE, "r");
494 Serial.println("Error opening card file " CARD_FILE);
499 while (f.available()) {
501 // skip comment lines
507 String wcode = f.readStringUntil(',');
508 String wname = f.readStringUntil('\n');
510 unsigned int newcode = wcode.toInt();
513 Serial.print("Line: code='");
516 Serial.print(newcode);
517 Serial.print(") name='");
521 if (code == newcode) {
522 // Serial.println(" - FOUND IT");
532 // add an entry to the log
533 void logEntry(time_t when, uint32_t card)
535 unsigned char entry[8];
537 File f = SPIFFS.open(LOG_FILE, "a");
539 Serial.println("Error opening log file");
543 // compose the record to write
544 ((uint32_t *)entry)[0] = when;
545 ((uint32_t *)entry)[1] = card;
550 // produce a copy of the log file
551 String printLog(int last)
554 File f = SPIFFS.open(LOG_FILE, "r");
555 if (!f) return String("Could not open log file");
557 unsigned char entry[8];
558 uint32_t * data = (uint32_t *)entry;
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 :-";
569 while (f.available()) {
572 out += getDate( data[0] );
574 out += getTime( data[0] );
579 out += "Emergency Release";
582 String whom = findKeyfob(data[1]);
585 out += "Unknown keyfob";
590 out += " (" + String(data[1]) + ")";
600 static InputDebounce release_button;
602 /********************************************
606 // some serial, for debug
607 Serial.begin(115200);
609 // The lock mechanism
610 pinMode(MOSFET, OUTPUT);
611 digitalWrite(MOSFET, LOCK_CLOSE);
613 // lock sense microswitch
614 pinMode(SENSE, INPUT_PULLUP);
616 // emergency door release switch
617 pinMode(ERELEASE, INPUT);
619 // indicators on the keyfob reader
620 pinMode(BUZZER, OUTPUT);
621 pinMode(DOORLED, OUTPUT);
622 digitalWrite(BUZZER, BUZZ_OFF);
623 digitalWrite(DOORLED, DLED_RED);
625 Serial.println("DoorLock. Testing WiFi config...");
627 // if we have no config, enter config mode
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);
636 Serial.println("Configuring OTA update");
637 ArduinoOTA.setPassword(www_password);
638 ArduinoOTA.onStart([]() {
639 Serial.println("Start");
641 ArduinoOTA.onEnd([]() {
642 Serial.println("\nEnd");
644 ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
645 Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
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");
655 //ArduinoOTA.begin();
658 Serial.println("Entering normal doorlock mode.");
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 );
674 // advertise we exist via MDNS
675 if (!MDNS.begin("doorlock")) {
676 Serial.println("Error setting up MDNS responder.");
678 MDNS.addService("http", "tcp", 80);
681 // enable internal flash filesystem
684 // init wiegand keyfob reader
685 Serial.println("Configuring Wiegand keyfob reader");
686 wg.begin(WD0, WD0, WD1, WD1);
688 // setup button debounce for the release switch
689 release_button.setup(ERELEASE, 20, InputDebounce::PIM_EXT_PULL_DOWN_RES);
691 Serial.println("Requesting time from network");
692 // listener port for replies from NTP
693 udp.begin(localPort);
694 setSyncProvider(ntp_fetch);
696 Serial.println("Hackspace doorlock v1.2 READY");
699 unsigned long locktime = 0;
704 digitalWrite(DOORLED, DLED_GREEN);
705 digitalWrite(MOSFET, LOCK_OPEN);
707 digitalWrite(BUZZER, BUZZ_ON);
709 digitalWrite(BUZZER, BUZZ_OFF);
711 digitalWrite(BUZZER, BUZZ_ON);
713 digitalWrite(BUZZER, BUZZ_OFF);
720 // is the latch held open ?
722 if (locktime + LATCH_HOLD < millis()) {
724 digitalWrite(MOSFET, LOCK_CLOSE);
725 digitalWrite(DOORLED, DLED_RED);
728 // handle web requests
729 server.handleClient();
730 if (ota_enabled) ArduinoOTA.handle();
732 unsigned int ertime = release_button.process(millis());
733 unsigned int count = release_button.getStateOnCount();
734 static unsigned last_count = 0;
736 if (count != last_count) {
738 Serial.println("Door Release button triggered.");
742 // buttons is still pressed, do nothing
746 // handle card swipes
747 if (wg.available()) {
748 unsigned long code = wg.getCode();
750 Serial.print("wiegand HEX = ");
751 Serial.print(code,HEX);
752 Serial.print(", DECIMAL= ");
754 Serial.print(", TYPE W");
755 Serial.println(wg.getWiegandType());
757 String who = findKeyfob(code);
759 Serial.print("Unlocking door for ");
762 logEntry(now(), code);
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);