Basic REST API

This commit is contained in:
Jean Jacques Avril 2022-02-19 03:06:28 +01:00
parent e2c7a65e7c
commit f2fd5bc3b6
11 changed files with 319 additions and 68 deletions

View File

@ -9,8 +9,19 @@
- Allow integrity check for duplicate pin numbers, rfid etc. - Allow integrity check for duplicate pin numbers, rfid etc.
* Use text templates for Display * Use text templates for Display
* Rebuild WEB UI -> Mobile first approach. More limited features but therefore easy to understand * 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 ## Log
### 13.02.2022 ### 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. + 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

12
data/static/error404.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Error 404: Page not found.</h1>
</body>
</html>

12
data/static/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Welcome on ESP8266</h1>
</body>
</html>

View File

@ -13,6 +13,9 @@ platform = espressif8266
board = nodemcuv2 board = nodemcuv2
framework = arduino framework = arduino
board_build.filesystem = littlefs 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 = lib_deps =
xreef/PCF8574 library@^2.2.1 xreef/PCF8574 library@^2.2.1
marcoschwartz/LiquidCrystal_I2C@^1.1.4 marcoschwartz/LiquidCrystal_I2C@^1.1.4

View File

@ -41,6 +41,10 @@ void Interface::showMessage(String msg1, String msg2, unsigned long display_time
this->_msg_display_time = display_time; this->_msg_display_time = display_time;
this->setState(states::MSG); this->setState(states::MSG);
} }
int Interface::getState(){
return _display_state;
}
void Interface::render() void Interface::render()
{ {
if (this->_keyboard->available()) if (this->_keyboard->available())

View File

@ -37,6 +37,7 @@ public:
void begin(Keyboard* Keyboard); void begin(Keyboard* Keyboard);
void render(); void render();
void setState(states state); void setState(states state);
int getState();
void greetUser(String username); void greetUser(String username);
String getPin(); String getPin();
bool pinAvailable(); bool pinAvailable();

View File

@ -14,10 +14,19 @@ bool UserDb::add_user(User &user)
{ {
File db = LittleFS.open(filename, "a"); 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"); if (db)
{
if (db.size() > 0)
db.write("\n");
String csvline = user.toCSVString();
db.write(csvline.c_str());
db.close(); db.close();
return true; return true;
} }
else
return false;
}
User UserDb::user_by_pin(String cmp) User UserDb::user_by_pin(String cmp)
{ {
#ifdef DEBUG #ifdef DEBUG
@ -27,7 +36,7 @@ User UserDb::user_by_pin(String cmp)
do do
{ {
User temp = *it; User temp = *it;
if (temp.enabled) if (temp.match)
return temp; return temp;
++it; ++it;
} while (it.has_next()); } while (it.has_next());
@ -42,7 +51,7 @@ User UserDb::user_by_rfid(String cmp)
do do
{ {
User temp = *it; User temp = *it;
if (temp.enabled) if (temp.match)
return temp; return temp;
++it; ++it;
} while (it.has_next()); } while (it.has_next());
@ -50,15 +59,20 @@ User UserDb::user_by_rfid(String cmp)
} }
User UserDb::user_by_uid(unsigned long 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 #ifdef DEBUG
Serial.println("Searching for user with user id " + String(cmp)); Serial.println("Searching for user with id " + cmp);
#endif #endif
Iterator it = begin(); Iterator it = begin_with_filter(&cmp, USERATTRIBUTES::USER_ID);
do do
{ {
User temp = *it; User temp = *it;
if (temp.uid == cmp) if (temp.match)
return temp; return temp;
++it; ++it;
} while (it.has_next()); } while (it.has_next());

View File

@ -28,12 +28,20 @@ namespace userdb
mutable String rfid_uid; mutable String rfid_uid;
mutable String user_pin; mutable String user_pin;
mutable bool enabled = false; 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; }
bool operator==(const User &o) const { return uid == o.uid; } bool operator==(const User &o) const { return uid == o.uid; }
String toString() String toString()
{ {
return "ID: " + String(uid) + " Name: " + last_name + " " + first_name + " Enabled: " + (enabled ? "Yes" : "No") + " LineNo.: " + String(line) + "[RFID/PIN]" + rfid_uid + "//" + user_pin; 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 class UserDb
@ -49,45 +57,12 @@ namespace userdb
User user_by_pin(String); User user_by_pin(String);
User user_by_rfid(String); User user_by_rfid(String);
User user_by_uid(unsigned long); User user_by_uid(unsigned long);
inline User read_csv_line(String &instring, uint32_t &line, String *match, USERATTRIBUTES attr) User user_by_uid(String cmp);
{ bool drop(){
int locations[5]; LittleFS.remove(filename);
int location_index = 0; File n = LittleFS.open(filename,"a");
int str_length = instring.length() - 1; n.close();
for (int i = 0; i < str_length; i++) return true;
{
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;
} }
static inline User read_csv_line(File &instream, uint32_t &line, String *match, USERATTRIBUTES attr) static inline User read_csv_line(File &instream, uint32_t &line, String *match, USERATTRIBUTES attr)
{ {
@ -106,6 +81,7 @@ namespace userdb
return res; return res;
res.enabled = instream.read() == '1' ? true : false; res.enabled = instream.read() == '1' ? true : false;
res.line = line; res.line = line;
res.match = true;
return res; return res;
} }
bool remove_user(User &user); bool remove_user(User &user);
@ -119,7 +95,7 @@ namespace userdb
while (old_db.available()) while (old_db.available())
{ {
if (curr_line == line) if (curr_line == line)
while (old_db.read() != '\n') while (old_db.available()&&old_db.read() != '\n')
{ {
} }
else else
@ -194,10 +170,8 @@ namespace userdb
{ {
if (db_file.available()) 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); current = read_csv_line(db_file, line, match, filter_attr);
// Position stream pointer to beginning of the next line
while (db_file.available()) while (db_file.available())
{ {
if (db_file.read() == '\n') if (db_file.read() == '\n')
@ -223,7 +197,7 @@ namespace userdb
bool available = true; bool available = true;
uint32_t line = 0; uint32_t line = 0;
}; };
Iterator begin() { return Iterator(LittleFS.open("userdb.csv", "r")); } Iterator begin() { return Iterator(LittleFS.open(filename, "r")); }
Iterator begin_with_filter(String *match, USERATTRIBUTES attr) { return Iterator(LittleFS.open("userdb.csv", "r"), match, attr); } Iterator begin_with_filter(String *match, USERATTRIBUTES attr) { return Iterator(LittleFS.open(filename, "r"), match, attr); }
}; };
} }

176
src/WebConsole.cpp Normal file
View File

@ -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<capacity> 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<unsigned long>();
else
{
_server->send(500, "text/json", "{\"error\":\"UserId not provided.\"}");
return;
}
created.first_name = doc["first_name"].as<String>();
created.last_name = doc["last_name"].as<String>();
created.rfid_uid = doc["rfid_uid"].as<String>();
created.user_pin = doc["user_pin"].as<String>();
created.enabled = doc["enabled"].as<bool>();
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\"}");
}
}

40
src/WebConsole.h Normal file
View File

@ -0,0 +1,40 @@
#pragma once
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <uri/UriBraces.h>
#include <uri/UriRegex.h>
#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;
};
}

