krona-staff/apps/KronaStaffTerminal/Services/FingerprintScanner.cs
Ochenstarik b88ff9195c
Some checks are pending
ci / verify (push) Waiting to run
fix: harden terminal scanner and background operation
2026-09-18 00:49:34 +07:00

293 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
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 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 секунд не получены три скана.");
}
}
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("Этот палец уже зарегистрирован. Регистрация отменена.");
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
{
_captureTask?.Wait(TimeSpan.FromSeconds(2));
}
catch (AggregateException exception)
{
CrashLogService.Write(exception.Flatten());
}
_captureCancellation = null;
_captureTask = null;
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();
}