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;
92 /* compose and send an NTP time request packet */
95 if (ntpServerIP == INADDR_NONE) {
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;
102 Serial.print("Got NTP server " NTP_SERVER " address ");
103 Serial.println(ntpServerIP);
105 Serial.println("DNS lookup of " NTP_SERVER " failed.");
110 ntp_lasttry = millis();
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;
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);
130 Serial.println("Sending NTP request");
133 /* request a time update from NTP and parse the result */
136 while (udp.parsePacket() > 0); // discard old udp packets
139 uint32_t beginWait = millis();
141 while (millis() - beginWait < 2500) {
142 int size = udp.parsePacket();
143 if (size >= NTP_PACKET_SIZE) {
144 udp.read(packetBuffer, NTP_PACKET_SIZE);
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;
151 ntp_lastset = millis();
152 Serial.print("NTP update unixtime=");
153 Serial.println(unixtime);
157 Serial.println("No NTP response");
161 /* how big is a file */
162 int fileSize(const char *filename)
165 File file = SPIFFS.open(filename, "r");
174 /* HTTP page request for / */
178 int sec = millis() / 1000;
183 snprintf(mtime, 16, "%dd %02d:%02d:%02d", day, hr % 24, mi % 60, sec % 60);
185 String out = "<html>\
187 <title>Door Lock</title>\
189 body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; }\
194 <p>Uptime: " + (String)mtime + "</p>\n";
196 if (timeStatus() == timeSet) {
198 out += "<p>Time now: " + getDate(when) + " " + getTime(when) + "</p>\n";
203 if (SPIFFS.info(fs_info)) {
204 out += "<p>Onboard Flash disk: - Size:"+String(fs_info.totalBytes)+" Used:"+String(fs_info.usedBytes)+"</p>\n";
207 out += "<p>Lock is currently ";
208 if (digitalRead(SENSE) == HIGH) out += "LOCKED"; else out += "OPEN";
211 if (SPIFFS.exists(CARD_FILE)) {
213 out += "<p>Cardfile: " + String(CARD_FILE) + " is " + fileSize(CARD_FILE) + " bytes";
214 int count = sanityCheck(CARD_FILE);
216 out += ", in an invalid file";
218 out += ", contains " + String(count) + " keyfob IDs";
219 out += " - <a href=\"/download\">Download</a>";
226 <li><a href=\"/reset\">Reset Configuration</a>\
227 <li><a href=\"/upload\">Upload Cardlist</a>";
229 if (SPIFFS.exists(LOG_FILE)) {
230 out += "<li><a href=\"/wipelog\">Wipe log file</a>";
231 out += "<li><a href=\"/viewlog?count=30\">View entry log</a>";
232 out += "<li><a href=\"/download_logfile\">Download full logfile</a>";
239 server.send( 200, "text/html", out);
244 String out = "<html>";
246 out += "<title>Card entry log</title>";
251 if (server.hasArg("count")) {
252 count = server.arg("count").toInt();
254 out += printLog(count);
256 out += "</body></html>";
257 server.send(200, "text/html", out);
260 void handleDownload()
262 if (!server.authenticate(www_username, www_password))
263 return server.requestAuthentication();
265 if (!SPIFFS.exists(CARD_FILE)) {
266 server.send(404, "text/plain", "Card file not found");
270 File f = SPIFFS.open(CARD_FILE, "r");
271 server.streamFile(f, "text/csv");
277 if (!server.authenticate(www_username, www_password))
278 return server.requestAuthentication();
280 SPIFFS.remove(LOG_FILE);
281 server.send(200, "text/plain", "logfile deleted");
284 // need to do this chunked.
285 // https://github.com/luc-github/ESP3D/blob/master/esp3d/webinterface.cpp#L214-L394
286 void handleDownloadLogfile()
288 if (!server.authenticate(www_username, www_password))
289 return server.requestAuthentication();
291 File f = SPIFFS.open(LOG_FILE, "r");
293 server.send(404, "text/plain", "logfile not found");
297 server.setContentLength(CONTENT_LENGTH_UNKNOWN);
298 server.sendHeader("Content-Type", "text/csv", true);
299 server.sendHeader("Cache-Control", "no-cache");
302 unsigned char entry[8];
303 uint32_t * data = (uint32_t *)entry;
305 while (f.available()) {
308 out += getDate( data[0] );
310 out += getTime( data[0] );
311 out += "," + String(data[1]) + ",";
314 out += "Emergency Release";
316 String whom = findKeyfob(data[1]);
318 out += "Unknown keyfob";
324 server.sendContent(out);
327 server.sendContent("");
330 void handleNotFound() {
331 String out = "File Not found\n\n";
332 server.send(404, "text/plain", out);
335 // User wants to reset config
337 if (!server.authenticate(www_username, www_password))
338 return server.requestAuthentication();
340 server.send(200, "text/plain", "Rebooting to config manager...\n\n");
349 void 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\" />\
353 Select 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);
362 int upload_code = 200;
364 void handleFileUpload()
366 if (server.uri() != "/upload") return;
368 if (!server.authenticate(www_username, www_password))
369 return server.requestAuthentication();
371 HTTPUpload& upload = server.upload();
372 if (upload.status == UPLOAD_FILE_START) {
375 uploadFile = SPIFFS.open(CARD_TMPFILE, "w");
377 upload_error = "error opening file";
378 Serial.println("Opening tmpfile failed!");
382 if (upload.status == UPLOAD_FILE_WRITE) {
384 if (uploadFile.write(upload.buf, upload.currentSize) != upload.currentSize) {
385 upload_error = "write error";
390 if (upload.status == UPLOAD_FILE_END) {
397 void handleUploadComplete()
399 String out = "Upload finished.";
400 if (upload_code != 200) {
401 out += "Error: "+upload_error;
404 // upload with no errors, replace old one
405 SPIFFS.remove(CARD_FILE);
406 SPIFFS.rename(CARD_TMPFILE, CARD_FILE);
408 out += "</p><a href=\"/\">Back</a>";
409 server.send(upload_code, "text/plain", out);
414 server.send(200, "text/plain", "");
417 String getTime(time_t when)
421 int m = minute(when);
422 int s = second(when);
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";
434 String getDate(time_t when)
438 ans += String(year(when)) + "-" + String(month(when)) + "-" + String(day(when));
443 int sanityCheck(const char * filename)
447 File f = SPIFFS.open(filename, "r");
449 Serial.print("Sanity Check: Could not open ");
450 Serial.println(filename);
453 while (f.available()) {
455 // skip comment lines
461 String wcode = f.readStringUntil(',');
462 String wname = f.readStringUntil('\n');
463 unsigned int newcode = wcode.toInt();
465 if (newcode != 0) count++;
472 String findKeyfob(unsigned int code)
474 File f = SPIFFS.open(CARD_FILE, "r");
476 Serial.println("Error opening card file " CARD_FILE);
481 while (f.available()) {
483 // skip comment lines
489 String wcode = f.readStringUntil(',');
490 String wname = f.readStringUntil('\n');
492 unsigned int newcode = wcode.toInt();
495 Serial.print("Line: code='");
498 Serial.print(newcode);
499 Serial.print(") name='");
503 if (code == newcode) {
504 // Serial.println(" - FOUND IT");
514 // add an entry to the log
515 void logEntry(time_t when, uint32_t card)
517 unsigned char entry[8];
519 File f = SPIFFS.open(LOG_FILE, "a");
521 Serial.println("Error opening log file");
525 // compose the record to write
526 ((uint32_t *)entry)[0] = when;
527 ((uint32_t *)entry)[1] = card;
532 // produce a copy of the log file
533 String printLog(int last)
536 File f = SPIFFS.open(LOG_FILE, "r");
537 if (!f) return String("Could not open log file");
539 unsigned char entry[8];
540 uint32_t * data = (uint32_t *)entry;
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);
547 out += "Last " + String(last) + " log entries :-";
551 while (f.available()) {
554 out += getDate( data[0] );
556 out += getTime( data[0] );
561 out += "Emergency Release";
564 String whom = findKeyfob(data[1]);
567 out += "Unknown keyfob";
572 out += " (" + String(data[1]) + ")";
582 static InputDebounce release_button;
584 /********************************************
588 // some serial, for debug
589 Serial.begin(115200);
591 // The lock mechanism
592 pinMode(MOSFET, OUTPUT);
593 digitalWrite(MOSFET, LOCK_CLOSE);
595 // lock sense microswitch
596 pinMode(SENSE, INPUT_PULLUP);
598 // emergency door release switch
599 pinMode(ERELEASE, INPUT);
601 // indicators on the keyfob reader
602 pinMode(BUZZER, OUTPUT);
603 pinMode(DOORLED, OUTPUT);
604 digitalWrite(BUZZER, BUZZ_OFF);
605 digitalWrite(DOORLED, DLED_RED);
607 Serial.println("DoorLock. Testing WiFi config...");
609 // if we have no config, enter config mode
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...
616 wfm.autoConnect(MANAGER_AP);
618 Serial.println("Enabling OTA update");
619 ArduinoOTA.setPassword(www_password);
620 ArduinoOTA.onStart([]() {
621 Serial.println("Start");
623 ArduinoOTA.onEnd([]() {
624 Serial.println("\nEnd");
626 ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
627 Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
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");
638 Serial.println("Entering normal doorlock mode.");
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 );
645 server.on( "/viewlog", handleViewLog );
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 );
653 // advertise we exist via MDNS
654 if (!MDNS.begin("doorlock")) {
655 Serial.println("Error setting up MDNS responder.");
657 MDNS.addService("http", "tcp", 80);
660 // enable internal flash filesystem
663 // init wiegand keyfob reader
664 Serial.println("Configuring Wiegand keyfob reader");
665 wg.begin(WD0, WD0, WD1, WD1);
667 // setup button debounce for the release switch
668 release_button.setup(ERELEASE, 20, InputDebounce::PIM_EXT_PULL_DOWN_RES);
670 Serial.println("Requesting time from network");
671 // listener port for replies from NTP
672 udp.begin(localPort);
673 setSyncProvider(ntp_fetch);
675 Serial.println("Hackspace doorlock v1. READY");
678 unsigned long locktime = 0;
683 digitalWrite(DOORLED, DLED_GREEN);
684 digitalWrite(MOSFET, LOCK_OPEN);
686 digitalWrite(BUZZER, BUZZ_ON);
688 digitalWrite(BUZZER, BUZZ_OFF);
690 digitalWrite(BUZZER, BUZZ_ON);
692 digitalWrite(BUZZER, BUZZ_OFF);
699 // is the latch held open ?
701 if (locktime + LATCH_HOLD < millis()) {
703 digitalWrite(MOSFET, LOCK_CLOSE);
704 digitalWrite(DOORLED, DLED_RED);
707 // handle web requests
708 server.handleClient();
711 unsigned int ertime = release_button.process(millis());
712 unsigned int count = release_button.getStateOnCount();
713 static unsigned last_count = 0;
715 if (count != last_count) {
717 Serial.println("Door Release button triggered.");
721 // buttons is still pressed, do nothing
725 // handle card swipes
726 if (wg.available()) {
727 unsigned long code = wg.getCode();
729 Serial.print("wiegand HEX = ");
730 Serial.print(code,HEX);
731 Serial.print(", DECIMAL= ");
733 Serial.print(", TYPE W");
734 Serial.println(wg.getWiegandType());
736 String who = findKeyfob(code);
738 Serial.print("Unlocking door for ");
741 logEntry(now(), code);
745 // has ntp failed, do we need to try again?
746 if (ntp_lastset == 0 && ntp_lasttry + NTP_RETRY < millis()) {
747 Serial.println("Ask Time service to try again");
748 setSyncProvider(ntp_fetch);