commit 94c087ece4ec85950a634fb0219a77917f14f588 Author: Matthew Frost Date: Mon Jun 12 19:17:28 2023 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6ff475 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +include/secrets.h +.DS_Store +upload_params.ini \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d4f336c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "files.associations": { + "array": "cpp", + "deque": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "string_view": "cpp", + "initializer_list": "cpp", + "regex": "cpp" + } +} \ No newline at end of file diff --git a/Insomnia-local-acl-api-spec.yaml b/Insomnia-local-acl-api-spec.yaml new file mode 100644 index 0000000..c1e07dc --- /dev/null +++ b/Insomnia-local-acl-api-spec.yaml @@ -0,0 +1,188 @@ +_type: export +__export_format: 4 +__export_date: 2023-06-12T09:12:10.701Z +__export_source: insomnia.desktop.app:v2023.2.2 +resources: + - _id: req_fab13abf4f6146a3b0db6955b4968837 + parentId: wrk_3465ebd3f0ef47dcac39369bc6f6475e + modified: 1686560719521 + created: 1680522514230 + url: http://reader-hostname/users/ + name: Get Card ACL's + description: "" + method: GET + body: + mimeType: application/json + text: "" + parameters: [] + headers: + - id: pair_cf9752f319fc4ce2bbc5c1d802d7a82b + name: Content-Type + value: application/json + description: "" + authentication: + type: basic + useISO88591: false + disabled: false + username: admin + password: admin + metaSortKey: -1680522514230 + isPrivate: false + settingStoreCookies: true + settingSendCookies: true + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingRebuildPath: true + settingFollowRedirects: global + _type: request + - _id: wrk_3465ebd3f0ef47dcac39369bc6f6475e + parentId: null + modified: 1680522505347 + created: 1680522505347 + name: my-spec.yaml + description: "" + scope: design + _type: workspace + - _id: req_fbe2da3ea1b942149289048a145c2e1f + parentId: wrk_3465ebd3f0ef47dcac39369bc6f6475e + modified: 1686561076133 + created: 1686523684626 + url: http://reader-hostname/users/create + name: Create Card ACL + description: "" + method: POST + body: + mimeType: multipart/form-data + params: + - name: cardId + value: "1234" + id: pair_601e929fe2cf4f35a8fef542dbdff232 + - id: pair_8156862f80b84f988f5677eec2332f70 + name: desc + value: Person A + description: "" + parameters: [] + headers: + - id: pair_cf9752f319fc4ce2bbc5c1d802d7a82b + name: Content-Type + value: multipart/form-data + description: "" + authentication: + type: basic + useISO88591: false + disabled: false + username: admin + password: admin + metaSortKey: -1678709813012.5 + isPrivate: false + settingStoreCookies: true + settingSendCookies: true + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingRebuildPath: true + settingFollowRedirects: global + _type: request + - _id: req_60a5d5a1e2d444b18d05c1892f23ba0c + parentId: wrk_3465ebd3f0ef47dcac39369bc6f6475e + modified: 1686560730675 + created: 1686527136306 + url: http://reader-hostname/users/update + name: Update Card ACL + description: "" + method: POST + body: + mimeType: multipart/form-data + params: + - name: cardId + value: "1234" + id: pair_601e929fe2cf4f35a8fef542dbdff232 + - id: pair_fb6e32685ee1484da24a96d5a445dab5 + name: newCardId + value: "1234" + description: "" + - id: pair_688c548f6db94924be254a3b4593862c + name: desc + value: Test + description: "" + parameters: [] + headers: + - id: pair_cf9752f319fc4ce2bbc5c1d802d7a82b + name: Content-Type + value: multipart/form-data + description: "" + authentication: + type: basic + useISO88591: false + disabled: false + username: admin + password: admin + metaSortKey: -1678256637708.125 + isPrivate: false + settingStoreCookies: true + settingSendCookies: true + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingRebuildPath: true + settingFollowRedirects: global + _type: request + - _id: req_df860cce04cf49c98c9abc4e92ec5a72 + parentId: wrk_3465ebd3f0ef47dcac39369bc6f6475e + modified: 1686560728749 + created: 1686526808857 + url: http://reader-hostname/users/remove + name: Remove Card ACL + description: "" + method: POST + body: + mimeType: multipart/form-data + params: + - name: cardId + value: "1234" + id: pair_601e929fe2cf4f35a8fef542dbdff232 + parameters: [] + headers: + - id: pair_cf9752f319fc4ce2bbc5c1d802d7a82b + name: Content-Type + value: multipart/form-data + description: "" + authentication: + type: basic + useISO88591: false + disabled: false + username: admin + password: admin + metaSortKey: -1677803462403.75 + isPrivate: false + settingStoreCookies: true + settingSendCookies: true + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingRebuildPath: true + settingFollowRedirects: global + _type: request + - _id: env_72c9ec50419adc3e8768056161ce4d086c486204 + parentId: wrk_3465ebd3f0ef47dcac39369bc6f6475e + modified: 1680522505349 + created: 1680522505349 + name: Base Environment + data: {} + dataPropertyOrder: null + color: null + isPrivate: false + metaSortKey: 1680522505349 + _type: environment + - _id: jar_72c9ec50419adc3e8768056161ce4d086c486204 + parentId: wrk_3465ebd3f0ef47dcac39369bc6f6475e + modified: 1680522505350 + created: 1680522505350 + name: Default Jar + cookies: [] + _type: cookie_jar + - _id: spc_deaf3f00ef8847429199de99aca75d06 + parentId: wrk_3465ebd3f0ef47dcac39369bc6f6475e + modified: 1680522505347 + created: 1680522505347 + fileName: my-spec.yaml + contents: "" + contentType: yaml + _type: api_spec diff --git a/README b/README new file mode 100644 index 0000000..2c9ee60 --- /dev/null +++ b/README @@ -0,0 +1,23 @@ +This project is a card reader system for Techinc that uses a WIEGAND based reader and allows platform.io based user flags to allow change of certain features. + + +# Currently supported flags: + +-DBOARD1 : Used to set Pins for BOARD1 config +-DBOARD2 : Used to set Pins for BOARD2 config +-DRELAY1 : Enable the First Relay +-DRELAY2 : Not implemented yet +-DWIFI : Enable WIFI and use secrets.h to manage. +-DWEB_SERVER : Enable Web Server +-DSERIAL_DEBUG : Enable Serial Console output and debug. +-DWEB_SERIAL_DEBUG : Enable Web Serial console to monitor for issues (does not support auth, needs -DWEB_SERVER) +-DNET_FAIL_SAFE : If WIFI disconnects release door / relay. +-DWEB_OTA_UPDATE : Present Web Interface on /update for managing firmware over the air (needs -DWEB_SERVER) +-DLOCAL_ACL : Enable local ACL list +-DLOCAL_ACL_API : Enable API for managing ACL's (needs -DWEB_SERVER and -DLOCAL_ACL) +-DLATCH_DOOR : Release RELAY1 for a a certain amount of time in this case its 1 second +-DTOGGLE_DOOR : Instead of Relasing relay for 1 second keep it in the opposite state to what it was at the time the card was scanned. + + +# Important Notes: +* The webserver uses Spiffs to manage the HTML files for the web server you need to run ("Upload Filesystem Image" step via serial or use the OTA upgrade and run "Upload Filesystem Image OTA" step and uplaod it.) \ No newline at end of file diff --git a/data/index.html b/data/index.html new file mode 100644 index 0000000..e129d3a --- /dev/null +++ b/data/index.html @@ -0,0 +1,30 @@ + + + ESP Web Server + + + + + +

