diff --git a/.config.json-sample b/.config.json-sample new file mode 100644 index 0000000..88f67f3 --- /dev/null +++ b/.config.json-sample @@ -0,0 +1,7 @@ +{ + "mode": "capture", + "domain": { + "name": "UCTH", + "code": "ucth" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9b8cc8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.config.json diff --git a/README.md b/README.md index 4a305a9..3e5f369 100644 --- a/README.md +++ b/README.md @@ -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``` \ No newline at end of file diff --git a/app.py b/app.py index fde8605..08d0ece 100644 --- a/app.py +++ b/app.py @@ -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) \ No newline at end of file diff --git a/public/app.js b/public/capture/app.js similarity index 93% rename from public/app.js rename to public/capture/app.js index 147bc8f..aa5ddaf 100644 --- a/public/app.js +++ b/public/capture/app.js @@ -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 } ); diff --git a/public/index.html b/public/capture/index.html similarity index 99% rename from public/index.html rename to public/capture/index.html index 47b0044..e3e1c5c 100644 --- a/public/index.html +++ b/public/capture/index.html @@ -18,7 +18,7 @@

- UNICAL Biometrics Enrolment Wizard + {{domain.name}} Biometrics Enrolment Wizard


diff --git a/public/clockio/app.js b/public/clockio/app.js new file mode 100644 index 0000000..c688a79 --- /dev/null +++ b/public/clockio/app.js @@ -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; + } + } +}) \ No newline at end of file diff --git a/public/clockio/fingerprint.gif b/public/clockio/fingerprint.gif new file mode 100644 index 0000000..89dd1f1 Binary files /dev/null and b/public/clockio/fingerprint.gif differ diff --git a/public/clockio/fingerprint.png b/public/clockio/fingerprint.png new file mode 100644 index 0000000..2377b29 Binary files /dev/null and b/public/clockio/fingerprint.png differ diff --git a/public/clockio/index.html b/public/clockio/index.html new file mode 100644 index 0000000..98a3d83 --- /dev/null +++ b/public/clockio/index.html @@ -0,0 +1,93 @@ + + + Fingerprint Service + + + + + + +
+
+
+ +
+
+ Loading... +
+
+

+ {{domain.name}} Biometric Clock-IN Service +

+
+
+
+
+
+
+
+ + +

+
+ Start Service + Stop Service +
+
+
+
{{errorMessage}}
+
+
+
+
+
+

Configure Departments

+
+
{{d.name}}
+
+
Save
+
+
+

{{savedDepartments.length}}

+
Configured Departments • edit
+ +

{{records.length}}

+
Total Records • version {{records.timestamp}}
+ +

{{today}}

+
Today's Date
+ +

{{started ? 'Started' : 'Stopped'}}

+
Service status
+ +
+

{{lastProfile}}

+
Last Clocked-In Profile • {{latestReportDate}}
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/requirement.txt b/requirement.txt index fa88e45..4445eda 100644 --- a/requirement.txt +++ b/requirement.txt @@ -1,2 +1,4 @@ python-socketio -eventlet \ No newline at end of file +simple-websocket +flask +requests \ No newline at end of file