make the fingerprint work
This commit is contained in:
@@ -0,0 +1,558 @@
|
||||
#include "FingerprintManager.h"
|
||||
#include "global.h"
|
||||
|
||||
#include <Adafruit_Fingerprint.h>
|
||||
|
||||
bool FingerprintManager::connect() {
|
||||
|
||||
// initialize input pins
|
||||
pinMode(touchRingPin, INPUT_PULLDOWN);
|
||||
|
||||
Serial.println("\n\nAdafruit finger detect test");
|
||||
|
||||
// set the data rate for the sensor serial port
|
||||
finger.begin(57600);
|
||||
delay(50);
|
||||
if (finger.verifyPassword()) {
|
||||
Serial.println("Found fingerprint sensor!");
|
||||
} else {
|
||||
delay(5000); // wait a bit longer for sensor to start before 2nd try (usually after a OTA-Update the esp32 is faster with startup than the fingerprint sensor)
|
||||
if (finger.verifyPassword()) {
|
||||
Serial.println("Found fingerprint sensor!");
|
||||
} else {
|
||||
Serial.println("Did not find fingerprint sensor :(");
|
||||
connected = false;
|
||||
return connected;
|
||||
}
|
||||
}
|
||||
finger.LEDcontrol(FINGERPRINT_LED_FLASHING, 25, FINGERPRINT_LED_BLUE, 0); // sensor connected signal
|
||||
|
||||
Serial.println(F("Reading sensor parameters"));
|
||||
finger.getParameters();
|
||||
Serial.print(F("Status: 0x")); Serial.println(finger.status_reg, HEX);
|
||||
Serial.print(F("Sys ID: 0x")); Serial.println(finger.system_id, HEX);
|
||||
Serial.print(F("Capacity: ")); Serial.println(finger.capacity);
|
||||
Serial.print(F("Security level: ")); Serial.println(finger.security_level);
|
||||
Serial.print(F("Device address: ")); Serial.println(finger.device_addr, HEX);
|
||||
Serial.print(F("Packet len: ")); Serial.println(finger.packet_len);
|
||||
Serial.print(F("Baud rate: ")); Serial.println(finger.baud_rate);
|
||||
|
||||
finger.getTemplateCount();
|
||||
Serial.print("Sensor contains "); Serial.print(finger.templateCount); Serial.println(" templates");
|
||||
|
||||
loadFingerListFromPrefs();
|
||||
|
||||
connected = true;
|
||||
return connected;
|
||||
|
||||
//updateTouchState(false);
|
||||
}
|
||||
|
||||
void FingerprintManager::updateTouchState(bool touched)
|
||||
{
|
||||
if ((touched != lastTouchState) || (ignoreTouchRing != lastIgnoreTouchRing)) {
|
||||
// check if sensor or ring is touched
|
||||
if (touched) {
|
||||
// turn touch indicator on:
|
||||
finger.LEDcontrol(FINGERPRINT_LED_FLASHING, 25, FINGERPRINT_LED_BLUE, 0);
|
||||
} else {
|
||||
// turn touch indicator off:
|
||||
setLedRingReady();
|
||||
}
|
||||
}
|
||||
lastTouchState = touched;
|
||||
lastIgnoreTouchRing = ignoreTouchRing;
|
||||
|
||||
}
|
||||
|
||||
|
||||
Match FingerprintManager::scanFingerprint() {
|
||||
|
||||
Match match;
|
||||
match.scanResult = ScanResult::error;
|
||||
|
||||
if (!connected) {
|
||||
return match;
|
||||
}
|
||||
|
||||
|
||||
// finger detection by capacitive touchRing state (increased sensitivy but error prone due to rain)
|
||||
bool ringTouched = false;
|
||||
if (!ignoreTouchRing)
|
||||
{
|
||||
if (isRingTouched())
|
||||
ringTouched = true;
|
||||
if (ringTouched || lastTouchState) {
|
||||
updateTouchState(true);
|
||||
//Serial.println("touched");
|
||||
} else {
|
||||
updateTouchState(false);
|
||||
match.scanResult = ScanResult::noFinger;
|
||||
return match;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
bool doAnotherScan = true;
|
||||
int scanPass = 0;
|
||||
while (doAnotherScan)
|
||||
{
|
||||
doAnotherScan = false;
|
||||
scanPass++;
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// STEP 1: Get Image from Sensor
|
||||
///////////////////////////////////////////////////////////
|
||||
bool doImaging = true;
|
||||
int imagingPass = 0;
|
||||
while (doImaging)
|
||||
{
|
||||
doImaging = false;
|
||||
imagingPass++;
|
||||
//Serial.println(String("Get Image try ") + imagingPass);
|
||||
match.returnCode = finger.getImage();
|
||||
switch (match.returnCode) {
|
||||
case FINGERPRINT_OK:
|
||||
// Important: do net set touch state to true yet! Reason:
|
||||
// - if touchRing is NOT ignored, updateTouchState(true) was already called a few lines up, ring is already flashing red
|
||||
// - if touchRing IS ignored, wait for next step because image still can be "too messy" (=raindrop on sensor), and we don't want to flash red in this case
|
||||
//updateTouchState(true);
|
||||
//Serial.println("Image taken");
|
||||
break;
|
||||
case FINGERPRINT_NOFINGER:
|
||||
case FINGERPRINT_PACKETRECIEVEERR: // occurs from time to time, handle it like a "nofinger detected but touched" situation
|
||||
if (ringTouched) {
|
||||
// no finger on sensor but ring was touched -> ring event
|
||||
//Serial.println("ring touched");
|
||||
updateTouchState(true);
|
||||
if (imagingPass < 15) // up to x image passes in a row are taken after touch ring was touched until noFinger will raise a noMatchFound event
|
||||
{
|
||||
doImaging = true; // scan another image
|
||||
//delay(50);
|
||||
break;
|
||||
} else {
|
||||
//Serial.println("15 times no image after touching ring");
|
||||
match.scanResult = ScanResult::noMatchFound;
|
||||
return match;
|
||||
}
|
||||
} else {
|
||||
if (ignoreTouchRing && scanPass > 1) {
|
||||
// the scan(s) in last iteration(s) have not found any match, now the finger was released (=no finger) -> return "no match" as result
|
||||
match.scanResult = ScanResult::noMatchFound;
|
||||
} else {
|
||||
match.scanResult = ScanResult::noFinger;
|
||||
updateTouchState(false);
|
||||
}
|
||||
return match;
|
||||
}
|
||||
case FINGERPRINT_IMAGEFAIL:
|
||||
Serial.println("Imaging error");
|
||||
updateTouchState(true);
|
||||
return match;
|
||||
default:
|
||||
Serial.println("Unknown error");
|
||||
return match;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// STEP 2: Convert Image to feature map
|
||||
///////////////////////////////////////////////////////////
|
||||
match.returnCode = finger.image2Tz();
|
||||
switch (match.returnCode) {
|
||||
case FINGERPRINT_OK:
|
||||
//Serial.println("Image converted");
|
||||
updateTouchState(true);
|
||||
break;
|
||||
case FINGERPRINT_IMAGEMESS:
|
||||
Serial.println("Image too messy");
|
||||
return match;
|
||||
case FINGERPRINT_PACKETRECIEVEERR:
|
||||
Serial.println("Communication error");
|
||||
return match;
|
||||
case FINGERPRINT_FEATUREFAIL:
|
||||
Serial.println("Could not find fingerprint features");
|
||||
return match;
|
||||
case FINGERPRINT_INVALIDIMAGE:
|
||||
Serial.println("Could not find fingerprint features");
|
||||
return match;
|
||||
default:
|
||||
Serial.println("Unknown error");
|
||||
return match;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// STEP 3: Search DB for matching features
|
||||
///////////////////////////////////////////////////////////
|
||||
match.returnCode = finger.fingerSearch();
|
||||
if (match.returnCode == FINGERPRINT_OK) {
|
||||
// found a match!
|
||||
finger.LEDcontrol(FINGERPRINT_LED_ON, 0, FINGERPRINT_LED_PURPLE);
|
||||
|
||||
match.scanResult = ScanResult::matchFound;
|
||||
match.matchId = finger.fingerID;
|
||||
match.matchConfidence = finger.confidence;
|
||||
match.matchName = fingerList[finger.fingerID];
|
||||
|
||||
} else if (match.returnCode == FINGERPRINT_PACKETRECIEVEERR) {
|
||||
Serial.println("Communication error");
|
||||
|
||||
} else if (match.returnCode == FINGERPRINT_NOTFOUND) {
|
||||
Serial.println(String("Did not find a match. (Scan #") + scanPass + String(" of 5)"));
|
||||
match.scanResult = ScanResult::noMatchFound;
|
||||
if (scanPass < 5) // max 5 Scans until no match found is given back as result
|
||||
doAnotherScan = true;
|
||||
|
||||
} else {
|
||||
Serial.println("Unknown error");
|
||||
}
|
||||
|
||||
} //while
|
||||
return match;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Preferences
|
||||
void FingerprintManager::loadFingerListFromPrefs() {
|
||||
Preferences preferences;
|
||||
preferences.begin("fingerList", true);
|
||||
int counter = 0;
|
||||
for (int i=1; i<=200; i++) {
|
||||
String key = String(i);
|
||||
if (preferences.isKey(key.c_str())) {
|
||||
fingerList[i] = preferences.getString(key.c_str(), String("@empty"));
|
||||
counter++;
|
||||
}
|
||||
else
|
||||
fingerList[i] = String("@empty");
|
||||
}
|
||||
Serial.println(String(counter) + " fingers loaded from preferences.");
|
||||
if (counter != finger.templateCount)
|
||||
notifyClients(String("Warning: Fingerprint count mismatch! ") + finger.templateCount + " fingerprints stored on sensor, but we are aware of " + counter + " fingerprints.");
|
||||
preferences.end();
|
||||
}
|
||||
|
||||
|
||||
// Add/Enroll fingerprint
|
||||
NewFinger FingerprintManager::enrollFinger(int id, String name) {
|
||||
|
||||
NewFinger newFinger;
|
||||
newFinger.enrollResult = EnrollResult::error;
|
||||
|
||||
lastTouchState = true; // after enrollment, scan mode kicks in again. Force update of the ring light back to normal on first iteration of scan mode.
|
||||
|
||||
|
||||
notifyClients(String("Enrollment for id #") + id + " started. We need to scan your finger 5 times until enrollment is completed.");
|
||||
|
||||
|
||||
// Repeat n times to get better resulting templates (as stated in R503 documentation up to 6 combined image samples possible, but I got an communication error when trying more than 5 samples, so dont go >5)
|
||||
for (int nTimes=1; nTimes<=5; nTimes++)
|
||||
{
|
||||
notifyClients(String("Take #" + String(nTimes))+ " (place your finger on the sensor until led ring stops flashing, then remove it).");
|
||||
|
||||
if (nTimes != 1) // not on first run
|
||||
{
|
||||
//delay(2000);
|
||||
newFinger.returnCode = 0xFF;
|
||||
while (newFinger.returnCode != FINGERPRINT_NOFINGER) {
|
||||
newFinger.returnCode = finger.getImage();
|
||||
}
|
||||
}
|
||||
|
||||
Serial.print("Taking image sample "); Serial.print(nTimes); Serial.print(": ");
|
||||
finger.LEDcontrol(FINGERPRINT_LED_FLASHING, 25, FINGERPRINT_LED_PURPLE, 0);
|
||||
newFinger.returnCode = 0xFF;
|
||||
while (newFinger.returnCode != FINGERPRINT_OK) {
|
||||
newFinger.returnCode = finger.getImage();
|
||||
switch (newFinger.returnCode) {
|
||||
case FINGERPRINT_OK:
|
||||
Serial.print("taken, ");
|
||||
break;
|
||||
case FINGERPRINT_NOFINGER:
|
||||
break;
|
||||
case FINGERPRINT_PACKETRECIEVEERR:
|
||||
Serial.print("Communication error, ");
|
||||
break;
|
||||
case FINGERPRINT_IMAGEFAIL:
|
||||
Serial.print("Imaging error, ");
|
||||
break;
|
||||
default:
|
||||
Serial.print("Unknown error, ");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// OK success!
|
||||
|
||||
newFinger.returnCode = finger.image2Tz(nTimes);
|
||||
switch (newFinger.returnCode) {
|
||||
case FINGERPRINT_OK:
|
||||
Serial.print("converted");
|
||||
break;
|
||||
case FINGERPRINT_IMAGEMESS:
|
||||
Serial.print("too messy");
|
||||
return newFinger;
|
||||
case FINGERPRINT_PACKETRECIEVEERR:
|
||||
Serial.print("Communication error");
|
||||
return newFinger;
|
||||
case FINGERPRINT_FEATUREFAIL:
|
||||
Serial.print("Could not find fingerprint features");
|
||||
return newFinger;
|
||||
case FINGERPRINT_INVALIDIMAGE:
|
||||
Serial.print("Could not find fingerprint features");
|
||||
return newFinger;
|
||||
default:
|
||||
Serial.print("Unknown error");
|
||||
return newFinger;
|
||||
}
|
||||
finger.LEDcontrol(FINGERPRINT_LED_ON, 0, FINGERPRINT_LED_PURPLE);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// OK converted!
|
||||
Serial.println();
|
||||
Serial.print("Creating model for #"); Serial.println(id);
|
||||
|
||||
newFinger.returnCode = finger.createModel();
|
||||
if (newFinger.returnCode == FINGERPRINT_OK) {
|
||||
Serial.println("Prints matched!");
|
||||
} else if (newFinger.returnCode == FINGERPRINT_PACKETRECIEVEERR) {
|
||||
Serial.println("Communication error");
|
||||
return newFinger;
|
||||
} else if (newFinger.returnCode == FINGERPRINT_ENROLLMISMATCH) {
|
||||
Serial.println("Fingerprints did not match");
|
||||
return newFinger;
|
||||
} else {
|
||||
Serial.println("Unknown error");
|
||||
return newFinger;
|
||||
}
|
||||
|
||||
Serial.print("ID "); Serial.println(id);
|
||||
newFinger.returnCode = finger.storeModel(id);
|
||||
if (newFinger.returnCode == FINGERPRINT_OK) {
|
||||
Serial.println("Stored!");
|
||||
newFinger.enrollResult = EnrollResult::ok;
|
||||
// save to prefs
|
||||
fingerList[id] = name;
|
||||
Preferences preferences;
|
||||
preferences.begin("fingerList", false);
|
||||
preferences.putString(String(id).c_str(), name);
|
||||
preferences.end();
|
||||
|
||||
} else if (newFinger.returnCode == FINGERPRINT_PACKETRECIEVEERR) {
|
||||
Serial.println("Communication error");
|
||||
return newFinger;
|
||||
} else if (newFinger.returnCode == FINGERPRINT_BADLOCATION) {
|
||||
Serial.println("Could not store in that location");
|
||||
return newFinger;
|
||||
} else if (newFinger.returnCode == FINGERPRINT_FLASHERR) {
|
||||
Serial.println("Error writing to flash");
|
||||
return newFinger;
|
||||
} else {
|
||||
Serial.println("Unknown error");
|
||||
return newFinger;
|
||||
}
|
||||
|
||||
//finger.LEDcontrol(FINGERPRINT_LED_OFF, 0, FINGERPRINT_LED_RED);
|
||||
|
||||
return newFinger;
|
||||
|
||||
}
|
||||
|
||||
|
||||
void FingerprintManager::deleteFinger(int id) {
|
||||
|
||||
if ((id > 0) && (id <= 200)) {
|
||||
int8_t result = finger.deleteModel(id);
|
||||
if (result != FINGERPRINT_OK) {
|
||||
notifyClients(String("Delete of finger template #") + id + " from sensor failed with code " + result);
|
||||
return;
|
||||
|
||||
} else {
|
||||
fingerList[id] = "@empty";
|
||||
Preferences preferences;
|
||||
preferences.begin("fingerList", false);
|
||||
preferences.remove (String(id).c_str());
|
||||
preferences.end();
|
||||
Serial.println(String("Finger template #") + id + " deleted from sensor and prefs.");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
void FingerprintManager::renameFinger(int id, String newName) {
|
||||
if ((id > 0) && (id <= 200)) {
|
||||
Preferences preferences;
|
||||
preferences.begin("fingerList", false);
|
||||
preferences.putString(String(id).c_str(), newName);
|
||||
preferences.end();
|
||||
Serial.println(String("Finger template #") + id + " renamed from " + fingerList[id] + " to " + newName);
|
||||
fingerList[id] = newName;
|
||||
}
|
||||
}
|
||||
|
||||
String FingerprintManager::getFingerListAsHtmlOptionList() {
|
||||
String htmlOptions = "";
|
||||
int counter = 0;
|
||||
for (int i=1; i<=200; i++) {
|
||||
if (fingerList[i].compareTo("@empty") != 0) {
|
||||
String option;
|
||||
if (counter == 0)
|
||||
option = "<option value=\"" + String(i) + "\" selected>" + String(i) + " - " + fingerList[i] + "</option>";
|
||||
else
|
||||
option = "<option value=\"" + String(i) + "\">" + String(i) + " - " + fingerList[i] + "</option>";
|
||||
htmlOptions += option;
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
return htmlOptions;
|
||||
}
|
||||
|
||||
void FingerprintManager::setIgnoreTouchRing(bool state) {
|
||||
if (ignoreTouchRing != state) {
|
||||
ignoreTouchRing = state;
|
||||
if (state == true)
|
||||
notifyClients("IgnoreTouchRing is now 'on'");
|
||||
else
|
||||
notifyClients("IgnoreTouchRing is now 'off'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool FingerprintManager::isRingTouched() {
|
||||
if (digitalRead(touchRingPin) == LOW) // LOW = touched. Caution: touchSignal on this pin occour only once (at beginning of touching the ring, not every iteration if you keep your finger on the ring)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FingerprintManager::isFingerOnSensor() {
|
||||
// get an image
|
||||
uint8_t returnCode = finger.getImage();
|
||||
if (returnCode == FINGERPRINT_OK) {
|
||||
// try to find fingerprint features in image, because image taken does not already means finger on sensor, could also be a raindrop
|
||||
returnCode = finger.image2Tz();
|
||||
if (returnCode == FINGERPRINT_OK)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void FingerprintManager::setLedRingError() {
|
||||
finger.LEDcontrol(FINGERPRINT_LED_ON, 0, FINGERPRINT_LED_RED);
|
||||
}
|
||||
|
||||
void FingerprintManager::setLedRingWifiConfig() {
|
||||
finger.LEDcontrol(FINGERPRINT_LED_BREATHING, 250, FINGERPRINT_LED_RED);
|
||||
}
|
||||
|
||||
void FingerprintManager::setLedRingReady() {
|
||||
if (!ignoreTouchRing)
|
||||
finger.LEDcontrol(FINGERPRINT_LED_BREATHING, 250, FINGERPRINT_LED_BLUE);
|
||||
else
|
||||
finger.LEDcontrol(FINGERPRINT_LED_ON, 0, FINGERPRINT_LED_BLUE); // just an indicator for me to see if touch ring is active or not
|
||||
}
|
||||
|
||||
bool FingerprintManager::deleteAll() {
|
||||
if (finger.emptyDatabase() == FINGERPRINT_OK)
|
||||
{
|
||||
bool rc;
|
||||
Preferences preferences;
|
||||
rc = preferences.begin("fingerList", false);
|
||||
if (rc)
|
||||
rc = preferences.clear();
|
||||
preferences.end();
|
||||
|
||||
for (int i=1; i<=200; i++) {
|
||||
fingerList[i] = String("@empty");
|
||||
};
|
||||
|
||||
return rc;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
uint8_t FingerprintManager::writeNotepad(uint8_t pageNumber, const char *text, uint8_t length) {
|
||||
uint8_t data[34];
|
||||
|
||||
if (length>32)
|
||||
length = 32;
|
||||
|
||||
data[0] = FINGERPRINT_WRITENOTEPAD;
|
||||
data[1] = pageNumber;
|
||||
for (int i=0; i<length; i++)
|
||||
data[i+2] = text[i];
|
||||
|
||||
Adafruit_Fingerprint_Packet packet(FINGERPRINT_COMMANDPACKET, sizeof(data), data);
|
||||
finger.writeStructuredPacket(packet);
|
||||
if (finger.getStructuredPacket(&packet) != FINGERPRINT_OK)
|
||||
return FINGERPRINT_PACKETRECIEVEERR;
|
||||
if (packet.type != FINGERPRINT_ACKPACKET)
|
||||
return FINGERPRINT_PACKETRECIEVEERR;
|
||||
return packet.data[0];
|
||||
}
|
||||
|
||||
|
||||
uint8_t FingerprintManager::readNotepad(uint8_t pageNumber, char *text, uint8_t length) {
|
||||
uint8_t data[2];
|
||||
|
||||
data[0] = FINGERPRINT_READNOTEPAD;
|
||||
data[1] = pageNumber;
|
||||
|
||||
Adafruit_Fingerprint_Packet packet(FINGERPRINT_COMMANDPACKET, sizeof(data), data);
|
||||
finger.writeStructuredPacket(packet);
|
||||
if (finger.getStructuredPacket(&packet) != FINGERPRINT_OK)
|
||||
return FINGERPRINT_PACKETRECIEVEERR;
|
||||
if (packet.type != FINGERPRINT_ACKPACKET)
|
||||
return FINGERPRINT_PACKETRECIEVEERR;
|
||||
|
||||
if (packet.data[0] == FINGERPRINT_OK) {
|
||||
// read data payload
|
||||
for (uint8_t i=0; i<length; i++) {
|
||||
text[i] = packet.data[i+1];
|
||||
}
|
||||
}
|
||||
|
||||
return packet.data[0];
|
||||
|
||||
}
|
||||
|
||||
|
||||
String FingerprintManager::getPairingCode() {
|
||||
char buffer[33];
|
||||
buffer[32] = 0; // null termination needed for convertion to string at the end
|
||||
if (readNotepad(0, (char*)buffer, 32) == FINGERPRINT_OK)
|
||||
return String((char*)buffer);
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
bool FingerprintManager::setPairingCode(String pairingCode) {
|
||||
if (writeNotepad(0, pairingCode.c_str(), 32) == FINGERPRINT_OK)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// ToDo: support sensor replacement by enable transferring of sensor DB to another sensor
|
||||
void FingerprintManager::exportSensorDB() {
|
||||
|
||||
}
|
||||
|
||||
void FingerprintManager::importSensorDB() {
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#ifndef FINGERPRINTMANAGER_H
|
||||
#define FINGERPRINTMANAGER_H
|
||||
|
||||
#include <Adafruit_Fingerprint.h>
|
||||
#include <Preferences.h>
|
||||
#include "global.h"
|
||||
|
||||
#define mySerial Serial0
|
||||
|
||||
#define FINGERPRINT_WRITENOTEPAD 0x18 // Write Notepad on sensor
|
||||
#define FINGERPRINT_READNOTEPAD 0x19 // Read Notepad from sensor
|
||||
|
||||
|
||||
/*
|
||||
By using the touch ring as an additional input to the image sensor the sensitivity is much higher for door bell ring events. Unfortunately
|
||||
we cannot differ between touches on the ring by fingers or rain drops, so rain on the ring will cause false alarms.
|
||||
*/
|
||||
const int touchRingPin = 10; // touch/wakeup pin connected to fingerprint sensor
|
||||
|
||||
enum class ScanResult { noFinger, matchFound, noMatchFound, error };
|
||||
enum class EnrollResult { ok, error };
|
||||
|
||||
struct Match {
|
||||
ScanResult scanResult = ScanResult::noFinger;
|
||||
uint16_t matchId = 0;
|
||||
String matchName = "unknown";
|
||||
uint16_t matchConfidence = 0;
|
||||
uint8_t returnCode = 0;
|
||||
};
|
||||
|
||||
struct NewFinger {
|
||||
EnrollResult enrollResult = EnrollResult::error;
|
||||
uint8_t returnCode = 0;
|
||||
};
|
||||
|
||||
class FingerprintManager {
|
||||
private:
|
||||
Adafruit_Fingerprint finger = Adafruit_Fingerprint(&mySerial);
|
||||
bool lastTouchState = false;
|
||||
String fingerList[201];
|
||||
int fingerCountOnSensor = 0;
|
||||
bool ignoreTouchRing = false; // set to true when the sensor is usually exposed to rain to avoid false ring events. Can also be set conditional by a rain sensor over MQTT
|
||||
bool lastIgnoreTouchRing = false;
|
||||
|
||||
void updateTouchState(bool touched);
|
||||
bool isRingTouched();
|
||||
void loadFingerListFromPrefs();
|
||||
void disconnect();
|
||||
uint8_t writeNotepad(uint8_t pageNumber, const char *text, uint8_t length);
|
||||
uint8_t readNotepad(uint8_t pageNumber, char *text, uint8_t length);
|
||||
|
||||
|
||||
|
||||
public:
|
||||
bool connected;
|
||||
bool connect();
|
||||
Match scanFingerprint();
|
||||
NewFinger enrollFinger(int id, String name);
|
||||
void deleteFinger(int id);
|
||||
void renameFinger(int id, String newName);
|
||||
String getFingerListAsHtmlOptionList();
|
||||
void setIgnoreTouchRing(bool state);
|
||||
bool isFingerOnSensor();
|
||||
void setLedRingError();
|
||||
void setLedRingWifiConfig();
|
||||
void setLedRingReady();
|
||||
String getPairingCode();
|
||||
bool setPairingCode(String pairingCode);
|
||||
|
||||
bool deleteAll();
|
||||
|
||||
|
||||
// functions for sensor replacement
|
||||
void exportSensorDB();
|
||||
void importSensorDB();
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,133 @@
|
||||
#include "SettingsManager.h"
|
||||
#include <Crypto.h>
|
||||
|
||||
bool SettingsManager::loadWifiSettings() {
|
||||
Preferences preferences;
|
||||
if (preferences.begin("wifiSettings", true)) {
|
||||
wifiSettings.ssid = preferences.getString("ssid", String(""));
|
||||
wifiSettings.password = preferences.getString("password", String(""));
|
||||
wifiSettings.hostname = preferences.getString("hostname", String("FingerprintDoorbell"));
|
||||
preferences.end();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool SettingsManager::loadAppSettings() {
|
||||
Preferences preferences;
|
||||
if (preferences.begin("appSettings", true)) {
|
||||
appSettings.mqttServer = preferences.getString("mqttServer", String(""));
|
||||
appSettings.mqttUsername = preferences.getString("mqttUsername", String(""));
|
||||
appSettings.mqttPassword = preferences.getString("mqttPassword", String(""));
|
||||
appSettings.mqttRootTopic = preferences.getString("mqttRootTopic", String("fingerprintDoorbell"));
|
||||
appSettings.ntpServer = preferences.getString("ntpServer", String("pool.ntp.org"));
|
||||
appSettings.sensorPin = preferences.getString("sensorPin", "00000000");
|
||||
appSettings.sensorPairingCode = preferences.getString("pairingCode", "");
|
||||
appSettings.sensorPairingValid = preferences.getBool("pairingValid", false);
|
||||
preferences.end();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsManager::saveWifiSettings() {
|
||||
Preferences preferences;
|
||||
preferences.begin("wifiSettings", false);
|
||||
preferences.putString("ssid", wifiSettings.ssid);
|
||||
preferences.putString("password", wifiSettings.password);
|
||||
preferences.putString("hostname", wifiSettings.hostname);
|
||||
preferences.end();
|
||||
}
|
||||
|
||||
void SettingsManager::saveAppSettings() {
|
||||
Preferences preferences;
|
||||
preferences.begin("appSettings", false);
|
||||
preferences.putString("mqttServer", appSettings.mqttServer);
|
||||
preferences.putString("mqttUsername", appSettings.mqttUsername);
|
||||
preferences.putString("mqttPassword", appSettings.mqttPassword);
|
||||
preferences.putString("mqttRootTopic", appSettings.mqttRootTopic);
|
||||
preferences.putString("ntpServer", appSettings.ntpServer);
|
||||
preferences.putString("sensorPin", appSettings.sensorPin);
|
||||
preferences.putString("pairingCode", appSettings.sensorPairingCode);
|
||||
preferences.putBool("pairingValid", appSettings.sensorPairingValid);
|
||||
preferences.end();
|
||||
}
|
||||
|
||||
WifiSettings SettingsManager::getWifiSettings() {
|
||||
return wifiSettings;
|
||||
}
|
||||
|
||||
void SettingsManager::saveWifiSettings(WifiSettings newSettings) {
|
||||
wifiSettings = newSettings;
|
||||
saveWifiSettings();
|
||||
}
|
||||
|
||||
AppSettings SettingsManager::getAppSettings() {
|
||||
return appSettings;
|
||||
}
|
||||
|
||||
void SettingsManager::saveAppSettings(AppSettings newSettings) {
|
||||
appSettings = newSettings;
|
||||
saveAppSettings();
|
||||
}
|
||||
|
||||
bool SettingsManager::isWifiConfigured() {
|
||||
if (wifiSettings.ssid.isEmpty() || wifiSettings.password.isEmpty())
|
||||
return false;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SettingsManager::deleteAppSettings() {
|
||||
bool rc;
|
||||
Preferences preferences;
|
||||
rc = preferences.begin("appSettings", false);
|
||||
if (rc)
|
||||
rc = preferences.clear();
|
||||
preferences.end();
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool SettingsManager::deleteWifiSettings() {
|
||||
bool rc;
|
||||
Preferences preferences;
|
||||
rc = preferences.begin("wifiSettings", false);
|
||||
if (rc)
|
||||
rc = preferences.clear();
|
||||
preferences.end();
|
||||
return rc;
|
||||
}
|
||||
|
||||
String SettingsManager::generateNewPairingCode() {
|
||||
|
||||
/* Create a SHA256 hash */
|
||||
SHA256 hasher;
|
||||
|
||||
/* Put some unique values as input in our new hash */
|
||||
hasher.doUpdate( String(esp_random()).c_str() ); // random number
|
||||
hasher.doUpdate( String(millis()).c_str() ); // time since boot
|
||||
hasher.doUpdate(getTimestampString().c_str()); // current time (if NTP is available)
|
||||
hasher.doUpdate(appSettings.mqttUsername.c_str());
|
||||
hasher.doUpdate(appSettings.mqttPassword.c_str());
|
||||
hasher.doUpdate(wifiSettings.ssid.c_str());
|
||||
hasher.doUpdate(wifiSettings.password.c_str());
|
||||
|
||||
/* Compute the final hash */
|
||||
byte hash[SHA256_SIZE];
|
||||
hasher.doFinal(hash);
|
||||
|
||||
// Convert our 32 byte hash to 32 chars long hex string. When converting the entire hash to hex we would need a length of 64 chars.
|
||||
// But because we only want a length of 32 we only use the first 16 bytes of the hash. I know this will increase possible collisions,
|
||||
// but for detecting a sensor replacement (which is the use-case here) it will still be enough.
|
||||
char hexString[33];
|
||||
hexString[32] = 0; // null terminatation byte for converting to string later
|
||||
for (byte i=0; i < 16; i++) // use only the first 16 bytes of hash
|
||||
{
|
||||
sprintf(&hexString[i*2], "%02x", hash[i]);
|
||||
}
|
||||
|
||||
return String((char*)hexString);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#ifndef SETTINGSMANAGER_H
|
||||
#define SETTINGSMANAGER_H
|
||||
|
||||
#include <Preferences.h>
|
||||
#include "global.h"
|
||||
|
||||
struct WifiSettings {
|
||||
String ssid = "";
|
||||
String password = "";
|
||||
String hostname = "";
|
||||
};
|
||||
|
||||
struct AppSettings {
|
||||
String mqttServer = "";
|
||||
String mqttUsername = "";
|
||||
String mqttPassword = "";
|
||||
String mqttRootTopic = "fingerprintDoorbell";
|
||||
String ntpServer = "pool.ntp.org";
|
||||
String sensorPin = "00000000";
|
||||
String sensorPairingCode = "";
|
||||
bool sensorPairingValid = false;
|
||||
};
|
||||
|
||||
class SettingsManager {
|
||||
private:
|
||||
WifiSettings wifiSettings;
|
||||
AppSettings appSettings;
|
||||
|
||||
void saveWifiSettings();
|
||||
void saveAppSettings();
|
||||
|
||||
public:
|
||||
bool loadWifiSettings();
|
||||
bool loadAppSettings();
|
||||
|
||||
WifiSettings getWifiSettings();
|
||||
void saveWifiSettings(WifiSettings newSettings);
|
||||
|
||||
AppSettings getAppSettings();
|
||||
void saveAppSettings(AppSettings newSettings);
|
||||
|
||||
bool isWifiConfigured();
|
||||
|
||||
bool deleteAppSettings();
|
||||
bool deleteWifiSettings();
|
||||
|
||||
String generateNewPairingCode();
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,9 @@
|
||||
#ifndef GLOBAL_H
|
||||
#define GLOBAL_H
|
||||
|
||||
#include <WString.h>
|
||||
|
||||
extern void notifyClients(String message);
|
||||
extern String getTimestampString();
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,805 @@
|
||||
/***************************************************
|
||||
Main of FingerprintDoorbell
|
||||
****************************************************/
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <DNSServer.h>
|
||||
#include <time.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <ElegantOTA.h>
|
||||
#include <SPIFFS.h>
|
||||
#include <PubSubClient.h>
|
||||
#include "FingerprintManager.h"
|
||||
#include "SettingsManager.h"
|
||||
#include "global.h"
|
||||
|
||||
enum class Mode { scan, enroll, wificonfig, maintenance };
|
||||
|
||||
const char* VersionInfo = "0.4";
|
||||
|
||||
// ===================================================================================================================
|
||||
// Caution: below are not the credentials for connecting to your home network, they are for the Access Point mode!!!
|
||||
// ===================================================================================================================
|
||||
const char* WifiConfigSsid = "FingerprintDoorbell-Config"; // SSID used for WiFi when in Access Point mode for configuration
|
||||
const char* WifiConfigPassword = "12345678"; // password used for WiFi when in Access Point mode for configuration. Min. 8 chars needed!
|
||||
IPAddress WifiConfigIp(192, 168, 4, 1); // IP of access point in wifi config mode
|
||||
|
||||
const long gmtOffset_sec = 0; // UTC Time
|
||||
const int daylightOffset_sec = 0; // UTC Time
|
||||
const int doorbellOutputPin = 5; // pin connected to the doorbell (when using hardware connection instead of mqtt to ring the bell)
|
||||
const int doorOpenerOutputPin = 6; // pin connected to the door opener (when using hardware connection instead of mqtt to open the door)
|
||||
#ifdef CUSTOM_GPIOS
|
||||
const int customOutput1 = 18; // not used internally, but can be set over MQTT
|
||||
const int customOutput2 = 26; // not used internally, but can be set over MQTT
|
||||
const int customInput1 = 21; // not used internally, but changes are published over MQTT
|
||||
const int customInput2 = 22; // not used internally, but changes are published over MQTT
|
||||
bool customInput1Value = false;
|
||||
bool customInput2Value = false;
|
||||
#endif
|
||||
|
||||
const int logMessagesCount = 5;
|
||||
String logMessages[logMessagesCount]; // log messages, 0=most recent log message
|
||||
bool shouldReboot = false;
|
||||
unsigned long wifiReconnectPreviousMillis = 0;
|
||||
unsigned long mqttReconnectPreviousMillis = 0;
|
||||
|
||||
String enrollId;
|
||||
String enrollName;
|
||||
Mode currentMode = Mode::scan;
|
||||
|
||||
FingerprintManager fingerManager;
|
||||
SettingsManager settingsManager;
|
||||
bool needMaintenanceMode = false;
|
||||
|
||||
const byte DNS_PORT = 53;
|
||||
DNSServer dnsServer;
|
||||
AsyncWebServer webServer(80); // AsyncWebServer on port 80
|
||||
AsyncEventSource events("/events"); // event source (Server-Sent events)
|
||||
|
||||
WiFiClient espClient;
|
||||
PubSubClient mqttClient(espClient);
|
||||
long lastMsg = 0;
|
||||
char msg[50];
|
||||
int value = 0;
|
||||
bool mqttConfigValid = true;
|
||||
|
||||
|
||||
Match lastMatch;
|
||||
|
||||
void addLogMessage(const String& message) {
|
||||
// shift all messages in array by 1, oldest message will die
|
||||
for (int i=logMessagesCount-1; i>0; i--)
|
||||
logMessages[i]=logMessages[i-1];
|
||||
logMessages[0]=message;
|
||||
}
|
||||
|
||||
String getLogMessagesAsHtml() {
|
||||
String html = "";
|
||||
for (int i=logMessagesCount-1; i>=0; i--) {
|
||||
if (logMessages[i]!="")
|
||||
html = html + logMessages[i] + "<br>";
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
String getTimestampString(){
|
||||
struct tm timeinfo;
|
||||
if(!getLocalTime(&timeinfo)){
|
||||
Serial.println("Failed to obtain time");
|
||||
return "no time";
|
||||
}
|
||||
|
||||
char buffer[25];
|
||||
strftime(buffer,sizeof(buffer),"%Y-%m-%d %H:%M:%S %Z", &timeinfo);
|
||||
String datetime = String(buffer);
|
||||
return datetime;
|
||||
}
|
||||
|
||||
/* wait for maintenance mode or timeout 5s */
|
||||
bool waitForMaintenanceMode() {
|
||||
needMaintenanceMode = true;
|
||||
unsigned long startMillis = millis();
|
||||
while (currentMode != Mode::maintenance) {
|
||||
if ((millis() - startMillis) >= 5000ul) {
|
||||
needMaintenanceMode = false;
|
||||
return false;
|
||||
}
|
||||
delay(50);
|
||||
}
|
||||
needMaintenanceMode = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Replaces placeholder in HTML pages
|
||||
String processor(const String& var){
|
||||
if(var == "LOGMESSAGES"){
|
||||
return getLogMessagesAsHtml();
|
||||
} else if (var == "FINGERLIST") {
|
||||
return fingerManager.getFingerListAsHtmlOptionList();
|
||||
} else if (var == "HOSTNAME") {
|
||||
return settingsManager.getWifiSettings().hostname;
|
||||
} else if (var == "VERSIONINFO") {
|
||||
return VersionInfo;
|
||||
} else if (var == "WIFI_SSID") {
|
||||
return settingsManager.getWifiSettings().ssid;
|
||||
} else if (var == "WIFI_PASSWORD") {
|
||||
if (settingsManager.getWifiSettings().password.isEmpty())
|
||||
return "";
|
||||
else
|
||||
return "********"; // for security reasons the wifi password will not left the device once configured
|
||||
} else if (var == "MQTT_SERVER") {
|
||||
return settingsManager.getAppSettings().mqttServer;
|
||||
} else if (var == "MQTT_USERNAME") {
|
||||
return settingsManager.getAppSettings().mqttUsername;
|
||||
} else if (var == "MQTT_PASSWORD") {
|
||||
return settingsManager.getAppSettings().mqttPassword;
|
||||
} else if (var == "MQTT_ROOTTOPIC") {
|
||||
return settingsManager.getAppSettings().mqttRootTopic;
|
||||
} else if (var == "NTP_SERVER") {
|
||||
return settingsManager.getAppSettings().ntpServer;
|
||||
}
|
||||
|
||||
return String();
|
||||
}
|
||||
|
||||
|
||||
// send LastMessage to websocket clients
|
||||
void notifyClients(String message) {
|
||||
String messageWithTimestamp = "[" + getTimestampString() + "]: " + message;
|
||||
Serial.println(messageWithTimestamp);
|
||||
addLogMessage(messageWithTimestamp);
|
||||
events.send(getLogMessagesAsHtml().c_str(),"message",millis(),1000);
|
||||
|
||||
String mqttRootTopic = settingsManager.getAppSettings().mqttRootTopic;
|
||||
mqttClient.publish((String(mqttRootTopic) + "/lastLogMessage").c_str(), message.c_str());
|
||||
}
|
||||
|
||||
void updateClientsFingerlist(String fingerlist) {
|
||||
Serial.println("New fingerlist was sent to clients");
|
||||
events.send(fingerlist.c_str(),"fingerlist",millis(),1000);
|
||||
}
|
||||
|
||||
|
||||
bool doPairing() {
|
||||
String newPairingCode = settingsManager.generateNewPairingCode();
|
||||
|
||||
if (fingerManager.setPairingCode(newPairingCode)) {
|
||||
AppSettings settings = settingsManager.getAppSettings();
|
||||
settings.sensorPairingCode = newPairingCode;
|
||||
settings.sensorPairingValid = true;
|
||||
settingsManager.saveAppSettings(settings);
|
||||
notifyClients("Pairing successful.");
|
||||
return true;
|
||||
} else {
|
||||
notifyClients("Pairing failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
bool checkPairingValid() {
|
||||
AppSettings settings = settingsManager.getAppSettings();
|
||||
|
||||
if (!settings.sensorPairingValid) {
|
||||
if (settings.sensorPairingCode.isEmpty()) {
|
||||
// first boot, do pairing automatically so the user does not have to do this manually
|
||||
return doPairing();
|
||||
} else {
|
||||
Serial.println("Pairing has been invalidated previously.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
String actualSensorPairingCode = fingerManager.getPairingCode();
|
||||
//Serial.println("Awaited pairing code: " + settings.sensorPairingCode);
|
||||
//Serial.println("Actual pairing code: " + actualSensorPairingCode);
|
||||
|
||||
if (actualSensorPairingCode.equals(settings.sensorPairingCode))
|
||||
return true;
|
||||
else {
|
||||
if (!actualSensorPairingCode.isEmpty()) {
|
||||
// An empty code means there was a communication problem. So we don't have a valid code, but maybe next read will succeed and we get one again.
|
||||
// But here we just got an non-empty pairing code that was different to the awaited one. So don't expect that will change in future until repairing was done.
|
||||
// -> invalidate pairing for security reasons
|
||||
AppSettings settings = settingsManager.getAppSettings();
|
||||
settings.sensorPairingValid = false;
|
||||
settingsManager.saveAppSettings(settings);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool initWifi() {
|
||||
// Connect to Wi-Fi
|
||||
WifiSettings wifiSettings = settingsManager.getWifiSettings();
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
|
||||
WiFi.setHostname(wifiSettings.hostname.c_str()); //define hostname
|
||||
WiFi.begin(wifiSettings.ssid.c_str(), wifiSettings.password.c_str());
|
||||
int counter = 0;
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(1000);
|
||||
Serial.println("Waiting for WiFi connection...");
|
||||
counter++;
|
||||
if (counter > 30)
|
||||
return false;
|
||||
}
|
||||
Serial.println("Connected!");
|
||||
|
||||
// Print ESP32 Local IP Address
|
||||
Serial.println(WiFi.localIP());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void initWiFiAccessPointForConfiguration() {
|
||||
WiFi.softAPConfig(WifiConfigIp, WifiConfigIp, IPAddress(255, 255, 255, 0));
|
||||
WiFi.softAP(WifiConfigSsid, WifiConfigPassword);
|
||||
|
||||
// if DNSServer is started with "*" for domain name, it will reply with
|
||||
// provided IP to all DNS request
|
||||
dnsServer.start(DNS_PORT, "*", WifiConfigIp);
|
||||
|
||||
Serial.print("AP IP address: ");
|
||||
Serial.println(WifiConfigIp);
|
||||
}
|
||||
|
||||
void onOTAStart() {
|
||||
// Log when OTA has started
|
||||
Serial.println("OTA update started!");
|
||||
// <Add your own code here>
|
||||
}
|
||||
|
||||
void onOTAProgress(size_t current, size_t final) {
|
||||
static unsigned long ota_progress_millis = 0;
|
||||
// Log every 1 second
|
||||
if (millis() - ota_progress_millis > 1000) {
|
||||
ota_progress_millis = millis();
|
||||
Serial.printf("OTA Progress Current: %u bytes, Final: %u bytes\n", current, final);
|
||||
}
|
||||
}
|
||||
|
||||
void onOTAEnd(bool success) {
|
||||
// Log when OTA has finished
|
||||
if (success) {
|
||||
Serial.println("OTA update finished successfully!");
|
||||
} else {
|
||||
Serial.println("There was an error during OTA update!");
|
||||
}
|
||||
// <Add your own code here>
|
||||
}
|
||||
|
||||
|
||||
|
||||
void startWebserver(){
|
||||
|
||||
// Initialize SPIFFS
|
||||
if(!SPIFFS.begin(true)){
|
||||
Serial.println("An Error has occurred while mounting SPIFFS");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// webserver for normal operating or wifi config?
|
||||
if (currentMode == Mode::wificonfig)
|
||||
{
|
||||
// =================
|
||||
// WiFi config mode
|
||||
// =================
|
||||
|
||||
webServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
request->send(SPIFFS, "/wificonfig.html", String(), false, processor);
|
||||
});
|
||||
|
||||
webServer.on("/save", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
if(request->hasArg("hostname"))
|
||||
{
|
||||
Serial.println("Save wifi config");
|
||||
WifiSettings settings = settingsManager.getWifiSettings();
|
||||
settings.hostname = request->arg("hostname");
|
||||
settings.ssid = request->arg("ssid");
|
||||
if (request->arg("password").equals("********")) // password is replaced by wildcards when given to the browser, so if the user didn't changed it, don't save it
|
||||
settings.password = settingsManager.getWifiSettings().password; // use the old, already saved, one
|
||||
else
|
||||
settings.password = request->arg("password");
|
||||
settingsManager.saveWifiSettings(settings);
|
||||
shouldReboot = true;
|
||||
}
|
||||
request->redirect("/");
|
||||
});
|
||||
|
||||
|
||||
webServer.onNotFound([](AsyncWebServerRequest *request){
|
||||
AsyncResponseStream *response = request->beginResponseStream("text/html");
|
||||
response->printf("<!DOCTYPE html><html><head><title>FingerprintDoorbell</title><meta http-equiv=\"refresh\" content=\"0; url=http://%s\" /></head><body>", WiFi.softAPIP().toString().c_str());
|
||||
response->printf("<p>Please configure your WiFi settings <a href='http://%s'>here</a> to connect FingerprintDoorbell to your home network.</p>", WiFi.softAPIP().toString().c_str());
|
||||
response->print("</body></html>");
|
||||
request->send(response);
|
||||
});
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// =======================
|
||||
// normal operating mode
|
||||
// =======================
|
||||
events.onConnect([](AsyncEventSourceClient *client){
|
||||
if(client->lastId()){
|
||||
Serial.printf("Client reconnected! Last message ID it got was: %u\n", client->lastId());
|
||||
}
|
||||
//send event with message "ready", id current millis
|
||||
// and set reconnect delay to 1 second
|
||||
client->send(getLogMessagesAsHtml().c_str(),"message",millis(),1000);
|
||||
});
|
||||
webServer.addHandler(&events);
|
||||
|
||||
|
||||
// Route for root / web page
|
||||
webServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
request->send(SPIFFS, "/index.html", String(), false, processor);
|
||||
});
|
||||
|
||||
webServer.on("/enroll", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
if(request->hasArg("startEnrollment"))
|
||||
{
|
||||
enrollId = request->arg("newFingerprintId");
|
||||
enrollName = request->arg("newFingerprintName");
|
||||
currentMode = Mode::enroll;
|
||||
}
|
||||
request->redirect("/");
|
||||
});
|
||||
|
||||
webServer.on("/editFingerprints", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
if(request->hasArg("selectedFingerprint"))
|
||||
{
|
||||
if(request->hasArg("btnDelete"))
|
||||
{
|
||||
int id = request->arg("selectedFingerprint").toInt();
|
||||
waitForMaintenanceMode();
|
||||
fingerManager.deleteFinger(id);
|
||||
currentMode = Mode::scan;
|
||||
}
|
||||
else if (request->hasArg("btnRename"))
|
||||
{
|
||||
int id = request->arg("selectedFingerprint").toInt();
|
||||
String newName = request->arg("renameNewName");
|
||||
fingerManager.renameFinger(id, newName);
|
||||
}
|
||||
}
|
||||
request->redirect("/");
|
||||
});
|
||||
|
||||
webServer.on("/settings", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
if(request->hasArg("btnSaveSettings"))
|
||||
{
|
||||
Serial.println("Save settings");
|
||||
AppSettings settings = settingsManager.getAppSettings();
|
||||
settings.mqttServer = request->arg("mqtt_server");
|
||||
settings.mqttUsername = request->arg("mqtt_username");
|
||||
settings.mqttPassword = request->arg("mqtt_password");
|
||||
settings.mqttRootTopic = request->arg("mqtt_rootTopic");
|
||||
settings.ntpServer = request->arg("ntpServer");
|
||||
settingsManager.saveAppSettings(settings);
|
||||
request->redirect("/");
|
||||
shouldReboot = true;
|
||||
} else {
|
||||
request->send(SPIFFS, "/settings.html", String(), false, processor);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
webServer.on("/pairing", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
if(request->hasArg("btnDoPairing"))
|
||||
{
|
||||
Serial.println("Do (re)pairing");
|
||||
doPairing();
|
||||
request->redirect("/");
|
||||
} else {
|
||||
request->send(SPIFFS, "/settings.html", String(), false, processor);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
webServer.on("/factoryReset", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
if(request->hasArg("btnFactoryReset"))
|
||||
{
|
||||
notifyClients("Factory reset initiated...");
|
||||
|
||||
if (!fingerManager.deleteAll())
|
||||
notifyClients("Finger database could not be deleted.");
|
||||
|
||||
if (!settingsManager.deleteAppSettings())
|
||||
notifyClients("App settings could not be deleted.");
|
||||
|
||||
if (!settingsManager.deleteWifiSettings())
|
||||
notifyClients("Wifi settings could not be deleted.");
|
||||
|
||||
request->redirect("/");
|
||||
shouldReboot = true;
|
||||
} else {
|
||||
request->send(SPIFFS, "/settings.html", String(), false, processor);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
webServer.on("/deleteAllFingerprints", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
if(request->hasArg("btnDeleteAllFingerprints"))
|
||||
{
|
||||
notifyClients("Deleting all fingerprints...");
|
||||
|
||||
if (!fingerManager.deleteAll())
|
||||
notifyClients("Finger database could not be deleted.");
|
||||
|
||||
request->redirect("/");
|
||||
|
||||
} else {
|
||||
request->send(SPIFFS, "/settings.html", String(), false, processor);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
webServer.onNotFound([](AsyncWebServerRequest *request){
|
||||
request->send(404);
|
||||
});
|
||||
|
||||
|
||||
} // end normal operating mode
|
||||
|
||||
|
||||
// common url callbacks
|
||||
webServer.on("/reboot", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
request->redirect("/");
|
||||
shouldReboot = true;
|
||||
});
|
||||
|
||||
webServer.on("/bootstrap.min.css", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
request->send(SPIFFS, "/bootstrap.min.css", "text/css");
|
||||
});
|
||||
|
||||
|
||||
// Enable Over-the-air updates at http://<IPAddress>/update
|
||||
ElegantOTA.begin(&webServer);
|
||||
ElegantOTA.onStart(onOTAStart);
|
||||
ElegantOTA.onProgress(onOTAProgress);
|
||||
ElegantOTA.onEnd(onOTAEnd);
|
||||
// Start server
|
||||
webServer.begin();
|
||||
// Init time by NTP Client
|
||||
configTime(gmtOffset_sec, daylightOffset_sec, "pool.ntp.org");
|
||||
notifyClients("System booted successfully!");
|
||||
|
||||
}
|
||||
|
||||
|
||||
void mqttCallback(char* topic, byte* message, unsigned int length) {
|
||||
Serial.print("Message arrived on topic: ");
|
||||
Serial.print(topic);
|
||||
Serial.print(". Message: ");
|
||||
String messageTemp;
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
Serial.print((char)message[i]);
|
||||
messageTemp += (char)message[i];
|
||||
}
|
||||
Serial.println();
|
||||
|
||||
// Check incomming message for interesting topics
|
||||
if (String(topic) == String(settingsManager.getAppSettings().mqttRootTopic) + "/ignoreTouchRing") {
|
||||
if(messageTemp == "on"){
|
||||
fingerManager.setIgnoreTouchRing(true);
|
||||
}
|
||||
else if(messageTemp == "off"){
|
||||
fingerManager.setIgnoreTouchRing(false);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CUSTOM_GPIOS
|
||||
if (String(topic) == String(settingsManager.getAppSettings().mqttRootTopic) + "/customOutput1") {
|
||||
if(messageTemp == "on"){
|
||||
digitalWrite(customOutput1, HIGH);
|
||||
}
|
||||
else if(messageTemp == "off"){
|
||||
digitalWrite(customOutput1, LOW);
|
||||
}
|
||||
}
|
||||
if (String(topic) == String(settingsManager.getAppSettings().mqttRootTopic) + "/customOutput2") {
|
||||
if(messageTemp == "on"){
|
||||
digitalWrite(customOutput2, HIGH);
|
||||
}
|
||||
else if(messageTemp == "off"){
|
||||
digitalWrite(customOutput2, LOW);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void connectMqttClient() {
|
||||
if (!mqttClient.connected() && mqttConfigValid) {
|
||||
Serial.print("(Re)connect to MQTT broker...");
|
||||
// Attempt to connect
|
||||
bool connectResult;
|
||||
|
||||
// connect with or witout authentication
|
||||
String lastWillTopic = settingsManager.getAppSettings().mqttRootTopic + "/lastLogMessage";
|
||||
String lastWillMessage = "FingerprintDoorbell disconnected unexpectedly";
|
||||
if (settingsManager.getAppSettings().mqttUsername.isEmpty() || settingsManager.getAppSettings().mqttPassword.isEmpty())
|
||||
connectResult = mqttClient.connect(settingsManager.getWifiSettings().hostname.c_str(),lastWillTopic.c_str(), 1, false, lastWillMessage.c_str());
|
||||
else
|
||||
connectResult = mqttClient.connect(settingsManager.getWifiSettings().hostname.c_str(), settingsManager.getAppSettings().mqttUsername.c_str(), settingsManager.getAppSettings().mqttPassword.c_str(), lastWillTopic.c_str(), 1, false, lastWillMessage.c_str());
|
||||
|
||||
if (connectResult) {
|
||||
// success
|
||||
Serial.println("connected");
|
||||
// Subscribe
|
||||
mqttClient.subscribe((settingsManager.getAppSettings().mqttRootTopic + "/ignoreTouchRing").c_str(), 1); // QoS = 1 (at least once)
|
||||
#ifdef CUSTOM_GPIOS
|
||||
mqttClient.subscribe((settingsManager.getAppSettings().mqttRootTopic + "/customOutput1").c_str(), 1); // QoS = 1 (at least once)
|
||||
mqttClient.subscribe((settingsManager.getAppSettings().mqttRootTopic + "/customOutput2").c_str(), 1); // QoS = 1 (at least once)
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
if (mqttClient.state() == 4 || mqttClient.state() == 5) {
|
||||
mqttConfigValid = false;
|
||||
notifyClients("Failed to connect to MQTT Server: bad credentials or not authorized. Will not try again, please check your settings.");
|
||||
} else {
|
||||
notifyClients(String("Failed to connect to MQTT Server, rc=") + mqttClient.state() + ", try again in 30 seconds");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void doScan()
|
||||
{
|
||||
Match match = fingerManager.scanFingerprint();
|
||||
String mqttRootTopic = settingsManager.getAppSettings().mqttRootTopic;
|
||||
switch(match.scanResult)
|
||||
{
|
||||
case ScanResult::noFinger:
|
||||
// standard case, occurs every iteration when no finger touchs the sensor
|
||||
if (match.scanResult != lastMatch.scanResult) {
|
||||
Serial.println("no finger");
|
||||
mqttClient.publish((String(mqttRootTopic) + "/ring").c_str(), "off");
|
||||
mqttClient.publish((String(mqttRootTopic) + "/matchId").c_str(), "-1");
|
||||
mqttClient.publish((String(mqttRootTopic) + "/matchName").c_str(), "");
|
||||
mqttClient.publish((String(mqttRootTopic) + "/matchConfidence").c_str(), "-1");
|
||||
}
|
||||
break;
|
||||
case ScanResult::matchFound:
|
||||
notifyClients( String("Match Found: ") + match.matchId + " - " + match.matchName + " with confidence of " + match.matchConfidence );
|
||||
if (match.scanResult != lastMatch.scanResult) {
|
||||
if (checkPairingValid()) {
|
||||
digitalWrite(doorOpenerOutputPin, HIGH);
|
||||
mqttClient.publish((String(mqttRootTopic) + "/ring").c_str(), "off");
|
||||
mqttClient.publish((String(mqttRootTopic) + "/matchId").c_str(), String(match.matchId).c_str());
|
||||
mqttClient.publish((String(mqttRootTopic) + "/matchName").c_str(), match.matchName.c_str());
|
||||
mqttClient.publish((String(mqttRootTopic) + "/matchConfidence").c_str(), String(match.matchConfidence).c_str());
|
||||
Serial.println("MQTT message sent: Open the door!");
|
||||
delay(1000);
|
||||
digitalWrite(doorOpenerOutputPin, LOW);
|
||||
} else {
|
||||
notifyClients("Security issue! Match was not sent by MQTT because of invalid sensor pairing! This could potentially be an attack! If the sensor is new or has been replaced by you do a (re)pairing in settings page.");
|
||||
}
|
||||
}
|
||||
delay(3000); // wait some time before next scan to let the LED blink
|
||||
break;
|
||||
case ScanResult::noMatchFound:
|
||||
notifyClients(String("No Match Found (Code ") + match.returnCode + ")");
|
||||
if (match.scanResult != lastMatch.scanResult) {
|
||||
digitalWrite(doorbellOutputPin, HIGH);
|
||||
mqttClient.publish((String(mqttRootTopic) + "/ring").c_str(), "on");
|
||||
mqttClient.publish((String(mqttRootTopic) + "/matchId").c_str(), "-1");
|
||||
mqttClient.publish((String(mqttRootTopic) + "/matchName").c_str(), "");
|
||||
mqttClient.publish((String(mqttRootTopic) + "/matchConfidence").c_str(), "-1");
|
||||
Serial.println("MQTT message sent: ring the bell!");
|
||||
delay(1000);
|
||||
digitalWrite(doorbellOutputPin, LOW);
|
||||
} else {
|
||||
delay(1000); // wait some time before next scan to let the LED blink
|
||||
}
|
||||
break;
|
||||
case ScanResult::error:
|
||||
notifyClients(String("ScanResult Error (Code ") + match.returnCode + ")");
|
||||
break;
|
||||
};
|
||||
lastMatch = match;
|
||||
|
||||
}
|
||||
|
||||
void doEnroll()
|
||||
{
|
||||
int id = enrollId.toInt();
|
||||
if (id < 1 || id > 200) {
|
||||
notifyClients("Invalid memory slot id '" + enrollId + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
NewFinger finger = fingerManager.enrollFinger(id, enrollName);
|
||||
if (finger.enrollResult == EnrollResult::ok) {
|
||||
notifyClients("Enrollment successfull. You can now use your new finger for scanning.");
|
||||
updateClientsFingerlist(fingerManager.getFingerListAsHtmlOptionList());
|
||||
} else if (finger.enrollResult == EnrollResult::error) {
|
||||
notifyClients(String("Enrollment failed. (Code ") + finger.returnCode + ")");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void reboot()
|
||||
{
|
||||
notifyClients("System is rebooting now...");
|
||||
delay(1000);
|
||||
|
||||
mqttClient.disconnect();
|
||||
espClient.stop();
|
||||
dnsServer.stop();
|
||||
webServer.end();
|
||||
WiFi.disconnect();
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
|
||||
void setup()
|
||||
{
|
||||
// open serial monitor for debug infos
|
||||
Serial.begin(115200);
|
||||
//while (!Serial); // For Yun/Leo/Micro/Zero/...
|
||||
//delay(2000);
|
||||
Serial.println("Hello");
|
||||
// initialize GPIOs
|
||||
pinMode(doorbellOutputPin, OUTPUT);
|
||||
#ifdef CUSTOM_GPIOS
|
||||
pinMode(customOutput1, OUTPUT);
|
||||
pinMode(customOutput2, OUTPUT);
|
||||
pinMode(customInput1, INPUT_PULLDOWN);
|
||||
pinMode(customInput2, INPUT_PULLDOWN);
|
||||
#endif
|
||||
Serial.println("Hello2");
|
||||
settingsManager.loadWifiSettings();
|
||||
settingsManager.loadAppSettings();
|
||||
Serial.println("Hello3");
|
||||
fingerManager.connect();
|
||||
Serial.println("Hello4");
|
||||
if (!checkPairingValid())
|
||||
notifyClients("Security issue! Pairing with sensor is invalid. This could potentially be an attack! If the sensor is new or has been replaced by you do a (re)pairing in settings page. MQTT messages regarding matching fingerprints will not been sent until pairing is valid again.");
|
||||
Serial.println("Hello5");
|
||||
if (fingerManager.isFingerOnSensor() || !settingsManager.isWifiConfigured())
|
||||
{
|
||||
// ring touched during startup or no wifi settings stored -> wifi config mode
|
||||
currentMode = Mode::wificonfig;
|
||||
Serial.println("Started WiFi-Config mode");
|
||||
fingerManager.setLedRingWifiConfig();
|
||||
initWiFiAccessPointForConfiguration();
|
||||
startWebserver();
|
||||
|
||||
} else {
|
||||
Serial.println("Started normal operating mode");
|
||||
currentMode = Mode::scan;
|
||||
if (initWifi()) {
|
||||
startWebserver();
|
||||
if (settingsManager.getAppSettings().mqttServer.isEmpty()) {
|
||||
mqttConfigValid = false;
|
||||
notifyClients("Error: No MQTT Broker is configured! Please go to settings and enter your server URL + user credentials.");
|
||||
} else {
|
||||
delay(5000);
|
||||
IPAddress mqttServerIp;
|
||||
if (WiFi.hostByName(settingsManager.getAppSettings().mqttServer.c_str(), mqttServerIp))
|
||||
{
|
||||
mqttConfigValid = true;
|
||||
Serial.println("IP used for MQTT server: " + mqttServerIp.toString());
|
||||
mqttClient.setServer(mqttServerIp , 1883);
|
||||
mqttClient.setCallback(mqttCallback);
|
||||
connectMqttClient();
|
||||
}
|
||||
else {
|
||||
mqttConfigValid = false;
|
||||
notifyClients("MQTT Server '" + settingsManager.getAppSettings().mqttServer + "' not found. Please check your settings.");
|
||||
}
|
||||
}
|
||||
if (fingerManager.connected)
|
||||
fingerManager.setLedRingReady();
|
||||
else
|
||||
fingerManager.setLedRingError();
|
||||
} else {
|
||||
fingerManager.setLedRingError();
|
||||
shouldReboot = true;
|
||||
}
|
||||
|
||||
}
|
||||
Serial.println("Hello6");
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
ElegantOTA.loop();
|
||||
// shouldReboot flag for supporting reboot through webui
|
||||
if (shouldReboot) {
|
||||
reboot();
|
||||
}
|
||||
|
||||
// Reconnect handling
|
||||
if (currentMode != Mode::wificonfig)
|
||||
{
|
||||
unsigned long currentMillis = millis();
|
||||
// reconnect WiFi if down for 30s
|
||||
if ((WiFi.status() != WL_CONNECTED) && (currentMillis - wifiReconnectPreviousMillis >= 30000ul)) {
|
||||
Serial.println("Reconnecting to WiFi...");
|
||||
WiFi.disconnect();
|
||||
WiFi.reconnect();
|
||||
wifiReconnectPreviousMillis = currentMillis;
|
||||
}
|
||||
|
||||
// reconnect mqtt if down
|
||||
if (!settingsManager.getAppSettings().mqttServer.isEmpty()) {
|
||||
if (!mqttClient.connected() && (currentMillis - mqttReconnectPreviousMillis >= 30000ul)) {
|
||||
connectMqttClient();
|
||||
mqttReconnectPreviousMillis = currentMillis;
|
||||
}
|
||||
mqttClient.loop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// do the actual loop work
|
||||
switch (currentMode)
|
||||
{
|
||||
case Mode::scan:
|
||||
if (fingerManager.connected)
|
||||
doScan();
|
||||
break;
|
||||
|
||||
case Mode::enroll:
|
||||
doEnroll();
|
||||
currentMode = Mode::scan; // switch back to scan mode after enrollment is done
|
||||
break;
|
||||
|
||||
case Mode::wificonfig:
|
||||
dnsServer.processNextRequest(); // used for captive portal redirect
|
||||
break;
|
||||
|
||||
case Mode::maintenance:
|
||||
// do nothing, give webserver exclusive access to sensor (not thread-safe for concurrent calls)
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
// enter maintenance mode (no continous scanning) if requested
|
||||
if (needMaintenanceMode)
|
||||
currentMode = Mode::maintenance;
|
||||
|
||||
#ifdef CUSTOM_GPIOS
|
||||
// read custom inputs and publish by MQTT
|
||||
bool i1;
|
||||
bool i2;
|
||||
i1 = (digitalRead(customInput1) == HIGH);
|
||||
i2 = (digitalRead(customInput2) == HIGH);
|
||||
|
||||
String mqttRootTopic = settingsManager.getAppSettings().mqttRootTopic;
|
||||
if (i1 != customInput1Value) {
|
||||
if (i1)
|
||||
mqttClient.publish((String(mqttRootTopic) + "/customInput1").c_str(), "on");
|
||||
else
|
||||
mqttClient.publish((String(mqttRootTopic) + "/customInput1").c_str(), "off");
|
||||
}
|
||||
|
||||
if (i2 != customInput2Value) {
|
||||
if (i2)
|
||||
mqttClient.publish((String(mqttRootTopic) + "/customInput2").c_str(), "on");
|
||||
else
|
||||
mqttClient.publish((String(mqttRootTopic) + "/customInput2").c_str(), "off");
|
||||
}
|
||||
|
||||
customInput1Value = i1;
|
||||
customInput2Value = i2;
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user