ESP Web Server

+ %BUTTONPLACEHOLDER% + + + \ No newline at end of file diff --git a/include/ACL.h b/include/ACL.h new file mode 100644 index 0000000..fbbef9f --- /dev/null +++ b/include/ACL.h @@ -0,0 +1,44 @@ +#ifndef ACL_H +#define ACL_H + +#include +#include +#include +#include + +#ifdef WEB_SERVER + #ifdef WEB_SERIAL_DEBUG + #include + #endif +#endif + +struct User { + String cardId; + String desc; +}; + +class ACL { +public: + ACL(); + ACL(std::initializer_list userList); + ~ACL(); + + + void addUser(const String& cardId, const String& desc); + void updateUser(const String& cardId, const String& newCardId, const String& newdesc); + bool removeUser(const String& cardId); + bool validateAccess(const String& cardId); + int getACLSize() const; + + // Additional EEPROM functions + void saveToEEPROM(); + void loadFromEEPROM(); + + const User* getACL() const { return acl; } // Getter for acl + +private: + User* acl; + int aclSize; +}; + +#endif diff --git a/include/README b/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/include/buzzer_ctl.h b/include/buzzer_ctl.h new file mode 100644 index 0000000..128fc50 --- /dev/null +++ b/include/buzzer_ctl.h @@ -0,0 +1,11 @@ +#ifndef BUZZER_CTL_H + #define BUZZER_CTL_H + + #include "hardware.h" + + void short_beep(); + void granted_beep(); + void denied_beep(); + void clear_states(); + +#endif \ No newline at end of file diff --git a/include/hardware.h b/include/hardware.h new file mode 100644 index 0000000..e38de41 --- /dev/null +++ b/include/hardware.h @@ -0,0 +1,37 @@ +#ifndef HARDWARE_H +#define HARDWARE_H +#include + + #ifdef WEB_SERVER + #ifdef WEB_SERIAL_DEBUG + #include + #endif + #endif + + #ifdef BOARD1 + #define TAMPER_PIN 26 + #define LEDCTL_PIN 32 + #define DATA0_PIN 34 + #define DATA1_PIN 36 + #define RELAY1_PIN 32 + #define RELAY2_PIN 33 + #define RELAY_DELAY 3000 + #endif + + #ifdef BOARD2 + #define TAMPER_PIN 0 + #define ALARM_PIN 33 + #define WIEGAND_PIN 32 + #define LEDCTL_PIN 25 + #define DATA0_PIN 26 + #define DATA1_PIN 27 + #define RELAY1_PIN 25 + #define RELAY2_PIN 19 + #define RELAY_DELAY 3000 + #endif + + void controlRelay(); + void unlockDoor(bool silent); + void lockDoor(); + void toggleDoor(); +#endif diff --git a/include/main.h b/include/main.h new file mode 100644 index 0000000..693c6d6 --- /dev/null +++ b/include/main.h @@ -0,0 +1,26 @@ +#ifndef MAIN_H + #define MAIN_H + + #include "hardware.h" + #include + #include "secrets.h" + + #ifdef WIFI + #include "WiFi.h" + #endif + + #ifdef WEB_SERVER + #include "web_server.h" + #ifdef WEB_SERIAL_DEBUG + #include + #endif + #ifdef WEB_OTA_UPDATE + #include + #endif + #endif + + #ifdef BUZZER + #include "buzzer_ctl.h" + #endif + +#endif \ No newline at end of file diff --git a/include/secrets.h.dist b/include/secrets.h.dist new file mode 100644 index 0000000..a7bc2c0 --- /dev/null +++ b/include/secrets.h.dist @@ -0,0 +1,25 @@ +#ifndef SECRETS_H + #define SECRETS_H + + #ifdef WIFI + const char* ssid = "WIFI"; + const char* password = "123"; + #endif + + #ifdef WEB_SERVER + const char* http_username = "admin"; + const char* http_password = "admin"; + #endif + + #ifdef LOCAL_ACL + #include "ACL.h" + + ACL acl = { + {"1234", "Card 1"}, + {"4567", "Card 2"}, + {"7891", "Card 3"} + }; + + #endif + +#endif \ No newline at end of file diff --git a/include/web_server.h b/include/web_server.h new file mode 100644 index 0000000..6f4a4b1 --- /dev/null +++ b/include/web_server.h @@ -0,0 +1,19 @@ +#ifndef WEB_SERVER_H + #define WEB_SERVER_H + + #include + #include "ESPAsyncWebServer.h" + #include "SPIFFS.h" + #include + #include + + // Set web server port number to 80 + AsyncWebServer server(80); + + #ifdef LOCAL_ACL + #include "ACL.h" + ACL acl = { + }; + #endif + +#endif \ No newline at end of file diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..77e34a7 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,34 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32-evb] +platform = espressif32 +board = esp32-evb +framework = arduino +build_flags = -DBOARD1 -DRELAY1 -DWIFI -DWEB_SERVER -DNET_FAIL_SAFE -DWEB_OTA_UPDATE -DLOCAL_ACL -DLOCAL_ACL_API -DLATCH_DOOR +lib_deps = + ottowinter/ESPAsyncWebServer-esphome@^3.0.0 + ayushsharma82/WebSerial@^1.4.0 + ayushsharma82/AsyncElegantOTA@^2.2.7 + bblanchon/ArduinoJson@^6.21.2 + +[env:mcu-esp32s] +platform = espressif32 +framework = arduino +board = nodemcu-32s +build_flags = -DBOARD2 -DBUZZER -DRELAY1 -DWIFI -DWEB_SERVER -DNET_FAIL_SAFE -DWEB_OTA_UPDATE -DLOCAL_ACL -DLOCAL_ACL_API -DLATCH_DOOR +lib_deps = + ottowinter/ESPAsyncWebServer-esphome@^3.0.0 + ayushsharma82/WebSerial@^1.4.0 + ayushsharma82/AsyncElegantOTA@^2.2.7 + bblanchon/ArduinoJson@^6.21.2 +extra_scripts = platformio_upload.py +extra_configs = upload_params.ini + diff --git a/platformio_upload.py b/platformio_upload.py new file mode 100644 index 0000000..1c5f0a6 --- /dev/null +++ b/platformio_upload.py @@ -0,0 +1,53 @@ +# Allows PlatformIO to upload directly to AsyncElegantOTA +# +# To use: +# - copy this script into the same folder as your platformio.ini +# - set the following for your project in platformio.ini: +# +# extra_scripts = platformio_upload.py +# upload_protocol = custom +# upload_url = +# +# An example of an upload URL: +# upload_URL = http://192.168.1.123/update + +import requests +import hashlib +Import('env') + +try: + from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor + from tqdm import tqdm +except ImportError: + env.Execute("$PYTHONEXE -m pip install requests_toolbelt") + env.Execute("$PYTHONEXE -m pip install tqdm") + from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor + from tqdm import tqdm + +def on_upload(source, target, env): + firmware_path = str(source[0]) + upload_url = env.GetProjectOption('upload_url') + + with open(firmware_path, 'rb') as firmware: + md5 = hashlib.md5(firmware.read()).hexdigest() + firmware.seek(0) + encoder = MultipartEncoder(fields={ + 'MD5': md5, + 'firmware': ('firmware', firmware, 'application/octet-stream')} + ) + + bar = tqdm(desc='Upload Progress', + total=encoder.len, + dynamic_ncols=True, + unit='B', + unit_scale=True, + unit_divisor=1024 + ) + + monitor = MultipartEncoderMonitor(encoder, lambda monitor: bar.update(monitor.bytes_read - bar.n)) + + response = requests.post(upload_url, data=monitor, headers={'Content-Type': monitor.content_type}) + bar.close() + print(response,response.text) + +env.Replace(UPLOADCMD=on_upload) diff --git a/src/ACL.cpp b/src/ACL.cpp new file mode 100644 index 0000000..944c242 --- /dev/null +++ b/src/ACL.cpp @@ -0,0 +1,133 @@ +#include "ACL.h" + +ACL::ACL() { + acl = nullptr; + aclSize = 0; +} + +ACL::ACL(std::initializer_list userList) { + aclSize = userList.size(); + acl = new User[aclSize]; + + int i = 0; + for (const User& user : userList) { + acl[i] = user; + i++; + } +} + +ACL::~ACL() { + if (acl != nullptr) { + delete[] acl; + } +} + +void ACL::addUser(const String& cardId, const String& desc) { + User newUser; + newUser.cardId = cardId; + newUser.desc = desc; + + User* newACL = new User[aclSize + 1]; + for (int i = 0; i < aclSize; i++) { + newACL[i] = acl[i]; + } + newACL[aclSize] = newUser; + + aclSize++; + acl = newACL; +} + +void ACL::updateUser(const String& cardId, const String& newCardId, const String& newdesc) { + for (int i = 0; i < aclSize; i++) { + if (acl[i].cardId == cardId) { + acl[i].cardId = newCardId; + acl[i].desc = newdesc; + return; // Exit the loop and method if user is found and updated + } + } +} + +bool ACL::removeUser(const String& cardId) { + int userIndex = -1; + for (int i = 0; i < aclSize; i++) { + if (acl[i].cardId == cardId) { + userIndex = i; + break; + } + } + + if (userIndex == -1) { + return false; // User not found + } + + User* newACL = new User[aclSize - 1]; + int j = 0; + for (int i = 0; i < aclSize; i++) { + if (i != userIndex) { + newACL[j] = acl[i]; + j++; + } + } + + aclSize--; + acl = newACL; + return true; // User removed successfully +} + +bool ACL::validateAccess(const String& cardId) { + for (int i = 0; i < aclSize; i++) { + + #ifdef SERIAL_DEBUG + Serial.println("Checking ACL match for Card: " + String(cardId) + " Against Card: " + String(acl[i].cardId) + " desc: " + String(acl[i].desc)); + #endif + #ifdef WEB_SERIAL_DEBUG + WebSerial.println("Checking ACL match for Card: " + String(cardId) + " Against Card: " + String(acl[i].cardId) + " desc: " + String(acl[i].desc)); + #endif + if (acl[i].cardId.equals(cardId)) { + #ifdef SERIAL_DEBUG + Serial.println("Card matched and authenticated."); + #endif + #ifdef WEB_SERIAL_DEBUG + WebSerial.println("Card matched and authenticated."); + #endif + return true; + } + } + #ifdef SERIAL_DEBUG + Serial.println("Card did not match."); + #endif + #ifdef WEB_SERIAL_DEBUG + WebSerial.println("Card did not match."); + #endif + return false; +} + +Preferences preferences; + + +int ACL::getACLSize() const { + return aclSize; +} + +void ACL::loadFromEEPROM() { + preferences.begin("acl", true); + aclSize = preferences.getUInt("aclSize", 0); + acl = new User[aclSize]; + for (int i = 0; i < aclSize; i++) { + String key = "user_" + String(i); + acl[i].cardId = preferences.getString((key + "_cardId").c_str(), ""); + acl[i].desc = preferences.getString((key + "_desc").c_str(), ""); + } + preferences.end(); +} + +void ACL::saveToEEPROM() { + preferences.begin("acl", false); + preferences.putUInt("aclSize", aclSize); + for (int i = 0; i < aclSize; i++) { + String key = "user_" + String(i); + preferences.putString((key + "_cardId").c_str(), acl[i].cardId.c_str()); + preferences.putString((key + "_desc").c_str(), acl[i].desc.c_str()); + } + preferences.end(); +} \ No newline at end of file diff --git a/src/buzzer_ctl.cpp b/src/buzzer_ctl.cpp new file mode 100644 index 0000000..153c5ff --- /dev/null +++ b/src/buzzer_ctl.cpp @@ -0,0 +1,40 @@ +#include "buzzer_ctl.h" + +void short_beep() +{ + #ifdef ALARM_PIN + digitalWrite(ALARM_PIN, LOW); + delay(50); + digitalWrite(ALARM_PIN, HIGH); + #endif +} + +void granted_beep() +{ + #ifdef ALARM_PIN + digitalWrite(ALARM_PIN, LOW); + delay(50); + digitalWrite(ALARM_PIN, HIGH); + delay(50); + digitalWrite(ALARM_PIN, LOW); + delay(50); + digitalWrite(ALARM_PIN, HIGH); + delay(50); + digitalWrite(ALARM_PIN, LOW); + delay(50); + digitalWrite(ALARM_PIN, HIGH); + #endif +} + +void denied_beep(void) +{ + #ifdef ALARM_PIN + for (int i=0; i<100; i++) + { + delay(4); + digitalWrite(ALARM_PIN, LOW); + delay(4); + digitalWrite(ALARM_PIN, HIGH); + } + #endif +} \ No newline at end of file diff --git a/src/hardware.cpp b/src/hardware.cpp new file mode 100644 index 0000000..36c6f07 --- /dev/null +++ b/src/hardware.cpp @@ -0,0 +1,50 @@ +#include "hardware.h" +#include "buzzer_ctl.h" + void controlRelay(int relayPin, bool on) { + digitalWrite(relayPin, on ? HIGH : LOW); // Turn the relay on or off + } + +void unlockDoor(bool silent) { + #ifdef BUZZER + if (!silent) { + granted_beep(); + } + #endif + #ifdef RELAY1 + controlRelay(RELAY1_PIN,false); + #endif + #ifdef SERIAL_DEBUG + Serial.println("door unlocked"); + #endif + + #ifdef WEB_SERIAL_DEBUG + WebSerial.println("door unlocked"); + #endif +} + +void lockDoor() { + #ifdef RELAY1 + controlRelay(RELAY1_PIN,true); + #endif + #ifdef SERIAL_DEBUG + Serial.println("door locked"); + #endif + + #ifdef WEB_SERIAL_DEBUG + WebSerial.println("door locked"); + #endif +} + + +void toggleDoor() { + #ifdef RELAY1 + int relayState = digitalRead(RELAY1_PIN); + // Toggle the relay based on the current state + if (relayState == LOW) { + lockDoor(); + } else { + unlockDoor(false); + } + #endif +} + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..e9c062d --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,477 @@ +#include "main.h" + +volatile uint64_t cardData = 0; +volatile bool newDataAvailable = false; +unsigned long lastDataTime = 0; +const unsigned long displayDelay = 1000; // Delay in milliseconds after which the data is displayed +const unsigned long wifiRebootTimeout = 20000; // Delay before reboot after disconnect +unsigned int bitCount = 0; // Variable to keep track of the bit count +unsigned int maxBitCount = 34; // Variable to make sure reader does not exceed maxiumum bits + + +#ifdef WIFI + +void connectToWiFi() { + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + + #ifdef SERIAL_DEBUG + Serial.print("Connecting to WiFi .."); + #endif + + unsigned long startTime = millis(); // Record the start time of connection + + while (WiFi.status() != WL_CONNECTED) { + delay(500); + + #ifdef SERIAL_DEBUG + Serial.print("."); + #endif + + // Check if it's time to reboot + if (millis() - startTime >= wifiRebootTimeout) { // Reboot after 20 seconds + #ifdef SERIAL_DEBUG + Serial.println("\nFailed to connect. Rebooting..."); + #endif + ESP.restart(); // Reboot the Arduino board + } + } + + #ifdef SERIAL_DEBUG + Serial.println("WiFi connected"); + #endif +} + +void GetWifiInfo(){ + if(WiFi.status() == WL_CONNECTED) { + #ifdef SERIAL_DEBUG + Serial.print("[*] Network information for "); + Serial.println(ssid); + Serial.println("[+] BSSID : " + WiFi.BSSIDstr()); + Serial.print("[+] Gateway IP : "); + Serial.println(WiFi.gatewayIP()); + Serial.print("[+] Subnet Mask : "); + Serial.println(WiFi.subnetMask()); + Serial.println((String)"[+] RSSI : " + WiFi.RSSI() + " dB"); + Serial.print("[+] ESP32 IP : "); + Serial.println(WiFi.localIP()); + #endif + } +} + +void WiFiStationDisconnected(WiFiEvent_t event, WiFiEventInfo_t info){ + + #ifdef NET_FAIL_SAFE + unlockDoor(true); + #endif + + #ifdef SERIAL_DEBUG + Serial.println("Disconnected from WiFi access point"); + Serial.print("WiFi lost connection. Reason: "); + Serial.println(info.wifi_sta_disconnected.reason); + Serial.println("Trying to Reconnect"); + #endif + + connectToWiFi(); +} + +void WiFiStationConnected(WiFiEvent_t event, WiFiEventInfo_t info){ + #ifdef SERIAL_DEBUG + Serial.println("Connected to AP successfully!"); + #endif + #ifdef NET_FAIL_SAFE + lockDoor(); + #endif +} + + +void WiFiGotIP(WiFiEvent_t event, WiFiEventInfo_t info){ + #ifdef SERIAL_DEBUG + #ifdef WIFI + GetWifiInfo(); + #endif + #endif +} +#endif + +#ifdef WEB_SERVER + String outputState(int output){ + if(digitalRead(output)){ + return "checked"; + } + else { + return ""; + } +} + +// Replaces placeholder with button section in your web page +String processor(const String& var){ + //Serial.println(var); + if(var == "BUTTONPLACEHOLDER"){ + String buttons = ""; + + #ifdef RELAY1 + buttons += "

