using System.Text; using libzkfpcsharp; namespace KronaStaffTerminal.Services; public enum ScannerState { Disconnected, Connected, Busy, DriverError } public sealed record ScannerDeviceInfo( int Index, string Model, string SerialNumber, string DisplayName); public interface IFingerprintScanner : IDisposable { ScannerState State { get; } int DeviceCount { get; } int ReaderIndex { get; } IReadOnlyList DiscoveredDevices { get; } bool IsConnected { get; } int SkippedTemplateCount { get; } event Action? FingerRecognized; event Action? RegistrationProgress; event Action? RegistrationCompleted; event Action? StatusChanged; event Action? StateChanged; event Action? ConnectionLost; event Action? RegistrationCancelled; IReadOnlyList DiscoverDevices(); void Connect(int readerIndex, IEnumerable<(int FingerprintId, string Template)> templates); void Disconnect(); void BeginRegistration(int fingerprintId); void CancelRegistration(); } public sealed class FingerprintScanner : IFingerprintScanner { 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; private List _discoveredDevices = []; private List<(int FingerprintId, string Template)> _lastLoadedTemplates = []; private CancellationTokenSource? _reconnectCancellation; private Task? _reconnectTask; private bool _autoReconnectEnabled = true; public event Action? FingerRecognized; public event Action? RegistrationProgress; event Action? IFingerprintScanner.RegistrationCompleted { add => RegistrationCompleted += value; remove => RegistrationCompleted -= value; } public event Action? RegistrationCompleted; public event Action? StatusChanged; public event Action? StateChanged; public event Action? ConnectionLost; public event Action? RegistrationCancelled; public ScannerState State { get; private set; } = ScannerState.Disconnected; public int DeviceCount { get; private set; } public int ReaderIndex { get; private set; } public int SkippedTemplateCount { get; private set; } public IReadOnlyList DiscoveredDevices => _discoveredDevices; public bool IsConnected => _device != IntPtr.Zero && State == ScannerState.Connected; public IReadOnlyList DiscoverDevices() { lock (_gate) { var devices = new List(); var seenSerials = new HashSet(StringComparer.OrdinalIgnoreCase); var initResult = zkfp2.Init(); if (initResult != 0 && initResult != 1) { CrashLogService.WriteDiagnostic($"zkfp2.Init returned {initResult} during discovery."); _discoveredDevices = devices; DeviceCount = 0; return devices; } try { var rawCount = Math.Max(0, zkfp2.GetDeviceCount()); for (var i = 0; i < rawCount; i++) { // If this device is currently open by us, use it or skip opening IntPtr handle; var shouldClose = false; if (_device != IntPtr.Zero && ReaderIndex == i) { handle = _device; } else { handle = zkfp2.OpenDevice(i); shouldClose = true; } if (handle == IntPtr.Zero) { continue; } try { var serial = QueryDeviceStringParameter(handle, 1101); if (string.IsNullOrWhiteSpace(serial)) { serial = QueryDeviceStringParameter(handle, 1102); } if (string.IsNullOrWhiteSpace(serial)) { serial = QueryDeviceStringParameter(handle, 106); } var model = QueryDeviceStringParameter(handle, 1015); if (string.IsNullOrWhiteSpace(model)) { model = "ZKTeco SLK20R"; } // Eliminate duplicate devices: // ZKFinger SDK 5.3 often exposes 2 device slots for 1 physical USB scanner. // Filter out ghost handles by checking serial number deduplication. if (!string.IsNullOrWhiteSpace(serial)) { if (seenSerials.Contains(serial)) { CrashLogService.WriteDiagnostic($"Device index {i} skipped as duplicate serial: {serial}"); continue; } seenSerials.Add(serial); } else if (devices.Count > 0 && rawCount > 1) { // If no serial number could be queried but a previous reader was already found // on SLK20R driver, skip the secondary phantom slot. CrashLogService.WriteDiagnostic($"Device index {i} skipped as phantom secondary reader slot."); continue; } var displayName = !string.IsNullOrWhiteSpace(serial) ? $"{model} ({serial})" : $"{model} (сканер №{devices.Count + 1})"; devices.Add(new ScannerDeviceInfo(i, model, serial, displayName)); } finally { if (shouldClose && handle != IntPtr.Zero) { zkfp2.CloseDevice(handle); } } } } finally { if (_device == IntPtr.Zero) { zkfp2.Terminate(); } } _discoveredDevices = devices; DeviceCount = devices.Count; CrashLogService.WriteDiagnostic($"Device discovery completed: {devices.Count} unique device(s) found."); return devices; } } public void Connect(int readerIndex, IEnumerable<(int FingerprintId, string Template)> templates) { _lastLoadedTemplates = templates.ToList(); _autoReconnectEnabled = true; StopReconnectLoop(); ConnectInternal(readerIndex, _lastLoadedTemplates); } private void ConnectInternal(int readerIndex, IReadOnlyList<(int FingerprintId, string Template)> templates) { Disconnect(); SkippedTemplateCount = 0; var result = zkfp2.Init(); if (result != 0 && result != 1) { SetState(ScannerState.DriverError, $"Драйвер сканера вернул ошибку инициализации (код {result})."); CrashLogService.WriteDiagnostic($"Scanner initialization failed: code {result}"); StartReconnectLoop(); throw new InvalidOperationException($"SDK ZKTeco не инициализировано (код {result}). Проверьте драйвер."); } var available = DiscoverDevices(); if (available.Count == 0) { zkfp2.Terminate(); SetState(ScannerState.Disconnected, "Подключённые сканеры не найдены."); CrashLogService.WriteDiagnostic("Scanner connect failed: no devices found."); StartReconnectLoop(); throw new InvalidOperationException("Подключённые сканеры не найдены. Подключите USB-сканер."); } var chosenDevice = available.FirstOrDefault(d => d.Index == readerIndex) ?? available[0]; var deviceIndex = chosenDevice.Index; var candidate = zkfp2.OpenDevice(deviceIndex); if (candidate == IntPtr.Zero) { zkfp2.Terminate(); SetState(ScannerState.DriverError, "Не удалось открыть сканер. Возможно, устройство занято."); CrashLogService.WriteDiagnostic($"Failed to open scanner at index {deviceIndex}."); StartReconnectLoop(); throw new InvalidOperationException("Не удалось открыть сканер. Переподключите USB-кабель."); } ReaderIndex = deviceIndex; _device = candidate; _database = zkfp2.DBInit(); if (_database == IntPtr.Zero) { zkfp2.CloseDevice(_device); _device = IntPtr.Zero; zkfp2.Terminate(); SetState(ScannerState.DriverError, "Не удалось инициализировать базу биометрических шаблонов."); CrashLogService.WriteDiagnostic("zkfp2.DBInit failed."); StartReconnectLoop(); 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)); var modelInfo = !string.IsNullOrWhiteSpace(chosenDevice.SerialNumber) ? $"{chosenDevice.Model} [SN: {chosenDevice.SerialNumber}]" : chosenDevice.Model; SetState(ScannerState.Connected, $"Сканер {modelInfo} подключён. Приложите палец."); CrashLogService.WriteDiagnostic($"Scanner connected: {modelInfo} at index {deviceIndex}. Loaded {templates.Count - SkippedTemplateCount} templates."); } public void BeginRegistration(int fingerprintId) { lock (_gate) { _registrationFingerprintId = fingerprintId; _registrationCount = 0; _registrationExpiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(1); } SetState(ScannerState.Busy, "Регистрация: приложите один и тот же палец 3 раза, каждый раз убирая его со стекла."); } public void CancelRegistration() { lock (_gate) { _registrationFingerprintId = null; _registrationCount = 0; _registrationExpiresAtUtc = default; } SetState(IsConnected ? ScannerState.Connected : ScannerState.Disconnected, "Регистрация отпечатка отменена."); RegistrationCancelled?.Invoke(); } 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) { HandleConnectionLoss("Потеряна связь со сканером (сбой проверки состояния)."); return; } nextHealthCheck = DateTimeOffset.UtcNow.AddSeconds(5); } var capturedLength = TemplateSize; var acquireResult = zkfp2.AcquireFingerprint(device, image, captured, ref capturedLength); if (acquireResult == 0) { var now = DateTimeOffset.UtcNow; if (!_fingerOnSensor && now - _lastProcessedCapture > TimeSpan.FromMilliseconds(500)) { _fingerOnSensor = true; _lastProcessedCapture = now; ProcessCapture(database, captured, capturedLength); } } else if (acquireResult is -6 or -7 or -8 or -10 or -17) { // Negative critical device error codes indicate USB disconnect or driver fault HandleConnectionLoss($"Ошибка сканера (код {acquireResult})."); return; } else { _fingerOnSensor = false; } Thread.Sleep(150); } } catch (Exception exception) { CrashLogService.Write(exception); if (!cancellationToken.IsCancellationRequested) { HandleConnectionLoss($"Сбой потока чтения сканера: {exception.Message}"); } } } private void HandleConnectionLoss(string reason) { CrashLogService.WriteDiagnostic($"Scanner connection lost: {reason}"); SetState(ScannerState.Disconnected, reason); ConnectionLost?.Invoke(); if (_autoReconnectEnabled) { StartReconnectLoop(); } } private void StartReconnectLoop() { lock (_gate) { if (_reconnectTask is not null && !_reconnectTask.IsCompleted) return; _reconnectCancellation = new CancellationTokenSource(); var token = _reconnectCancellation.Token; _reconnectTask = Task.Run(() => ReconnectLoopAsync(token)); } } private void StopReconnectLoop() { lock (_gate) { _reconnectCancellation?.Cancel(); _reconnectCancellation?.Dispose(); _reconnectCancellation = null; _reconnectTask = null; } } private async Task ReconnectLoopAsync(CancellationToken token) { CrashLogService.WriteDiagnostic("Starting background scanner auto-reconnect loop..."); while (!token.IsCancellationRequested) { try { await Task.Delay(2500, token); if (token.IsCancellationRequested) return; var devices = DiscoverDevices(); if (devices.Count > 0) { CrashLogService.WriteDiagnostic($"Scanner detected during reconnect: {devices[0].DisplayName}. Connecting..."); ConnectInternal(ReaderIndex, _lastLoadedTemplates); return; // Successfully reconnected! } } catch (OperationCanceledException) { return; } catch (Exception ex) { CrashLogService.WriteDiagnostic($"Reconnect attempt failed: {ex.Message}"); } } } private void ExpireRegistrationIfNeeded() { lock (_gate) { if (_registrationFingerprintId is null || DateTimeOffset.UtcNow < _registrationExpiresAtUtc) return; _registrationFingerprintId = null; _registrationCount = 0; _registrationExpiresAtUtc = default; SetState(ScannerState.Connected, "Регистрация отпечатка отменена: за 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; SetState(ScannerState.Connected, "Этот палец уже зарегистрирован. Регистрация отменена."); RegistrationCancelled?.Invoke(); return; } } if (_registrationCount > 0 && zkfp2.DBMatch(database, captured, _registrationSamples[_registrationCount - 1]) <= 0) { SetState(ScannerState.Busy, "Это другой палец. Приложите тот же палец ещё раз."); return; } Array.Clear(_registrationSamples[_registrationCount]); Array.Copy(captured, _registrationSamples[_registrationCount], capturedLength); _registrationCount++; RegistrationProgress?.Invoke(_registrationCount); if (_registrationCount < 3) { SetState(ScannerState.Busy, $"Скан принят: {_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) { var templateBase64 = zkfp2.BlobToBase64(merged, mergedLength); RegistrationCompleted?.Invoke(fingerprintId, templateBase64); SetState(ScannerState.Connected, "Отпечаток успешно зарегистрирован."); CrashLogService.WriteDiagnostic($"Fingerprint id {fingerprintId} successfully merged and enrolled."); } else { SetState(ScannerState.Connected, $"Не удалось объединить сканы (код {result}). Повторите регистрацию."); CrashLogService.WriteDiagnostic($"Fingerprint enrollment merge failed: code {result}"); } return; } var foundId = 0; var score = 0; if (zkfp2.DBIdentify(database, captured, ref foundId, ref score) == 0) { FingerRecognized?.Invoke(foundId, score); } else { SetState(ScannerState.Connected, "Отпечаток не распознан."); } } } private void SetState(ScannerState newState, string message) { State = newState; StatusChanged?.Invoke(message); StateChanged?.Invoke(newState, message); } 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); if (State != ScannerState.DriverError) { State = ScannerState.Disconnected; } } } private static string QueryDeviceStringParameter(IntPtr device, int paramCode) { var buffer = new byte[256]; var size = buffer.Length; if (zkfp2.GetParameters(device, paramCode, buffer, ref size) == 0 && size > 0) { var len = 0; while (len < size && buffer[len] != 0) len++; if (len > 0) { return Encoding.ASCII.GetString(buffer, 0, len).Trim(); } } return ""; } public static bool ProbeSession0Support(out string diagnostic) { try { var init = zkfp2.Init(); if (init != 0 && init != 1) { diagnostic = $"zkfp2.Init failed with code {init} in current session."; return false; } var count = zkfp2.GetDeviceCount(); zkfp2.Terminate(); if (count <= 0) { diagnostic = "ZKFinger driver reports 0 devices in Session 0 / non-interactive context."; return false; } diagnostic = $"ZKFinger driver reported {count} device(s)."; return true; } catch (Exception ex) { diagnostic = $"Exception probing ZKFinger in Session 0: {ex.Message}"; return false; } } public void Dispose() { _autoReconnectEnabled = false; StopReconnectLoop(); try { Disconnect(); } catch (TimeoutException exception) { CrashLogService.Write(exception); } } }