Files
esp32-fingerprint-safe/app/main.cpp
2025-12-12 15:06:20 +01:00

680 lines
17 KiB
C++

#include <SmingCore.h>
#include <Network/Mqtt/MqttBuffer.h>
#include <Data/WebHelpers/base64.h>
#include <ArduinoJson.h>
#include <MultipartParser.h>
#include <HttpMultipartResource.h>
#include <OtaUpgradeStream.h>
#include "utils.h"
#include "main.h"
CMain g_Main;
CMain::CMain() : m_FingerLogic(this)
{
}
static void fileUploadMapper(HttpFiles& files)
{
files["firmware"] = new OtaUpgradeStream;
}
static int onMessageDelivered(MqttClient& client, mqtt_message_t* message)
{
Serial1 << _F("Message with id ") << message->puback.message_id << _F(" and QoS ") << message->puback.qos
<< _F(" was delivered successfully.") << endl;
return 0;
}
// Callback for messages, arrived from MQTT server
static int onMessageReceived(MqttClient& client, mqtt_message_t* message)
{
Serial1 << _F("Received: ") << MqttBuffer(message->publish.topic_name) << ':' << endl;
Serial1 << '\t' << MqttBuffer(message->publish.content) << endl;
return 0;
}
void CMain::OnStationGotIP(IpAddress ip, IpAddress mask, IpAddress gateway)
{
m_StationConnected = true;
debugf("GOTIP - IP: %s, MASK: %s, GW: %s\n", ip.toString().c_str(), mask.toString().c_str(), gateway.toString().c_str());
StartMqttClient();
}
void CMain::OnStationDisconnect(const String& ssid, MacAddress bssid, WifiDisconnectReason reason)
{
m_StationConnected = false;
debugf("DISCONNECT - SSID: %s, REASON: %d", ssid.c_str(), WifiEvents.getDisconnectReasonDesc(reason).c_str());
}
int CMain::MqttOnConnect(MqttClient& client, mqtt_message_t* message)
{
debugf("MQTT Connected!");
MqttSendMessage("safeweb/status", "online");
MqttSendDescription();
return 0;
}
void CMain::MqttOnDisconnect(TcpClient& client, bool flag)
{
if(flag == true)
debugf("MQTT Broker Disconnected!!");
else
debugf("MQTT Broker Unreachable!!");
// Restart connection attempt after few seconds
if(!m_StationConnected)
m_MqttTimer.initializeMs(2 * 1000, TimerDelegate(&CMain::StartMqttClient, this)).start();
}
void CMain::MqttSendDescription()
{
if(m_Mqtt.getConnectionState() != eTCS_Connected) {
StartMqttClient(); // Auto reconnect
}
}
void CMain::MqttSendMessage(const char *topic, const char *msg)
{
if(m_Mqtt.getConnectionState() != eTCS_Connected) {
StartMqttClient(); // Auto reconnect
}
m_Mqtt.publish(topic, msg);
}
void CMain::StartMqttClient()
{
m_MqttTimer.stop();
if(!m_StationConnected || !Settings().m_MqttURL.Port || !Settings().m_MqttURL.Host)
return;
if(!m_Mqtt.setWill("safeweb/status", "offline", MqttClient::getFlags(MQTT_QOS_AT_LEAST_ONCE, MQTT_RETAIN_TRUE))) {
debugf("Unable to mqtt.setWill");
}
m_Mqtt.setEventHandler(MQTT_TYPE_PUBACK, onMessageDelivered);
m_Mqtt.setConnectedHandler(MqttDelegate(&CMain::MqttOnConnect, this));
m_Mqtt.setCompleteDelegate(TcpClientCompleteDelegate(&CMain::MqttOnDisconnect, this));
m_Mqtt.setMessageHandler(onMessageReceived);
m_Mqtt.connect(Settings().m_MqttURL, Settings().m_aMqttName);
}
void CMain::Init(HardwareSerial &serial)
{
m_Settings.Load();
//m_Settings.Save();
WifiAccessPoint.enable(false);
if(m_Settings.m_aSSID[0])
{
debugf("Station: %s", m_Settings.m_aSSID);
WifiStation.enable(true);
WifiStation.config(m_Settings.m_aSSID, m_Settings.m_aPSK);
if(m_Settings.m_DHCP && m_Settings.m_aHostname[0])
WifiStation.setHostname(m_Settings.m_aHostname);
if(!m_Settings.m_DHCP && !m_Settings.m_Address.isNull())
WifiStation.setIP(m_Settings.m_Address, m_Settings.m_Netmask, m_Settings.m_Gateway);
WifiStation.connect();
}
else
{
debugf("Access Point 'admin': %s", m_Settings.m_aPassword);
WifiAccessPoint.config("safeweb", m_Settings.m_aPassword, AUTH_WPA2_PSK);
WifiAccessPoint.enable(true);
}
WifiEvents.onStationGotIP(StationGotIPDelegate(&CMain::OnStationGotIP, this));
WifiEvents.onStationDisconnect(StationDisconnectDelegate(&CMain::OnStationDisconnect, this));
m_FTP.listen(21);
m_FTP.addUser(m_Settings.m_aUsername, m_Settings.m_aPassword);
HttpServerSettings settings;
settings.closeOnContentError = false;
settings.keepAliveSeconds = 5;
m_HttpServer.configure(settings);
m_HttpServer.setBodyParser(MIME_JSON, bodyToStringParser);
m_HttpServer.setBodyParser(MIME_FORM_MULTIPART, formMultipartParser);
m_HttpServer.listen(80);
m_HttpServer.paths.set("/api", HttpPathDelegate(&CMain::HttpOnApi, this));
m_HttpServer.paths.set("/api/state", HttpPathDelegate(&CMain::HttpOnApi, this));
m_HttpServer.paths.set("/api/dashboard", HttpPathDelegate(&CMain::HttpOnApi, this));
m_HttpServer.paths.set("/api/fingerprint", HttpPathDelegate(&CMain::HttpOnApi, this));
m_HttpServer.paths.set("/api/fingerprint/label", HttpPathDelegate(&CMain::HttpOnApi, this));
m_HttpServer.paths.set("/api/fingerprint/delete", HttpPathDelegate(&CMain::HttpOnApi, this));
m_HttpServer.paths.set("/api/fingerprint/enroll", HttpPathDelegate(&CMain::HttpOnApi, this));
m_HttpServer.paths.set("/api/fingerprint/security", HttpPathDelegate(&CMain::HttpOnApi, this));
m_HttpServer.paths.set("/api/unlock", HttpPathDelegate(&CMain::HttpOnApi, this));
m_HttpServer.paths.set("/api/reset", HttpPathDelegate(&CMain::HttpOnApi, this));
m_HttpServer.paths.set("/upgrade", new HttpMultipartResource(fileUploadMapper, HttpResourceDelegate(&CMain::HttpOnUpload, this)));
m_HttpServer.paths.setDefault(HttpPathDelegate(&CMain::HttpOnFile, this));
m_FingerPrint.Init(serial, 0xFFFFFFFF, 0x00000000);
m_FingerLogic.Init(&m_FingerPrint);
m_LightSleepTimer.initializeMs(60 * 1000, TimerDelegate(&CMain::EnterLightSleep, this));
}
static void wakeupCallback() {
debugf("Wakeing up @ %lu", millis());
debugf("Wakeing up @ %s", SystemClock.getSystemTimeString().c_str());
wifi_fpm_close();
wifi_set_opmode(STATION_MODE);
wifi_station_connect();
system_soft_wdt_feed();
}
void CMain::EnterLightSleep()
{
debugf("Going to sleep @ %lu", millis());
debugf("Going to sleep @ %s", SystemClock.getSystemTimeString().c_str());
wifi_station_disconnect();
wifi_set_opmode(NULL_MODE);
wifi_fpm_set_sleep_type(LIGHT_SLEEP_T);
wifi_fpm_open();
wifi_fpm_set_wakeup_cb(wakeupCallback);
wifi_fpm_do_sleep(0xFFFFFFF);
system_soft_wdt_feed();
}
bool CMain::HttpAuthorized(HttpRequest &request, HttpResponse &response)
{
String auth = request.getHeader("Authorization");
if(auth.startsWith("Basic "))
{
int headerLength = auth.length() - 6;
if(headerLength <= 64)
{
auth = base64_decode(auth.c_str() + 6, headerLength);
if(auth)
{
int sep = auth.indexOf(':');
if(sep != -1)
{
String username = auth.substring(0, sep);
String password = auth.substring(sep + 1);
if(username == m_Settings.m_aUsername && password == m_Settings.m_aPassword)
return true;
}
}
}
}
response.code = HTTP_STATUS_UNAUTHORIZED;
response.setHeader("WWW-Authenticate", "Basic realm=\"safeweb\"");
response.setHeader("401 Wrong credentials", "Authentication required");
response.setHeader("Connection", "close");
return false;
}
void CMain::HttpOnApi(HttpRequest &request, HttpResponse &response)
{
if(!HttpAuthorized(request, response))
return;
String path = request.uri.Path;
if(path.length() < 6)
{
response.code = HTTP_STATUS_NOT_FOUND;
return;
}
String endpoint = path.substring(5);
DynamicJsonDocument jsonReq(1024);
deserializeJson(jsonReq, request.getBody());
DynamicJsonDocument jsonResp(1024);
response.setAllowCrossDomainOrigin("*");
response.setHeader("Access-Control-Allow-Headers", "Content-Type");
if(request.method == HTTP_OPTIONS)
{
response.code = HTTP_STATUS_OK;
return;
}
HttpStatus status = HandleApi(request.method, endpoint, jsonReq, jsonResp);
if(status != HTTP_STATUS_OK)
{
response.code = status;
return;
}
String respString;
serializeJson(jsonResp, respString);
response.setContentType(MIME_JSON);
response.sendString(respString);
}
HttpStatus CMain::HandleApi(HttpMethod method, String endpoint, JsonDocument &req, JsonDocument &resp)
{
if(endpoint == "state" || endpoint == "dashboard")
{
if(method != HTTP_GET) return HTTP_STATUS_METHOD_NOT_ALLOWED;
JsonObject state = resp.createNestedObject("state");
state["unlocked"] = (bool)!digitalRead(SAFELOCK_DETECT_PIN);
state["opened"] = (bool)digitalRead(DOOR_DETECT_PIN);
state["battery"] = (system_adc_read() / 1024.f) / 0.237;
if(endpoint == "dashboard")
{
JsonObject network = resp.createNestedObject("network");
if(WifiStation.isEnabled())
{
network["type"] = "Station";
network["ssid"] = WifiStation.getSSID();
network["channel"] = WifiStation.getChannel();
network["dhcp"] = WifiStation.isEnabledDHCP();
network["rssi"] = WifiStation.getRssi();
network["signal"] = Rssi2Quality(network["rssi"]);
network["address"] = WifiStation.getIP().toString();
}
else
{
network["type"] = "Access Point";
network["ssid"] = WifiAccessPoint.getSSID();
network["channel"] = WifiStation.getChannel();
network["dhcp"] = true;
network["address"] = WifiAccessPoint.getIP().toString();
}
}
}
else if(endpoint == "unlock")
{
if(method != HTTP_POST) return HTTP_STATUS_METHOD_NOT_ALLOWED;
if(!req.containsKey("unlock"))
return HTTP_STATUS_BAD_REQUEST;
if(req["unlock"])
LockUnlock();
}
else if(endpoint == "reset")
{
if(method != HTTP_POST) return HTTP_STATUS_METHOD_NOT_ALLOWED;
if(!req.containsKey("reset"))
return HTTP_STATUS_BAD_REQUEST;
if(req["reset"])
System.restart(500);
}
else if(endpoint == "fingerprint")
{
if(method == HTTP_GET)
{
JsonArray fingerprints = resp.createNestedArray("fingerprints");
uint16_t tmp = Settings().m_FingerPrints.count();
for(uint16_t i = 0; i < tmp; i++)
{
JsonObject obj = fingerprints.createNestedObject();
const CSettings::CFingerPrint &finger = Settings().m_FingerPrints.valueAt(i);
obj["num"] = finger.m_FingerNum;
obj["label"] = String(finger.m_aLabel);
char aHexDigest[SHA256_SIZE*2+1];
bytes2hex(finger.m_aDigest, sizeof(finger.m_aDigest), aHexDigest, sizeof(aHexDigest));
obj["digest"] = String(aHexDigest);
}
resp["securityLevel"] = Settings().m_SecurityLevel;
}
else if(method == HTTP_POST)
{
if(!req.containsKey("securityLevel"))
return HTTP_STATUS_BAD_REQUEST;
int newLevel = req["securityLevel"].as<int>();
if(newLevel > FINGERPRINT_SECURITY_LEVEL_5 || newLevel < FINGERPRINT_SECURITY_LEVEL_1)
return HTTP_STATUS_BAD_REQUEST;
if(newLevel != Settings().m_SecurityLevel)
{
Settings().m_SecurityLevel = newLevel;
Settings().Save();
FingerLogic().SetSecurityLevel(newLevel);
}
}
else
return HTTP_STATUS_METHOD_NOT_ALLOWED;
}
else if(endpoint == "fingerprint/label")
{
if(method != HTTP_POST) return HTTP_STATUS_METHOD_NOT_ALLOWED;
if(!req.containsKey("index") || !req.containsKey("label"))
return HTTP_STATUS_BAD_REQUEST;
int index = req["index"].as<int>() - 1;
String label = req["label"];
if(index < 0 || index >= Settings().m_FingerPrints.count())
return HTTP_STATUS_BAD_REQUEST;
CSettings::CFingerPrint &finger = Settings().m_FingerPrints.valueAt(index);
strncpy(finger.m_aLabel, label.c_str(), sizeof(finger.m_aLabel));
Settings().Save();
}
else if(endpoint == "fingerprint/delete")
{
if(method != HTTP_POST) return HTTP_STATUS_METHOD_NOT_ALLOWED;
if(!req.containsKey("index"))
return HTTP_STATUS_BAD_REQUEST;
int index = req["index"].as<int>() - 1;
if(index < 0 || index >= Settings().m_FingerPrints.count())
return HTTP_STATUS_BAD_REQUEST;
const CSettings::CFingerPrint &finger = Settings().m_FingerPrints.valueAt(index);
FingerPrint().DeleteTemplate(finger.m_FingerNum, 1);
Settings().m_FingerPrints.removeAt(index);
Settings().Save();
}
else if(endpoint == "fingerprint/enroll")
{
if(method != HTTP_POST) return HTTP_STATUS_METHOD_NOT_ALLOWED;
if(req.containsKey("cancel"))
{
if(req["cancel"].as<bool>() && m_Enrolling)
{
FingerLogic().EnrollFinger(false);
m_Enrolling = false;
m_Enrolled = false;
}
}
else
{
if(!req.containsKey("enrolling"))
return HTTP_STATUS_BAD_REQUEST;
bool enrolling = req["enrolling"];
if(enrolling && !m_Enrolling)
{
if(m_Enrolled)
{
resp["done"] = true;
resp["message"] = m_EnrollMessage;
m_Enrolled = false;
}
else
{
resp["error"] = m_EnrollMessage;
}
}
else if(enrolling && m_Enrolling)
{
resp["done"] = false;
resp["message"] = m_EnrollMessage;
}
else // if(!enrolling)
{
if(!req.containsKey("label"))
return HTTP_STATUS_BAD_REQUEST;
if(!m_Enrolling)
{
FingerLogic().EnrollFinger();
m_Enrolling = true;
m_Enrolled = false;
m_EnrollLabel = String(req["label"].as<const char *>());
resp["done"] = false;
m_EnrollMessage = "Started enroll process. Please place your finger on the sensor.";
resp["message"] = m_EnrollMessage;
}
else
{
resp["done"] = true;
m_Enrolled = false;
resp["message"] = m_EnrollMessage;
}
}
}
}
else
{
return HTTP_STATUS_NOT_FOUND;
}
return HTTP_STATUS_OK;
}
void CMain::HttpOnFile(HttpRequest &request, HttpResponse &response)
{
if(!HttpAuthorized(request, response))
return;
String file = request.uri.Path;
if(file == "/")
file = "index.html";
if(file[0] == '/')
file = file.substring(1);
if(file[0] == '.')
{
response.code = HTTP_STATUS_FORBIDDEN;
return;
}
response.setCache(86400, true); // It's important to use cache for better performance.
response.sendFile(file);
}
int CMain::HttpOnUpload(HttpServerConnection& connection, HttpRequest& request, HttpResponse& response)
{
if(!HttpAuthorized(request, response))
return 1;
ReadWriteStream* file = request.files["firmware"];
auto otaStream = static_cast<OtaUpgradeStream*>(file);
if(otaStream == nullptr) {
debug_e("Something went wrong with the file upload");
return 1;
}
if(response.isSuccess() && !otaStream->hasError()) {
// defer the reboot by 1000 milliseconds to give time to the web server to return the response
System.restart(1000);
response.sendFile("otadone.html");
response.headers[HTTP_HEADER_CONNECTION] = "close";
return 0;
}
response.code = HTTP_STATUS_BAD_REQUEST;
response.setContentType(MIME_HTML);
String html = toString(otaStream->errorCode);
response.headers[HTTP_HEADER_CONTENT_LENGTH] = html.length();
response.sendString(html);
return 0;
}
void CMain::CheckSleep()
{
if(!m_FingerPlaced &&
!m_LockUnlocked &&
!m_DoorOpened)
{
m_LightSleepTimer.startOnce();
}
else
{
m_LightSleepTimer.stop();
}
}
void CMain::OnFinger(bool finger)
{
m_FingerPlaced = finger;
m_FingerLogic.OnFinger(finger);
if(finger)
MqttSendMessage("safeweb/finger/placed", "true");
else
MqttSendMessage("safeweb/finger/placed", "false");
CheckSleep();
}
void CMain::OnLock(bool unlocked)
{
m_LockUnlocked = unlocked;
if(unlocked)
MqttSendMessage("safeweb/lock/unlocked", "true");
else
MqttSendMessage("safeweb/lock/unlocked", "false");
CheckSleep();
}
void CMain::OnDoor(bool opened)
{
m_DoorOpened = opened;
if(opened)
MqttSendMessage("safeweb/door/opened", "true");
else
MqttSendMessage("safeweb/door/opened", "false");
CheckSleep();
}
void CMain::FingerPower(bool enable)
{
const int pin = FINGER_ENABLE_PIN;
digitalWrite(pin, !enable);
if(enable)
{
wifi_set_sleep_level(MAX_SLEEP_T);
wifi_set_listen_interval(3);
wifi_set_sleep_type(MODEM_SLEEP_T);
}
else
{
wifi_set_sleep_type(LIGHT_SLEEP_T);
}
}
void CMain::LockSendBytes(uint8_t *pBytes, uint8_t len)
{
const int pin = SAFELOCK_DATA_PIN;
// Init
digitalWrite(pin, 0);
delayMicroseconds(10 * 1000);
digitalWrite(pin, 1);
delayMicroseconds(10 * 1000);
// Send data, calculate checksum and send checksum
uint8_t chk = 0x00;
for(uint8_t i = 0; i <= len; i++)
{
uint8_t byte;
if(i == len)
{
byte = chk;
}
else
{
byte = pBytes[i];
chk ^= byte;
}
for(int8_t j = 0; j < 8; j++)
{
digitalWrite(pin, 0);
delayMicroseconds(100);
digitalWrite(pin, 1);
uint8_t val = byte & (1 << j);
if(val)
delayMicroseconds(300);
else
delayMicroseconds(100);
}
digitalWrite(pin, 0);
delayMicroseconds(100);
digitalWrite(pin, 1);
delayMicroseconds(8);
}
}
void CMain::LockSendCode(uint8_t pass[8])
{
uint8_t packet[9] = {0x51};
memcpy(&packet[1], pass, 8);
LockSendBytes(packet, sizeof(packet));
}
void CMain::LockUnlock()
{
LockSendCode(Settings().m_aLockCode);
}
void CMain::EnrollMessage(const char *msg, bool error)
{
m_EnrollMessage = msg;
if(error)
m_Enrolling = false;
}
void CMain::OnFingerVerified(uint16_t fingerNum, uint8_t digest[SHA256_SIZE])
{
int fingerIndex = Settings().m_FingerPrints.indexOf(fingerNum);
if(fingerIndex == -1)
{
debugf("OnFingerVerified: fingerIndex == -1");
return;
}
const CSettings::CFingerPrint &finger = Settings().m_FingerPrints.valueAt(fingerIndex);
if(memcmp(digest, finger.m_aDigest, SHA256_SIZE) != 0)
{
debugf("OnFingerVerified: SHA256 mismatch");
return;
}
LockUnlock();
debugf("OnFingerVerified: OK!!!");
}
void CMain::OnFingerEnrolled(uint16_t fingerNum, uint8_t digest[SHA256_SIZE])
{
CSettings::CFingerPrint finger;
finger.m_FingerNum = fingerNum;
strncpy(finger.m_aLabel, m_EnrollLabel.c_str(), sizeof(finger.m_aLabel));
memcpy(finger.m_aDigest, digest, SHA256_SIZE);
char aHexDigest[SHA256_SIZE*2+1];
bytes2hex(finger.m_aDigest, sizeof(finger.m_aDigest), aHexDigest, sizeof(aHexDigest));
debugf("OnFingerEnrolled: \"%s\"", aHexDigest);
Settings().m_FingerPrints[fingerNum] = finger;
Settings().Save();
m_Enrolled = true;
m_Enrolling = false;
m_EnrolledFinger = finger;
char aBuf[512];
m_snprintf(aBuf, sizeof(aBuf), "Successfully enrolled new finger \"%s\".", finger.m_aLabel);
EnrollMessage(aBuf);
debugf("OnFingerEnrolled: OK!!!");
}