Output - Relay 1

"; + #endif + #ifdef BUZZER + buttons += "

Output - Buzzer

"; + #endif + + return buttons; + } + return String(); +} + +#ifdef LOCAL_ACL + #ifdef LOCAL_ACL_API + // Handler for the '/users' endpoint to list all users + void handleListUsers(AsyncWebServerRequest* request) { + + if(!request->authenticate(http_username, http_password)) + return request->requestAuthentication(); + // Create a JSON array to store the users + StaticJsonDocument<512> jsonDoc; + JsonArray usersArray = jsonDoc.to(); + + // Retrieve the ACL data using the getter function + const User* aclData = acl.getACL(); + + // Iterate over each user in the ACL and add it to the JSON array + for (int i = 0; i < acl.getACLSize(); i++) { + JsonObject user = usersArray.createNestedObject(); + user["cardId"] = aclData[i].cardId; + user["desc"] = aclData[i].desc; + } + + // Convert the JSON array to a string + String response; + serializeJson(usersArray, response); + + // Set the response content type to JSON + request->send(200, "application/json", response); + } + + + + void handleCreateUser(AsyncWebServerRequest* request) { + if(!request->authenticate(http_username, http_password)) + return request->requestAuthentication(); + + if(request->hasParam("cardId", true)) {} //This is important, otherwise the sketch will crash if there is no body + else { + AsyncWebServerResponse *response = request->beginResponse(400, "application/json", "{'msg':'No cardId'}"); + request->send(response); + } + + if(request->hasParam("desc", true)) {} //This is important, otherwise the sketch will crash if there is no body + else { + AsyncWebServerResponse *response = request->beginResponse(400, "application/json", "{'msg':'No desc'}"); + request->send(response); + } + + String cardId = String(request->arg("cardId")); + String desc = String(request->arg("desc")); + + if (acl.validateAccess(String(cardId))) { + AsyncWebServerResponse *response = request->beginResponse(400, "application/json", "{'msg':'Duplicate ACL'}"); + request->send(response); + } else { + acl.addUser(cardId, desc); + acl.saveToEEPROM(); + request->send(201); // Create + } + + } + + // Handler for the '/users/update' endpoint to remove a user + void handleUpdateUser(AsyncWebServerRequest* request) { + if(!request->authenticate(http_username, http_password)) + return request->requestAuthentication(); + + if(request->hasParam("cardId", true)) {} //This is important, otherwise the sketch will crash if there is no body + else { + AsyncWebServerResponse *response = request->beginResponse(400, "application/json", "{'msg':'No cardId'}"); + request->send(response); + } + + if(request->hasParam("newCardId", true)) {} //This is important, otherwise the sketch will crash if there is no body + else { + AsyncWebServerResponse *response = request->beginResponse(400, "application/json", "{'msg':'No newCardId'}"); + request->send(response); + } + + if(request->hasParam("desc", true)) {} //This is important, otherwise the sketch will crash if there is no body + else { + AsyncWebServerResponse *response = request->beginResponse(400, "application/json", "{'msg':'No desc'}"); + request->send(response); + } + String cardId = String(request->arg("cardId")); + String newCardId = String(request->arg("newCardId")); + String desc = String(request->arg("desc")); + acl.updateUser(cardId, newCardId, desc); + acl.saveToEEPROM(); + request->send(201); // Created + } + + // Handler for the '/users/remove' endpoint to remove a user + void handleRemoveUser(AsyncWebServerRequest* request) { + if(!request->authenticate(http_username, http_password)) + return request->requestAuthentication(); + + if(request->hasParam("cardId", true)) {} //This is important, otherwise the sketch will crash if there is no body + else { + AsyncWebServerResponse *response = request->beginResponse(400, "application/json", "{'msg':'No cardId'}"); + request->send(response); + } + String cardId = String(request->arg("cardId")); + acl.removeUser(cardId); + acl.saveToEEPROM(); + request->send(201); // Created + } + #endif + #endif + + +#endif + +void handleInterrupt(int bitValue) { + static unsigned long lastInterruptTime = 0; + unsigned long interruptTime = micros(); + + if (interruptTime - lastInterruptTime > 200) { + cardData <<= 1; + cardData |= bitValue; + newDataAvailable = true; + bitCount++; // Increment the bit count + } + + lastInterruptTime = interruptTime; + lastDataTime = millis(); // Update the time of last received data +} + +void handleData0Interrupt() { + handleInterrupt(0); +} + +void handleData1Interrupt() { + handleInterrupt(1); +} + + +void setup() { + #ifdef SERIAL_DEBUG + Serial.begin(9600); + #endif + + #ifdef WEB_SERVER + WiFi.disconnect(true); + #endif + + #ifdef LOCAL_ACL + acl.loadFromEEPROM(); + #endif + + // Initialize SPIFFS + #ifdef WEB_SERVER + if(!SPIFFS.begin(true)){ + #ifdef SERIAL_DEBUG + Serial.println("An Error has occurred while mounting SPIFFS"); + #endif + return; + } + + #ifdef WEB_OTA_UPDATE + AsyncElegantOTA.begin(&server, http_username, http_password); + #endif + + #ifdef WEB_SERIAL_DEBUG + WebSerial.begin(&server); + #endif + + #endif + + pinMode(DATA0_PIN, INPUT_PULLUP); + pinMode(DATA1_PIN, INPUT_PULLUP); + + #ifdef RELAY1 + pinMode(RELAY1_PIN, OUTPUT); + #endif + + attachInterrupt(digitalPinToInterrupt(DATA0_PIN), handleData0Interrupt, FALLING); + attachInterrupt(digitalPinToInterrupt(DATA1_PIN), handleData1Interrupt, FALLING); + + #ifdef WIFI + WiFi.onEvent(WiFiStationConnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_CONNECTED); + WiFi.onEvent(WiFiGotIP, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP); + WiFi.onEvent(WiFiStationDisconnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); + connectToWiFi(); + #endif + + #ifdef RELAY1 + #ifdef SERIAL_DEBUG + Serial.println("Enabling Relay: Locking Door"); + #endif + lockDoor(); + #endif + + #ifdef RELAY2 + pinMode(RELAY2_PIN, OUTPUT); + #endif + + #ifdef BUZZER + pinMode(ALARM_PIN, OUTPUT); + // Do not set to low or it will constantly beep. + digitalWrite(ALARM_PIN, HIGH); + #endif + +#ifdef WEB_SERVER + // Route for root / web page + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)) + return request->requestAuthentication(); + request->send(SPIFFS, "/index.html", String(), false, processor); + }); + + server.onNotFound([](AsyncWebServerRequest *request){ + request->send(404, "text/plain", "The content you are looking for was not found."); + }); + + // Send a GET request to /gpio?output=&state= + server.on("/gpio", HTTP_GET, [] (AsyncWebServerRequest *request) { + String inputMessage1; + String inputMessage2; + + if(!request->authenticate(http_username, http_password)) + return request->requestAuthentication(); + + // GET input1 value on /gpio?output=&state= + if (request->hasParam("output") && request->hasParam("state")) { + inputMessage1 = request->getParam("output")->value(); + inputMessage2 = request->getParam("state")->value(); + digitalWrite(inputMessage1.toInt(), inputMessage2.toInt()); + } + else { + inputMessage1 = "No message sent"; + inputMessage2 = "No message sent"; + } + + #ifdef SERIAL_DEBUG + Serial.print("GPIO: "); + Serial.print(inputMessage1); + Serial.print(" - Set to: "); + Serial.println(inputMessage2); + request->send(200, "text/plain", "OK"); + #endif + + #ifdef WEB_SERIAL_DEBUG + WebSerial.print("GPIO: "); + WebSerial.print(inputMessage1); + WebSerial.print(" - Set to: "); + WebSerial.println(inputMessage2); + #endif + }); + + + + #ifdef LOCAL_ACL + #ifdef LOCAL_ACL_API + server.on("/users", HTTP_GET, handleListUsers); + server.on("/users/create", HTTP_POST, handleCreateUser); + server.on("/users/update", HTTP_POST, handleUpdateUser); + server.on("/users/remove", HTTP_POST, handleRemoveUser); + #endif + #endif + + server.begin(); +#endif + +} + +void loop() { + + if (newDataAvailable) { + newDataAvailable = false; + lastDataTime = millis(); // Reset the time of last received data + } + + if (millis() - lastDataTime >= displayDelay && cardData != 0 && bitCount == maxBitCount) { + + uint64_t facilityID = (cardData >> 17) & 0xFFFF; + uint64_t cardID = (cardData >> 1) & 0xFFFF; + + #ifdef SERIAL_DEBUG + Serial.print("Facility ID (Decimal): "); + Serial.println(facilityID); + Serial.print("Facility ID (Binary): "); + Serial.println(facilityID, BIN); + + Serial.print("Card ID (Decimal): "); + Serial.println(cardID); + Serial.print("Card ID (Binary): "); + Serial.println(cardID, BIN); + + Serial.print("Card Data (Binary): "); + Serial.println(cardData, BIN); + Serial.print("Total Bits Received: "); + Serial.println(bitCount); + #endif + #ifdef WEB_SERIAL_DEBUG + WebSerial.print("Facility ID (Decimal): "); + WebSerial.println(facilityID); + WebSerial.print("Facility ID (Binary): "); + WebSerial.println(facilityID, BIN); + + WebSerial.print("Card ID (Decimal): "); + WebSerial.println(cardID); + WebSerial.print("Card ID (Binary): "); + WebSerial.println(cardID, BIN); + + WebSerial.print("Card Data (Binary): "); + WebSerial.println(cardData, BIN); + WebSerial.print("Total Bits Received: "); + WebSerial.println(bitCount); + #endif + String fullCardID = String(facilityID)+":"+String(cardID); + + #ifdef SERIAL_DEBUG + Serial.print("Full Card (Tinance2): "); + Serial.println(fullCardID); + #endif + + #ifdef WEB_SERIAL_DEBUG + WebSerial.print("Full Card (Tinance2): "); + WebSerial.println(fullCardID); + #endif + cardData = 0; // Reset the card data + lastDataTime = millis(); // Reset the time of last received data + bitCount = 0; // Reset the bit count + + + #ifdef LOCAL_ACL + if (acl.validateAccess(String(cardID))) { + #ifdef SERIAL_DEBUG + Serial.println("LOCAL_AUTH: Access granted!"); + #endif + // Perform actions for authorized access + #ifdef LATCH_DOOR + unlockDoor(false); + delay(RELAY_DELAY); + lockDoor(); + #endif + #ifdef TOGGLE_DOOR + toggleDoor(); + delay(RELAY_DELAY); + #endif + } else { + #ifdef SERIAL_DEBUG + Serial.println("LOCAL_AUTH: Access denied!"); + #endif + // Perform actions for denied access + #ifdef BUZZER + denied_beep(); + #endif + } + #endif + + } +} \ No newline at end of file diff --git a/upload_params.ini.dist b/upload_params.ini.dist new file mode 100644 index 0000000..a8c2a49 --- /dev/null +++ b/upload_params.ini.dist @@ -0,0 +1,3 @@ +[env] +upload_protocol = custom +upload_url = http://admin:admin@remotehost/update \ No newline at end of file