Compare commits
2 Commits
2604c5e859
...
f2145f98b6
| Author | SHA1 | Date | |
|---|---|---|---|
| f2145f98b6 | |||
| 4babd2693b |
BIN
CAD/klingel v1.3mf
Normal file
BIN
CAD/klingel v1.3mf
Normal file
Binary file not shown.
2793
CAD/klingel v1.step
Normal file
2793
CAD/klingel v1.step
Normal file
File diff suppressed because it is too large
Load Diff
7
FingerprintDoorbell/.gitignore
vendored
Normal file
7
FingerprintDoorbell/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
doc/wiring.pptx
|
||||
builds
|
||||
10
FingerprintDoorbell/.vscode/extensions.json
vendored
Normal file
10
FingerprintDoorbell/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
||||
6
FingerprintDoorbell/.vscode/settings.json
vendored
Normal file
6
FingerprintDoorbell/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"string": "cpp",
|
||||
"functional": "cpp"
|
||||
}
|
||||
}
|
||||
201
FingerprintDoorbell/README.md
Normal file
201
FingerprintDoorbell/README.md
Normal file
@ -0,0 +1,201 @@
|
||||
# FingerprintDoorbell
|
||||
|
||||
## What is FingerprintDoorbell?
|
||||
It's more or less a doorbell with the ability to scan finger prints or a fingerprint reader with the ability to act as doorbell, depending on your perspective ;-). But lets speak some images:
|
||||
|
||||
<img src="https://raw.githubusercontent.com/frickelzeugs/FingerprintDoorbell/master/doc/images/doorbell-sample.jpg" width="400">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/frickelzeugs/FingerprintDoorbell/master/doc/images/web-manage.png" width="600">
|
||||
<img src="https://raw.githubusercontent.com/frickelzeugs/FingerprintDoorbell/master/doc/images/web-settings.png" width="600">
|
||||
|
||||
## How does it work?
|
||||
If you put your finger on the sensor the system looks for a matching fingerprint. If it doesn't find one, it rings the bell (MQTT message is published and an GPIO pin is set to high). If a match was found, the matching finger ID together with a name and confidence will be published as MQTT messagage. In combination with a home automation solution (like OpenHAB, ioBroker, Home Assistant...) you can then trigger your door opener or smart lock. You can also define actions depending on the finger that was detected, like left thumb opens front door, right thumb opens garage, middle finger...
|
||||
|
||||
|
||||
|
||||
## What do I need?
|
||||
- fingerprint reader Grow R503 (available at https://de.aliexpress.com/i/33053783539.html)
|
||||
- an ESP32 microcontroller (I would prefer the mini-version, because of it's compact size, e.g. available here https://de.aliexpress.com/item/1005001621844145.html or from any other dealer of your trust)
|
||||
|
||||
# How to build the hardware
|
||||
## Wiring
|
||||
<img src="https://raw.githubusercontent.com/frickelzeugs/FingerprintDoorbell/master/doc/images/wiring.png" >
|
||||
|
||||
I would not solder the cables directly to the ESP32 but recommend using a connector between ESP32 and Sensor. Otherwise, the cables must first be fed through the 25mm hole for the sensor and then soldered on. Replacing the Sensor will be a pain then. The original plug used on the sensor is a 6-pin Micro JST SH 1.00mm but it'll be fine to use any other 6-pin connector that you have on hand if you replace both sides.
|
||||
|
||||
Just as an inpiration:
|
||||
|
||||
<img src="https://raw.githubusercontent.com/frickelzeugs/FingerprintDoorbell/master/doc/images/esp32-shield.jpg" width="300">
|
||||
|
||||
I made a small shield board for the ESP32 mini that contains a 6 pin Micro JST SH 1.00mm socket, a DC-DC step down converter and some screw terminals for the power supply (in my case 24V DC). So I'm now able to easily replace the sensor or ESP32 without touching my soldering iron.
|
||||
|
||||
Caution: Please do not forget to ground the sensor housing. Otherwise it can lead to unexpected restarts or even damages the ESP32 if you are electrostatically charged. I had occasional restarts when touching the sensor after I was electrostatically charged from the floor mats in the car.
|
||||
|
||||
# Flashing the firmware
|
||||
## Method 1 (recommended!):
|
||||
This is the absolute preferred method over method 2, which was the standard method in the past. Thanks to the great work of the [ESP Web Tools](https://esphome.github.io/esp-web-tools/) project, flashing your ESP is now as easy as pie. All you need to do is connect your ESP32 module via USB and simply flash FingerprintDoorbell via your browser (Chrome, Edge or Opera required):
|
||||
|
||||
### --> [FingerprintDoorbell Flasher](https://frickelzeugs.github.io/FingerprintDoorbell-flasher/)
|
||||
|
||||
|
||||
## Method 2: use precompiled binaries
|
||||
To flash the ESP32 with the precompiled binaries you can use the python based esptool from espressif. You'll need a running Python environment on your system, so get Python first here if you don't already have it: [Python download](https://www.python.org/downloads/)
|
||||
|
||||
After python is available run the following command in your shell/cmd to install esptool:
|
||||
```
|
||||
pip install esptool
|
||||
```
|
||||
If there is a `command not found: pip` error but you have installed Python try to use pip3 insted of pip.
|
||||
```
|
||||
pip3 install esptool
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
Now esptool should be available in your shell/cmd. Check it by running:
|
||||
```
|
||||
esptool.py
|
||||
```
|
||||
|
||||
Now that esptool.py is available you can continue to flash the firmware. To start you will need 5 files:
|
||||
|
||||
- bootloader_dio_40m.bin
|
||||
- boot_app0.bin
|
||||
- partitions.bin
|
||||
|
||||
download [here](https://raw.githubusercontent.com/frickelzeugs/FingerprintDoorbell/master/doc/bootloader.zip)
|
||||
|
||||
- firmware.bin
|
||||
- spiffs.bin
|
||||
|
||||
contained in the [Release packages](https://github.com/frickelzeugs/FingerprintDoorbell/releases)
|
||||
|
||||
### Flashing
|
||||
Copy all 5 files in a local folder, open your command line/shell, navigate to this folder and execute:
|
||||
|
||||
```
|
||||
esptool.py --chip esp32 --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x1000 bootloader_dio_40m.bin 0x8000 partitions.bin 0xe000 boot_app0.bin 0x10000 firmware.bin 2686976 spiffs.bin
|
||||
```
|
||||
|
||||
If everything has worked your output should look something like this:
|
||||
|
||||
```
|
||||
esptool.py v3.2
|
||||
Found 2 serial ports
|
||||
Serial port COM4
|
||||
Connecting....
|
||||
Chip is ESP32-D0WDQ6 (revision 1)
|
||||
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None
|
||||
Crystal is 40MHz
|
||||
MAC: 9c:9c:1f:c7:ac:a0
|
||||
Uploading stub...
|
||||
Running stub...
|
||||
Stub running...
|
||||
Changing baud rate to 460800
|
||||
Changed.
|
||||
Configuring flash size...
|
||||
Auto-detected Flash size: 4MB
|
||||
Flash will be erased from 0x00001000 to 0x00005fff...
|
||||
Flash will be erased from 0x00008000 to 0x00008fff...
|
||||
Flash will be erased from 0x0000e000 to 0x0000ffff...
|
||||
Flash will be erased from 0x00010000 to 0x00121fff...
|
||||
Flash will be erased from 0x00290000 to 0x003fffff...
|
||||
Compressed 17104 bytes to 11191...
|
||||
Wrote 17104 bytes (11191 compressed) at 0x00001000 in 0.6 seconds (effective 225.1 kbit/s)...
|
||||
Hash of data verified.
|
||||
Compressed 3072 bytes to 128...
|
||||
Wrote 3072 bytes (128 compressed) at 0x00008000 in 0.1 seconds (effective 300.7 kbit/s)...
|
||||
Hash of data verified.
|
||||
Compressed 8192 bytes to 47...
|
||||
Wrote 8192 bytes (47 compressed) at 0x0000e000 in 0.1 seconds (effective 463.9 kbit/s)...
|
||||
Hash of data verified.
|
||||
Compressed 1119504 bytes to 666873...
|
||||
Wrote 1119504 bytes (666873 compressed) at 0x00010000 in 16.1 seconds (effective 555.0 kbit/s)...
|
||||
Hash of data verified.
|
||||
Compressed 1507328 bytes to 36014...
|
||||
Wrote 1507328 bytes (36014 compressed) at 0x00290000 in 7.5 seconds (effective 1599.3 kbit/s)...
|
||||
Hash of data verified.
|
||||
|
||||
Leaving...
|
||||
Hard resetting via RTS pin...
|
||||
```
|
||||
|
||||
Your device should now boot up and the LED ring should start flashing slowly red ("breathing") to signal that it's currently in WiFi Config mode. Proceed with the configuration of the device.
|
||||
|
||||
## Method 3: build and flash with Visual Studio Code and PlattformIO
|
||||
If you want to build the firmware on your own (e.g. if you want to modify the code or you don't trust binaries) please follow the instructions below. I don't want to go into details here and assume that you already have experience with IDEs or using Git repos:
|
||||
* Download and install [Visual Studio Code and PlattformIO Extension](https://platformio.org/platformio-ide).
|
||||
* Install [Git](https://git-scm.com/downloads) if you don't have already
|
||||
* Clone this GitHub repo and open the project workspace in VS Code (you can do this in one step from within VS Code)
|
||||
* Open the PlatformIO extension from the left sided toolbar and from the "Project Tasks" tree choose
|
||||
* esp32doit-devkit-v1 -> General -> Build (creates firmware.bin)
|
||||
* esp32doit-devkit-v1 -> Platform -> Build Filesystem Image (creates spiffs.bin containing HTML and CSS files)
|
||||
* if the build finishes successfully you can start uploading to your ESP32 by using the following tasks
|
||||
* esp32doit-devkit-v1 -> General -> Upload
|
||||
* esp32doit-devkit-v1 -> Platform -> Upload Filesystem Image
|
||||
|
||||
# Configuration
|
||||
## WiFi Connection
|
||||
If no WiFi settings are configured (e.g. on a fresh install) the device will automatically boot into WiFi configuration mode (LED ring is breathing red). Once your WiFi connection is configured the device will never enter WiFi config mode again, even if the WiFi is not available or it cannot connect because of errors. If you later want to enter WiFi configuration mode again you have to press and hold your finger at least 10s on the sensor while powering on the device (or trigger a reboot through WebUI).
|
||||
|
||||
When in WiFi config mode FingerprintDoorbell will act as an AccessPoint an creates it's own Network with the Name "FingerprintDoorbell-Config". Connect to this network with your PC/Mobile and Password "12345678". You should then get a "captive portal" notification, which you should bring you to the browser with the WiFi config already open. If the captive portal thing does not work please open a browser manually and visit "http://192.168.4.1".
|
||||
|
||||
<img src="https://raw.githubusercontent.com/frickelzeugs/FingerprintDoorbell/master/doc/images/web-wificonfig.png" width="300">
|
||||
|
||||
Enter your settings and click "Save and restart" to bring the device back to normal operation mode. If everything had worked the LED ring should first flash blue while bootup and starts breathing blue if connection to your WiFi is running. When connected to your WiFi the WebUI of Fingerprintdoorbell should be available under http://fingerprintdoorbell (if you used the default hostname in WiFi configuration). Now you can start enrolling ("teaching") your fingerprints.
|
||||
|
||||
## Managing fingerprints
|
||||
The sensor has the capacity for storing up to 200 fingerprints. Theses memory slots are used as ID together with a name to increase human readability. To enroll new fingerprints enter a ID and name (optional) in the "Add/Replace fingerprint" section and click "Start enrollment". Now the system asks you to place and lift your finger to the sensor for 5 times. The 5 passes of scanning helps the sensor to improve its recognition rate. Don't try to vary your placing/position too much, because the enrollment process may fail if the 5 preceeding scans differ too much from each other and cannot be combined to one fingerprint template.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/frickelzeugs/FingerprintDoorbell/master/doc/images/web-manage.png" width="300">
|
||||
|
||||
If enrollment has completed successfull you can now test if your fingerprint matches.
|
||||
|
||||
## Configure MQTT connection
|
||||
Matching fingerprints (and also ring events) are published as messages to your MQTT broker at certain topics. For this you will have to configure your MQTT Broker settings in FingerprintDoorbell. If your broker does not need authentification by username and password just leave this fields empty. You can also specify a custom root topic under which FingerprintDoorbell publishes its messages or leave the default "fingerprintDoorbell" if you're fine with that.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/frickelzeugs/FingerprintDoorbell/master/doc/images/web-settings.png" width="300">
|
||||
|
||||
| MQTT Topic | Action | Values |
|
||||
| ------------------------------------ | --------- | -------- |
|
||||
| fingerprintDoorbell/ring | publish | "off" by default, on a ring event switching to "on" for 1s |
|
||||
| fingerprintDoorbell/matchId | publish | "-1" by default, if a match was found the value holds the matching id (e.g. "27") for 3s |
|
||||
| fingerprintDoorbell/matchName | publish | "" by default, if a match was found the value holds the matching name for 3s |
|
||||
| fingerprintDoorbell/matchConfidence | publish | "" by default, if a match was found the value holds the conficence (number between "1" and "400", 1=low, 400=very high) for 3s |
|
||||
| fingerprintDoorbell/ignoreTouchRing | subscribe | read by FingerprintDoorbell and enables/disables the touch ring (see FAQ below for details) |
|
||||
|
||||
## Advanced Actions
|
||||
### Firmware Update
|
||||
If you've managed to walk the bumpy path of flashing the firmware on the ESP32 for the first time, dont't worry: every further firmware update will be a piece of cake. FingerprintDoorbell is using the really cool Library [AsyncElegantOTA](https://github.com/ayushsharma82/AsyncElegantOTA) to make this as handy as possible. You don't even have to pull the microcontroller out of the wall and connect it to your computer, because the "OTA" in "AsyncElegantOTA" is for "Over-the-air" updates. All you need to do is to browse to the settings page of the WebUI and hit "Firmware update". In the following Dialog you have to upload 2 files
|
||||
|
||||
- firmware.bin for the "Firmware" radio button
|
||||
- spiffs.bin for the "Filesystem" radio button
|
||||
|
||||
Done. Reboot your system to get the new firmware live.
|
||||
|
||||
### Pairing a new Sensor
|
||||
For security reasons the ESP32 and Sensor will be coupled together, so if the sensor is replaced (e.g. an attackers connects his own sensor to the ESP32 with his fingerprints on it) this will be detected. In this case the pairing will be marked as broken and no further match events are sent by MQTT from now on (even if you connect the old sensor again). But keep calm, the doorbell function will still continue to work and ring events are sent by MQTT so you don't miss your long awaited package delivery. You'll see an error message in the log window that requests you to renew the pairing. If the sensor replacement was done by yourself or no attack took place please choose the option "Pairing a new Sensor" to pair the sensor with the ESP32.
|
||||
|
||||
### Factory Reset
|
||||
As the name already says this will delete all your settings and fingerprints from the device. You'll have a blank device in WiFi Config mode when choosing this option. Be careful!
|
||||
|
||||
# FAQ
|
||||
## What does the different colors/blinking styles of the LED ring mean?
|
||||
|LED ring color| sequence | Meaning |
|
||||
| -------- | -------- | -------- |
|
||||
| red | permanent | System is in error state |
|
||||
| red | breathing | System in WiFi config mode |
|
||||
| red | flashing | Finger on sensor detected (no match found yet) |
|
||||
| blue | permanent | System ready (touch ring ignored) |
|
||||
| blue | breathing | System ready (touch ring active) |
|
||||
| blue | flashing | System startup (not ready yet) |
|
||||
| purple | solid | Fingerprint match found or when in enrollment mode this means pass is finished, lift your finger |
|
||||
| purple | flashing | Enrollment active (waiting for finger) |
|
||||
|
||||
## What is the MQTT topic "fingerprintDoorbell/ignoreTouchRing" for and how to use it?
|
||||
If your sensor is mounted in a dry environment and cannot be hit by rain you can skip this section. Otherwise please read further. The sensor consists mainly of two parts: the black sensor surface and a metal ring divided by the led ring around the sensor surface. The sensor surface will only recognize a finger touch if the larger part of the finger was on the sensor and not only a small tip. Also only short touches are not recognizes, because no image could be captured in this short timespan. Because a visitor who just wants to ring the bell doesn't pay particular attention to putting his finger completely on the sensor, I do not only evaluate the image sensor itself, but also consider the finger detection signal (pin 5) the sensor is providing. This signal is already triggered if you slightly touch the sensor and even if you only touch the metal ring and not yet the sensor surface.
|
||||
|
||||
This ring is a capacitive touch sensor that works similar to the touch display of your smartphone. And if you may know touch displays and rain drops are not best friends, because they can lead to false inputs. This will usually be a problem if your sensor is mounted in a place exposed to rain and even if it's mounted under the roof overhang it may be hit by horizontal rain during a storm and causing false ring events. And believe me: your wife will not be happy if the storm rings on your door at 3 AM ;-)
|
||||
|
||||
So after this experience I could have added an option to disable this ring permanent in the settings but I decided to go another way and make this option conditional. Why? Because in >95% of the cases I really want this high sensitivity of the sensor, just not on stormy and rainy days. Fortunately I already had the current weather conditions available in my smart home via a rain sensor. So I added the MQTT topic "fingerprintDoorbell/ignoreTouchRing" which can be set to "on" or "off". In my case OpenHAB sets this value to "on" when it's raining and wind speed is over a certain level. Since then I have had no more problems with disturbing the peace at night.
|
||||
2
FingerprintDoorbell/_config.yml
Normal file
2
FingerprintDoorbell/_config.yml
Normal file
@ -0,0 +1,2 @@
|
||||
theme: jekyll-theme-cayman
|
||||
markdown: GFM
|
||||
6
FingerprintDoorbell/data/bootstrap.min.css
vendored
Normal file
6
FingerprintDoorbell/data/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
150
FingerprintDoorbell/data/index.html
Normal file
150
FingerprintDoorbell/data/index.html
Normal file
@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- created with https://bootstrapformbuilder.com/ -->
|
||||
<title>FingerprintDoorbell</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" charset="utf-8">
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||
<style>
|
||||
.alert-custom{
|
||||
background-color:#cecece;
|
||||
color:rgb(0, 0, 0);
|
||||
}
|
||||
.form-horizontal{
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
if (!!window.EventSource) {
|
||||
var source = new EventSource('/events');
|
||||
|
||||
source.addEventListener('open', function(e) {
|
||||
console.log("Events Connected");
|
||||
}, false);
|
||||
|
||||
source.addEventListener('error', function(e) {
|
||||
if (e.target.readyState != EventSource.OPEN) {
|
||||
console.log("Events Disconnected");
|
||||
}
|
||||
}, false);
|
||||
|
||||
// event is fired when a new message from server was received
|
||||
source.addEventListener('message', function(e) {
|
||||
console.log("message", e.data);
|
||||
document.getElementById('logMessages').innerHTML = event.data;
|
||||
}, false);
|
||||
|
||||
// event is fired when server side fingerlist was changed (e.g. enrollment of new finger)
|
||||
source.addEventListener('fingerlist', function(e) {
|
||||
console.log("fingerlist", e.data);
|
||||
document.getElementById('selectedFingerprint').innerHTML = event.data;
|
||||
}, false);
|
||||
|
||||
}
|
||||
|
||||
function askForNewName(e)
|
||||
{
|
||||
var list = document.getElementById("selectedFingerprint");
|
||||
var strOldName = list.options[list.selectedIndex].text;
|
||||
strOldName = strOldName.substr(strOldName.indexOf('-') + 2);
|
||||
newname = prompt('New Name?',strOldName);
|
||||
if (newname === null)
|
||||
document.getElementById('renameNewName').value = strOldName;
|
||||
else
|
||||
document.getElementById('renameNewName').value = newname;
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="navbar navbar-inverse">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/">%HOSTNAME%</a>
|
||||
</div>
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="active"><a href="#">Fingerprints</a></li>
|
||||
<li><a href="settings">Settings</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<p></p>
|
||||
<div class="alert alert-custom" id="logMessages" role="alert">%LOGMESSAGES%</div>
|
||||
|
||||
<form class="form-horizontal" action="/editFingerprints">
|
||||
<fieldset>
|
||||
|
||||
<!-- Form Name -->
|
||||
<legend>Manage fingerprints</legend>
|
||||
|
||||
<!-- Select Multiple -->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="selectedFingerprint">Fingerprints in DB</label>
|
||||
<div class="col-md-4">
|
||||
<select id="selectedFingerprint" name="selectedFingerprint" class="form-control" size="10">
|
||||
<!--option value="1">1 - Option one</option-->
|
||||
%FINGERLIST%
|
||||
</select>
|
||||
<input type="hidden" id="renameNewName" name="renameNewName" type="text" class="form-control input-md">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button (Double) -->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="btnRename"></label>
|
||||
<div class="col-md-8">
|
||||
<button id="btnRename" name="btnRename" class="btn btn-info" onclick="askForNewName(event)">Rename</button>
|
||||
<button id="btnDelete" name="btnDelete" class="btn btn-danger">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<form class="form-horizontal" action="/enroll">
|
||||
<fieldset>
|
||||
|
||||
<!-- Form Name -->
|
||||
<legend>Add/Replace fingerprint</legend>
|
||||
|
||||
<!-- Text input-->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="newFingerprintId">Memory slot (1-200)</label>
|
||||
<div class="col-md-4">
|
||||
<input id="newFingerprintId" name="newFingerprintId" type="text" placeholder="1-200" class="form-control input-md" required="">
|
||||
<small class="text-muted">The sensor has 200 memory slots available for storing fingerprints. The choosen slot number will also be used as an ID when matches are published by MQTT.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text input-->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="newFingerprintName">Name</label>
|
||||
<div class="col-md-4">
|
||||
<input id="newFingerprintName" name="newFingerprintName" type="text" placeholder="(optional)" class="form-control input-md">
|
||||
<small class="text-muted">Just for human readability you can additionally assign an name to your slot number. The name will also been published by MQTT.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button -->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="startEnrollment"></label>
|
||||
<div class="col-md-4">
|
||||
<button id="startEnrollment" name="startEnrollment" class="btn btn-success">Start enrollment</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<p></p>
|
||||
<nav class="navbar navbar-default ">
|
||||
<div class="container-fluid">
|
||||
<p class="navbar-text">FingerprintDoorbell, Version %VERSIONINFO%</p>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
151
FingerprintDoorbell/data/settings.html
Normal file
151
FingerprintDoorbell/data/settings.html
Normal file
@ -0,0 +1,151 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- created with https://bootstrapformbuilder.com/ -->
|
||||
<title>FingerprintDoorbell</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" charset="utf-8">
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||
<style>
|
||||
.alert-custom{
|
||||
background-color:#cecece;
|
||||
color:rgb(0, 0, 0);
|
||||
}
|
||||
.form-horizontal{
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
if (!!window.EventSource) {
|
||||
var source = new EventSource('/events');
|
||||
|
||||
source.addEventListener('open', function(e) {
|
||||
console.log("Events Connected");
|
||||
}, false);
|
||||
|
||||
source.addEventListener('error', function(e) {
|
||||
if (e.target.readyState != EventSource.OPEN) {
|
||||
console.log("Events Disconnected");
|
||||
}
|
||||
}, false);
|
||||
|
||||
// event is fired when a new message from server was received
|
||||
source.addEventListener('message', function(e) {
|
||||
console.log("message", e.data);
|
||||
document.getElementById('logMessages').innerHTML = event.data;
|
||||
}, false);
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="navbar navbar-inverse">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/">%HOSTNAME%</a>
|
||||
</div>
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a href="/">Fingerprints</a></li>
|
||||
<li class="active"><a href="#">Settings</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="alert alert-custom" id="logMessages" role="alert">%LOGMESSAGES%</div>
|
||||
|
||||
<form class="form-horizontal" action="/settings">
|
||||
<fieldset>
|
||||
|
||||
<!-- Form Name -->
|
||||
<legend>Settings</legend>
|
||||
|
||||
<!-- Text input-->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="mqtt_server">MQTT Server (Broker)</label>
|
||||
<div class="col-md-4">
|
||||
<input id="mqtt_server" name="mqtt_server" type="text" placeholder="Address of your MQTT Broker" class="form-control input-md" value="%MQTT_SERVER%" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="mqtt_username">MQTT Username</label>
|
||||
<div class="col-md-4">
|
||||
<input id="mqtt_username" name="mqtt_username" type="text" placeholder="Username for connecting to your MQTT Broker" class="form-control input-md" value="%MQTT_USERNAME%">
|
||||
<small class="text-muted">Leave empty if your broker is not requiring authentication.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="mqtt_password">MQTT Password</label>
|
||||
<div class="col-md-4">
|
||||
<input id="mqtt_password" name="mqtt_password" type="text" placeholder="Password for connecting to your MQTT Broker" class="form-control input-md" value="%MQTT_PASSWORD%">
|
||||
<small class="text-muted">Leave empty if your broker is not requiring authentication.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="mqtt_rootTopic">MQTT Root Topic</label>
|
||||
<div class="col-md-4">
|
||||
<input id="mqtt_rootTopic" name="mqtt_rootTopic" type="text" placeholder="Root topic where FingerprintDoorbell publishes its messages" class="form-control input-md" value="%MQTT_ROOTTOPIC%" required>
|
||||
<small class="text-muted">Published Topics (=write)<br>
|
||||
- "%MQTT_ROOTTOPIC%/ring"<br>
|
||||
- "%MQTT_ROOTTOPIC%/matchId"<br>
|
||||
- "%MQTT_ROOTTOPIC%/matchName"<br>
|
||||
- "%MQTT_ROOTTOPIC%/matchConfidence"<br>
|
||||
- "%MQTT_ROOTTOPIC%/lastLogMessage"<br>
|
||||
Subscribed Topics (=read)<br>
|
||||
- "%MQTT_ROOTTOPIC%/ignoreTouchRing"
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="ntpServer">NTP Server</label>
|
||||
<div class="col-md-4">
|
||||
<input id="ntpServer" name="ntpServer" type="text" placeholder="URL to NTP server" class="form-control input-md" value="%NTP_SERVER%">
|
||||
<small class="text-muted">Used for timestamps in log panel. If you don't specify any, time will be always null.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button -->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="btnSaveSettings"></label>
|
||||
<div class="col-md-4">
|
||||
<button id="btnSaveSettings" name="btnSaveSettings" class="btn btn-success">Save and Restart</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<form class="form-horizontal">
|
||||
<fieldset>
|
||||
|
||||
<!-- Form Name -->
|
||||
<legend>Advanced Actions</legend>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="btnFirmwareUpdate"></label>
|
||||
<div class="col-md-4">
|
||||
<button id="btnFirmwareUpdate" name="btnFirmwareUpdate" class="btn btn-info" type="submit" formaction="update">Firmware-Update </button>
|
||||
<button id="btnDoPairing" name="btnDoPairing" class="btn btn-warning" type="submit" formaction="pairing">Pairing a new sensor </button>
|
||||
<button id="btnDeleteAllFingerprints" name="btnDeleteAllFingerprints" class="btn btn-danger" type="submit" formaction="deleteAllFingerprints" onclick="return confirm('This will delete all fingerprints. Are you sure you wanna do that?')">Delete all Fingerprints</button>
|
||||
<button id="btnFactoryReset" name="btnFactoryReset" class="btn btn-danger" type="submit" formaction="factoryReset" onclick="return confirm('This will delete all fingerprints, your settings and your WiFi configuration. Are you sure you wanna do that?')">Factory-Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
|
||||
<p></p>
|
||||
<nav class="navbar navbar-default ">
|
||||
<div class="container-fluid">
|
||||
<p class="navbar-text">FingerprintDoorbell, Version %VERSIONINFO%</p>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
69
FingerprintDoorbell/data/wificonfig.html
Normal file
69
FingerprintDoorbell/data/wificonfig.html
Normal file
@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WiFi Configuration</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" type="text/css" href="bootstrap.min.css">
|
||||
<style>
|
||||
.form-horizontal{
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
h1{
|
||||
margin-left: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>%HOSTNAME%</h1>
|
||||
<div class="alert alert-danger" role="alert"><b>You are currently in WiFi configuration mode!</b></br></br>In this mode the device acts as a WiFi access point to which you can temporarily connect to with your smartphone/pc
|
||||
for configuring the access to your regular WiFi network. <br><br>
|
||||
After saving the new WiFi configuration the device will restart and try to connect to your network. If this fails the device will not go back to WiFi config mode automatically. You have to put the device manually in WiFi config mode by press and hold the sensor surface for 10 s while powering up the device.
|
||||
</div>
|
||||
|
||||
<form class="form-horizontal" action="/save">
|
||||
<fieldset>
|
||||
|
||||
<!-- Form Name -->
|
||||
<legend>WiFi Configuration</legend>
|
||||
|
||||
<!-- Text input-->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="ssid">SSID</label>
|
||||
<div class="col-md-6">
|
||||
<input id="ssid" name="ssid" type="text" placeholder="SSID of your WiFi network" class="form-control input-md" value="%WIFI_SSID%" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text input-->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="password">WiFi Password</label>
|
||||
<div class="col-md-8">
|
||||
<input id="password" name="password" type="text" placeholder="Password of your WiFi network" class="form-control input-md" value="%WIFI_PASSWORD%" required>
|
||||
<small class="text-muted">For security reason the current password is not displayed here, but you can set a new one.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text input-->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="hostname">Hostname</label>
|
||||
<div class="col-md-8">
|
||||
<input id="hostname" name="hostname" type="text" placeholder="Hostname of your FingerprintDoorbell" class="form-control input-md" value="%HOSTNAME%" required>
|
||||
<small class="text-muted">The name under which this device will be available in your network. Also used in the title of the web frontend. <br>Hint: just leave it "FingerprintDoorbell" unless you have multiple devices and want to differentiate between them.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button (Double) -->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="save"></label>
|
||||
<div class="col-md-8">
|
||||
<button id="save" name="save" class="btn btn-success">Save and restart</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
BIN
FingerprintDoorbell/doc/bootloader.zip
Normal file
BIN
FingerprintDoorbell/doc/bootloader.zip
Normal file
Binary file not shown.
BIN
FingerprintDoorbell/doc/images/doorbell-sample.jpg
Normal file
BIN
FingerprintDoorbell/doc/images/doorbell-sample.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 543 KiB |
BIN
FingerprintDoorbell/doc/images/esp32-shield.jpg
Normal file
BIN
FingerprintDoorbell/doc/images/esp32-shield.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 668 KiB |
BIN
FingerprintDoorbell/doc/images/web-manage.png
Normal file
BIN
FingerprintDoorbell/doc/images/web-manage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
FingerprintDoorbell/doc/images/web-settings.png
Normal file
BIN
FingerprintDoorbell/doc/images/web-settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
FingerprintDoorbell/doc/images/web-wificonfig.png
Normal file
BIN
FingerprintDoorbell/doc/images/web-wificonfig.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
FingerprintDoorbell/doc/images/wiring.png
Normal file
BIN
FingerprintDoorbell/doc/images/wiring.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 421 KiB |
55
FingerprintDoorbell/fingerprintdoorbell.code-workspace
Normal file
55
FingerprintDoorbell/fingerprintdoorbell.code-workspace
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"files.associations": {
|
||||
"string": "cpp",
|
||||
"functional": "cpp",
|
||||
"*.tcc": "cpp",
|
||||
"bitset": "cpp",
|
||||
"exception": "cpp",
|
||||
"algorithm": "cpp",
|
||||
"iterator": "cpp",
|
||||
"memory": "cpp",
|
||||
"limits": "cpp",
|
||||
"streambuf": "cpp",
|
||||
"regex": "cpp",
|
||||
"tuple": "cpp",
|
||||
"type_traits": "cpp",
|
||||
"array": "cpp",
|
||||
"cctype": "cpp",
|
||||
"clocale": "cpp",
|
||||
"cmath": "cpp",
|
||||
"cstdarg": "cpp",
|
||||
"cstddef": "cpp",
|
||||
"cstdint": "cpp",
|
||||
"cstdio": "cpp",
|
||||
"cstdlib": "cpp",
|
||||
"cstring": "cpp",
|
||||
"ctime": "cpp",
|
||||
"cwchar": "cpp",
|
||||
"cwctype": "cpp",
|
||||
"deque": "cpp",
|
||||
"unordered_map": "cpp",
|
||||
"unordered_set": "cpp",
|
||||
"vector": "cpp",
|
||||
"system_error": "cpp",
|
||||
"fstream": "cpp",
|
||||
"initializer_list": "cpp",
|
||||
"iomanip": "cpp",
|
||||
"iosfwd": "cpp",
|
||||
"istream": "cpp",
|
||||
"new": "cpp",
|
||||
"ostream": "cpp",
|
||||
"numeric": "cpp",
|
||||
"sstream": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"cinttypes": "cpp",
|
||||
"utility": "cpp",
|
||||
"typeinfo": "cpp"
|
||||
}
|
||||
}
|
||||
}
|
||||
39
FingerprintDoorbell/include/README
Normal file
39
FingerprintDoorbell/include/README
Normal file
@ -0,0 +1,39 @@
|
||||
|
||||
This directory is intended for project header files.
|
||||
|
||||
A header file is a file containing C declarations and macro definitions
|
||||
to be shared between several project source files. You request the use of a
|
||||
header file in your project source file (C, C++, etc) located in `src` folder
|
||||
by including it, with the C preprocessing directive `#include'.
|
||||
|
||||
```src/main.c
|
||||
|
||||
#include "header.h"
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Including a header file produces the same results as copying the header file
|
||||
into each source file that needs it. Such copying would be time-consuming
|
||||
and error-prone. With a header file, the related declarations appear
|
||||
in only one place. If they need to be changed, they can be changed in one
|
||||
place, and programs that include the header file will automatically use the
|
||||
new version when next recompiled. The header file eliminates the labor of
|
||||
finding and changing all the copies as well as the risk that a failure to
|
||||
find one copy will result in inconsistencies within a program.
|
||||
|
||||
In C, the usual convention is to give header files names that end with `.h'.
|
||||
It is most portable to use only letters, digits, dashes, and underscores in
|
||||
header file names, and at most one dot.
|
||||
|
||||
Read more about using header files in official GCC documentation:
|
||||
|
||||
* Include Syntax
|
||||
* Include Operation
|
||||
* Once-Only Headers
|
||||
* Computed Includes
|
||||
|
||||
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
||||
46
FingerprintDoorbell/lib/README
Normal file
46
FingerprintDoorbell/lib/README
Normal file
@ -0,0 +1,46 @@
|
||||
|
||||
This directory is intended for project specific (private) libraries.
|
||||
PlatformIO will compile them to static libraries and link into executable file.
|
||||
|
||||
The source code of each library should be placed in a an own separate directory
|
||||
("lib/your_library_name/[here are source files]").
|
||||
|
||||
For example, see a structure of the following two libraries `Foo` and `Bar`:
|
||||
|
||||
|--lib
|
||||
| |
|
||||
| |--Bar
|
||||
| | |--docs
|
||||
| | |--examples
|
||||
| | |--src
|
||||
| | |- Bar.c
|
||||
| | |- Bar.h
|
||||
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||
| |
|
||||
| |--Foo
|
||||
| | |- Foo.c
|
||||
| | |- Foo.h
|
||||
| |
|
||||
| |- README --> THIS FILE
|
||||
|
|
||||
|- platformio.ini
|
||||
|--src
|
||||
|- main.c
|
||||
|
||||
and a contents of `src/main.c`:
|
||||
```
|
||||
#include <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
PlatformIO Library Dependency Finder will find automatically dependent
|
||||
libraries scanning project source files.
|
||||
|
||||
More information about PlatformIO Library Dependency Finder
|
||||
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||
26
FingerprintDoorbell/platformio.ini
Normal file
26
FingerprintDoorbell/platformio.ini
Normal file
@ -0,0 +1,26 @@
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env:esp32doit-devkit-v1]
|
||||
platform = espressif32@6.10.0
|
||||
board = seeed_xiao_esp32c3
|
||||
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
lib_deps =
|
||||
ESP32Async/AsyncTCP
|
||||
ESP32Async/ESPAsyncWebServer
|
||||
ayushsharma82/ElegantOTA
|
||||
knolleary/PubSubClient@^2.8
|
||||
adafruit/Adafruit Fingerprint Sensor Library@^2.1.0
|
||||
intrbiz/Crypto@^1.0.0
|
||||
lib_ldf_mode = deep+
|
||||
build_flags=-D ELEGANTOTA_USE_ASYNC_WEBSERVER=1
|
||||
#build_flags = -D CUSTOM_GPIOS #uncomment this line if you'd like to enable customgpio support
|
||||
558
FingerprintDoorbell/src/FingerprintManager.cpp
Normal file
558
FingerprintDoorbell/src/FingerprintManager.cpp
Normal file
@ -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() {
|
||||
|
||||
}
|
||||
|
||||
79
FingerprintDoorbell/src/FingerprintManager.h
Normal file
79
FingerprintDoorbell/src/FingerprintManager.h
Normal file
@ -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
|
||||
133
FingerprintDoorbell/src/SettingsManager.cpp
Normal file
133
FingerprintDoorbell/src/SettingsManager.cpp
Normal file
@ -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);
|
||||
}
|
||||
|
||||
51
FingerprintDoorbell/src/SettingsManager.h
Normal file
51
FingerprintDoorbell/src/SettingsManager.h
Normal file
@ -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
|
||||
9
FingerprintDoorbell/src/global.h
Normal file
9
FingerprintDoorbell/src/global.h
Normal file
@ -0,0 +1,9 @@
|
||||
#ifndef GLOBAL_H
|
||||
#define GLOBAL_H
|
||||
|
||||
#include <WString.h>
|
||||
|
||||
extern void notifyClients(String message);
|
||||
extern String getTimestampString();
|
||||
|
||||
#endif
|
||||
805
FingerprintDoorbell/src/main.cpp
Normal file
805
FingerprintDoorbell/src/main.cpp
Normal file
@ -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
|
||||
|
||||
}
|
||||
|
||||
11
FingerprintDoorbell/test/README
Normal file
11
FingerprintDoorbell/test/README
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
This directory is intended for PlatformIO Unit Testing and project tests.
|
||||
|
||||
Unit Testing is a software testing method by which individual units of
|
||||
source code, sets of one or more MCU program modules together with associated
|
||||
control data, usage procedures, and operating procedures, are tested to
|
||||
determine whether they are fit for use. Unit testing finds problems early
|
||||
in the development cycle.
|
||||
|
||||
More information about PlatformIO Unit Testing:
|
||||
- https://docs.platformio.org/page/plus/unit-testing.html
|
||||
94
README.md
94
README.md
@ -1,3 +1,93 @@
|
||||
# Doorbell
|
||||
# 🔐 Intelligente Türklingel mit ESP32C3
|
||||
|
||||
Dieses Projekt nutzt einen **ESP32C3 Super Mini**, einen **R503 Fingerabdrucksensor** sowie ein **3x4 Tastenfeld**, um eine smarte Türklingel mit Zutrittskontrolle zu realisieren.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Komponenten
|
||||
|
||||
- [ESP32-C3 Super Mini Board](https://de.aliexpress.com/item/1005005097410991.html)
|
||||
- [R503 Fingerabdrucksensor](https://datasheet.lcsc.com/lcsc/1811141221_FPM-Fingerprint-R503_C83050.pdf)
|
||||
- [3x4 Matrix Keypad (Tastenfeld)](https://www.handsontec.com/dataspecs/module/Keypad%203x4.pdf)
|
||||
- Jumper-Kabel, Stromversorgung (5 V oder USB), Gehäuse etc.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Funktionen
|
||||
|
||||
- Authentifizierung über Fingerabdruck
|
||||
- PIN-Code Eingabe über 3x4 Keypad
|
||||
- Kombination aus Fingerabdruck und PIN möglich
|
||||
- Ansteuerung von z. B. Türöffner oder Alarmanlage
|
||||
- Optional: Web-Anbindung über WiFi zur Protokollierung
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Schaltplan (Verdrahtung)
|
||||
|
||||
### 🔹 R503 Fingerabdrucksensor
|
||||
|
||||
| **R503 Sensor** | **ESP32C3 Pin** | **Beschreibung** |
|
||||
|------------------|------------------|-------------------------------|
|
||||
| VCC | 3V3 | Stromversorgung (3.3 V) |
|
||||
| GND | GND | Masse |
|
||||
| TX | GPIO20 | Daten zum ESP (RX) |
|
||||
| RX | GPIO21 | Daten vom ESP (TX) |
|
||||
| Touch INT | GPIO10 | Interrupt-Signal vom Sensor |
|
||||
|
||||
📄 [R503 Datenblatt (PDF)](https://datasheet.lcsc.com/lcsc/1811141221_FPM-Fingerprint-R503_C83050.pdf)
|
||||
|
||||
---
|
||||
|
||||
### 🔹 3x4 Tastenfeld (Keypad)
|
||||
|
||||
| **Keypad Pin** | **ESP32C3 Pin** | **Beschreibung** |
|
||||
|----------------|------------------|-----------------------|
|
||||
| ROW0 | GPIO12 | Zeile 1 |
|
||||
| ROW1 | GPIO13 | Zeile 2 |
|
||||
| ROW2 | GPIO14 | Zeile 3 |
|
||||
| ROW3 | GPIO15 | Zeile 4 |
|
||||
| COL0 | GPIO16 | Spalte 1 |
|
||||
| COL1 | GPIO17 | Spalte 2 |
|
||||
| COL2 | GPIO18 | Spalte 3 |
|
||||
|
||||
> Du kannst die Pins nach Wunsch ändern – achte nur auf Konflikte mit anderen Komponenten.
|
||||
|
||||
---
|
||||
|
||||
## 📸 ESP32C3 Super Mini Pinout
|
||||
|
||||

|
||||
|
||||
Quelle: [SENTHILRAJ-K/ESP32-C3-SuperMini (GitHub)](https://github.com/SENTHILRAJ-K/ESP32-C3-SuperMini)
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Benötigte Libraries (Arduino)
|
||||
|
||||
- `Adafruit_Fingerprint` (für R503)
|
||||
- `Keypad.h` (für 3x4 Keypad)
|
||||
- `WiFi.h` (für spätere Netzwerkintegration)
|
||||
- ggf. `ESPAsyncWebServer`, `EEPROM`, `Preferences` u. a.
|
||||
|
||||
---
|
||||
|
||||
## 🚧 ToDo
|
||||
|
||||
- [ ] Fingerabdruck-Registrierung über Tastenkombination
|
||||
- [ ] PIN-Code Backup-Funktion
|
||||
- [ ] Web-Interface zur Verwaltung
|
||||
- [ ] Daten-Logging (SD-Karte oder Cloud)
|
||||
|
||||
---
|
||||
|
||||
## 📝 Lizenz
|
||||
|
||||
MIT – freie Nutzung für private und kommerzielle Projekte.
|
||||
|
||||
---
|
||||
|
||||
## 📬 Kontakt
|
||||
|
||||
Bei Fragen oder Vorschlägen: einfach melden!
|
||||
|
||||
Smarter fingerprint türöffner
|
||||
Loading…
x
Reference in New Issue
Block a user