203 lines
7.3 KiB
C#
203 lines
7.3 KiB
C#
using libzkfpcsharp;
|
||
|
||
namespace KronaStaffTerminal.Services;
|
||
|
||
public sealed class FingerprintScanner : IDisposable
|
||
{
|
||
private const int TemplateSize = 2048;
|
||
private readonly object _gate = new();
|
||
private readonly byte[][] _registrationSamples = [new byte[TemplateSize], new byte[TemplateSize], new byte[TemplateSize]];
|
||
private IntPtr _device = IntPtr.Zero;
|
||
private IntPtr _database = IntPtr.Zero;
|
||
private CancellationTokenSource? _captureCancellation;
|
||
private int _registrationCount;
|
||
private int? _registrationFingerprintId;
|
||
private DateTimeOffset _lastProcessedCapture = DateTimeOffset.MinValue;
|
||
private bool _fingerOnSensor;
|
||
|
||
public event Action<int, int>? FingerRecognized;
|
||
public event Action<int>? RegistrationProgress;
|
||
public event Action<int, string>? RegistrationCompleted;
|
||
public event Action<string>? StatusChanged;
|
||
|
||
public int DeviceCount { get; private set; }
|
||
public int ReaderIndex { get; private set; }
|
||
public bool IsConnected => _device != IntPtr.Zero;
|
||
|
||
public void Connect(int readerIndex, IEnumerable<(int FingerprintId, string Template)> templates)
|
||
{
|
||
Disconnect();
|
||
var result = zkfp2.Init();
|
||
if (result != 0)
|
||
{
|
||
throw new InvalidOperationException($"SDK ZKTeco не инициализировано (код {result}).");
|
||
}
|
||
|
||
DeviceCount = zkfp2.GetDeviceCount();
|
||
if (DeviceCount <= 0)
|
||
{
|
||
zkfp2.Terminate();
|
||
throw new InvalidOperationException("Подключённые сканеры не найдены.");
|
||
}
|
||
|
||
ReaderIndex = Math.Clamp(readerIndex, 0, DeviceCount - 1);
|
||
_device = zkfp2.OpenDevice(ReaderIndex);
|
||
if (_device == IntPtr.Zero)
|
||
{
|
||
zkfp2.Terminate();
|
||
throw new InvalidOperationException($"Не удалось открыть сканер №{ReaderIndex + 1}.");
|
||
}
|
||
|
||
_database = zkfp2.DBInit();
|
||
if (_database == IntPtr.Zero)
|
||
{
|
||
zkfp2.CloseDevice(_device);
|
||
_device = IntPtr.Zero;
|
||
zkfp2.Terminate();
|
||
throw new InvalidOperationException("Не удалось создать базу распознавания.");
|
||
}
|
||
|
||
foreach (var item in templates)
|
||
{
|
||
var template = zkfp2.Base64ToBlob(item.Template);
|
||
zkfp2.DBAdd(_database, item.FingerprintId, template);
|
||
}
|
||
|
||
_captureCancellation = new CancellationTokenSource();
|
||
_ = Task.Run(() => CaptureLoop(_captureCancellation.Token));
|
||
StatusChanged?.Invoke($"Сканер №{ReaderIndex + 1} подключён. Приложите палец.");
|
||
}
|
||
|
||
public void BeginRegistration(int fingerprintId)
|
||
{
|
||
lock (_gate)
|
||
{
|
||
_registrationFingerprintId = fingerprintId;
|
||
_registrationCount = 0;
|
||
}
|
||
|
||
StatusChanged?.Invoke("Регистрация: приложите один и тот же палец 3 раза, каждый раз убирая его со стекла.");
|
||
}
|
||
|
||
public void CancelRegistration()
|
||
{
|
||
lock (_gate)
|
||
{
|
||
_registrationFingerprintId = null;
|
||
_registrationCount = 0;
|
||
}
|
||
}
|
||
|
||
private void CaptureLoop(CancellationToken cancellationToken)
|
||
{
|
||
var parameter = new byte[4];
|
||
var size = 4;
|
||
var width = 0;
|
||
var height = 0;
|
||
zkfp2.GetParameters(_device, 1, parameter, ref size);
|
||
zkfp2.ByteArray2Int(parameter, ref width);
|
||
size = 4;
|
||
zkfp2.GetParameters(_device, 2, parameter, ref size);
|
||
zkfp2.ByteArray2Int(parameter, ref height);
|
||
var image = new byte[Math.Max(1, width * height)];
|
||
var captured = new byte[TemplateSize];
|
||
|
||
while (!cancellationToken.IsCancellationRequested)
|
||
{
|
||
var capturedLength = TemplateSize;
|
||
if (zkfp2.AcquireFingerprint(_device, image, captured, ref capturedLength) == 0)
|
||
{
|
||
var now = DateTimeOffset.UtcNow;
|
||
if (!_fingerOnSensor && now - _lastProcessedCapture > TimeSpan.FromMilliseconds(500))
|
||
{
|
||
_fingerOnSensor = true;
|
||
_lastProcessedCapture = now;
|
||
ProcessCapture(captured, capturedLength);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_fingerOnSensor = false;
|
||
}
|
||
Thread.Sleep(150);
|
||
}
|
||
}
|
||
|
||
private void ProcessCapture(byte[] captured, int capturedLength)
|
||
{
|
||
lock (_gate)
|
||
{
|
||
if (_registrationFingerprintId is int fingerprintId)
|
||
{
|
||
if (_registrationCount > 0 && zkfp2.DBMatch(_database, captured, _registrationSamples[_registrationCount - 1]) <= 0)
|
||
{
|
||
StatusChanged?.Invoke("Это другой палец. Приложите тот же палец ещё раз.");
|
||
return;
|
||
}
|
||
|
||
Array.Clear(_registrationSamples[_registrationCount]);
|
||
Array.Copy(captured, _registrationSamples[_registrationCount], capturedLength);
|
||
_registrationCount++;
|
||
RegistrationProgress?.Invoke(_registrationCount);
|
||
if (_registrationCount < 3)
|
||
{
|
||
StatusChanged?.Invoke($"Скан принято: {_registrationCount}/3. Уберите палец и приложите снова.");
|
||
return;
|
||
}
|
||
|
||
var merged = new byte[TemplateSize];
|
||
var mergedLength = TemplateSize;
|
||
var result = zkfp2.DBMerge(_database, _registrationSamples[0], _registrationSamples[1], _registrationSamples[2], merged, ref mergedLength);
|
||
if (result == 0)
|
||
{
|
||
zkfp2.DBDel(_database, fingerprintId);
|
||
result = zkfp2.DBAdd(_database, fingerprintId, merged);
|
||
}
|
||
|
||
_registrationFingerprintId = null;
|
||
_registrationCount = 0;
|
||
if (result == 0)
|
||
{
|
||
RegistrationCompleted?.Invoke(fingerprintId, zkfp2.BlobToBase64(merged, mergedLength));
|
||
StatusChanged?.Invoke("Отпечаток успешно зарегистрирован.");
|
||
}
|
||
else
|
||
{
|
||
StatusChanged?.Invoke($"Не удалось сохранить отпечаток (код {result}). Повторите регистрацию.");
|
||
}
|
||
return;
|
||
}
|
||
|
||
var foundId = 0;
|
||
var score = 0;
|
||
if (zkfp2.DBIdentify(_database, captured, ref foundId, ref score) == 0)
|
||
{
|
||
FingerRecognized?.Invoke(foundId, score);
|
||
}
|
||
else
|
||
{
|
||
StatusChanged?.Invoke("Отпечаток не распознан.");
|
||
}
|
||
}
|
||
}
|
||
|
||
public void Disconnect()
|
||
{
|
||
_captureCancellation?.Cancel();
|
||
_captureCancellation = null;
|
||
Thread.Sleep(200);
|
||
if (_database != IntPtr.Zero)
|
||
{
|
||
zkfp2.DBFree(_database);
|
||
_database = IntPtr.Zero;
|
||
}
|
||
if (_device != IntPtr.Zero)
|
||
{
|
||
zkfp2.CloseDevice(_device);
|
||
_device = IntPtr.Zero;
|
||
}
|
||
zkfp2.Terminate();
|
||
}
|
||
|
||
public void Dispose() => Disconnect();
|
||
}
|