From f2fd5bc3b6e693562354b1904111a0409d115008 Mon Sep 17 00:00:00 2001 From: Jean Jacques Avril Date: Sat, 19 Feb 2022 03:06:28 +0100 Subject: [PATCH] Basic REST API --- Notes.md | 11 +++ data/static/error404.html | 12 +++ data/static/index.html | 12 +++ platformio.ini | 3 + src/Interface.cpp | 4 + src/Interface.h | 1 + src/UserDb.cpp | 30 +++++-- src/UserDb.h | 64 ++++---------- src/WebConsole.cpp | 176 ++++++++++++++++++++++++++++++++++++++ src/WebConsole.h | 40 +++++++++ src/main.cpp | 34 ++++---- 11 files changed, 319 insertions(+), 68 deletions(-) create mode 100644 data/static/error404.html create mode 100644 data/static/index.html create mode 100644 src/WebConsole.cpp create mode 100644 src/WebConsole.h diff --git a/Notes.md b/Notes.md index 172c4df..b827aac 100644 --- a/Notes.md +++ b/Notes.md @@ -9,8 +9,19 @@ - Allow integrity check for duplicate pin numbers, rfid etc. * Use text templates for Display * Rebuild WEB UI -> Mobile first approach. More limited features but therefore easy to understand +### 19.02.2022 +* CSV upload +* Admin authentification +* Api for device settings +* capitve portal +* (Optional) REST user update +* Create WEB UI without React + ------ ## Log ### 13.02.2022 + Finally I found a solution for using a json document with variable file size. Import and export works well - till you have more than 300 users. +### 19.02.2022 ++ Stable and fast csv database implemented. ++ Basic REST API: read, create and delete users / read and drop csv db diff --git a/data/static/error404.html b/data/static/error404.html new file mode 100644 index 0000000..aa3bce1 --- /dev/null +++ b/data/static/error404.html @@ -0,0 +1,12 @@ + + + + + + + Document + + +

Error 404: Page not found.

+ + \ No newline at end of file diff --git a/data/static/index.html b/data/static/index.html new file mode 100644 index 0000000..9847b8e --- /dev/null +++ b/data/static/index.html @@ -0,0 +1,12 @@ + + + + + + + Document + + +

Welcome on ESP8266

