648 lines
24 KiB
C#
648 lines
24 KiB
C#
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<ScannerDeviceInfo> DiscoveredDevices { get; }
|
||
bool IsConnected { get; }
|
||
int SkippedTemplateCount { get; }
|
||
|
||
event Action<int, int>? FingerRecognized;
|
||
event Action<int>? RegistrationProgress;
|
||
event Action<int, string>? RegistrationCompleted;
|
||
event Action<string>? StatusChanged;
|
||
event Action<ScannerState, string>? StateChanged;
|
||
event Action? ConnectionLost;
|
||
event Action? RegistrationCancelled;
|
||
|
||
IReadOnlyList<ScannerDeviceInfo> 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<ScannerDeviceInfo> _discoveredDevices = [];
|
||
private List<(int FingerprintId, string Template)> _lastLoadedTemplates = [];
|
||
private CancellationTokenSource? _reconnectCancellation;
|
||
private Task? _reconnectTask;
|
||
private bool _autoReconnectEnabled = true;
|
||
|
||
public event Action<int, int>? FingerRecognized;
|
||
public event Action<int>? RegistrationProgress;
|
||
event Action<int, string>? IFingerprintScanner.RegistrationCompleted
|
||
{
|
||
add => RegistrationCompleted += value;
|
||
remove => RegistrationCompleted -= value;
|
||
}
|
||
public event Action<int, string>? RegistrationCompleted;
|
||
public event Action<string>? StatusChanged;
|
||
public event Action<ScannerState, string>? 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<ScannerDeviceInfo> DiscoveredDevices => _discoveredDevices;
|
||
public bool IsConnected => _device != IntPtr.Zero && State == ScannerState.Connected;
|
||
|
||
public IReadOnlyList<ScannerDeviceInfo> DiscoverDevices()
|
||
{
|
||
lock (_gate)
|
||
{
|
||
var devices = new List<ScannerDeviceInfo>();
|
||
var seenSerials = new HashSet<string>(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);
|
||
}
|
||
}
|
||
}
|