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? FingerRecognized; public event Action? RegistrationProgress; public event Action? RegistrationCompleted; public event Action? StatusChanged; 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})."); } DeviceCount = 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(); _ = 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(); }