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? FingerRecognized; public event Action? RegistrationProgress; public event Action? RegistrationCompleted; public event Action? 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); } } }