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;
93 /* User requests to enable OTA mode */
97 if (!server.authenticate(www_username, www_password))
98 return server.requestAuthentication();
100 Serial.println("Enabling OTA Mode.");
107 /* compose and send an NTP time request packet */
110 if (ntpServerIP == INADDR_NONE) {
111 if (WiFi.hostByName(NTP_SERVER, ntpServerIP) == 1) {
112 if (ntpServerIP == IPAddress(1,0,0,0)) {
113 Serial.println("DNS lookup failed for " NTP_SERVER " try again later.");
114 ntpServerIP = INADDR_NONE;
117 Serial.print("Got NTP server " NTP_SERVER " address ");
118 Serial.println(ntpServerIP);
120 Serial.println("DNS lookup of " NTP_SERVER " failed.");
125 ntp_lasttry = millis();
126 memset(packetBuffer, 0, NTP_PACKET_SIZE);
127 // Initialize values needed to form NTP request
128 // (see URL above for details on the packets)
129 packetBuffer[0] = 0b11100011; // LI, Version, Mode
130 packetBuffer[1] = 0; // Stratum, or type of clock
131 packetBuffer[2] = 6; // Polling Interval
132 packetBuffer[3] = 0xEC; // Peer Clock Precision
133 // 8 bytes of zero for Root Delay & Root Dispersion
134 packetBuffer[12] = 49;
135 packetBuffer[13] = 0x4E;
136 packetBuffer[14] = 49;
137 packetBuffer[15] = 52;
139 // all NTP fields have been given values, now
140 // you can send a packet requesting a timestamp:
141 udp.beginPacket(ntpServerIP, 123); //NTP requests are to port 123
142 udp.write(packetBuffer, NTP_PACKET_SIZE);
145 Serial.println("Sending NTP request");
148 /* request a time update from NTP and parse the result */
151 while (udp.parsePacket() > 0); // discard old udp packets
154 uint32_t beginWait = millis();
156 while (millis() - beginWait < 2500) {
157 int size = udp.parsePacket();
158 if (size >= NTP_PACKET_SIZE) {
159 udp.read(packetBuffer, NTP_PACKET_SIZE);
161 // this is NTP time (seconds since Jan 1 1900):
162 unsigned long secsSince1900 = packetBuffer[40] << 24 | packetBuffer[41] << 16 | packetBuffer[42] << 8 | packetBuffer[43];
163 const unsigned long seventyYears = 2208988800UL;
164 time_t unixtime = secsSince1900 - seventyYears;
166 ntp_lastset = millis();
167 Serial.print("NTP update unixtime=");
168 Serial.println(unixtime);
172 Serial.println("No NTP response");
176 /* how big is a file */
177 int fileSize(const char *filename)
180 File file = SPIFFS.open(filename, "r");
189 /* HTTP page request for / */
193 int sec = millis() / 1000;
198 snprintf(mtime, 16, "%dd %02d:%02d:%02d", day, hr % 24, mi % 60, sec % 60);
200 String out = "<html>\
202 <title>Door Lock</title>\
204 body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; }\
209 <p>Uptime: " + (String)mtime + "</p>\n";
211 if (timeStatus() == timeSet) {
213 out += "<p>Time now: " + getDate(when) + " " + getTime(when) + "</p>\n";
218 if (SPIFFS.info(fs_info)) {
219 out += "<p>Onboard Flash disk: - Size:"+String(fs_info.totalBytes)+" Used:"+String(fs_info.usedBytes)+"</p>\n";
222 out += "<p>Lock is currently ";
223 if (digitalRead(SENSE) == HIGH) out += "LOCKED"; else out += "OPEN";
226 if (SPIFFS.exists(CARD_FILE)) {
228 out += "<p>Cardfile: " + String(CARD_FILE) + " is " + fileSize(CARD_FILE) + " bytes";
229 int count = sanityCheck(CARD_FILE);
231 out += ", in an invalid file";
233 out += ", contains " + String(count) + " keyfob IDs";
234 out += " - <a href=\"/download\">Download</a>";
241 <li><a href=\"/reset\">Reset Configuration</a>\
242 <li><a href=\"/upload\">Upload Cardlist</a>";
244 if (SPIFFS.exists(LOG_FILE)) {
245 out += "<li><a href=\"/wipelog\">Wipe log file</a>";
246 out += "<li><a href=\"/viewlog?count=30\">View entry log</a>";
247 out += "<li><a href=\"/download_logfile\">Download full logfile</a>";
251 out += "<li>OTA Updates ENABLED.";
253 out += "<li><a href=\"/enable_ota\">Enable OTA Updates</a>";
260 server.send( 200, "text/html", out);
265 String out = "<html>";
267 out += "<title>Card entry log</title>";
272 if (server.hasArg("count")) {
273 count = server.arg("count").toInt();
275 out += printLog(count);
277 out += "</body></html>";
278 server.send(200, "text/html", out);
281 void handleDownload()
283 if (!server.authenticate(www_username, www_password))
284 return server.requestAuthentication();
286 if (!SPIFFS.exists(CARD_FILE)) {
287 server.send(404, "text/plain", "Card file not found");
291 File f = SPIFFS.open(CARD_FILE, "r");
292 server.streamFile(f, "text/csv");
298 if (!server.authenticate(www_username, www_password))
299 return server.requestAuthentication();
301 SPIFFS.remove(LOG_FILE);
302 server.send(200, "text/plain", "logfile deleted");
305 // need to do this chunked.
306 // https://github.com/luc-github/ESP3D/blob/master/esp3d/webinterface.cpp#L214-L394
307 void handleDownloadLogfile()
309 if (!server.authenticate(www_username, www_password))
310 return server.requestAuthentication();
312 File f = SPIFFS.open(LOG_FILE, "r");
314 server.send(404, "text/plain", "logfile not found");
318 server.setContentLength(CONTENT_LENGTH_UNKNOWN);
319 server.sendHeader("Content-Type", "text/csv", true);
320 server.sendHeader("Cache-Control", "no-cache");
323 unsigned char entry[8];
324 uint32_t * data = (uint32_t *)entry;
326 while (f.available()) {
329 out += getDate( data[0] );
331 out += getTime( data[0] );
332 out += "," + String(data[1]) + ",";
335 out += "Emergency Release";
337 String whom = findKeyfob(data[1]);
339 out += "Unknown keyfob";
345 server.sendContent(out);
348 server.sendContent("");
351 void handleNotFound() {
352 String out = "File Not found\n\n";
353 server.send(404, "text/plain", out);
356 // User wants to reset config
358 if (!server.authenticate(www_username, www_password))
359 return server.requestAuthentication();
361 server.send(200, "text/plain", "Rebooting to config manager...\n\n");
370 void handleUploadRequest() {
371 String out = "<html><head><title>Upload Keyfob list</title></head><body>\
372 <form enctype=\"multipart/form-data\" action=\"/upload\" method=\"POST\">\
373 <input type=\"hidden\" name=\"MAX_FILE_SIZE\" value=\"32000\" />\
374 Select file to upload: <input name=\"file\" type=\"file\" />\
375 <input type=\"submit\" value=\"Upload file\" />\
376 </form></body></html>";
377 server.send(200, "text/html", out);
383 int upload_code = 200;
385 void handleFileUpload()
387 if (server.uri() != "/upload") return;
389 if (!server.authenticate(www_username, www_password))
390 return server.requestAuthentication();
392 HTTPUpload& upload = server.upload();
393 if (upload.status == UPLOAD_FILE_START) {
396 uploadFile = SPIFFS.open(CARD_TMPFILE, "w");
398 upload_error = "error opening file";
399 Serial.println("Opening tmpfile failed!");
403 if (upload.status == UPLOAD_FILE_WRITE) {
405 if (uploadFile.write(upload.buf, upload.currentSize) != upload.currentSize) {
406 upload_error = "write error";
411 if (upload.status == UPLOAD_FILE_END) {
418 void handleUploadComplete()
420 String out = "Upload finished.";
421 if (upload_code != 200) {
422 out += "Error: "+upload_error;
425 // upload with no errors, replace old one
426 SPIFFS.remove(CARD_FILE);
427 SPIFFS.rename(CARD_TMPFILE, CARD_FILE);
429 out += "</p><a href=\"/\">Back</a>";
430 server.send(upload_code, "text/plain", out);
435 server.send(200, "text/plain", "");
438 String getTime(time_t when)
442 int m = minute(when);
443 int s = second(when);
445 if (h<10) ans += "0";
446 ans += String(h) + ":";
447 if (m<10) ans += "0";
448 ans += String(m) + ":";
449 if (s<10) ans += "0";
455 String getDate(time_t when)
459 ans += String(year(when)) + "-" + String(month(when)) + "-" + String(day(when));
464 int sanityCheck(const char * filename)
468 File f = SPIFFS.open(filename, "r");
470 Serial.print("Sanity Check: Could not open ");
471 Serial.println(filename);
474 while (f.available()) {
476 // skip comment lines
482 String wcode = f.readStringUntil(',');
483 String wname = f.readStringUntil('\n');
484 unsigned int newcode = wcode.toInt();
486 if (newcode != 0) count++;
493 String findKeyfob(unsigned int code)
495 File f = SPIFFS.open(CARD_FILE, "r");
497 Serial.println("Error opening card file " CARD_FILE);
502 while (f.available()) {
504 // skip comment lines
510 String wcode = f.readStringUntil(',');
511 String wname = f.readStringUntil('\n');
513 unsigned int newcode = wcode.toInt();
516 Serial.print("Line: code='");
519 Serial.print(newcode);
520 Serial.print(") name='");
524 if (code == newcode) {
525 // Serial.println(" - FOUND IT");
535 // add an entry to the log
536 void logEntry(time_t when, uint32_t card)
538 unsigned char entry[8];
540 File f = SPIFFS.open(LOG_FILE, "a");
542 Serial.println("Error opening log file");
546 // compose the record to write
547 ((uint32_t *)entry)[0] = when;
548 ((uint32_t *)entry)[1] = card;
553 // produce a copy of the log file
554 String printLog(int last)
557 File f = SPIFFS.open(LOG_FILE, "r");
558 if (!f) return String("Could not open log file");
560 unsigned char entry[8];
561 uint32_t * data = (uint32_t *)entry;
564 // print only the last N items
565 int pos = f.size() / 8;
566 if (pos > last) pos -= last; else pos = 0;
567 f.seek( pos * 8, SeekSet);
568 out += "Last " + String(last) + " log entries :-";
572 while (f.available()) {
575 out += getDate( data[0] );
577 out += getTime( data[0] );
582 out += "Emergency Release";
585 String whom = findKeyfob(data[1]);
588 out += "Unknown keyfob";
593 out += " (" + String(data[1]) + ")";
603 static InputDebounce release_button;
605 /********************************************
609 // some serial, for debug
610 Serial.begin(115200);
612 // The lock mechanism
613 pinMode(MOSFET, OUTPUT);
614 digitalWrite(MOSFET, LOCK_CLOSE);
616 // lock sense microswitch
617 pinMode(SENSE, INPUT_PULLUP);
619 // emergency door release switch
620 pinMode(ERELEASE, INPUT);
622 // indicators on the keyfob reader
623 pinMode(BUZZER, OUTPUT);
624 pinMode(DOORLED, OUTPUT);
625 digitalWrite(BUZZER, BUZZ_OFF);
626 digitalWrite(DOORLED, DLED_RED);
628 Serial.println("DoorLock. Testing WiFi config...");
630 // if we have no config, enter config mode
632 // Only wait in config mode for 3 minutes max
633 wfm.setConfigPortalTimeout(180);
634 // Try to connect to the old Ap for this long
635 wfm.setConnectTimeout(60);
636 // okay, lets try and connect...
637 wfm.autoConnect(MANAGER_AP);
639 Serial.println("Configuring OTA update");
640 ArduinoOTA.setPassword(www_password);
641 ArduinoOTA.onStart([]() {
642 Serial.println("Start");
644 ArduinoOTA.onEnd([]() {
645 Serial.println("\nEnd");
647 ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
648 Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
650 ArduinoOTA.onError([](ota_error_t error) {
651 Serial.printf("Error[%u]: ", error);
652 if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
653 else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
654 else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
655 else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
656 else if (error == OTA_END_ERROR) Serial.println("End Failed");
658 //ArduinoOTA.begin();
661 Serial.println("Entering normal doorlock mode.");
663 // we have config, enable web server
664 server.on( "/", handleRoot );
665 server.on( "/reset", handleReset );
666 server.on( "/download", handleDownload );
667 server.on( "/wipelog", handleWipelog );
668 server.on( "/viewlog", handleViewLog );
669 server.on( "/enable_ota", enable_ota );
670 server.on( "/download_logfile", handleDownloadLogfile );
671 server.onFileUpload( handleFileUpload);
672 server.on( "/upload", HTTP_GET, handleUploadRequest);
673 server.on( "/upload", HTTP_POST, handleUploadComplete);
674 server.onNotFound( handleNotFound );
677 // advertise we exist via MDNS
678 if (!MDNS.begin("doorlock")) {
679 Serial.println("Error setting up MDNS responder.");
681 MDNS.addService("http", "tcp", 80);
684 // enable internal flash filesystem
687 // init wiegand keyfob reader
688 Serial.println("Configuring Wiegand keyfob reader");
689 wg.begin(WD0, WD0, WD1, WD1);
691 // setup button debounce for the release switch
692 release_button.setup(ERELEASE, 20, InputDebounce::PIM_EXT_PULL_DOWN_RES);
694 Serial.println("Requesting time from network");
695 // listener port for replies from NTP
696 udp.begin(localPort);
697 setSyncProvider(ntp_fetch);
699 Serial.println("Hackspace doorlock v1.2 READY");
702 unsigned long locktime = 0;
707 digitalWrite(DOORLED, DLED_GREEN);
708 digitalWrite(MOSFET, LOCK_OPEN);
710 digitalWrite(BUZZER, BUZZ_ON);
712 digitalWrite(BUZZER, BUZZ_OFF);
714 digitalWrite(BUZZER, BUZZ_ON);
716 digitalWrite(BUZZER, BUZZ_OFF);
723 // is the latch held open ?
725 if (locktime + LATCH_HOLD < millis()) {
727 digitalWrite(MOSFET, LOCK_CLOSE);
728 digitalWrite(DOORLED, DLED_RED);
731 // handle web requests
732 server.handleClient();
733 if (ota_enabled) ArduinoOTA.handle();
735 unsigned int ertime = release_button.process(millis());
736 unsigned int count = release_button.getStateOnCount();
737 static unsigned last_count = 0;
739 if (count != last_count) {
741 Serial.println("Door Release button triggered.");
745 // buttons is still pressed, do nothing
749 // handle card swipes
750 if (wg.available()) {
751 unsigned long code = wg.getCode();
753 Serial.print("wiegand HEX = ");
754 Serial.print(code,HEX);
755 Serial.print(", DECIMAL= ");
757 Serial.print(", TYPE W");
758 Serial.println(wg.getWiegandType());
760 String who = findKeyfob(code);
762 Serial.print("Unlocking door for ");
765 logEntry(now(), code);
769 // has ntp failed, do we need to try again?
770 if (ntp_lastset == 0 && ntp_lasttry + NTP_RETRY < millis()) {
771 Serial.println("Ask Time service to try again");
772 setSyncProvider(ntp_fetch);