+ + \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index aa162ac..28dfe43 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,6 +13,9 @@ platform = espressif8266 board = nodemcuv2 framework = arduino board_build.filesystem = littlefs +monitor_speed = 115200 ; set to the baud rate you pass to Serial.begin(…) +monitor_filters = esp8266_exception_decoder, default +build_type = debug lib_deps = xreef/PCF8574 library@^2.2.1 marcoschwartz/LiquidCrystal_I2C@^1.1.4 diff --git a/src/Interface.cpp b/src/Interface.cpp index e3816cd..1d2db98 100644 --- a/src/Interface.cpp +++ b/src/Interface.cpp @@ -41,6 +41,10 @@ void Interface::showMessage(String msg1, String msg2, unsigned long display_time this->_msg_display_time = display_time; this->setState(states::MSG); } + +int Interface::getState(){ + return _display_state; +} void Interface::render() { if (this->_keyboard->available()) diff --git a/src/Interface.h b/src/Interface.h index 299042f..ad8e01f 100644 --- a/src/Interface.h +++ b/src/Interface.h @@ -37,6 +37,7 @@ public: void begin(Keyboard* Keyboard); void render(); void setState(states state); + int getState(); void greetUser(String username); String getPin(); bool pinAvailable(); diff --git a/src/UserDb.cpp b/src/UserDb.cpp index 4dc4a42..181c077 100644 --- a/src/UserDb.cpp +++ b/src/UserDb.cpp @@ -14,10 +14,19 @@ bool UserDb::add_user(User &user) { File db = LittleFS.open(filename, "a"); - db.write("\n" + String(user.uid) + ";" + user.first_name + ";" + user.last_name + ";" + user.rfid_uid + ";" + user.user_pin + ";" + user.enabled ? "1" : "0"); - db.close(); - return true; + if (db) + { + if (db.size() > 0) + db.write("\n"); + String csvline = user.toCSVString(); + db.write(csvline.c_str()); + db.close(); + return true; + } + else + return false; } + User UserDb::user_by_pin(String cmp) { #ifdef DEBUG @@ -27,7 +36,7 @@ User UserDb::user_by_pin(String cmp) do { User temp = *it; - if (temp.enabled) + if (temp.match) return temp; ++it; } while (it.has_next()); @@ -42,7 +51,7 @@ User UserDb::user_by_rfid(String cmp) do { User temp = *it; - if (temp.enabled) + if (temp.match) return temp; ++it; } while (it.has_next()); @@ -50,15 +59,20 @@ User UserDb::user_by_rfid(String cmp) } User UserDb::user_by_uid(unsigned long cmp) +{ + String cmps = String(cmp); + return user_by_uid(cmps); +} +User UserDb::user_by_uid(String cmp) { #ifdef DEBUG - Serial.println("Searching for user with user id " + String(cmp)); + Serial.println("Searching for user with id " + cmp); #endif - Iterator it = begin(); + Iterator it = begin_with_filter(&cmp, USERATTRIBUTES::USER_ID); do { User temp = *it; - if (temp.uid == cmp) + if (temp.match) return temp; ++it; } while (it.has_next()); diff --git a/src/UserDb.h b/src/UserDb.h index 3f8dacd..3cc6167 100644 --- a/src/UserDb.h +++ b/src/UserDb.h @@ -28,12 +28,20 @@ namespace userdb mutable String rfid_uid; mutable String user_pin; mutable bool enabled = false; + mutable bool match = false; bool operator<(const User &o) const { return uid < o.uid; } bool operator==(const User &o) const { return uid == o.uid; } String toString() { return "ID: " + String(uid) + " Name: " + last_name + " " + first_name + " Enabled: " + (enabled ? "Yes" : "No") + " LineNo.: " + String(line) + "[RFID/PIN]" + rfid_uid + "//" + user_pin; } + String toCSVString() + { + return String(uid) + ";" + first_name + ";" + last_name + ";" + rfid_uid + ";" + user_pin + ";" + (enabled ? "1" : "0"); + } + String toJSONString(){ + return "{\"uid\":\""+String(uid) + "\",\"first_name\":\"" + first_name + "\",\"last_name\":\"" + last_name + "\",\"rfid_uid\":\"" + rfid_uid + "\",\"user_pin\":\"" + user_pin + "\",\"enabled\":" + (enabled ? "true" : "false")+"}"; + } }; class UserDb @@ -49,45 +57,12 @@ namespace userdb User user_by_pin(String); User user_by_rfid(String); User user_by_uid(unsigned long); - inline User read_csv_line(String &instring, uint32_t &line, String *match, USERATTRIBUTES attr) - { - int locations[5]; - int location_index = 0; - int str_length = instring.length() - 1; - for (int i = 0; i < str_length; i++) - { - if (instring.charAt(i) == ';') - { - locations[location_index] = i; - location_index++; - } - } - User res; - if (attr == RFID_UID) - { - res.rfid_uid = instring.substring(locations[2] + 1, locations[3]); - if (!res.rfid_uid.equals(*match)) - return res; - res.user_pin = instring.substring(locations[3] + 1, locations[4]); - } - else if (attr == USER_PIN) - { - res.user_pin = instring.substring(locations[3] + 1, locations[4]); - if (!res.user_pin.equals(*match)) - return res; - res.rfid_uid = instring.substring(locations[2] + 1, locations[3]); - } - else - { - res.rfid_uid = instring.substring(locations[2] + 1, locations[3]); - res.user_pin = instring.substring(locations[3] + 1, locations[4]); - } - res.uid = instring.substring(0, locations[0]).toInt(); - res.first_name = instring.substring(locations[0] + 1, locations[1]); - res.last_name = instring.substring(locations[1] + 1, locations[2]); - res.enabled = instring.charAt(locations[4] + 1) == '1' ? true : false; - res.line = line; - return res; + User user_by_uid(String cmp); + bool drop(){ + LittleFS.remove(filename); + File n = LittleFS.open(filename,"a"); + n.close(); + return true; } static inline User read_csv_line(File &instream, uint32_t &line, String *match, USERATTRIBUTES attr) { @@ -106,6 +81,7 @@ namespace userdb return res; res.enabled = instream.read() == '1' ? true : false; res.line = line; + res.match = true; return res; } bool remove_user(User &user); @@ -119,7 +95,7 @@ namespace userdb while (old_db.available()) { if (curr_line == line) - while (old_db.read() != '\n') + while (old_db.available()&&old_db.read() != '\n') { } else @@ -194,10 +170,8 @@ namespace userdb { if (db_file.available()) { - // String temp = db_file.readStringUntil('\n'); - // current = read_csv_line(temp, line, match, filter_attr); - // line++; current = read_csv_line(db_file, line, match, filter_attr); + // Position stream pointer to beginning of the next line while (db_file.available()) { if (db_file.read() == '\n') @@ -223,7 +197,7 @@ namespace userdb bool available = true; uint32_t line = 0; }; - Iterator begin() { return Iterator(LittleFS.open("userdb.csv", "r")); } - Iterator begin_with_filter(String *match, USERATTRIBUTES attr) { return Iterator(LittleFS.open("userdb.csv", "r"), match, attr); } + Iterator begin() { return Iterator(LittleFS.open(filename, "r")); } + Iterator begin_with_filter(String *match, USERATTRIBUTES attr) { return Iterator(LittleFS.open(filename, "r"), match, attr); } }; } \ No newline at end of file diff --git a/src/WebConsole.cpp b/src/WebConsole.cpp new file mode 100644 index 0000000..375929a --- /dev/null +++ b/src/WebConsole.cpp @@ -0,0 +1,176 @@ +#include "WebConsole.h" + +using namespace webconsole; + +WebConsole::WebConsole() {} +WebConsole::~WebConsole() +{ + _server->close(); + delete (_server); +} +bool WebConsole::init(userdb::UserDb *userdb) +{ + _server = new ESP8266WebServer(80); + this->userdb = userdb; + _server->begin(); + _server->on("/userdb", HTTPMethod::HTTP_DELETE, std::bind(&WebConsole::_dropUserDb, this)); + _server->on("/userdb", HTTPMethod::HTTP_GET, std::bind(&WebConsole::_getUserDb, this)); + _server->on("/rfid", std::bind(&WebConsole::_catchRFID, this)); + _server->on(UriBraces("/user/{}"), HTTPMethod::HTTP_DELETE, std::bind(&WebConsole::_deleteUser, this)); + _server->on(UriBraces("/user/{}"), HTTPMethod::HTTP_GET, std::bind(&WebConsole::_getUser, this)); + _server->on(UriBraces("/user/{}"), HTTPMethod::HTTP_PUT, std::bind(&WebConsole::_createUser, this)); + _server->on(UriBraces("/get/{}"), std::bind(&WebConsole::_deleteUser, this)); + //_server->on("/bypin",std::bind(&WebConsole::_findPin,this)); + _server->onNotFound(std::bind(&WebConsole::_handleStatic, this)); + return true; +} +void WebConsole::attachRfid(Rfid *rfid){ + this->rfid=rfid; +} +void WebConsole::serve() +{ + if (catch_rfid && rfid != nullptr && rfid->available()) + { + rfid_buffer = rfid->getID(); + catch_rfid_updated = true; + catch_rfid = false; + } + _server->handleClient(); +} +bool WebConsole::isInterceptingRfid(){ + return catch_rfid; +} +void WebConsole::_handleStatic() +{ + String path = path_prefix + _server->uri(); + Serial.print("Request " + path); + File src = LittleFS.open(path, "r"); + if (src.isDirectory()) + { + path += "index.html"; + src.close(); + src = LittleFS.open(path, "r"); + } + if (!src) + { + path = "/static/error404.html"; + src.close(); + src = LittleFS.open(path, "r"); + } + + Serial.println(" resolved to " + path); + if (src) + { + String content_type = "text/html"; + _server->streamFile(src, content_type); + src.close(); + } + else + { + _server->send(500, "text/plain", "Internal error 500"); + } +} + +void WebConsole::_getUserDb() +{ + File src = LittleFS.open("userdb.csv", "r"); + if (src) + { + _server->streamFile(src, "text/csv"); + } + src.close(); +} +void WebConsole::_deleteUser() +{ + if (userdb == nullptr) + { + _server->send(500, "text/json", "{\"error\":\"UserDb not initialized\"}"); + return; + } + String uid = _server->pathArg(0); + userdb::User del = userdb->user_by_uid(uid); + if (del.match == true) + { + userdb->remove_user(del); + _server->send(200, "text/plain", del.toJSONString()); + } + else + { + _server->send(500, "text/json", "{\"error\":\"User not found.\"}"); + } +} +void WebConsole::_getUser() +{ + if (userdb == nullptr) + { + _server->send(500, "text/json", "{\"error\":\"UserDb not initialized\"}"); + return; + } + String uid = _server->pathArg(0); + userdb::User res = userdb->user_by_uid(uid); + if (res.match == true) + { + _server->send(200, "text/json", res.toJSONString()); + } + else + { + _server->send(500, "text/json", "{\"error\":\"User not found.\"}"); + } +} +void WebConsole::_createUser() +{ + userdb::User created; + String body = _server->arg("plain"); + const int capacity = 1024; + StaticJsonDocument doc; + DeserializationError err = deserializeJson(doc, body); + if (err.code() != err.Ok) + { + Serial.println(err.c_str()); + Serial.println(body); + String response = "{\"error\":\""; + response += err.c_str(); + response += "\"}"; + _server->send(500, "text/json", response); + return; + } + if (!_server->pathArg(0).isEmpty()) + created.uid = _server->pathArg(0).toInt(); + else if (!doc["uid"].isNull()) + created.uid = doc["uid"].as(); + else + { + _server->send(500, "text/json", "{\"error\":\"UserId not provided.\"}"); + return; + } + created.first_name = doc["first_name"].as(); + created.last_name = doc["last_name"].as(); + created.rfid_uid = doc["rfid_uid"].as(); + created.user_pin = doc["user_pin"].as(); + created.enabled = doc["enabled"].as(); + userdb->add_user(created); + _server->send(200, "text/json", created.toJSONString()); +} +void WebConsole::_dropUserDb() +{ + if (userdb->drop()) + _server->send(500, "text/json", "{\"ok\":\"UserDb dropped.\"}"); + else + _server->send(500, "text/json", "{\"error\":\"UserDb could not be dropped.\"}"); +} +void WebConsole::_catchRFID() +{ + if (catch_rfid_updated) + { + String response = "{\"rfid_uid\":\"" + rfid_buffer + "\"}"; + _server->send(500, "text/json", response); + catch_rfid_updated = false; + } + else if(catch_rfid){ + _server->send(500, "text/json", "{\"ok\":\"already activated\"}"); + } + else{ + catch_rfid = true; + _server->send(500, "text/json", "{\"ok\":\"now activated\"}"); + } +} diff --git a/src/WebConsole.h b/src/WebConsole.h new file mode 100644 index 0000000..b3328d4 --- /dev/null +++ b/src/WebConsole.h @@ -0,0 +1,40 @@ +#pragma once +#include +#include +#include +#include +#include "LittleFS.h" +#include "ArduinoJson.h" +#include "UserDb.h" +#include "Rfid.h" +namespace webconsole +{ + static const char path_prefix[] PROGMEM = "/static"; + static ESP8266WebServer _server(80); + class WebConsole + { + public: + WebConsole(); + ~WebConsole(); + + bool init(userdb::UserDb *userdb); + void attachRfid(Rfid *rfid); + void serve(); + bool isInterceptingRfid(); + private: + void _handleStatic(); + void _getUserDb(); + void _deleteUser(); + void _getUser(); + void _createUser(); + void _dropUserDb(); + void _catchRFID(); + ESP8266WebServer* _server; + userdb::UserDb *userdb = nullptr; + bool catch_rfid = false; + bool catch_rfid_updated = false; + String rfid_buffer; + Rfid *rfid = nullptr; + }; + +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 3b0b3c9..02b2e5c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,13 +5,13 @@ #include "Rfid.h" #include "Interface.h" #include -#include -//#include "Users.h" +#include "WebConsole.h" #include "UserDb.h" #include "Persistence.h" // File persistence Persistence persistence; userdb::UserDb userdatabase("userdb.csv"); +webconsole::WebConsole web; // Rfid Rfid rfid; // i2C Bus @@ -32,52 +32,56 @@ IPAddress subnet(255, 255, 255, 0); void setup() { Persistence::Configuration config = persistence.loadConfig(); - // users.ImportFromPersistence(); - Serial.begin(9600); + Serial.begin(115200); Serial.println("Starting System"); - Serial.println("\t1. Network config: " + WiFi.softAPConfig(local_IP, gateway, subnet) ? "Ready" : "Failed!"); + Serial.print("\t1. Network config ->"); + Serial.println(WiFi.softAPConfig(local_IP, gateway, subnet) ? "Ready" : "Failed!"); + Serial.print("\t2 AP setup " + String(config.SSID)+ " -> "); if (strlen(config.PASS) > 0) - Serial.println("\t1. AP setup with password: " + WiFi.softAP(config.SSID, config.PASS) ? "Ready" : "Failed!"); + Serial.println(WiFi.softAP(config.SSID, config.PASS) ? "Ready" : "Failed!"); else - Serial.println("\t1. AP setup without password: " +WiFi.softAP(config.SSID) ? "Ready" : "Failed!"); - delay(250); + Serial.println(WiFi.softAP(config.SSID) ? "Ready" : "Failed!"); + delay(150); #ifdef DEBUG userdatabase.print_to_serial(); #endif + web.init(&userdatabase); keyboard.begin(&Wire); rfid.begin(); + web.attachRfid(&rfid); iface.begin(&keyboard); - userdatabase.remove_user_by_line("userdb.csv",2); } void loop() { - keyboard.scanAsync(); rfid.scan(); + web.serve(); + keyboard.scanAsync(); userdb::User login_user; - if (iface.pinAvailable()) + if(web.isInterceptingRfid()&&iface.getState()==0){ + iface.showMessage("WebUI connected", "Awaiting RFID", 3000); + } + else if (iface.pinAvailable()) { unsigned long delta = millis(); login_user = userdatabase.user_by_pin(iface.getPin()); - Serial.println("Query duration: " +String(millis()-delta)); + Serial.println("Query duration: " + String(millis() - delta)); if (login_user.enabled == false) { iface.showMessage("Login failed!", "-> Pin incorrect", 3000); return; } - } else if (rfid.available()) { unsigned long delta = millis(); login_user = userdatabase.user_by_rfid(rfid.getID()); - Serial.println("Query duration: " +String(millis()-delta)); + Serial.println("Query duration: " + String(millis() - delta)); if (login_user.enabled == false) { iface.showMessage("Login failed!", "-> Unkown Card", 3000); return; } - } if (login_user.enabled == true)