* Implement ClockIN service for IDID Biometrics

* Make Biometrics service configurable to domain
* Use native threading with flask (remove 'eventlet')
* Other minor UI adjustment.
This commit is contained in:
2022-01-18 09:17:26 +01:00
parent 1ec205e3d5
commit e48beab2f5
11 changed files with 408 additions and 58 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"mode": "capture",
"domain": {
"name": "UCTH",
"code": "ucth"
}
}
+1
View File
@@ -0,0 +1 @@
.config.json
+3 -1
View File
@@ -10,7 +10,9 @@ This service uses the libfprint library (version 2 and newwer) to capture finger
## PIP Packages
1. pip3 install python-socketio
2. pip3 install eventlet
2. pip3 install simple-websocket
3. pip3 install flask
4. pip3 install requests
# How to run
```python3 app.py```
+175 -53
View File
@@ -1,21 +1,36 @@
import eventlet
import json
import socketio
import uuid
import base64
import os
import requests
from flask import Flask
import gi
gi.require_version("FPrint", "2.0")
gi.require_version("Gio", "2.0")
from gi.repository import FPrint, GLib, Gio
sio = socketio.Server()
static_files = {
'/assets': './public',
'/': {'filename': './public/index.html'}
config = {
'domain': {
'name': 'UNICAL',
'code': 'uc'
},
'mode': 'capture'
}
app = socketio.WSGIApp(sio, static_files=static_files)
configFileExists = os.path.exists('.config.json');
if configFileExists:
with open('.config.json') as reader:
config = json.loads(reader.read());
sio = socketio.Server(async_mode="threading")
app = Flask(__name__)
static_files = {
'/assets': './public/{}'.format(config['mode']),
'/': { 'filename': './public/{}/index.html'.format(config['mode']) }
}
app.wsgi_app = socketio.WSGIApp(sio, static_files=static_files)
stopEnrollment = False
def progress(device, completed_stages, prnt, sessionId, error):
@@ -57,29 +72,20 @@ def enroll(sessionId, data):
sio.emit('fprint-enroll-done', { 'ok': False, 'message': e})
finally:
device.close_sync()
is_verify_active = False
device = None;
def open_dev(sessionId):
device = None;
ctx = FPrint.Context()
devices = ctx.get_devices()
if (len(devices) == 0):
print("There is no attached device.")
else:
device = devices[0]
if (not device):
print("There is no attached device")
if (not device.is_open()):
device.open_sync()
while (not device.is_open()):
print('Opening...')
return device
def verify(sessionId, data):
def verify_start(sessionId, data):
global device;
global is_verify_active;
ctx = FPrint.Context()
devices = ctx.get_devices()
if (len(devices) == 0):
print("There is no attached device.")
sio.emit('fprint-verify-start-error', "There is no attached device.");
return;
else:
device = devices[0]
if (not device):
@@ -90,36 +96,127 @@ def verify(sessionId, data):
while (not device.is_open()):
print('Opening...')
with open('adebisi-fa.jpg', 'r+b') as f:
img = f.read()
prtRecord = FPrint.Print.deserialize(img)
canceller = Gio.Cancellable.new()
(match, prtFound) = device.verify_sync(prtRecord, canceller, None)
if (match):
sio.emit('fprint-verify-done', { 'ok': True, 'data': data }, room=sessionId)
else:
sio.emit('fprint-verify-done', { 'ok': False }, room=sessionId)
device.close_sync()
def verify_short(sessionId, data):
d = open_dev(sessionId)
with open('adebisi-fa.jpg', 'r+b') as f:
img = f.read()
prtRecord = FPrint.Print.deserialize(img)
(match, print) = d.verify_sync(prtRecord, None, None)
if (match):
sio.emit('fprint-verify-done', { 'ok': True, 'data': data }, room=sessionId)
while True:
with open('adebisi-fa.jpg', 'r+b') as f:
img = f.read()
prtRecord = FPrint.Print.deserialize(img)
(match, prtFound) = device.verify_sync(prtRecord, Gio.Cancellable.new(), None)
if (match):
sio.emit('fprint-verify-start-done', { 'ok': True, 'data': data }, room=sessionId)
else:
sio.emit('fprint-verify-start-done', { 'ok': False }, room=sessionId)
if not is_verify_active:
break;
if device.is_open():
device.close_sync()
pass
is_identify_active = False
identify_canceller = None
raw_prints = None
def identify_start(sessionId, data):
global device;
global is_identify_active;
global identify_canceller;
global raw_prints;
ctx = FPrint.Context()
devices = ctx.get_devices()
if (len(devices) == 0):
print("There is no attached device.")
sio.emit('fprint-identify-start-error', "There is no attached device.");
return;
else:
sio.emit('fprint-verify-done', { 'ok': False }, room=sessionId)
d.close_sync()
device = devices[0]
if (not device):
print("There is no attached device")
if (not device.is_open()):
device.open_sync()
while (not device.is_open()):
print('Opening...')
sio.emit('fprint-identify-start-done', room=sessionId)
is_identify_active = True
while True:
prtRecords = [];
with open('adebisi-fa.jpg', 'r+b') as f:
img = f.read()
prtRecords = [FPrint.Print.deserialize(img)]
try:
identify_canceller = Gio.Cancellable.new();
(match, prtFound) = device.identify_sync(prtRecords, identify_canceller, None)
if (match):
sio.emit('fprint-identify-done', { 'ok': True, 'name': 'Adebisi Foluso A.', 'phone': "+2348068287222" }, room=sessionId)
print('Found.')
else:
sio.emit('fprint-identify-done', { 'ok': False }, room=sessionId)
print('Not found.')
except:
print("Identification stopped due to error.")
sio.emit("fprint-identify-stop-done", { 'ok': True, 'message': 'Identification stopped due to error.' })
is_identify_active = False;
finally:
if not is_identify_active:
print('Identification is active. Quiting...')
break;
pass
if device.is_open():
try:
device.close_sync()
except:
print('An error occured while closing device.');
finally:
print('It appears device has already been removed.')
# events
@sio.on("fprint-verify")
def fprint_verify(sid, data):
verify_short(sid, data)
@sio.on("fprint-verify-stop")
def fprint_verify_stop(sid, data):
global is_verify_active;
if (device.is_open()):
try:
device.close_sync();
sio.emit("fprint-verify-stop-done", { 'ok': True, 'message': 'Verifycation stopped.' })
return;
except:
print('An error occured while closing device.');
finally:
print("It appears device has already been removed.")
else:
sio.emit("fprint-verify-stop-done", { 'ok': False, 'message': 'Verifycation was inactive.' })
is_verify_active = False;
pass
@sio.on("fprint-verify-start")
def fprint_verify_start(sid, data):
verify_start(sid, data)
def stop_fprint_identify():
global is_identify_active;
global identify_canceller;
if identify_canceller:
identify_canceller.cancel()
sio.emit("fprint-identify-stop-done", { 'ok': True, 'message': 'Identification stopped.' })
is_identify_active = False;
@sio.on("fprint-identify-start")
def fprint_identify_start(sid, data):
identify_start(sid, data)
@sio.on("fprint-identify-stop")
def fprint_identify_stop(sid, data):
stop_fprint_identify()
@sio.on("fprint-enroll")
def fprint_enroll(sid, data):
print('Enrolling {}'.format(data['finger']))
@@ -133,7 +230,32 @@ def capture(sid, data):
@sio.event
def connect(sid, data):
# print({ 'sid': sid, 'event': 'connect', 'data': data});
global config;
if config['mode'] == 'clockio':
global raw_prints;
stop_fprint_identify();
config['started'] = is_identify_active
domain_code = config['domain']['code'];
response = requests.get("https://apis.a2loft.net/identity/{}/units".format(domain_code))
responseJson = json.loads(response.text)
if responseJson['ok']:
config['departments'] = responseJson['data'];
domain_fprint_file = '{}/fprint-sync/.{}-fprint-data.json'.format(os.getenv("HOME"), domain_code);
print(domain_fprint_file);
if os.path.exists(domain_fprint_file):
with open(domain_fprint_file) as f:
records = json.loads(f.read());
raw_prints = records['profiles'];
prints_dict = {};
for profile in raw_prints:
for fp in profile['fingerprints']:
prints_dict[fp] = profile
config['records'] = {
'length': len(records['profiles']),
'timestamp': records['timestamp']
};
sio.emit('config', config);
pass
@sio.event
@@ -141,7 +263,6 @@ def disconnect(sid):
print({ 'sid': sid, 'event': 'disconnect'});
pass
@sio.on('capture-other')
def capture_other(sid, data):
print({ "event": "capture_other", "data": data })
@@ -150,4 +271,5 @@ def capture_other(sid, data):
with open('/tmp/libfprint-server', 'w') as f:
f.write(str(os.getpid()));
eventlet.wsgi.server(eventlet.listen(('127.0.0.1', 9000), reuse_port=False), app)
if __name__ == '__main__':
app.run(port=9000)
+7 -2
View File
@@ -4,6 +4,7 @@ var app = new Vue({
data: {
step: 0,
phone: '',
domain: { code: 'uc', name: 'UNICAL' },
errorMessage: '',
otp: '',
busy: {
@@ -34,6 +35,10 @@ var app = new Vue({
this.socket.on("connect", () => { })
this.socket.on("config", (data) => {
this.domain = data.domain;
});
this.socket.on("fprint-enroll-error", (data) => {
console.log({ event: 'enroll-error', data });
swal("Oops!", `Operation failed. ${data}.`, "error");
@@ -81,7 +86,7 @@ var app = new Vue({
this.busy.login = true;
try {
var { data } = await axios.get(
this.authenticateUrl(`/uc/${this.staff.reference}`)
this.authenticateUrl(`/${this.domain.code}/${this.staff.reference}`)
);
if (!data.ok) {
swal("Oops!", `Operation failed. ${data.message}.` + this.staff.reference, "error");
@@ -121,7 +126,7 @@ var app = new Vue({
}
var { data } = await axios.post(
this.authenticateUrl(`/uc/fingerprints/${this.staff.reference}`),
this.authenticateUrl(`/${this.domain.code}/fingerprints/${this.staff.reference}`),
{ fingerprints }
);
@@ -18,7 +18,7 @@
</div>
<h1>
UNICAL Biometrics Enrolment Wizard
{{domain.name}} Biometrics Enrolment Wizard
</h1>
<hr />
<div v-if="step > 0" style="text-align: right">
+118
View File
@@ -0,0 +1,118 @@
var app = new Vue({
el: '#app',
data: {
domain: { },
errorMessage: '',
staff: { reference: '' },
department: '-',
departments: {},
started: false,
ready: false,
socket: null,
savedDepartments: [],
latestReportDate: new Date().toLocaleTimeString(),
lastProfile: 'None',
configuring: false
},
created: function () {
this.socket = io();
this.socket.on("connect", () => { })
this.socket.on("config", (data) => {
let deptStr = localStorage.getItem("idid-departments");
if (deptStr) this.savedDepartments = JSON.parse(deptStr);
data.departments.forEach(d => {
d.name = d.name.replace('*', '');
d.checked = this.savedDepartments.includes(d.id);
return d;
});
this.domain = data;
this.started = data.started;
this.today = new Date().toDateString();
this.records = data.records;
this.ready = true;
});
//#region Verify handlers
this.socket.on("fprint-verify-start-done", (data) => {
console.log({ event: 'fprint-verify-start-done', data});
this.started = true;
});
this.socket.on("fprint-verify-start-error", (data) => {
console.log({ event: 'fprint-verify-start-error', data });
swal("Oops!", `Operation failed. ${data}.`, "error");
});
this.socket.on("fprint-verify-stop-done", (data) => {
console.log({ event: 'fprint-verify-stop-done', data});
this.started = false;
});
this.socket.on("fprint-verify-stop-error", (data) => {
console.log({ event: 'fprint-verify-stop-error', data});
});
//#endregion
//#region Identify handlers
this.socket.on("fprint-identify-start-done", (data) => {
console.log({ event: 'fprint-identify-start-done', data});
this.started = true;
});
this.socket.on("fprint-identify-done", (data) => {
console.log({ event: 'fprint-identify-done', data});
if (data.ok)
this.lastProfile = data.name;
else
this.lastProfile = "No Match";
this.latestReportDate = new Date().toLocaleTimeString();
});
this.socket.on("fprint-identify-start-error", (data) => {
console.log({ event: 'fprint-identify-start-error', data});
console.log({ event: 'identify-error', data });
swal("Oops!", `Operation failed. ${data}.`, "error");
});
this.socket.on("fprint-identify-stop-done", (data) => {
console.log({ event: 'fprint-identify-stop-done', data});
this.started = false;
});
this.socket.on("fprint-identify-stop-error", (data) => {
console.log({ event: 'fprint-identify-stop-error', data});
});
//#endregion
},
methods: {
toggleIdentifyMode: async function () {
console.log("toggling clockin status ...");
this.socket.emit(`fprint-identify-${this.started ?'stop' : 'start'}`, {
departmentId: this.department
});
},
toggleCheck: async function (d) {
if (!d.checked) {
let dpts = this.domain.departments.filter(q => q.checked);
if (dpts.length == 10)
return alert("Maximum number of departments, 10, selected.");
}
d.checked = !d.checked;
},
saveDepartments() {
let tmp = this.domain.departments.filter(q => q.checked).map(d => d.id);
this.savedDepartments = tmp;
localStorage.setItem("idid-departments", JSON.stringify(tmp));
this.configuring = false;
}
}
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+93
View File
@@ -0,0 +1,93 @@
<html>
<head>
<title>Fingerprint Service</title>
<script src="https://cdn.socket.io/4.4.0/socket.io.min.js" integrity="sha384-1fOn6VtTq3PWwfsOrk45LnYcGosJwzMHv+Xh/Jx5303FVOXzEnw0EpLv30mtjmlj" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<style>
.disabledbutton {
pointer-events: none;
opacity: 0.4;
}
.bolden {
font-weight: bold;
}
</style>
</head>
<body style="font-size: 14px; font-family: 'Segoe UI';">
<div class="container" id="app">
<div class="col-md-12">
<div style="margin-top: 100px;">
</div>
<div v-if="!ready">
Loading...
</div>
<div v-if="ready">
<h1>
{{domain.name}} Biometric Clock-IN Service
</h1>
<hr />
<div>
<div style="margin-left: auto; margin-right: auto; padding: 10px;">
<form class="form">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<img src="/assets/fingerprint.png" style="width: 100%" v-if="!started"/>
<img src="/assets/fingerprint.gif" style="width: 100%" v-if="started" />
<br/><br/>
<div class="btn btn-block" v-bind:class="{ 'btn-success': !started, 'btn-danger': started }"
v-on:click="toggleIdentifyMode">
<span v-if="!started">Start Service</span>
<span v-else>Stop Service</span>
</div>
</div>
<div class="form-group" v-if="errorMessage">
<div class="alert alert-danger">{{errorMessage}}</div>
</div>
</div>
<div class="col-md-6">
<div style="padding: 20px">
<div v-if="configuring">
<h4>Configure Departments</h4>
<hr/>
<div v-on:click="toggleCheck(d)" v-bind:class="{ 'bolden': d.checked }" style="cursor: pointer; font-size: 16px;" v-for="d in domain.departments">{{d.name}}</div>
<hr/>
<div class="btn btn-success btn-block" @click="saveDepartments">Save</div>
</div>
<div v-if="!configuring">
<h3>{{savedDepartments.length}}</h3>
<div style="margin-bottom: 25px">Configured Departments &bull; <span style="color: red; font-size: 11px; cursor: pointer" @click="configuring=true">edit</span></div>
<h3>{{records.length}}</h3>
<div style="margin-bottom: 25px">Total Records &bull; <b>version {{records.timestamp}}</b></div>
<h3>{{today}}</h3>
<div style="margin-bottom: 25px">Today's Date</div>
<h3>{{started ? 'Started' : 'Stopped'}}</h3>
<div style="margin-bottom: 25px">Service status</div>
<div style="border-right: 2px solid green; border-left: 2px solid green; background-color: whitesmoke; padding: 8px">
<h3>{{lastProfile}}</h3>
<div>Last Clocked-In Profile <span style="font-size: 11px"> &bull; {{latestReportDate}}</span></div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- production version, optimized for size and speed -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
<script src="/assets/app.js"></script>
</body>
</html>
+3 -1
View File
@@ -1,2 +1,4 @@
python-socketio
eventlet
simple-websocket
flask
requests