* 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 ## PIP Packages
1. pip3 install python-socketio 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 # How to run
```python3 app.py``` ```python3 app.py```
+171 -49
View File
@@ -1,21 +1,36 @@
import eventlet import json
import socketio import socketio
import uuid import uuid
import base64 import base64
import os import os
import requests
from flask import Flask
import gi import gi
gi.require_version("FPrint", "2.0") gi.require_version("FPrint", "2.0")
gi.require_version("Gio", "2.0") gi.require_version("Gio", "2.0")
from gi.repository import FPrint, GLib, Gio from gi.repository import FPrint, GLib, Gio
config = {
sio = socketio.Server() 'domain': {
static_files = { 'name': 'UNICAL',
'/assets': './public', 'code': 'uc'
'/': {'filename': './public/index.html'} },
'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 stopEnrollment = False
def progress(device, completed_stages, prnt, sessionId, error): def progress(device, completed_stages, prnt, sessionId, error):
@@ -58,28 +73,19 @@ def enroll(sessionId, data):
finally: finally:
device.close_sync() device.close_sync()
def open_dev(sessionId): is_verify_active = False
device = None; 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()): def verify_start(sessionId, data):
device.open_sync() global device;
while (not device.is_open()): global is_verify_active;
print('Opening...')
return device
def verify(sessionId, data):
ctx = FPrint.Context() ctx = FPrint.Context()
devices = ctx.get_devices() devices = ctx.get_devices()
if (len(devices) == 0): if (len(devices) == 0):
print("There is no attached device.") print("There is no attached device.")
sio.emit('fprint-verify-start-error', "There is no attached device.");
return;
else: else:
device = devices[0] device = devices[0]
if (not device): if (not device):
@@ -90,36 +96,127 @@ def verify(sessionId, data):
while (not device.is_open()): while (not device.is_open()):
print('Opening...') print('Opening...')
with open('adebisi-fa.jpg', 'r+b') as f: while True:
img = f.read() with open('adebisi-fa.jpg', 'r+b') as f:
prtRecord = FPrint.Print.deserialize(img) img = f.read()
canceller = Gio.Cancellable.new() prtRecord = FPrint.Print.deserialize(img)
(match, prtFound) = device.verify_sync(prtRecord, canceller, None) (match, prtFound) = device.verify_sync(prtRecord, Gio.Cancellable.new(), None)
if (match): if (match):
sio.emit('fprint-verify-done', { 'ok': True, 'data': data }, room=sessionId) sio.emit('fprint-verify-start-done', { 'ok': True, 'data': data }, room=sessionId)
else: else:
sio.emit('fprint-verify-done', { 'ok': False }, room=sessionId) sio.emit('fprint-verify-start-done', { 'ok': False }, room=sessionId)
device.close_sync()
def verify_short(sessionId, data): if not is_verify_active:
d = open_dev(sessionId) break;
with open('adebisi-fa.jpg', 'r+b') as f:
img = f.read() if device.is_open():
prtRecord = FPrint.Print.deserialize(img) device.close_sync()
(match, print) = d.verify_sync(prtRecord, None, None) pass
if (match):
sio.emit('fprint-verify-done', { 'ok': True, 'data': data }, room=sessionId) 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: else:
sio.emit('fprint-verify-done', { 'ok': False }, room=sessionId) device = devices[0]
d.close_sync() 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 # events
@sio.on("fprint-verify") @sio.on("fprint-verify-stop")
def fprint_verify(sid, data): def fprint_verify_stop(sid, data):
verify_short(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 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") @sio.on("fprint-enroll")
def fprint_enroll(sid, data): def fprint_enroll(sid, data):
print('Enrolling {}'.format(data['finger'])) print('Enrolling {}'.format(data['finger']))
@@ -133,7 +230,32 @@ def capture(sid, data):
@sio.event @sio.event
def connect(sid, data): 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 pass
@sio.event @sio.event
@@ -141,7 +263,6 @@ def disconnect(sid):
print({ 'sid': sid, 'event': 'disconnect'}); print({ 'sid': sid, 'event': 'disconnect'});
pass pass
@sio.on('capture-other') @sio.on('capture-other')
def capture_other(sid, data): def capture_other(sid, data):
print({ "event": "capture_other", "data": data }) print({ "event": "capture_other", "data": data })
@@ -150,4 +271,5 @@ def capture_other(sid, data):
with open('/tmp/libfprint-server', 'w') as f: with open('/tmp/libfprint-server', 'w') as f:
f.write(str(os.getpid())); 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: { data: {
step: 0, step: 0,
phone: '', phone: '',
domain: { code: 'uc', name: 'UNICAL' },
errorMessage: '', errorMessage: '',
otp: '', otp: '',
busy: { busy: {
@@ -34,6 +35,10 @@ var app = new Vue({
this.socket.on("connect", () => { }) this.socket.on("connect", () => { })
this.socket.on("config", (data) => {
this.domain = data.domain;
});
this.socket.on("fprint-enroll-error", (data) => { this.socket.on("fprint-enroll-error", (data) => {
console.log({ event: 'enroll-error', data }); console.log({ event: 'enroll-error', data });
swal("Oops!", `Operation failed. ${data}.`, "error"); swal("Oops!", `Operation failed. ${data}.`, "error");
@@ -81,7 +86,7 @@ var app = new Vue({
this.busy.login = true; this.busy.login = true;
try { try {
var { data } = await axios.get( var { data } = await axios.get(
this.authenticateUrl(`/uc/${this.staff.reference}`) this.authenticateUrl(`/${this.domain.code}/${this.staff.reference}`)
); );
if (!data.ok) { if (!data.ok) {
swal("Oops!", `Operation failed. ${data.message}.` + this.staff.reference, "error"); swal("Oops!", `Operation failed. ${data.message}.` + this.staff.reference, "error");
@@ -121,7 +126,7 @@ var app = new Vue({
} }
var { data } = await axios.post( var { data } = await axios.post(
this.authenticateUrl(`/uc/fingerprints/${this.staff.reference}`), this.authenticateUrl(`/${this.domain.code}/fingerprints/${this.staff.reference}`),
{ fingerprints } { fingerprints }
); );
@@ -18,7 +18,7 @@
</div> </div>
<h1> <h1>
UNICAL Biometrics Enrolment Wizard {{domain.name}} Biometrics Enrolment Wizard
</h1> </h1>
<hr /> <hr />
<div v-if="step > 0" style="text-align: right"> <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 python-socketio
eventlet simple-websocket
flask
requests