krona-staff/apps/KronaStaffTerminal/Services/FingerprintScanner.cs
Ochenstarik 4d37350757
Some checks are pending
ci / verify (push) Waiting to run
feat(terminal): update attendance terminal to 0.2.4
2026-09-20 23:40:43 +07:00

335 lines
13 KiB
C#
Raw Permalink 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;
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}).");
}
// GetDeviceCount returns the number of physical readers for the SLK20R
// driver shipped with ZKFinger SDK 5.3. Do not subtract a service slot:
// on a workstation with one reader that would hide the only device.
DeviceCount = Math.Max(0, zkfp2.GetDeviceCount());
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);
}
}
}