View File

@ -5,13 +5,13 @@
#include "Rfid.h" #include "Rfid.h"
#include "Interface.h" #include "Interface.h"
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#include <vector> #include "WebConsole.h"
//#include "Users.h"
#include "UserDb.h" #include "UserDb.h"
#include "Persistence.h" #include "Persistence.h"
// File persistence // File persistence
Persistence persistence; Persistence persistence;
userdb::UserDb userdatabase("userdb.csv"); userdb::UserDb userdatabase("userdb.csv");
webconsole::WebConsole web;
// Rfid // Rfid
Rfid rfid; Rfid rfid;
// i2C Bus // i2C Bus
@ -32,30 +32,36 @@ IPAddress subnet(255, 255, 255, 0);
void setup() void setup()
{ {
Persistence::Configuration config = persistence.loadConfig(); Persistence::Configuration config = persistence.loadConfig();
// users.ImportFromPersistence(); Serial.begin(115200);
Serial.begin(9600);
Serial.println("Starting System"); 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) 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 else
Serial.println("\t1. AP setup without password: " +WiFi.softAP(config.SSID) ? "Ready" : "Failed!"); Serial.println(WiFi.softAP(config.SSID) ? "Ready" : "Failed!");
delay(250); delay(150);
#ifdef DEBUG #ifdef DEBUG
userdatabase.print_to_serial(); userdatabase.print_to_serial();
#endif #endif
web.init(&userdatabase);
keyboard.begin(&Wire); keyboard.begin(&Wire);
rfid.begin(); rfid.begin();
web.attachRfid(&rfid);
iface.begin(&keyboard); iface.begin(&keyboard);
userdatabase.remove_user_by_line("userdb.csv",2);
} }
void loop() void loop()
{ {
keyboard.scanAsync();
rfid.scan(); rfid.scan();
web.serve();
keyboard.scanAsync();
userdb::User login_user; 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(); unsigned long delta = millis();
login_user = userdatabase.user_by_pin(iface.getPin()); login_user = userdatabase.user_by_pin(iface.getPin());
@ -65,7 +71,6 @@ void loop()
iface.showMessage("Login failed!", "-> Pin incorrect", 3000); iface.showMessage("Login failed!", "-> Pin incorrect", 3000);
return; return;
} }
} }
else if (rfid.available()) else if (rfid.available())
{ {
@ -77,7 +82,6 @@ void loop()
iface.showMessage("Login failed!", "-> Unkown Card", 3000); iface.showMessage("Login failed!", "-> Unkown Card", 3000);
return; return;
} }
} }
if (login_user.enabled == true) if (login_user.enabled == true)