334 lines
13 KiB
C#
334 lines
13 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 Task? _captureTask;
|
||
private int _registrationCount;
|
||
private int? _registrationFingerprintId;
|
||
private DateTimeOffset _registrationExpiresAtUtc;
|
||
private DateTimeOffset _lastProcessedCapture = DateTimeOffset.MinValue;
|
||
private bool _fingerOnSensor;
|
||
private int _cleanupScheduled;
|
||
|
||
public event Action<int, int>? FingerRecognized;
|
||
public event Action<int>? RegistrationProgress;
|
||
public event Action<int, string>? RegistrationCompleted;
|
||
public event Action<string>? StatusChanged;
|
||
public event Action? ConnectionLost;
|
||
public event Action? RegistrationCancelled;
|
||
|
||
public int DeviceCount { get; private set; }
|
||
public int ReaderIndex { get; private set; }
|
||
public int SkippedTemplateCount { get; private set; }
|
||
public bool IsConnected => _device != IntPtr.Zero;
|
||
|
||
public void Connect(int readerIndex, IEnumerable<(int FingerprintId, string Template)> templates)
|
||
{
|
||
Disconnect();
|
||
SkippedTemplateCount = 0;
|
||
var result = zkfp2.Init();
|
||
if (result != 0)
|
||
{
|
||
throw new InvalidOperationException($"SDK ZKTeco не инициализировано (код {result}).");
|
||
}
|
||
|
||
// ZKFinger SDK 5.3 reports one service slot in addition to physical readers.
|
||
// The vendor C# sample uses GetDeviceCount() - 1 for this SDK generation.
|
||
DeviceCount = Math.Max(0, zkfp2.GetDeviceCount() - 1);
|
||
if (DeviceCount <= 0)
|
||
{
|
||
zkfp2.Terminate();
|
||
throw new InvalidOperationException("Подключённые сканеры не найдены.");
|
||
}
|
||
|
||
var preferredIndex = Math.Clamp(readerIndex, 0, DeviceCount - 1);
|
||
var candidateIndexes = new[] { preferredIndex }
|
||
.Concat(Enumerable.Range(0, DeviceCount).Where(index => index != preferredIndex));
|
||
foreach (var candidateIndex in candidateIndexes)
|
||
{
|
||
var candidateDevice = zkfp2.OpenDevice(candidateIndex);
|
||
if (candidateDevice == IntPtr.Zero) continue;
|
||
ReaderIndex = candidateIndex;
|
||
_device = candidateDevice;
|
||
break;
|
||
}
|
||
if (_device == IntPtr.Zero)
|
||
{
|
||
zkfp2.Terminate();
|
||
throw new InvalidOperationException("Ни один обнаруженный сканер не удалось открыть. Переподключите USB-кабель.");
|
||
}
|
||
|
||
_database = zkfp2.DBInit();
|
||
if (_database == IntPtr.Zero)
|
||
{
|
||
zkfp2.CloseDevice(_device);
|
||
_device = IntPtr.Zero;
|
||
zkfp2.Terminate();
|
||
throw new InvalidOperationException("Не удалось создать базу распознавания.");
|
||
}
|
||
|
||
foreach (var item in templates)
|
||
{
|
||
try
|
||
{
|
||
var template = Convert.FromBase64String(item.Template);
|
||
if (template.Length == 0 || zkfp2.DBAdd(_database, item.FingerprintId, template) != 0)
|
||
{
|
||
SkippedTemplateCount++;
|
||
}
|
||
}
|
||
catch (FormatException)
|
||
{
|
||
SkippedTemplateCount++;
|
||
}
|
||
}
|
||
|
||
_captureCancellation = new CancellationTokenSource();
|
||
var device = _device;
|
||
var database = _database;
|
||
_captureTask = Task.Run(() => CaptureLoop(device, database, _captureCancellation.Token));
|
||
StatusChanged?.Invoke($"Сканер №{ReaderIndex + 1} подключён. Приложите палец.");
|
||
}
|
||
|
||
public void BeginRegistration(int fingerprintId)
|
||
{
|
||
lock (_gate)
|
||
{
|
||
_registrationFingerprintId = fingerprintId;
|
||
_registrationCount = 0;
|
||
_registrationExpiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(1);
|
||
}
|
||
|
||
StatusChanged?.Invoke("Регистрация: приложите один и тот же палец 3 раза, каждый раз убирая его со стекла.");
|
||
}
|
||
|
||
public void CancelRegistration()
|
||
{
|
||
lock (_gate)
|
||
{
|
||
_registrationFingerprintId = null;
|
||
_registrationCount = 0;
|
||
_registrationExpiresAtUtc = default;
|
||
}
|
||
}
|
||
|
||
private void CaptureLoop(IntPtr device, IntPtr database, 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];
|
||
var nextHealthCheck = DateTimeOffset.UtcNow.AddSeconds(5);
|
||
|
||
try
|
||
{
|
||
while (!cancellationToken.IsCancellationRequested)
|
||
{
|
||
ExpireRegistrationIfNeeded();
|
||
if (DateTimeOffset.UtcNow >= nextHealthCheck)
|
||
{
|
||
var healthSize = 4;
|
||
if (zkfp2.GetParameters(device, 1, parameter, ref healthSize) != 0)
|
||
{
|
||
StatusChanged?.Invoke("Связь со сканером потеряна. Выполняется переподключение…");
|
||
ConnectionLost?.Invoke();
|
||
return;
|
||
}
|
||
nextHealthCheck = DateTimeOffset.UtcNow.AddSeconds(5);
|
||
}
|
||
|
||
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(database, captured, capturedLength);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_fingerOnSensor = false;
|
||
}
|
||
Thread.Sleep(150);
|
||
}
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
CrashLogService.Write(exception);
|
||
if (!cancellationToken.IsCancellationRequested)
|
||
{
|
||
StatusChanged?.Invoke("Ошибка сканера. Выполняется переподключение…");
|
||
ConnectionLost?.Invoke();
|
||
}
|
||
}
|
||
}
|
||
|
||
private void ExpireRegistrationIfNeeded()
|
||
{
|
||
lock (_gate)
|
||
{
|
||
if (_registrationFingerprintId is null || DateTimeOffset.UtcNow < _registrationExpiresAtUtc) return;
|
||
_registrationFingerprintId = null;
|
||
_registrationCount = 0;
|
||
_registrationExpiresAtUtc = default;
|
||
StatusChanged?.Invoke("Регистрация отпечатка отменена: за 60 секунд не получены три скана.");
|
||
RegistrationCancelled?.Invoke();
|
||
}
|
||
}
|
||
|
||
private void ProcessCapture(IntPtr database, byte[] captured, int capturedLength)
|
||
{
|
||
lock (_gate)
|
||
{
|
||
if (_registrationFingerprintId is int fingerprintId)
|
||
{
|
||
if (_registrationCount == 0)
|
||
{
|
||
var existingId = 0;
|
||
var existingScore = 0;
|
||
if (zkfp2.DBIdentify(database, captured, ref existingId, ref existingScore) == 0)
|
||
{
|
||
_registrationFingerprintId = null;
|
||
_registrationExpiresAtUtc = default;
|
||
StatusChanged?.Invoke("Этот палец уже зарегистрирован. Регистрация отменена.");
|
||
RegistrationCancelled?.Invoke();
|
||
return;
|
||
}
|
||
}
|
||
|
||
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;
|
||
_registrationExpiresAtUtc = default;
|
||
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();
|
||
try
|
||
{
|
||
if (_captureTask is not null && !_captureTask.Wait(TimeSpan.FromSeconds(2)))
|
||
{
|
||
if (Interlocked.Exchange(ref _cleanupScheduled, 1) == 0)
|
||
{
|
||
_ = _captureTask.ContinueWith(
|
||
_ =>
|
||
{
|
||
ReleaseNativeResources();
|
||
ConnectionLost?.Invoke();
|
||
},
|
||
CancellationToken.None,
|
||
TaskContinuationOptions.ExecuteSynchronously,
|
||
TaskScheduler.Default);
|
||
}
|
||
throw new TimeoutException(
|
||
"Поток чтения сканера не завершился вовремя. Ресурсы будут освобождены сразу после безопасной остановки чтения.");
|
||
}
|
||
}
|
||
catch (AggregateException exception)
|
||
{
|
||
CrashLogService.Write(exception.Flatten());
|
||
}
|
||
ReleaseNativeResources();
|
||
}
|
||
|
||
private void ReleaseNativeResources()
|
||
{
|
||
lock (_gate)
|
||
{
|
||
_captureCancellation?.Dispose();
|
||
_captureCancellation = null;
|
||
_captureTask = null;
|
||
_fingerOnSensor = false;
|
||
if (_database != IntPtr.Zero)
|
||
{
|
||
zkfp2.DBFree(_database);
|
||
_database = IntPtr.Zero;
|
||
}
|
||
if (_device != IntPtr.Zero)
|
||
{
|
||
zkfp2.CloseDevice(_device);
|
||
_device = IntPtr.Zero;
|
||
}
|
||
zkfp2.Terminate();
|
||
Interlocked.Exchange(ref _cleanupScheduled, 0);
|
||
}
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
try
|
||
{
|
||
Disconnect();
|
||
}
|
||
catch (TimeoutException exception)
|
||
{
|
||
CrashLogService.Write(exception);
|
||
}
|
||
}
|
||
}
|